--- title: "Sécurité logicielle : TD 9 Hackme" date: 2023-04-14 tags: ["Assembleur", "x86"] categories: ["Sécurité logicielle", "TD"] author: - Yorick Barbanneau --- ## Level 0 ### Première exécution du programme Le programme demande à 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! ``` Il doit donc s'attendre à un mot de passe bien précis. ### Avec strace Lors de l'exécution du programme à l'aide de `strace`, nous pouvons d'abord voir -- après les projections mémoire avec `mmap` et d'autres éléments -- des appels systèmes `write` pour afficher le message invitant à la saisie. 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 système `exit_group` est lancé avec 1 en paramètre (terminer tous les threads du processus). La commande `echo $?` lancé dans le terminal ayant exécuté `hack` 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 tout un tas d'autres chaines qui semble être des mots de passe: ``` [...] GLIBC_2.2 GLIBC_2.0 __gmon_start__ ZYh< ZYh0 8Eureu% UWVS [^_] R`jw}]nj ezsf ;*2$"( QnuuxMjm IAmSuperSecure GCC: (Debian 8.3.0-6) 8.3.0 [...] ``` `IAmSuperSecure` parait relativement intéressant. mais à ce state rien n'est sûr. Mais juste comme ça essayons tout de même un `lstrace` sur nore programme: ``` ltrace ./hackme __libc_start_main(0x80490a0, 1, 0xffb45f24, 0x8049590 wprintf(0x804a064, 0xf7f988cb, 0xf7c1ca2f, 0xf7f804a0This is level 0, welcome! What do you have to say? ) = 51 getline(0xffb45e18, 0xffb45e1c, 0xf7e1d620, 0xf7c76ca5MyBadPassword ) = 14 strcmp("MyBadPassword", "IAmSuperSecure") = 1 wprintf(0x804a048, 0xf7fbca40, 0, 0x80492c2Nope! ) = 6 exit(1 +++ exited (status 1) +++ ``` La sortie de `ltrace` une fois la saisie effectuée, montre qu'elle est comparée avec `IAmSuperSecure`, un essai le confirme. 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! ``` ## Level 1 Une fois pframe installé et le point d'arrêt positionné sur `strcmp@plt`, la *pile d'appel* montre que la fonction appelante est `r1()`. Une fois passé dans son contexte, analysons le code assembleur: ``` [...] (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 0x0804935e <+14>: call 0x8049220 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 => 0x08049372 <+34>: add $0x10,%esp 0x08049375 <+37>: test %eax,%eax ``` Nous pouvons voir que les adresses vers les chaines comparées par `strcmp()` sont positionnées sur la pile comme le montre les commentaires ci-dessus. Affichons le contenu pointés par celles-ci avec `gdb`: ``` (gdb) p (char*)($eax) $1 = 0x804f580 "ThisIsMyTest (gdb) p (char*)0x804d030 $2 = 0x804d030

"HelloDad" ``` Nous avons notre mot de passe `HelloDad`. Mais cette chaine ne figure pas dans la liste des chaines données par la commande `strings`, elle est donc obsurcie. Essayons donc de comprendre comment. Recommençons l'exécution avec un `watch` la chaine contenue sur le programme: ``` (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()`, passons à son désassemblage: ``` ... (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 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 ; \0 n'est pas trouvé ``` En observant le contenu de cette fonction, on comprend alors que le déchiffrement du mot de passe du niveau 2 se fait en retirant `0x9` à chaque caractère de la chaine contenue dans le programme. On retrouv ## Level 2 C'est reparti pour un tour! Nouvelle exécution du programme en plaç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 () ``` Cette fois-ci c'est la fonction `r2()` qui s'occupe du "déchiffrement, voici son code désassemblé : ``` (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 0x080493ae <+14>: call 0x8049220 0x080493b3 <+19>: mov %eax,%ebx 0x080493b5 <+21>: mov %eax,(%esp) 0x080493b8 <+24>: call 0x80492f0 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 => 0x080493ca <+42>: add $0x10,%esp 0x080493cd <+45>: test %eax,%eax 0x080493cf <+47>: jne 0x80493eb 0x080493d1 <+49>: sub $0xc,%esp 0x080493d4 <+52>: push $0x804a318 0x080493d9 <+57>: call 0x8049060 0x080493de <+62>: mov %ebx,(%esp) 0x080493e1 <+65>: call 0x8049050 0x080493e6 <+70>: add $0x18,%esp 0x080493e9 <+73>: pop %ebx 0x080493ea <+74>: ret 0x080493eb <+75>: call 0x8049280 End of assembler dump. ``` Profitons-en pour afficher le contenu de nos deux chaines: ``` (gdb) p (char*)0x804adec $10 = 0x804adec "R`jw}]nj" (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, ici on ajoute `0x9` à chaque caractère. Pour déchiffrer le mote de passe écrit dans le programme, j'ai écris un script Python : ```python 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 Après avoir lancé le programme et atteint la saisie du niveau 3, `Ctrl+c` envoi le signal `SIGINT` au programme, donne la main à l'invite de commande. voici le résultat de la commande `bt`: ``` [...] #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 puis en la désassemblant: ``` (gdb) frame 6 #6 0x08049403 in wut () (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 0x080493fe <+14>: call 0x8049220 => 0x08049403 <+19>: add $0x10,%esp 0x08049406 <+22>: cmpl $0x65727545,(%eax) <- intéressant 0x0804940c <+28>: jne 0x8049433 0x0804940e <+30>: cmpl $0x21614b,0x4(%eax) <- et encore interessant 0x08049415 <+37>: mov %eax,%ebx 0x08049417 <+39>: jne 0x8049433 0x08049419 <+41>: sub $0xc,%esp 0x0804941c <+44>: push $0x804a424 0x08049421 <+49>: call 0x8049060 0x08049426 <+54>: mov %ebx,(%esp) 0x08049429 <+57>: call 0x8049050 0x0804942e <+62>: add $0x18,%esp 0x08049431 <+65>: pop %ebx 0x08049432 <+66>: ret 0x08049433 <+67>: call 0x8049280 End of assembler dump. ``` 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: ```python #!/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)) ``` La seconde série de boucles de ce script sert à remettre les octets dans le bon ordre. En effet les données sont mises sur la pile et chaque élément de 4 octets doit être inversé. Le mot de passe est `EureKa!` ## Level 4 comme pour le niveau 3, il faut utiliser la technique du `ctrl+c` pour utiliser `bt`: ``` ... #5 0x08049248 in r () #6 0x080494e3 in aa () #7 0x080490d7 in main () ``` C'est la fonction `aa()` qui appelle `r()` (fonction de saicie), Interessons nous à elle en la désassamblant : ``` (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 0x080494de <+14>: call 0x8049220 => 0x080494e3 <+19>: mov %eax,(%esp) 0x080494e6 <+22>: mov %eax,%ebx 0x080494e8 <+24>: call 0x8049080 0x080494ed <+29>: add $0x10,%esp 0x080494f0 <+32>: cmp $0x4,%eax 0x080494f3 <+35>: jne 0x8049524 0x080494f5 <+37>: sub $0x8,%esp 0x080494f8 <+40>: push $0x804adf5 0x080494fd <+45>: push %ebx 0x080494fe <+46>: call 0x8049470 <- cet appel est intéressant non? 0x08049503 <+51>: add $0x10,%esp 0x08049506 <+54>: test %eax,%eax 0x08049508 <+56>: je 0x8049524 0x0804950a <+58>: sub $0xc,%esp 0x0804950d <+61>: push $0x804a4ec 0x08049512 <+66>: call 0x8049060 0x08049517 <+71>: mov %ebx,(%esp) 0x0804951a <+74>: call 0x8049050 0x0804951f <+79>: add $0x18,%esp 0x08049522 <+82>: pop %ebx 0x08049523 <+83>: ret 0x08049524 <+84>: call 0x8049280 End of assembler dump. ``` Cette fonction appelle une autre foncion : `bb()`. C'est elle qui semble se charger 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()` voic le code de cette fonction donné par `objdump` avec l'affichage des sauts : ``` 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 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 804948b: | | b8 01 00 00 00 mov $0x1,%eax 8049490: | | /----- eb 14 jmp 80494a6 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 80494a6: | | \--|-> 0f b6 0c 03 movzbl (%ebx,%eax,1),%ecx 80494aa: | | | 84 c9 test %cl,%cl 80494ac: | | \-- 75 ea jne 8049498 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 ``` Voici un déroullé des instructions principales de cette fonction : * `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* sur cachun des caractères de ce dernier nous permettra de trouver le mot de passe à saisir. On utilise pour celà la propriété suivante du *"ou exclusif"* $$A \oplus B = C \implies C \oplus B = A$$ Là encore un script Python permet de faire le travail pour nous: ```python #!/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: ```bash ./level4.py Level 4 text: what ``` Le mot de pase de ce niveau est donc `what` ## Niveau 5 Ici encore la technique du `ctrl+c` fonctionne bien: ``` [...] Program received signal SIGINT, Interrupt. 0xf7fc7559 in __kernel_vsyscall () (gdb) bt [...] #5 0x08049248 in r () #6 0x08049543 in yay () #7 0x080490dc in main () ``` Donc la fonction intéressante est `yay()`, voici le code assembleur avec les sauts affichés par `objdump` : ``` 08049530 : 8049530: 53 push %ebx 8049531: 83 ec 14 sub $0x14,%esp 8049534: 68 84 a5 04 08 push $0x804a584 8049539: e8 22 fb ff ff call 8049060 804953e: e8 dd fc ff ff call 8049220 8049543: 89 04 24 mov %eax,(%esp) 8049546: 89 c3 mov %eax,%ebx 8049548: e8 33 fb ff ff call 8049080 804954d: 83 c4 10 add $0x10,%esp 8049550: 83 f8 03 cmp $0x3,%eax 8049553: /----- 76 36 jbe 804958b 8049555: | 0f b6 03 movzbl (%ebx),%eax 8049558: | 89 da mov %ebx,%edx 804955a: | 31 c9 xor %ecx,%ecx 804955c: | 84 c0 test %al,%al 804955e: +----- 74 2b je 804958b 8049560: | /-> 83 c2 01 add $0x1,%edx 8049563: | | 31 c1 xor %eax,%ecx 8049565: | | 0f b6 02 movzbl (%edx),%eax 8049568: | | 84 c0 test %al,%al 804956a: | \-- 75 f4 jne 8049560 804956c: | 80 f9 43 cmp $0x43,%cl 804956f: +----- 75 1a jne 804958b 8049571: | 83 ec 0c sub $0xc,%esp 8049574: | 68 24 a6 04 08 push $0x804a624 8049579: | e8 e2 fa ff ff call 8049060 804957e: | 89 1c 24 mov %ebx,(%esp) 8049581: | e8 ca fa ff ff call 8049050 8049586: | 83 c4 18 add $0x18,%esp 8049589: | 5b pop %ebx 804958a: | c3 ret 804958b: \----> e8 f0 fc ff ff call 8049280 ``` Sans rentrer dans les détails instruction par instruction comme fait lors du niveau précédent, cette fonction: * Vérifie que la saisir utilisateur soit supérieure à 3 caractères (instructions `8049548` à `8049553`) * Initialise `%ecx` à 0 via un `xor` sur lui même (instruction `804955a`). * Boucle sur les caractères contenus dans la chaine saisie par l'utilisateur et réalise un `xor` de celui-ci avec `%ecx`. Le résultat de cette opération est stockée dans `%ecx` (instructions `8049555` à `804956a`). * Cette boucle s'arrete lorsque le caractère fin de chaine (`\0`) est trouvé (instruction `8049568`) * Compare `%ecx` à `0x43`, s'il y a égalité alors le programme continue, sinon la fonction `f()` est appelée, ammenant la mauvaise fin. Cat algorithme de chiffrement est vraiment faible, quelques ligne en Python permettent de générer des mots de passes valides: ```python #!/bin/env python3 import sys password = sys.argv[1] if len(password) < 3: print('Minimal password size: 3 char, get {}'.format(len(password))) sys.exit(1) xor=0 target=0x43 for letter in password: xor=xor^ord(letter) last_letter=xor^target print("here is your password: {}{}".format(password,chr(last_letter))) ``` L'utilisation est simple, on donne un mot de passe et il ajoute le caractère manquant pour arriver à `0x43`: ``` ./level5.py Toto here is your password: Totoc ```