Patching binaries with pwntools

So... what actually is patching binaries?

Patching binaries is quite as it seems by the name. It is basically changing some binary in order to make it work in a better way or simply to bypass some protection directly from assembly level.

In order to have a better understanding of it, we will be using the "be quick or be dead" picoCTF challenges.

If you download the be-quick-or-be-dead-1binary at the challenges folder at this repo, you can check it out yourself.

A good idea is, before anything, to check all the symbols at the programs

from pwn import *
elf = ELF('./be-quick-or-be-dead-1')

# List symbols at program
for key, address in elf.symbols.iteritems():
    print key, hex(address)

This way, you can load the binary as an ELF file and iterate through the items at the symbols table, that can be obtained with pwntools elf.symbols.iteritems.

Understanding the binary

In order to exploit the binary, first we need to understand what it is doing. That can be discovered for example using a disassembler, like IDA, radare2 or hopper.

Main function

The pseudo-code of the main is as the following:

int __cdecl main(int argc, const char **argv, const char **envp){
  header(*(_QWORD *)&argc, argv, envp);
  set_timer();
  get_key();
  print_flag(*(_QWORD *)&argc);
  return 0;
}

So we see that there is a function header, that prints the header of the challenge.

Set_timer function

Then, it calls the function set_timer() that is the following:

unsigned int set_timer(){
  if ( __sysv_signal(14, alarm_handler) == (__sighandler_t)-1LL ){
    printf("\n\nSomething went terribly wrong. \nPlease contact the admins with \"be-quick-or-be-dead-1.c:%d\".\n", 59LL);
    exit(0);
  }
  return alarm(1u);
}

First there is an error checking, just to see if the alarm signal will work properly. Then it sets up a 1 second alarm, that will be toggled when the time ends.

Get_key function

After this function, it will continue normally with the get_key()function, that calls calculate_key(), that works as the following:

signed __int64 calculate_key(){
  signed int v1; // [rsp+0h] [rbp-4h]
  v1 = 1878346557;
  do
    ++v1;
  while ( v1 != -538274182 );
  return 3756693114LL;
}

As you can see, it simply gets v1 and sums until v1 is -538274182, what will happen quite after the overflow. After that, it will return 3756693114LL.

Patching the binary - Solution 1

There are basically two ways of solving this challenge.

The easier one in my opinion is to simply "jump" the timer function, this way after the computation finishes the flag will be printed.

In order to do it with pwntools, we do as the following:

from pwn import *
elf = ELF('./be-quick-or-be-dead-1')

#Nulify alarm function
elf.asm(elf.symbols['alarm'], 'ret')
elf.save('./newbinary')

As before, we load the binary as ELF.

Then, we overwrite the assembly instruction that is at the beginning of the alarm function with a ret, this way as soon as the program enters at this function, it will return, not setting the timer.

In the end, a new patched binary is generated. In order to solve the challenge is enough to execute it.

Patching the binary - Solution 2

from pwn import *
elf = ELF('./be-quick-or-be-dead-1')

number = 3756693114
#Modify calculate_key function 
elf.asm(elf.symbols['calculate_key'], 'mov eax, %s\nret\n' % (hex(number)))
elf.save('./newbinary2')

As we could see from the analysis, the calculate_key function did a lot of unnecessary computation, just to return 3756693114.

So we modify that one just to mov that value to eax, that is the convention of the return values of functions, and than returns. The following C represent the new calculate_key function.

signed __int64 calculate_key(){
    return 3756693114LL;
}

Therefore, generating the patched binary and executing it, we successfully obtain the flag.

Last updated