cours/content/secu_logicielle/td9-hackme/index.md

16 KiB

title date tags categories author
Sécurité logicielle : TD 9 Hackme 2023-04-14
Assembleur
x86
Sécurité logicielle
TD
Yorick Barbanneau

Level 0

Première exécution du programme

= Il nous demande ̀a l'utilisateur une saisir, lorsque je rentre un texte, il répond Nope:

./hackme 
This is level 0, welcome! What do you have to say?
bonjour
Nope!

Avec strace

Lors de l'exécution du programme ̀a l'aide de strace, nous pouvons d'abord voir -- après les projections mémoire avec mmap et d'autres éléments -- ds appels systèmes write pour afficher le message invitant ̀a la saisir, Cet affichage est découpé en 4 parties (3 de 16 octets et une de 3)

Vient ensuite un appel système read pour lire la saisie sur l'entrée standard puis un write de 6 octets pour écrire Nope! sur la sortie standard.

Enfin un appel sytème exit_group est lancé avec 1 en param̀etre (terminer tous les threads du processus). La commande echo $? lancé dans le terminal ayant evécuté notre programme confirme que le processus a quitté avec 1 comme code de retour.

avec strings

L'exécution de strings hackme montre des choses interessantes. On y voit des traces des fonctions wprintf (qui pourrait être utile pour la suite), strlen. getline. On voit aussi IAmSuperSecure, comme dirait Bernard dans Day of the Tentacle This is all too easy! :

./hackme       
This is level 0, welcome! What do you have to say?
IAmSuperSecure
Ok, that was easy!

Le ltrace confirme :

ltrace ./hackme
__libc_start_main(0x80490a0, 1, 0xffec1274, 0x8049590 <unfinished ...>
wprintf(0x804a064, 0x40000, 7, 0x80495d3This is level 0, welcome! What do you have to say?
)                                         = 51
getline(0xffec1188, 0xffec118c, 0xf7f215c0, 0xf7db2096IAmsuperSecure
)                           = 15
strcmp("IAmsuperSecure", "IAmSuperSecure")                                        = 1
wprintf(0x804a048, 0xf7fd4950, 0, 0x80492c2Nope!
)                                      = 6
exit(1 <no return ...>
+++ exited (status 1) +++

Level 1

Une fois pframe installé, le point d'arrêt positionné, et remonté dans la fonction appelante

[...]
(gdb) bt
#0  0x08049030 in strcmp@plt ()
#1  0x08049372 in r1 ()
#2  0x080490c8 in main ()
(gdb) frame 1
#1  0x08049372 in r1 ()
(gdb) disass
Dump of assembler code for function r1:
   0x08049350 <+0>:	    push   %ebx
   0x08049351 <+1>:	    sub    $0x14,%esp
   0x08049354 <+4>:	    push   $0x804a184
   0x08049359 <+9>:	    call   0x8049060 <wprintf@plt>
   0x0804935e <+14>:	call   0x8049220 <r>
   0x08049363 <+19>:	pop    %edx
   0x08049364 <+20>:	pop    %ecx
   0x08049365 <+21>:	push   $0x804d030 <- voici l'adresse de la chaine source
   0x0804936a <+26>:	push   %eax       <- voici l'adresse de ma saisie
   0x0804936b <+27>:	mov    %eax,%ebx
   0x0804936d <+29>:	call   0x8049030 <strcmp@plt>
=> 0x08049372 <+34>:	add    $0x10,%esp
   0x08049375 <+37>:	test   %eax,%eax

nous pouvons voir que les chaines sont positionné sur la pile. Affichons les avec gdb:

(gdb) p (char*)($eax)
$1 = 0x804f580 "ThisIsMyTest
(gdb) p (char*)0x804d030
$2 = 0x804d030 <p> "HelloDad"

Nous avons notre mot de passe HelloDad. Recommçons l'exécution avec un watch sur notre variable:

(gdb) wa * (char*)0x804d030 
Hardware watchpoint 1: * (char*)0x804d030
[...]

Hardware watchpoint 1: *(char*)0x804d030

Old value = 81 'Q'
New value = 72 'H'
0x08049339 in z ()
(gdb) bt
#0  0x08049339 in z ()
#1  0x080490c3 in main ()

La fonction permettant le déchiffrage est z()

En observant le contenu de cette variable, on comprend alors que le déchiffrement du mot de passe du niveau 2 se fait en retirant 0x9 à chaque caractère de noure chaine

(gdb) p  (char*)0x804d030
$3 = 0x804d030 <p> "QnuuxMjm"

Ce fonctionnement est confirme par le code assembleur de z()

...
(gdb) disass
Dump of assembler code for function z:
   0x08049320 <+0>:	    mov    0x4(%esp),%edx
   0x08049324 <+4>:	    movzbl (%edx),%eax
   0x08049327 <+7>:	    test   %al,%al
   0x08049329 <+9>:	    je     0x8049340 <z+32>
   0x0804932b <+11>:	lea    0x0(%esi,%eiz,1),%esi
   0x0804932f <+15>:	nop

   0x08049330 <+16>:	sub    $0x9,%eax        ; retire 0x9 à %eax
   
   0x08049333 <+19>:	add    $0x1,%edx        ; incrémente %edx

   0x08049336 <+22>:	mov    %al,-0x1(%edx)   ; remets le caractère déchiffré 
                                                ; dans sa chaine

=> 0x08049339 <+25>:	movzbl (%edx),%eax      ; copie le caractère courant
                                                ; dans %eax (bits 1 à 8)

   0x0804933c <+28>:	test   %al,%al          ; boucle tant que le caractère
   0x0804933e <+30>:	jne    0x8049330 <z+16> ; \0 n'est pas trouvé

Level 2

C'est reparti pour un tour! Nouvelle exécution du programme en plçant un point d'arrêt sur srtcmp@plt et on arrive jusqu'au niveau 2:

On to level 2! So what do you want?
ThisIsTest

Breakpoint 1, 0x08049030 in strcmp@plt ()
(gdb) bt
#0  0x08049030 in strcmp@plt ()
#1  0x080493ca in r2 ()
#2  0x080490cd in main ()

Voici le code désassemblé de r2()

(gdb) frame 1
#1  0x080493ca in r2 ()
(gdb) disass
Dump of assembler code for function r2:
   0x080493a0 <+0>:	push   %ebx
   0x080493a1 <+1>:	sub    $0x14,%esp
   0x080493a4 <+4>:	push   $0x804a280
   0x080493a9 <+9>:	call   0x8049060 <wprintf@plt>
   0x080493ae <+14>:	call   0x8049220 <r>
   0x080493b3 <+19>:	mov    %eax,%ebx
   0x080493b5 <+21>:	mov    %eax,(%esp)
   0x080493b8 <+24>:	call   0x80492f0 <x1>
   0x080493bd <+29>:	pop    %eax
   0x080493be <+30>:	pop    %edx
   0x080493bf <+31>:	push   $0x804adec    <- adresse de la chaine
   0x080493c4 <+36>:	push   %ebx          <- adresse de notre saisie
   0x080493c5 <+37>:	call   0x8049030 <strcmp@plt>
=> 0x080493ca <+42>:	add    $0x10,%esp
   0x080493cd <+45>:	test   %eax,%eax
   0x080493cf <+47>:	jne    0x80493eb <r2+75>
   0x080493d1 <+49>:	sub    $0xc,%esp
   0x080493d4 <+52>:	push   $0x804a318
   0x080493d9 <+57>:	call   0x8049060 <wprintf@plt>
   0x080493de <+62>:	mov    %ebx,(%esp)
   0x080493e1 <+65>:	call   0x8049050 <free@plt>
   0x080493e6 <+70>:	add    $0x18,%esp
   0x080493e9 <+73>:	pop    %ebx
   0x080493ea <+74>:	ret
   0x080493eb <+75>:	call   0x8049280 <f>
End of assembler dump.

Profitons-en pour afficher le contenu de nos deux chaines:

(gdb) p (char*)0x804adec
$10 = 0x804adec "R`jw}]nj"
                 I a  Tea
(gdb) p (char*)$ebx
$11 = 0x804f5c0 "]qr|R|]n|}"

Code de x1() qui "chiffre" la saisie utilisateur du troisième mot de passe. C'est le chiffrement inverse de celui vu au niveau 1 -> on ajoute 0x9.

écrire un script python pour faire le job

cyphertext = "R`jw}]nj"
cleartext = ""
for l in range(len(cyphertext)):
    c += (chr(ord(cyphertext[l])-0x9))
print(cleartext)

ce qui nous donne 'IWantTea'

Level 3

[...]
#5  0x08049248 in r ()
#6  0x08049403 in wut ()
#7  0x080490d2 in main ()

r() se se charge de la saisie, mais que fait wut(), interessons nous à cette fonction d'abord passant dans sa frame:

(gdb) frame 6
#6  0x08049403 in wut ()

Puis en désassemblant son code:

(gdb) disass
Dump of assembler code for function wut:
   0x080493f0 <+0>:	push   %ebx
   0x080493f1 <+1>:	sub    $0x14,%esp
   0x080493f4 <+4>:	push   $0x804a37c
   0x080493f9 <+9>:	call   0x8049060 <wprintf@plt>
   0x080493fe <+14>:	call   0x8049220 <r>
=> 0x08049403 <+19>:	add    $0x10,%esp
   0x08049406 <+22>:	cmpl   $0x65727545,(%eax)  <- intéressant
   0x0804940c <+28>:	jne    0x8049433 <wut+67>
   0x0804940e <+30>:	cmpl   $0x21614b,0x4(%eax) <- et encore interessant
   0x08049415 <+37>:	mov    %eax,%ebx
   0x08049417 <+39>:	jne    0x8049433 <wut+67>
   0x08049419 <+41>:	sub    $0xc,%esp
   0x0804941c <+44>:	push   $0x804a424
   0x08049421 <+49>:	call   0x8049060 <wprintf@plt>
   0x08049426 <+54>:	mov    %ebx,(%esp)
   0x08049429 <+57>:	call   0x8049050 <free@plt>
   0x0804942e <+62>:	add    $0x18,%esp
   0x08049431 <+65>:	pop    %ebx
   0x08049432 <+66>:	ret
   0x08049433 <+67>:	call   0x8049280 <f>
End of assembler dump.

Après la saisie effectuée par l'utilisateur pour ce niveau 3, voici deux comparaisons intéressantes. La première s'effectue sur les 4 octets à l'adresse contenur dans %eax. La suivante sur le contenu à l'adresse de %eax + 0x4

Un script Python permet encore une fois de transformer ces deux valeurs en texte, le voici:


#!/bin/env python3

hextext = "6572754521614b"
finaltext = ""
cleartext = ""
for i in range(0, len(hextext) - 1, 2):
    c = '{}{}'.format(hextext[i], hextext[i+1])
    cleartext += (chr(int(c, 16)))
cur_size=0
bits_processed=0
for i in range(0, len(cleartext) - 1, 4):
    if (len(cleartext) - 4 * bits_processed) > 4:
        cur_size = 4
    else:
        cur_size = len(cleartext) - 4 * bits_processed
    for j in range(i+cur_size,i,-1):
        finaltext += cleartext[j-1]
    bits_processed+=1
print('Level 3 text: {}'.format(finaltext))

Le mot de passe est EureKa!

Level 4

Même cheminement que pour le niveau 3:

...
#5  0x08049248 in r ()
#6  0x080494e3 in aa ()
#7  0x080490d7 in main ()

Interessons nous à cette fonction:

(gdb) disass
Dump of assembler code for function aa:
   0x080494d0 <+0>:	    push   %ebx
   0x080494d1 <+1>:	    sub    $0x14,%esp
   0x080494d4 <+4>:	    push   $0x804a474
   0x080494d9 <+9>:	    call   0x8049060 <wprintf@plt>
   0x080494de <+14>:	call   0x8049220 <r>
=> 0x080494e3 <+19>:	mov    %eax,(%esp)
   0x080494e6 <+22>:	mov    %eax,%ebx
   0x080494e8 <+24>:	call   0x8049080 <strlen@plt>
   0x080494ed <+29>:	add    $0x10,%esp
   0x080494f0 <+32>:	cmp    $0x4,%eax
   0x080494f3 <+35>:	jne    0x8049524 <aa+84>
   0x080494f5 <+37>:	sub    $0x8,%esp
   0x080494f8 <+40>:	push   $0x804adf5
   0x080494fd <+45>:	push   %ebx
   0x080494fe <+46>:	call   0x8049470 <bb> <- wuat is bb?
   0x08049503 <+51>:	add    $0x10,%esp
   0x08049506 <+54>:	test   %eax,%eax
   0x08049508 <+56>:	je     0x8049524 <aa+84>
   0x0804950a <+58>:	sub    $0xc,%esp
   0x0804950d <+61>:	push   $0x804a4ec
   0x08049512 <+66>:	call   0x8049060 <wprintf@plt>
   0x08049517 <+71>:	mov    %ebx,(%esp)
   0x0804951a <+74>:	call   0x8049050 <free@plt>
   0x0804951f <+79>:	add    $0x18,%esp
   0x08049522 <+82>:	pop    %ebx
   0x08049523 <+83>:	ret
   0x08049524 <+84>:	call   0x8049280 <f>
End of assembler dump.

Cette fonction appelle une autre foncion : wut(). C'est elle qui semble se charcher de la vérification de la saisie.

Mais avant ça aa() met les élements en place:

  • mets l'adresse vers la zone mémoire contenant la saisie utilisateur dans l'adresse contenue dans %esp
  • puis copie cette adresse dans %ebx, afin de préparer l'appel à strlen()
  • cet appel positionnera le résultat dans %eax à partir de la chaine dans %ebx
  • %esp est incrémenté de 16 (0x10).
  • Ensuite le résultat de strlen() est comparé à 4, en cas de non égalité, le procgramme branche sur f() qui met fin à l'exécution.. Nous pouvons donc en déduite que notre saisie doit être de 4 cacactères exactement.
  • La pile est ensuite décrémentée de 8
  • 0x804adf5 est ensuite positionné sur la pile, Il semble que se soit une adresse. le contenu de cette espace mémoire est 0x66737a65.
  • %ebx est ensuite poussé sur la pile
  • bb() est appelée, c'est cette fonction qui se chage du 'déchiffrement'

La fonction bb

8049470:              56                     push   %esi
8049471:              53                     push   %ebx
8049472:              8b 5c 24 10            mov    0x10(%esp),%ebx
8049476:              8b 74 24 0c            mov    0xc(%esp),%esi
804947a:              0f b6 13               movzbl (%ebx),%edx
804947d:              84 d2                  test   %dl,%dl
804947f:    /-------- 74 2d                  je     80494ae <bb+0x3e>
8049481:    |         0f b6 06               movzbl (%esi),%eax
8049484:    |         83 f0 12               xor    $0x12,%eax
8049487:    |         38 c2                  cmp    %al,%dl
8049489: /--|-------- 75 35                  jne    80494c0 <bb+0x50>
804948b: |  |         b8 01 00 00 00         mov    $0x1,%eax
8049490: |  |  /----- eb 14                  jmp    80494a6 <bb+0x36>
8049492: |  |  |      8d b6 00 00 00 00      lea    0x0(%esi),%esi
8049498: |  |  |  /-> 0f b6 14 06            movzbl (%esi,%eax,1),%edx
804949c: |  |  |  |   83 c0 01               add    $0x1,%eax
804949f: |  |  |  |   83 f2 12               xor    $0x12,%edx
80494a2: |  |  |  |   38 ca                  cmp    %cl,%dl
80494a4: +--|--|--|-- 75 1a                  jne    80494c0 <bb+0x50>
80494a6: |  |  \--|-> 0f b6 0c 03            movzbl (%ebx,%eax,1),%ecx
80494aa: |  |     |   84 c9                  test   %cl,%cl
80494ac: |  |     \-- 75 ea                  jne    8049498 <bb+0x28>
80494ae: |  \-------> b8 01 00 00 00         mov    $0x1,%eax
80494b3: |            5b                     pop    %ebx
80494b4: |            5e                     pop    %esi
80494b5: |            c3                     ret
80494b6: |            8d b4 26 00 00 00 00   lea    0x0(%esi,%eiz,1),%esi
80494bd: |            8d 76 00               lea    0x0(%esi),%esi
80494c0: \----------> 31 c0                  xor    %eax,%eax
80494c2:              5b                     pop    %ebx
  • 8049472 : on mets en place le contenu de l'adresse contenant notre série de 4 octets mystères 0x66737a65 dans %ebx
  • 8049476 : l'adresse vers notre saisie est positionnée dans %esi
  • 804947a : l'octet de poids faible de %ebx est copié dans %edx
  • 804947d : et logique de %dl sur lui même, si le test est vrai alors le programme branche sur la fin "normale" de la fonction. Ceci signifirait que notre chaine mystère est vide (donc pas de mot de passe).
  • 8049481 : l'octet de poids faible de notre saisie %esi est positionné dans %eax
  • 8049484 : un xor est ensuite réalisé entre 0x12 et %eax
  • 8049487 : les bits de poids faible de %eax et %edx sont comparés
  • 8049489 : en cas d'inégalité, la fonction se termine.
  • 804948b : 0x1 est écrit dans %eax.
  • 8049490 : Branchement vers l'instruction 80494a6
  • 80494a6 : l'octet de poid faible de (%esi,%eax,1) correspondant à la lettre suivante de notre chaine mystère ecx
  • 80494aa : si l'opération booleene de %cl sur lui même est différente de zéro alors le probramme branche sur 8049498. Ce test permet de savoir si on est en fin de chaine (\0) sur notre chaine mystère. sinon le fil de code continue jusqu'à la fin de la fonction.
  • 8049498 : le programme prend le caractère suivant de notre saisir et le place dans %edx
  • 804949c : %eax est incrémenté de 1
  • 804949f : un xor est ensuite réalisé entre 0x12 et %edx
  • 80494a2 : les bits de poids faible de %eax et %edx sont comparés
  • 80494a4 : s'il ne sont pas égaux alors fin du programme, sinon nous revenons à l'instruction 80494a6.

En clair, notre chaine mystère est bien le mot de passe "chiffré", un simple xor avec 0x12 nous permettra de trouver le mot de passe à saisir.

Là encore un script Python permet de faire le travail pour nous:

#!/bin/env python3
hextext = "66737a65"
finaltext = ""
cleartext = ""
for i in range(0, len(hextext) - 1, 2):
    c = '{}{}'.format(hextext[i], hextext[i+1])
    cleartext += chr(int(c, 16) ^ 0x12)
cur_size=0
bits_processed=0
for i in range(0, len(cleartext) - 1, 4):
    if (len(cleartext) - 4 * bits_processed) > 4:
        cur_size = 4
    else:
        cur_size = len(cleartext) - 4 * bits_processed
    for j in range(i+cur_size,i,-1):
        finaltext += cleartext[j-1]
    bits_processed+=1
print('Level 4 text: {}'.format(finaltext))

Ce qui donne:

./level4.py
Level 4 text: what