Dans ce tutoriel, je vais rester très simple pour comprendre rapidement comme on peut arriver à nos fin. Cependant, la notion du langage C est prérequis car je ne vais pas expliquer en détail le code ci-dessous.
Admettons que nous avons ce programme qui demande une authentification pour accéder un contenu secret :
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char mdp[30]; printf("Password : "); scanf("%s", mdp); if(!strcmp(mdp,"pass")) { printf("Bon mot de passe!\n"); // Contenu secret } else { printf("Essait encore!\n"); } return 0; }
On compile le programme :
gcc main.c -o programme
Plusieurs outils permettent le contournement.
gdb ./programme (gdb) break main (gdb) run (gdb) disassemble
Dump of assembler code for function main: 0x00005555555547e0 <+0>: push %rbp 0x00005555555547e1 <+1>: mov %rsp,%rbp => 0x00005555555547e4 <+4>: sub $0x30,%rsp 0x00005555555547e8 <+8>: mov %fs:0x28,%rax 0x00005555555547f1 <+17>: mov %rax,-0x8(%rbp) 0x00005555555547f5 <+21>: xor %eax,%eax 0x00005555555547f7 <+23>: lea 0xf6(%rip),%rdi # 0x5555555548f4 0x00005555555547fe <+30>: mov $0x0,%eax 0x0000555555554803 <+35>: callq 0x555555554690 0x0000555555554808 <+40>: lea -0x30(%rbp),%rax 0x000055555555480c <+44>: mov %rax,%rsi 0x000055555555480f <+47>: lea 0xea(%rip),%rdi # 0x555555554900 0x0000555555554816 <+54>: mov $0x0,%eax 0x000055555555481b <+59>: callq 0x5555555546a0 0x0000555555554820 <+64>: lea -0x30(%rbp),%rax 0x0000555555554824 <+68>: lea 0xd8(%rip),%rsi # 0x555555554903 0x000055555555482b <+75>: mov %rax,%rdi 0x000055555555482e <+78>: callq 0x555555554698 0x0000555555554833 <+83>: test %eax,%eax 0x0000555555554835 <+85>: jne 0x555555554845 <main+101> 0x0000555555554837 <+87>: lea 0xca(%rip),%rdi # 0x555555554908 0x000055555555483e <+94>: callq 0x555555554680 0x0000555555554843 <+99>: jmp 0x555555554851 <main+113> 0x0000555555554845 <+101>: lea 0xce(%rip),%rdi # 0x55555555491a 0x000055555555484c <+108>: callq 0x555555554680 0x0000555555554851 <+113>: mov $0x0,%eax 0x0000555555554856 <+118>: mov -0x8(%rbp),%rdx 0x000055555555485a <+122>: xor %fs:0x28,%rdx 0x0000555555554863 <+131>: je 0x55555555486a <main+138> 0x0000555555554865 <+133>: callq 0x555555554688 0x000055555555486a <+138>: leaveq 0x000055555555486b <+139>: retq End of assembler dump.
La sortie nous affiche la suite d'instructions présente dans la fonction main. On remarque qu'il y a un registre jne qui doit sans doute être liée à la conditions pour tester si le mot de passe correspond à celui attendu. Dans la théorie, si l'on inverse la condition alors on pourra accéder à la partie secrète. Alors, il suffit de changer le registre jne (code 0x75) par je (code 0x74) à l'adresse mémoire correspondante. Ainsi :
(gdb) set {char} 0x0000555555554835=0x74 (gdb) continue Continuing. Password : test Bon mot de passe!
Par ailleur, les adresses présente sur le côté droit sont lisible avec la commande suivante :
(gdb) x/1s 0x555555554903 0x555555554903: "pass" (gdb) x/1s 0x555555554908 0x555555554908: "Bon mot de passe!"
Ainsi on peut même retrouver le mot de passe en clair!
gdb -q ./program break main run set dis intel info registers
Le format d'affichage :
x/o $eip = affiche en octal x/o <offset> = affiche en octal x/x $eip = affiche en hexadecimal x/u $eip = affiche en décimal x/t $eip = affichage en décimal x/3i $eip = affiche les 3 instructions suivante
Par défaut, on peut visualiser une seule unité (un mot / 4 octets) mais on peut voir plusieurs unité l'adresse cible :
x/12x $eip
strings programme
[...] Password : pass Bon mot de passe! Essait encore! ;*3$" [...]
On peut retrouver le mot clé “pass” qui est visible en clair!
objdump -D programme |grep -A30 "main>:"
Plus une lecture plus simple, on peut utiliser la syntaxe de intel : objdump -M intel -D programme
00000000000007e0 <main>: 7e0: 55 push %rbp 7e1: 48 89 e5 mov %rsp,%rbp 7e4: 48 83 ec 30 sub $0x30,%rsp 7e8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 7ef: 00 00 7f1: 48 89 45 f8 mov %rax,-0x8(%rbp) 7f5: 31 c0 xor %eax,%eax 7f7: 48 8d 3d f6 00 00 00 lea 0xf6(%rip),%rdi # 8f4 <_IO_stdin_used+0x4> 7fe: b8 00 00 00 00 mov $0x0,%eax 803: e8 88 fe ff ff callq 690 <.plt.got+0x10> 808: 48 8d 45 d0 lea -0x30(%rbp),%rax 80c: 48 89 c6 mov %rax,%rsi 80f: 48 8d 3d ea 00 00 00 lea 0xea(%rip),%rdi # 900 <_IO_stdin_used+0x10> 816: b8 00 00 00 00 mov $0x0,%eax 81b: e8 80 fe ff ff callq 6a0 <.plt.got+0x20> 820: 48 8d 45 d0 lea -0x30(%rbp),%rax 824: 48 8d 35 d8 00 00 00 lea 0xd8(%rip),%rsi # 903 <_IO_stdin_used+0x13> 82b: 48 89 c7 mov %rax,%rdi 82e: e8 65 fe ff ff callq 698 <.plt.got+0x18> 833: 85 c0 test %eax,%eax 835: 75 0e jne 845 <main+0x65> 837: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 908 <_IO_stdin_used+0x18> 83e: e8 3d fe ff ff callq 680 <.plt.got> 843: eb 0c jmp 851 <main+0x71> 845: 48 8d 3d ce 00 00 00 lea 0xce(%rip),%rdi # 91a <_IO_stdin_used+0x2a> 84c: e8 2f fe ff ff callq 680 <.plt.got> 851: b8 00 00 00 00 mov $0x0,%eax 856: 48 8b 55 f8 mov -0x8(%rbp),%rdx 85a: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx 861: 00 00 863: 74 05 je 86a <main+0x8a> 865: e8 1e fe ff ff callq 688 <.plt.got+0x8> 86a: c9 leaveq 86b: c3 retq 86c: 0f 1f 40 00 nopl 0x0(%rax)
On observe ici une instruction “jne” (à l'emplacement 75 0e) qui doit être liée à la condition pour comparer la saisie attendu par le bon mot de passe. Nous pouvons dans ce cas le remplacer avec une instruction “je” qui aura pour but d'accepter n'importe quelle saisie. Éditons ce code binaire :
hexedit --color programme
On remplace 75 0e par 74 0e puis on sauvegarde cette modification à l'aide de cette suite de touches :
<ctrl>+<w> <crtl>+<x>
Une fois que l'on relance le programm, on obtient la bonne réponse :
./programme Password : test Bon mot de passe!
Dans lequel il faut changer jne par ne (trouver la fonction strcmp)
r2 -w programme aa fs symbols f
0x000007e0 256 main 0x000006b0 43 entry0 0x00200dc8 0 obj.__JCR_LIST__ 0x000006e0 50 sym.deregister_tm_clones 0x00000720 66 sym.register_tm_clones 0x00000770 50 sym.__do_global_dtors_aux 0x00201010 1 obj.completed.7561 0x00200dc0 0 obj.__do_global_dtors_aux_fini_array_entry 0x000007b0 48 sym.frame_dummy 0x00200db8 0 obj.__frame_dummy_init_array_entry 0x00000a70 0 obj.__FRAME_END__ 0x00200dc8 0 obj.__JCR_END__ 0x00200dc0 0 loc.__init_array_end 0x00200dd0 0 obj._DYNAMIC 0x00200db8 0 loc.__init_array_start 0x0000092c 0 loc.__GNU_EH_FRAME_HDR 0x00200f90 0 obj._GLOBAL_OFFSET_TABLE_ 0x000008e0 2 sym.__libc_csu_fini 0x00201000 0 loc.data_start 0x00201010 0 loc._edata 0x000008e4 9 sym._fini 0x00201000 0 loc.__data_start 0x00201008 0 obj.__dso_handle 0x000008f0 4 obj._IO_stdin_used 0x00000870 101 sym.__libc_csu_init 0x00201018 0 loc._end 0x000006b0 43 sym._start 0x00201010 0 loc.__bss_start 0x000007e0 140 sym.main 0x00201010 0 obj.__TMC_END__ 0x00000658 23 sym._init
On sélectionne ensuite la fonction principale qui nous intéresse :
pdf@sym.main [0x000006b0]> pdf@sym.main ;-- main: / (fcn) sym.main 140 | sym.main (); | ; var int local_30h @ rbp-0x30 | ; var int local_8h @ rbp-0x8 | ; DATA XREF from 0x000006cd (entry0) | 0x000007e0 55 push rbp | 0x000007e1 4889e5 mov rbp, rsp | 0x000007e4 4883ec30 sub rsp, 0x30 ; '0' | 0x000007e8 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x1a60 ; '(' | 0x000007f1 488945f8 mov qword [rbp - local_8h], rax | 0x000007f5 31c0 xor eax, eax | 0x000007f7 488d3df60000. lea rdi, qword str.Password_: ; 0x8f4 ; str.Password_: ; "Password : " @ 0x8f4 | 0x000007fe b800000000 mov eax, 0 | 0x00000803 e888feffff call section_end..symtab | 0x00000808 488d45d0 lea rax, qword [rbp - local_30h] | 0x0000080c 4889c6 mov rsi, rax | 0x0000080f 488d3dea0000. lea rdi, qword 0x00000900 ; 0x900 ; "%s" | 0x00000816 b800000000 mov eax, 0 | 0x0000081b e880feffff call 0x6a0 | 0x00000820 488d45d0 lea rax, qword [rbp - local_30h] | 0x00000824 488d35d80000. lea rsi, qword str.pass ; 0x903 ; str.pass ; "pass" @ 0x903 | 0x0000082b 4889c7 mov rdi, rax | 0x0000082e e865feffff call 0x698 | 0x00000833 85c0 test eax, eax | ,=< 0x00000835 750e jne 0x845 | | 0x00000837 488d3dca0000. lea rdi, qword str.Bon_mot_de_passe_ ; 0x908 ; str.Bon_mot_de_passe_ ; "Bon mot de passe!" @ 0x908 | | 0x0000083e e83dfeffff call 0x680 | ,==< 0x00000843 eb0c jmp 0x851 | |`-> 0x00000845 488d3dce0000. lea rdi, qword str.Essait_encore_ ; 0x91a ; str.Essait_encore_ ; "Essait encore!" @ 0x91a | | 0x0000084c e82ffeffff call 0x680 | | ; JMP XREF from 0x00000843 (sym.main) | `--> 0x00000851 b800000000 mov eax, 0 | 0x00000856 488b55f8 mov rdx, qword [rbp - local_8h] | 0x0000085a 644833142528. xor rdx, qword fs:[0x28] | ,=< 0x00000863 7405 je 0x86a | | 0x00000865 e81efeffff call 0x688 | `-> 0x0000086a c9 leave \ 0x0000086b c3 ret
En un coup d'observation, on peut remarquer que le mot de passe est affiché en clair à l'adresse 0x00000824 :
0x00000824 488d35d80000. lea rsi, qword str.pass ; 0x903 ; str.pass ; "pass" @ 0x903
wx 743B @ offset (74=je / 75=jne) wa je @offset pd 256 pxw @offset pcp @offset (print dans le format de python) s= (liste les sections) s (voir sa position) px @offset (printe hexa) pz @offset (print data) s 0xoffset (se déplacer) àà+ (entrer en mode écriture)
f ~Wrong axf str.Wrong_passwd pdf@offset
Il est également possible d'analyser et d'éditer directement le programme compilé avec Vim!
vim programme
On se met en mode édition hexadécimal :
:%!xxd
On modifie “75 0e” par “74 0e” puis on se remet en format ASCII :
:%!xxd -r
Et on sauvegarde :
:w
On obtient ainsi la bonne réponse :
./programme Password : test Bon mot de passe!
hexdump -C programme |less readelf -a programme |less ht programme
Ou même en mode graphique avec edb debugger : https://github.com/eteran/edb-debugger