Intro

This challenge is about reverse engineering a binary called auth, that asks us for a password in order to get the flag. This challenge is pretty similar to the other reversing challenge from this CTF, Access Granted. However, this time we won’t be able to find the password in cleartext - for this one we have to dig deeper with Ghidra, to understand what is happening. I recommend you read the writeup of Access Granted, as this post goes through the basics of Ghidra. In the TryHackme Industrial Intrusion CTF this is the first challenge of the Reversing category, and it’s also labeled as “Easy”, just like the second reversing challenge. Personally however I think that the second reversing challenge is easier than this one here.

Quick analysis

What are we dealing with?

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ file auth          
auth: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=06ef6e45afa25c9ef8a775bde8bfabe48cdc0251, for GNU/Linux 3.2.0, not stripped

A seemingly simple Linux executable file. When running strings auth, we get

-- snip --                    
U[?] Enter unlock code:    
Error reading input       
[!] Access Denied!
flag.txt          
fopen           
[+] Access Granted! Flag: %s
Error reading flag
9*3$"                     
GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0                         
Scrt1.o     
-- snip --

Notice the strings that the program uses to either allow or deny entry. We can use these strings as reference points when looking at the assembler code / decompiled code. Because most likely, the code for the authentication logic and / or the password is near these strings.

Using Ghidra

We open Ghidra and load up the binary in the code browser tool. On the left hand side, go to the Symbol Tree section to find and expand functions and select main. Now we can see the entry point of this application. I will provide it’s decompiled code below and add a few comments to interesting lines. We can rename variables within Ghidra, to allow for better readability. I highly recommend to rename the variables as you explore the code and learn what the variable is used for. This way, if the variable is referenced ever again, you know it’s assumed purpose. If you keep doing this, you’ll end up with pretty code that is somewhat easy to understand. Lets go through the code together:

undefined8 main(void)
{
  int comparisonResult; // initially named "iVar1"
  char *pointerToUserInput; // initially this was named pcVar2 (pointer to a character)
  undefined8 programExitCode; // initially named "uVar3"
  size_t size_of_string; // initially "sVar4"
  FILE *__stream;
  long in_FS_OFFSET;
  undefined8 firstCharOfInput;
  undefined8 local_password; // interesting variable later on
  undefined8 userInput [8]; // user input gets stored here
  char local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_password = 0xefcdab8967452301;   // some weird hex, possible password
  printf("[?] Enter unlock code: ");  // prepares to read input
  pointerToUserInput = fgets((char *)userInput,0x40,stdin); // stores the pointer to the input variable after reading the input
  if (pointerToUserInput == (char *)0x0) {
    fwrite("Error reading input\n",1,0x14,stderr);
    programExitCode = 1;
  }
  else {
    size_of_string = strcspn((char *)userInput,"\r\n");
    *(undefined1 *)((long)userInput + size_of_string) = 0;
    size_of_string = strnlen((char *)userInput,0x40); // gets the length of the string and limits it at 64 bytes (0x40 in hex)
    if (size_of_string == 8) {
      firstCharOfInput = userInput[0]; // takes the first char of the user input
      transform(&firstCharOfInput,8); // calls a function "transform" on that first char
      // this compares the transformed first char of our input with the hex we saw earlier
      comparisonResult = memcmp(&firstCharOfInput,&local_password,8);
      if (comparisonResult == 0) { // if it matches, print the flag
        __stream = fopen("flag.txt","r");
        if (__stream == (FILE *)0x0) {
          perror("fopen");
          programExitCode = 1;
        }
        else {
          pointerToUserInput = fgets(local_118,0x100,__stream); // the variable gets reused here to store the pointer
          if (pointerToUserInput == (char *)0x0) {
            fwrite("Error reading flag\n",1,0x13,stderr); 
          }
          else {
            printf("[+] Access Granted! Flag: %s",local_118);
            // here are some of the strings we saw earlier when inspecting with "strings"
          }
          fclose(__stream);
          programExitCode = 0;
        }
      }
      else {
        puts("[!] Access Denied!");
        programExitCode = 1;
      }
    }
    else {
      puts("[!] Access Denied!");
      programExitCode = 1;
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return programExitCode;
}

TL;DR - Summary:

  • The program reads user input
  • It transforms ** only the first character** of our input by calling:
if (size_of_string == 8) {
 firstCharOfInput = userInput[0]; // takes the first char of the user input
 transform(&firstCharOfInput,8); // calls a function "transform" on that 
  • Our input should match 0xefcdab8967452301 after going through the transform() function
  • We should look into the transform() function
  • Always rename your variables when analyzing :)

The transform function

void transform(long param_1,ulong param_2)
{
  undefined8 index; // initially local_10

// index starts at 0. Run as long as it's smaller than param_2 and increment by 1
  for (index = 0; index < param_2; index = index + 1) {
	  // For each byte, it XORs the value with "0x55" and stores the result back at the same location
    *(byte *)(index + param_1) = *(byte *)(index + param_1) ^ 0x55;
  
  }
  return;
}

In other words, the transform function does the following: Input byte → XOR with 0x55 → Output byte

At first, it looked like only the first byte of user input is used! Per definition, we only “feed” the first char into the transform function when we call it:

firstCharOfInput = userInput[0]; // takes the first char of the user input
transform(&firstCharOfInput,8); // calls a function "transform" on that 

But the transform function will greedily go over all of the 8 bytes (second parameter used when calling “transform()”) and read them too (as seen in the for loop).

So what this code actually does is not transform the first - but all characters from our input!

Now, our goal is to:

  • Craft an input string that is 8 bytes long
  • When XORed byte-by-byte with 0x55, gives exactly the bytes of local_password we saw earlier:
local_password = 0xefcdab8967452301

Cracking the code

The transform() function XORs every byte with 0x55. So to reverse it, we need to XOR each byte of local_password with 0x55. Below you find each byte XORed with 0x55:

// The "local_password" - 0xefcdab8967452301
// or: (0x)ef cd ab 89 67 45 23 01
// XOR against 0x55
0xef ^ 0x55 = 0xba
0xcd ^ 0x55 = 0x98
0xab ^ 0x55 = 0xfe
0x89 ^ 0x55 = 0xdc
0x67 ^ 0x55 = 0x32
0x45 ^ 0x55 = 0x10
0x23 ^ 0x55 = 0x76
0x01 ^ 0x55 = 0x54

// Result:
{ 0xba, 0x98, 0xfe, 0xdc, 0x32, 0x10, 0x76, 0x54 }

Now, I tried to input the bytes in reverse order (LSB) directly, then using printf and piping it directly into the app:

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ ./auth                                                        
[?] Enter unlock code: 54761032dcfe98ba                                                       
[!] Access Denied!

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ printf '\x54\x76\x10\x32\xdc\xfe\x98\xba' | ./auth                                                                                                                
fopen: No such file or directory
[?] Enter unlock code:                               

The error message suggests that it tried to open a file but failed to do so. Lets put the file flag.txt in our current directory and see what happens:

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ echo lol > flag.txt                                                                       
┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]                
└─$ printf '\x54\x76\x10\x32\xdc\xfe\x98\xba' | ./auth

[?] Enter unlock code: [+] Access Granted! Flag: lol

It works… right?

I tried to connect to the CTF service with netcat and pipe the solution into the program like I did locally. For some reason, that didn’t work and I got no output. I am not sure why, but I assume, it was missing the newline (\n) character (like when you press enter on your keyboard).

──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ printf '\x54\x76\x10\x32\xdc\xfe\x98\xba' | nc 10.10.161.252 9005                     
^C                                                                                            
┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1] 
└─$ nc 10.10.161.252 9005
asd                                                                                           
[?] Enter unlock code: [!] Access Denied!
                                                                                              
┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]                
└─$ printf '\x54\x76\x10\x32\xdc\xfe\x98\xba' | nc 10.10.161.252 9005                      
^C

Close, yet far.

I tried to add a null-byte at to terminate the input among some other things, which didn’t work. After adding an escaped newline \n and calling cat which outputs a newline, I finally got it to work:

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]                              
└─$ printf '\x54\x76\x10\x32\xdc\xfe\x98\xba\x00' | nc 10.10.161.252 9005
^C
                                               
┌──(kali㉿kali)-[~/ctf/industrial-intrusion/reverse1]
└─$ { printf '\x54\x76\x10\x32\xdc\xfe\x98\xba\n'; cat; } | nc 10.10.161.252 9005

[?] Enter unlock code: [+] Access Granted! Flag: THM{Simple_tostart_nice_done_mwww}