undo

Undoing obfuscated functions in Ghidra.

3KB
archive
Open

This binary encodes our input data and is compared against a buffer. We can use Ghidra to determine how the flag is encrypted and reverse the encryption to get the flag.

This will be a good introduction to reversing obfuscated functions in Ghidra.

Running checksec on the binary, we notice all security features are enabled:

$ checksec undo
[*] '/home/joybuzzer/Documents/vunrotc/public/reverse-engineering/11-ghidra/undo/src/undo'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Static Analysis

Let's check the raw decompilation of main():

void main(undefined param_1)
{
  int in_GS_OFFSET;
  int local_44;
  char local_3d [41];
  int local_14;
  undefined1 *local_10;
  
  local_10 = &param_1;
  local_14 = *(int *)(in_GS_OFFSET + 0x14);
  puts("Enter your password");
  fflush(_stdout);
  fgets(local_3d,0x29,_stdin);
  printf("You entered: %s\n",local_3d);
  fflush(_stdout);
  encode((int)local_3d);
  local_44 = 0;
  do {
    if (0x28 < local_44) {
      win();
LAB_00011382:
      if (local_14 != *(int *)(in_GS_OFFSET + 0x14)) {
        __stack_chk_fail_local();
      }
      return;
    }
    if (enc_key[local_44] != buffer[local_44]) {
      puts("You lose!");
      goto LAB_00011382;
    }
    local_44 = local_44 + 1;
  } while( true );
}

This code is a mess. There is a label in the middle of the function, a do-while loop, and many wonky variables. Let's discuss how we can clean this up:

  • First, we'll rename variables to something more meaningful. This immediately makes the code easier to read.

  • Second, we'll remove the label by moving the code after the label to the jump.

  • Third, we can remove the canary check. We know it's there, and it doesn't affect control flow.

  • We notice that param_1 is never used, so we'll remove it. This changes the signature to void main(void).

  • Once this is done, we'll remove variables we no longer need and variables that aren't used.

After these changes, we get the following code:

This is a lot easier to read. We can simplify this further by switching the do-while loop for a while loop where the condition is checked.

This means that we're checking the first 0x28 = 40 bytes of enc_key against buffer. If they match, we call win(). If they don't match, we call puts("You lose!") and return.

We have a few things to check out:

  • What is enc_key?

  • What is buffer?

  • What does the encode() function do?

If we look at enc_key and buffer, we notice these variables are colored aqua. This means they are global variables. Local variables are colored yellow. Double-clicking on enc_key and buffer takes us to the location where they are defined.

This is the definition of enc_key:

We notice that enc_key is 40 bytes big and starts at 0x14080. The data in this array is undefined.

Now, let's check buffer:

On the left side, we see this data is initialized. The second column of data is the value at each byte. We can get Ghidra to copy this as a Python List using Right Click -> Copy Special -> Python List. With no extra effort, here is the value of buffer:

Now that we have our data, we can look at encode():

From this, we notice three things:

  • As it stands, encode() takes an int argument.

  • encode() must cast the input to an int, and then cast it again inside the function.

  • Ghidra doesn't know the return type.

From this, we can gather that Ghidra got the parameter type wrong. We can help Ghidra by changing the type to what we think it is. It appears that this function is casting to a char* and a byte*. A byte* is simply a signed char*. Let's change the type in Ghidra to char* and see what happens. At the same time, we know that enc_key is a char*, so we'll change the return type too.

This is a lot better. This function takes a string, performs byte-wise operations on each byte, and stores it in enc_key. It performs the following operation:

Both these operations are undoable, meaning:

This is our key to solving this problem! If we take the ouput buffer, and perform this operation on each byte, we'll get the correct input.

Writing a Solve Script

First, we must write a decode() function that reverses the input. This will take in the list and return a list with the decoded bytes.

We'll take our buffer and decode it:

Now, we need to convert this list to a string. We can do this with the bytes() function:

Finally, we can send this payload to the server:

This gives us the flag:

Last updated

Was this helpful?