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-1
binary 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
.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.
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.
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.
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
.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.
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 modified 3yr ago