0/ Intro
Yesterday, I discover an awesome feature of gdb: the concept of recording, and stepping backward in a binary.You read it good! With gdb, you can run a binary step some instructions and then reverse your steps. You can even change values in memory or register, and continue execution.
It's a feature I've searched for solving crackmes. Usually, in crackme you try some passwords, step through functions and subfunctions, then branches are taken depending on the password, and sometimes, you just want to step backward in order to try another branch.
I've never imagined that it's possible with a vanilla gdb.
Reference doc:
https://sourceware.org/gdb/onlinedocs/gdb/Process-Record-and-Replay.html
1/ A heavy weight crackme!
Here is an example of a crackme: mitsurugi@dojo:~/chall/reverseGDB$ cat heavyweightcrackme.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int heavycalc(char *s) {
int a=0;
printf("\tBig computation in subfunction\n");
a=strlen(s);
return a;
}
int main(int argc, char *argv[]) {
printf("Another Crackme\n");
if (heavycalc(argv[1]) == 6){
printf("Good Boy\n");
}
else {
printf("Bad boy\n");
}
return(0);
}
mitsurugi@dojo:~/chall/reverseGDB$
Nothing really hard, that's just for the demonstration.
The reverser have to input a 6 character long password in order to get the "Good boy" message. Any other length will tell "Bad Boy". The calculation is made in a subfunction.
2/ Reverse stepping GDB in action!
Just use your favorite gdb (higher than v7). mitsurugi@dojo:~/chall/reverseGDB$ gdb -q -nx heavyweightcrackme
Reading symbols from heavyweightcrackme...(no debugging symbols found)...done.
(gdb) b * main
Breakpoint 1 at 0x723
(gdb) r aaaa
Starting program: /home/mitsurugi/chall/reverseGDB/heavyweightcrackme aaaa
Breakpoint 1, 0x0000555555554723 in main ()
(gdb) record
Here, we start the recording. Everything will be recorded, memory, breakpoints, registers, flags and so on. Gdb will be able to step backward and forward since that point.
(gdb) c
Continuing.
Another Crackme
Big computation in subfunction
Bad boy
The next instruction is syscall exit_group. It will make the program exit. Do you want to stop the program?([y] or n) yes
Process record: inferior program stopped.
Program stopped.
0x00007ffff7af34c6 in __GI__exit (status=status@entry=0) at ../sysdeps/unix/sysv/linux/_exit.c:31
31 ../sysdeps/unix/sysv/linux/_exit.c: Aucun fichier ou dossier de ce type.
(gdb)
Ok, we fail at solving this challenge, and we see the "Bad boy" message. (Protip: when gdb ask you to stop the program, answer yes, you will stay in the recorded session).
The magic begins here, with a reverse-continue. It will fast forward until a breakpoint. We have only one, at main.
(gdb) reverse-continue
Continuing.
No more reverse-execution history.
0x0000555555554723 in main ()
(gdb) disass main
Dump of assembler code for function main:
=> 0x0000555555554723 <+0>: push %rbp
0x0000555555554724 <+1>: mov %rsp,%rbp
0x0000555555554727 <+4>: sub $0x10,%rsp
0x000055555555472b <+8>: mov %edi,-0x4(%rbp)
0x000055555555472e <+11>: mov %rsi,-0x10(%rbp)
0x0000555555554732 <+15>: lea 0xef(%rip),%rdi # 0x555555554828
0x0000555555554739 <+22>: callq 0x555555554590 <puts@plt>
0x000055555555473e <+27>: mov -0x10(%rbp),%rax
0x0000555555554742 <+31>: add $0x8,%rax
0x0000555555554746 <+35>: mov (%rax),%rax
0x0000555555554749 <+38>: mov %rax,%rdi
0x000055555555474c <+41>: callq 0x5555555546f0 <heavycalc> //Seems interesting
0x0000555555554751 <+46>: cmp $0x6,%eax
0x0000555555554754 <+49>: jne 0x555555554764 <main+65>
0x0000555555554756 <+51>: lea 0xdb(%rip),%rdi # 0x555555554838
0x000055555555475d <+58>: callq 0x555555554590 <puts@plt>
0x0000555555554762 <+63>: jmp 0x555555554770 <main+77>
0x0000555555554764 <+65>: lea 0xd6(%rip),%rdi # 0x555555554841
0x000055555555476b <+72>: callq 0x555555554590 <puts@plt>
0x0000555555554770 <+77>: mov $0x0,%eax
0x0000555555554775 <+82>: leaveq
0x0000555555554776 <+83>: retq
End of assembler dump.
(gdb)
From now on, we can single step through the binary, examinate memory, add breakpoints (yeah, dynamically!). The heavycalc function seemes interesting because it's return value is checked.
(gdb) b * 0x000055555555474c
Breakpoint 2 at 0x55555555474c
(gdb) c
Continuing.
Breakpoint 2, 0x000055555555474c in main ()
(gdb) nexti
0x0000555555554751 in main ()
(gdb) info reg rax
rax 0x4 4
(gdb)
And we can change the execution flow. We see that the rax is compared to six. What happens if we set a 6?Obviously, from now on, the subsequent execution log is deleted and a new execution log starting from the current address will be recorded. This means we will abandon the previously recorded "future" and begin recording a new "future".
(gdb) set $rax=6
Because GDB is in replay mode, changing the value of a register will make the
execution log unusable from this point onward. Change register rax?(y or n) y
(gdb) c
Continuing.
Good Boy
The next instruction is syscall exit_group. It will make the program exit.
Do you want to stop the program?([y] or n) yes
Process record: inferior program stopped.
Program stopped.
0x00007ffff7af34c6 in __GI__exit (status=status@entry=0) at ../sysdeps/unix/sysv/linux/_exit.c:31
31 in ../sysdeps/unix/sysv/linux/_exit.c
(gdb)
Yeah! We have a "Good boy" message. So, it means that heavycalc have to return 6 in order to win. But what do we have in "heavycalc"? Easy, just step backward, and dive into this function:
(gdb) reverse-continue
Continuing.
Breakpoint 2, 0x000055555555474c in main ()
(gdb) x/10i $rip
=> 0x55555555474c <main+41>: callq 0x5555555546f0 <heavycalc>
0x555555554751 <main+46>: cmp $0x6,%eax
0x555555554754 <main+49>: jne 0x555555554764 <main+65>
0x555555554756 <main+51>: lea 0xdb(%rip),%rdi # 0x555555554838
0x55555555475d <main+58>: callq 0x555555554590 <puts@plt>
0x555555554762 <main+63>: jmp 0x555555554770 <main+77>
0x555555554764 <main+65>: lea 0xd6(%rip),%rdi # 0x555555554841
0x55555555476b <main+72>: callq 0x555555554590 <puts@plt>
0x555555554770 <main+77>: mov $0x0,%eax
0x555555554775 <main+82>: leaveq
(gdb) stepi
0x00005555555546f0 in heavycalc ()
(gdb) disass heavycalc
Dump of assembler code for function heavycalc:
=> 0x00005555555546f0 <+0>: push %rbp
0x00005555555546f1 <+1>: mov %rsp,%rbp
0x00005555555546f4 <+4>: sub $0x20,%rsp
0x00005555555546f8 <+8>: mov %rdi,-0x18(%rbp)
0x00005555555546fc <+12>: movl $0x0,-0x4(%rbp)
0x0000555555554703 <+19>: lea 0xfe(%rip),%rdi # 0x555555554808
0x000055555555470a <+26>: callq 0x555555554590 <puts@plt>
0x000055555555470f <+31>: mov -0x18(%rbp),%rax
0x0000555555554713 <+35>: mov %rax,%rdi
0x0000555555554716 <+38>: callq 0x5555555545a0 <strlen@plt>
0x000055555555471b <+43>: mov %eax,-0x4(%rbp)
0x000055555555471e <+46>: mov -0x4(%rbp),%eax
0x0000555555554721 <+49>: leaveq
0x0000555555554722 <+50>: retq
End of assembler dump.
(gdb)
And we can inspect memory again, etc, etc..
3/ Outro
This feature is AWESOME \o/The concept of stepping backward, forward, inspecting memory and so on is full of fun. My next cracking session will be recorded, replayed and enhanced.
The gdb documentation gives a lot of options, actions and possibilities, this blogpost is just here to show a minimal reverseGDB sessions.
And the irony of reversing gdb while reversing binaries strikes on me :)
0xMitsurugi
Winners trains. Loosers complains.
Aucun commentaire:
Enregistrer un commentaire