Trouver le mot de passe d'un programme

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.

Code source

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

Analyse

Plusieurs outils permettent le contournement.

Avec GDB

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!

D'autres commandes avec cet outil

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

Avec String

strings programme
[...]
Password :
pass
Bon mot de passe!
Essait encore!
;*3$"
[...]

On peut retrouver le mot clé “pass” qui est visible en clair!

Avec objdump

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)

Avec Rabin2

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

Quelques commandes en vracs

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

Avec Vim

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!

Avec d'autres outils

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