Cyber FastTrack Spring 2021 Binary Exploitation Writeups

I recently competed in the Cyber FastTrack CTF placing 46th overall. The competition was fantastic and I learned a lot but web absolutely kicked my ass (as per usual). I’ll be releasing writeups for all the challenge categories however the Binary Exploitation section seemed long enough to warrant its own post.

I was able to solve all but the last 2 challenges. For several of them, I totally did not solve using the intended route but patching was so much easier. If there are any inaccuracies don’t hesitate to reach out on discord birch#9901.

BE01#

For this challenge, we are given a single file chicken.pdf, which just contains a picture of a chicken.

/img/Untitled.png

Typically with CTF challenges involving PDFs the first thing I do is run binwalk to see if there are any hidden files. Binwalk is a tool that searches through binary images (or any file for that matter) for embedded files and executable code, using magic bytes to find them.

[email protected]:~/ctfs/cyber-fast-track/BE01$ binwalk chicken.pdf 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PDF document, version: "1.4"
72            0x48            Zip archive data, at least v1.0 to extract, compressed size: 550522, uncompressed size: 550522, name: egg.zip
550609        0x866D1         End of Zip archive, footer length: 22
551319        0x86997         Zlib compressed data, default compression
6478358       0x62DA16        Zlib compressed data, default compression
6478601       0x62DB09        End of Zip archive, footer length: 22

Bingo. Binwalk found a zip archive called egg.zip embedded within chicken.pdf. Not only can binwalk find these files but it can also extract them! We can use binwalk -e {filename} to extract them.

[email protected]:~/ctfs/cyber-fast-track/BE01/_chicken.pdf.extracted$ ls
48.zip  62DA16  62DA16.zlib  86997  86997.zlib  egg.zip

Great! We have our egg.zip. The other files in the directory are just present due to the way PDFs are setup and we can ignore them. I’ve found that most of the time, the .zlib files aren’t super important to finding a solution for these type of problems.

Unzipping egg.zip reveals another file called chicken.zip

For the sake of time, there were 2 more zip files within chicken.zip finally resulting in egg.pdf which gives us our flag!

/img/Untitled%201.png

BE02#

Preliminary analysis#

For this challenge were given a file called rot13. Running file shows us that it’s a 64-bit ELF executable and checksec tells us it has all protections enabled.

[email protected]:~/ctfs/cyber-fast-track/BE02$ file rot13 
rot13: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ae33f99a63e5bcebe32a7662ac3daa6daca565f9, not stripped

[email protected]:~/ctfs/cyber-fast-track/BE02$ checksec rot13
[*] '/home/birch/programming/ctfs/cyber-fast-track/BE02/rot13'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Static analysis in Ghidra#

Since this is an easy problem, there’s probably a fairly trivial solution so next thing to do is open it up in Ghidra and examine the code. Looking at the main function de-compilation

undefined8 main(void)
{
  byte bVar1;
  byte bVar2;
  /* Some more variable declaractions */
  /* Removed for readability */ 
  size_t sVar3;
  char local_108 [32];
  char acStack137 [105];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  fflush(stdout);
  puts("\x1b[36m===================================\x1b[0m");
  puts("\x1b[31mROT IN sHELL\x1b[0m :: \x1b[33mROT13\'s your input!\x1b[0m");
  puts("\x1b[36m===================================\x1b[0m");
  printf("> ");
  fgets(acStack137 + 1,100,stdin); //Read user input into array
  sVar3 = strlen(acStack137 + 1);
  acStack137[sVar3] = '\0';        //Set the last character of the array to termator
  sVar3 = strlen(acStack137 + 1);
  **if (sVar3 < 0x21) {              // Checking to see how long the string is!**
    printf("\n\x1b[32mROT13 output:\x1b[0m\n> ");
    local_130 = 0;
    while( true ) {
      sVar3 = strlen(acStack137 + 1);
      if (sVar3 <= (ulong)(long)local_130) break;
      putchar((-1 / (((int)~(~(int)acStack137[(long)local_130 + 1] | 0x20U) / 0xd) * 2 + -0xb)) *
              0xd + (int)acStack137[(long)local_130 + 1]);
      local_130 = local_130 + 1;
    }
    putchar(10);
  }
  else { //HUH???? Probably a flag... how do we get here?
    local_128 = 0x61746e656d676553; local_120 = 0x756166206e6f6974;
    local_118 = 0x2e746c;
    local_c8 = 0x63617473202a2a2a; local_c0 = 0x696873616d73206b;
    local_b8 = 0x636574656420676e; local_b0 = 0x3a2a2a2a20646574;
    local_a8 = 0x776f6e6b6e753c20; local_a0 = 0x696d726574203e6e;
    local_98 = 0x6574616e;         local_94 = 100;
    local_e8 = 0x20646574726f6241; local_e0 = 0x75642065726f6328;
    local_d8 = 0x6465706d;
    local_d4 = 0x29;
    local_108[0] = 'w';
    /* This was compressed just for readability; TLDR its setting
    *  the values of an array to the corresponding values */ 
    local_108[1] = 0xf3;  local_108[2] = 0xdb;  local_108[3] = 0xff;
    local_108[4] = 0x38;  local_108[5] = 0xd2;  local_108[6] = 0xef;
    local_108[7] = 0xf;   local_108[8] = 0xeb;  local_108[9] = 199;
    local_108[10] = 0x1b; local_108[11] = 0xb3; local_108[12] = 0x33;
    local_108[13] = 0xd7; local_108[14] = 0xf7; local_108[15] = 0xdf;
    local_108[16] = 0x47; local_108[17] = 0x5e; local_108[18] = 0x30;
    local_108[19] = 0xf5;
    local_134 = 0;
    //This loop will modify the contents of the array local
    while (local_134 < 0x14) { //We now know the lenght of the return string 0x14
      bVar2 = (byte)local_134;
      bVar1 = -~-(~((~-local_108[local_134] + 0xb5U ^ bVar2) - 0x1b) - bVar2) ^ 0x13;
      local_108[local_134] = ((bVar1 << 6 | bVar1 >> 2) + 0x38 ^ bVar2) - 0x32;
      local_134 = local_134 + 1;
    }
    printf("\n\x1b[31m%s\n",&local_128);
    puts((char *)&local_c8);
    printf("%s\x1b[0m\n",&local_e8);
    printf("\n\x1b[32m%s\x1b[0m\n",local_108);
  }
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

We can see that the first if block contains some code which will perform a rot13 cipher on the supplied input, however that will only happen if the length of the input is less than 0x23 (Decimal: 33). The else statement contains some pretty juicy stuff, probably building a flag! Rather than trying to figure out what that’s doing just by reading, it would be much easier to just run it. Since we know the else will execute if the supplied input is longer than 33 characters, we can just do that!

Exploiting#

I used python to print a string of 40 A’s and piped it into the executable. It’s not important what the input actually is as long as the length is greater than 33.

[email protected]:~/ctfs/cyber-fast-track/BE02$ python3 -c "print('A'*40)" | ./rot13
===================================
ROT IN sHELL :: ROT13's your input!
===================================
> 
Segmentation fault.
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

**Flag: luckyNumber13 <-- NICE.**

And we can see that the code segfaults and gives us the flag. Whoop!

BM01#

For this challenge, the intended way is probably not what I did, but out of not wanting to learn GDB properly and laziness I just patched the binary instead.

Preliminary analysis#

For this challenge, we are given a single file program. Running file and checksec we can see that it’s a 64-bit ELF executable with all protections enabled.

[email protected]:~/ctfs/cyber-fast-track/BM01$ file program
program: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=df3699b182abb1c39bea5f8194ad50a89aab4107, not stripped

[email protected]:~/ctfs/cyber-fast-track/BM01$ checksec program 
[*] '/home/birch/ctfs/cyber-fast-track/BM01/program'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Running the program we get the following output…

[email protected]:~/ctfs/cyber-fast-track/BM01$ ./program
Какой пароль?
> test123123
неверный.

The prompt and the response are both in Russian, so I did a quick google translate just to see what it’s asking for. “Какой пароль?” = “What password?” and “неверный.” = “incorrect”. Great, so it wants a password.

Static analysis in Ghidra#

Opening the binary in Ghidra we can look at the decompilation of the main function.

undefined8 main(void)
{
  byte bVar1;
  byte bVar2;
  int iVar3;
  long in_FS_OFFSET;
  uint local_74;
  byte local_67 [15];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts(&DAT_001009e0);                    //Prints "Какой пароль?"
  printf("> ");
  fgets(local_58,0x3c,stdin);             //Read in some user input from stdin
  iVar3 = strcmp(&DAT_001009c8,local_58); //Compare the input to &DAT_00..
  if (iVar3 == 0) {                       //If the strings are equal do this
    // Setting the values of local_67 to the corresponding values
    local_67[0] = 0xe4;  local_67[1] = 100;   local_67[2] = 0xa6;
    local_67[3] = 0x90;  local_67[4] = 0x7c;  local_67[5] = 0xa6;
    local_67[6] = 0x75;  local_67[7] = 0xb8;  local_67[8] = 0xa4;
    local_67[9] = 0xd;   local_67[10] = 0xc;  local_67[11] = 0x7f;
    local_67[12] = 0x7e; local_67[13] = 0xf3; local_67[14] = 1;
    local_74 = 0;

    //This loop modifies the contents of the array
    while (local_74 < 0xf) {
      bVar2 = (byte)local_74;
      bVar1 = ~(~(~-((local_67[local_74] ^ 0xa5) - bVar2 ^ bVar2) ^ 0x8d) - 0xb);
      local_67[local_74] = (((bVar1 << 5 | bVar1 >> 3) + 0x37 ^ 0xe5) - 7 ^ bVar2) - 0x39;
      local_74 = local_74 + 1;
    }
    printf(&DAT_00100a08,local_67); //Return local_67
  }
  else {
    puts(&DAT_00100a37); //Prints "неверный."
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

From looking at the code, we can see the program reads in a string, and then compares it against whatever is stored in &DAT_001009c8. If the two strings are equal then it will print what is most likely a flag, and otherwise will print “incorrect”.

Instead of guessing for a password, it would be super nice if we could somehow modify the if(iVar3 == 0) check to succeed without knowing the password. First, it helps to know how strcmp() actually works. In C the function strcmp(dest, src) will compare two strings, returning 0 if they are equal, and some arbitrary non-zero value if they aren’t.

Diving one level deeper, lets look at the assembly for the comparison:

/img/Untitled%202.png

  1. It calls strcmp, which by convention stores the result in the EAX register.
  2. Then it calls TEST which sets the zero flag (ZF which is a CPU flag) depending on the result of a bitwise AND between EAX and EAX. If the result of the AND is 0 then ZF = 1 otherwise ZF = 0. The only time that the result of a bitwise AND equals 1 is when both of the parameters are equal (bitwise operation explanation).
  3. Lastly it does a JNZ (Jump if Not Zero) which checks the state of ZF. If it’s not 0, it will jump to the address passed as a parameter.

This understanding is important because we now know that if the strings are equal, the ZF will be set to 0 and it will NOT JUMP to the else and instead continue into the if block. By now you might have some idea of how we can get inside the if block without knowing the password. If we can change the JNZ to a JZ (Jump if Zero) whenever the strcmp returns a value that ISN’T zero (meaning the strings aren’t equal), we will enter into the if block and hopefully get a flag.

Precursor to patching: installing SavePatch.py#

When I initially tried to patch, export, and run, the program segfaults immediately. No good. Apparently when patching binaries in Ghidra, there is some strange behavior when it comes to exporting. I did some digging and came across an absolute life saver: SavePatch.py. This is a Ghidra script that allows for the correct saving and export of patched binaries. To install:

  1. git clone https://github.com/schlafwandler/ghidra_SavePatch
  2. Copy the SavePatch.py file to wherever you keep your Ghidra scripts
    • If you’re not sure open the scripts menu

      /img/Screen_Shot_2021-04-07_at_9.17.39_PM.png

    • Then click on manage script directories

      /img/Screen_Shot_2021-04-07_at_9.20.40_PM.png

    • And you should be able to see where Ghidra is looking for scripts

      /img/Screen_Shot_2021-04-07_at_9.23.24_PM.png

So now that we have SavePatch.py installed we can move on to the actual patching.

Patching the binary in Ghidra#

Patching in Ghidra is incredibly simple, you just hover over the instruction you would like to modify, right click, and press “Patch instruction” (or just Ctrl+Shift+G).

/img/Screen_Shot_2021-04-08_at_12.08.14_AM.png

Then we can change the instruction to whatever we like! In this case we just change JNZ to JZ.

/img/Screen_Shot_2021-04-08_at_12.09.53_AM.png

Changed instruction!

Now if we take a quick look at the decompiled code, we can see it looks quite different!

/* Removed start for brevity */
fgets(local_58,0x3c,stdin);
iVar3 = strcmp(&DAT_001009c8,local_58);
if (iVar3 == 0) {
    puts(&DAT_00100a37);
} else {
    local_67[0] = 0xe4;
    local_67[1] = 100;
    local_67[2] = 0xa6;
/* Removed end */

The big takeaway is that the content in the if and else statements swapped! Now we just need to export the binary, give it some random value, and hopefully we should get the flag.

Exporting the binary#

To export the binary using SavePatch.py all you need to do is highlight the section of the assembly that you patched, then “Select → From Highlight”.

/img/Screen_Shot_2021-04-08_at_12.16.28_AM.png

Then we need to actually run the Ghidra script! So we just go back to the script menu described earlier and search for SavePatch.py. Follow the prompts and then run the new binary!

[email protected]:~/ctfs/cyber-fast-track/BM01$ ./solution 
Какой пароль?
> solved!
верный!

**флаг: wh1te%BluE$R3d <- Flag!**

BM02#

Preliminary Analysis#

Just like the previous problem, we are given a 64-bit ELF binary with all protections enabled.

[email protected]:~/ctfs/cyber-fast-track/BM02$ file program 
program: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=710f0db5bebe00717527836216659a788cb94c26, not stripped

[email protected]:~/ctfs/cyber-fast-track/BM02$ checksec program
[*] '/home/birch/programming/ctfs/cyber-fast-track/BM02/program'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Running the program we see very minimal output…

[email protected]:~/ctfs/cyber-fast-track/BM02$ ./program
I'm not going to make it that easy for you.

Static Analysis in Ghidra#

Opening the file up we can initially see two interesting functions, main and printFlag. Let’s check out their decompilation.

Examining the main function, there isn’t much going on. It just prints a single line and then returns.

undefined8 main(void) {
  puts("I\'m not going to make it that easy for you."); 
  return 0;
}

Examining printFlag, we can see it is an obfuscated flag building function similar to the last problem. Again, not something that you would want to decipher by hand so it would be great to run it.

void printFlag(int param_1) {
  byte bVar1;
  byte bVar2;
  long in_FS_OFFSET;
  uint local_2c;
  byte local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (param_1 == 0x539) { //Checks to see if parameter passed into printFlag = 0x539
		/* Creates the flag using similar methods to prev problem */
    local_28[0] = 0x15;  local_28[1] = 0x70;  local_28[2] = 0xe5;
    local_28[3] = 100;   local_28[4] = 0x7a;  local_28[5] = 0xd4;
    .
		. //Same stuff as before 
		.
    local_2c = 0;
    while (local_2c < 0x13) {
      bVar2 = (byte)local_2c;
      bVar1 = ~-((~local_28[local_2c] + bVar2 ^ 0x48) - bVar2);
      bVar2 = ((bVar1 << 3 | bVar1 >> 5) - bVar2 ^ 0x5d) - 0x23 ^ bVar2;
      bVar1 = (bVar2 * '\x02' | bVar2 >> 7) + 0xbf;
      local_28[local_2c] = (bVar1 * ' ' | bVar1 >> 3) ^ 0x65;
      local_2c = local_2c + 1;
    }
    puts((char *)local_28);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Since the decompilation of the main and printFlag don’t give us too much insight besides the fact that param1 in printFlag must be 0x539 we should examine the assembly. I’ve included some annotations.

001007e3 55              PUSH       RBP
001007e4 48 89 e5        MOV        RBP,RSP
001007e7 48 83 ec 10     SUB        RSP,0x10
001007eb c7 45 fc        MOV        dword ptr [RBP + local_c],0x1   ;Store 1 in variable 
         01 00 00 00
001007f2 81 7d fc        CMP        dword ptr [RBP + local_c],0x539 ;Check to see if the above value is 0x539
         39 05 00 00
001007f9 75 0c           JNZ        LAB_00100807                    ;If they aren't equal it will skip the call to printFlag
001007fb 8b 45 fc        MOV        EAX,dword ptr [RBP + local_c]   ;Move value of variable into EAX
001007fe 89 c7           MOV        EDI,EAX
00100800 e8 a5 fe        CALL       printFlag                       ;Call print flag with parameter [RBP + local_c]                       
         ff ff
00100805 eb 0c           JMP        LAB_00100813

Patching and solution#

From the assembly, we can see that the value of [RBP + local_c] is what determines wether the printFlag function is called. So, if we can change the value stored in there to 0x539 then we can get our flag! Just like the last problem, I patched the binary in Ghidra. We don’t need to modify the instructions at all and instead can just change the second MOV to store 0x539 in [RBP + local_c]!

/img/Screen_Shot_2021-04-08_at_2.23.48_PM.png

Interestingly enough, the decompiled code changes as well! We can see that instead of calling puts() main will call printFlag with our parameter, neat!

undefined8 main(void) {
  printFlag(0x539); //THIS IS EXACTLY WHAT WE WANTED
  return 0;
}

Using the same technique as the last one we can save our patched binary using SavePatch.py and run, giving us the flag!

[email protected]:~/ctfs/cyber-fast-track/BM02$ ./solution 
Flag: patchItFixIt

BM03#

Preliminary analysis#

Surprise surprise, we are again given a 64-bit ELF executable with all protections enabled. Sweet.

[email protected]:~/ctfs/cyber-fast-track/BM03$ file flag 
flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a9eda2fd49375b7def242c1fd184baf0ff05a4c5, with debug_info, not stripped

[email protected]:~/ctfs/cyber-fast-track/BM03$ checksec flag
[*] '/home/birch/programming/ctfs/cyber-fast-track/BM03/flag'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Running the program we are given the following output

Flag:
       __       __                          _                      ____ __           
  ____/ /___   / /_   __  __ ____ _ ____ _ (_)____   ____ _       / __// /_ _      __
 Error displaying rest of flag

Static Analysis in Ghidra#

The two interesting functions in this binary are main and output(). Let’s look at the decompilation of them in Ghidra.

We can see that main is relatively simple, just printing the prompt and then calling output().

int main(void) {
  int rows;
  int cols;
  
  fflush(stdout);
  puts("\n\x1b[36m Flag:\x1b[0m");
  output(2,0x55);
  return 0;
}

The output function is more interesting as this is what actually prints the flag. We can see that there is an array defined for the flag that is 6 by 85. Looking back at the main function we notice it calls output with 2 as a row parameter! So its missing the last 4 rows.

void output(int rows,int cols) {
  long lVar1;
  undefined8 *puVar2;
  int (*paiVar3) [85];
  long in_FS_OFFSET;
  int i;
  int j;
  int flag [6] [85]; //Declaration of the flag 
  char flagChars [11];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  lVar1 = 0xff;
  puVar2 = &DAT_00100a00;
  paiVar3 = flag;
  while (lVar1 != 0) {
    lVar1 = lVar1 + -1;
    *(undefined8 *)*paiVar3 = *puVar2;
    puVar2 = puVar2 + 1;
    paiVar3 = (int (*) [85])(*paiVar3 + 2);
  }
  /* Characters used to build the flag */ 
  flagChars[0] = ' ';  flagChars[1] = '_'; flagChars[2] = '/';
  flagChars[3] = '\\'; flagChars[4] = '('; flagChars[5] = ')';
  flagChars[6] = '`';  flagChars[7] = ','; flagChars[8] = '|';
  flagChars[9] = '.'; flagChars[10] = '\0';

  /* Loop through the array and print the flag */
  i = 0;
  while (i < rows) {
    j = 0;
    while (j < cols) {
      putchar((int)flagChars[flag[i][j] / 100]);
      j = j + 1;
    }
    putchar(10);
    i = i + 1;
  }
  if (rows < 6) { //If the row parameter is less than 6, err...
    puts("\x1b[31m Error displaying rest of flag\x1b[0m");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Patching and solution#

Just like the last few times I solved this by patching the binary. We know that main is calling output with the row parameter 2, which means the whole flag isn’t printed. So to get the flag, we just need to change the parameter from 2 to 6 (or greater) and we should see the whole thing!

Looking at the assembly for main we can see where it is setting the variables that will be used in the call to output, 0x2 and 0x55.

/img/Screen_Shot_2021-04-08_at_3.36.51_PM.png

Using the same techniques as the past two, we can patch the binary to make that value whatever we want! In this case it should be 0x6 as thats the height of the array defined by output.

/img/Screen_Shot_2021-04-08_at_3.41.30_PM.png

Running the program, we get our flag!

Flag:
       __       __                          _                      ____ __           
  ____/ /___   / /_   __  __ ____ _ ____ _ (_)____   ____ _       / __// /_ _      __
 / __  // _ \ / __ \ / / / // __ `// __ `// // __ \ / __ `/      / /_ / __/| | /| / /
/ /_/ //  __// /_/ // /_/ // /_/ // /_/ // // / / // /_/ /      / __// /_  | |/ |/ / 
\__,_/ \___//_.___/ \__,_/ \__, / \__, //_//_/ /_/ \__, /______/_/   \__/  |__/|__/  
                          /____/ /____/           /____//_____/

BH01#

Preliminary analysis#

For the sake of shortness I won’t show checksec and file output, this binary was a stripped 64-bit executable with all protections enabled.

Running the program we get the following output. It asks us for a word, then sends us some garbage.

[email protected]:~/ctfs/cyber-fast-track/BH01$ ./program 
What is the magic word?
test **<- this is supplied by us**
�:�lO?d#����*���E	
Did you understand that?

Static analysis in Ghidra#

At first glance we don’t see any functions with names… great that means the binary is stripped. That isn’t a problem however because we can see the function entry which is where the program will start executing. We can see that the first parameter called in __libc_start_main is FUN_00101209 so lets check that out.

/img/Screen_Shot_2021-04-08_at_3.43.11_PM.png

This function is big and gross but it seems to be printing a flag! The interesting part is that the loop occurs a random amount of times, which is strange… We can see it is also reading in 40 characters supplied by us.

undefined8 FUN_00101209(void) {

/* Lots of variable declaractions */ 

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  tVar5 = time(&local_88);
  srand((uint)tVar5);
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_7d = 0x16120a05;
  local_79 = 0x18;
  puts("What is the magic word?");
  fflush(stdout);
  fgets((char *)&local_48,0x28,stdin);
  local_78 = 0x103f4f6c803a05b6;
  local_70 = 0x9f2abbb5f5e62364;
  local_68 = 0x982b00094580efba;
  local_60 = 0x3f4675fb93f9dfb2;
  local_58 = 0x1afcba39;
  iVar3 = rand();
  uVar4 = (int)*(char *)((long)&local_48 + (long)(int)*(char *)((long)&local_7d + (long)(iVar3 % 5))
                        ) - 0x5a;
  local_90 = 0;
  while (local_90 < uVar4) {
    bVar1 = ~*(byte *)((long)&local_78 + (ulong)local_90) + 0x2f;
    bVar2 = (byte)local_90;
    bVar1 = (~(~(0x57 - (~(bVar1 * -0x80 | bVar1 >> 1) - 0x33)) - 0x3f) ^ bVar2) + 0x4e ^ bVar2;
    bVar1 = ~((bVar1 * '\x02' | bVar1 >> 7) + 0x3d) ^ bVar2;
    bVar1 = ~(bVar1 << 3 | bVar1 >> 5);
    if ((int)uVar4 < 0) break;
    bVar1 = ~((bVar1 << 5 | bVar1 >> 3) - bVar2) - 0x17;
    bVar1 = ~((bVar1 * -0x80 | bVar1 >> 1) - bVar2);
    bVar1 = 0xad - ((bVar1 << 3 | bVar1 >> 5) + 0x3c);
    bVar1 = (bVar1 * ' ' | bVar1 >> 3) + bVar2;
    bVar1 = (bVar1 * '\b' | bVar1 >> 5) - bVar2;
    bVar1 = ~(bVar1 * -0x80 | bVar1 >> 1) ^ bVar2;
    bVar2 = (bVar1 * -0x40 | (byte)-bVar1 >> 2) - bVar2;
    bVar1 = ~(~(~(bVar2 * ' ' | bVar2 >> 3) ^ 0x45) - 8);
    *(byte *)((long)&local_78 + (ulong)local_90) =
         (0xd1 - ((bVar1 << 2 | bVar1 >> 6) ^ 0xef) ^ 0x65) - 0x3a;
    local_90 = local_90 + 1;
  }
  puts((char *)&local_78);
  puts("Did you understand that?");
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

Fuzzing#

I figured before moving on in our analysis, I should see what different values would come out as. Since we know that the function is reading in 40 characters, I just used python to generate a string of 40 ‘a’s and then piped it into the program

[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('a'*40)" | ./program 
What is the magic word?
Flag: ad#����*���E	
Did you understand that?

Wait what? We now see the start of a flag! Just continuing to fuzz I was interesting in what 40 ‘b’s would cause the program to print. It gives us another readable character! At this point I figured I would just work my way down the rest of the ascii table and see what the output of the program is…

[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('b'*40)" | ./program 
What is the magic word?
Flag: aLd#����*���E	
Did you understand that?
[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('c'*40)" | ./program 
What is the magic word?
Flag: aLi#����*���E	
Did you understand that?
[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('d'*40)" | ./program 
What is the magic word?
Flag: aLit����*���E	
Did you understand that?

Each time we supply a character further along in the alphabet we get more of the flag! So I jumped to the end of the alphabet.

[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('z'*40)" | ./program 
What is the magic word?
Flag: aLittLeObfuScatIonalCharAc9���
Did you understand that?

Ok so we’re close, but we’re still missing those last few characters. Since C handles chrarcters as their integer value, I figured I’d check what comes after z on the ascii table.

/img/Untitled%203.png

We can see that it’s {, |, }, and ~. Since there are 4 garbage values left it’s probably a safe bet to jump to ~ as it’s the last printable character in the ascii table. Sure enough, that gives us our flag.

[email protected]:~/ctfs/cyber-fast-track/BH01$ python3 -c "print('~'*40)" | ./program 
What is the magic word?
Flag: aLittLeObfuScatIonalCharActEr
Did you understand that?