Outils pour utilisateurs

Outils du site


misc:crackme_linux

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

misc:crackme_linux [2011/10/31 15:55] (Version actuelle)
Ligne 1: Ligne 1:
 +C'est un mirroir d'un article très intéressant sur le reversing : [[http://​www.unixgarden.com/​index.php/​programmation/​etude-dun-crackme-sous-linux%C2%A0-tiny-crackme|Étude d'un crackme sous Linux]]
 +
 +Un dossier de MISC a déjà été dédié au reverse engineering. Nicolas Brulez y avait notamment abordé quelques moyens de //​reverser//​ un programme ainsi que différents angles pour protéger un logiciel d'une étude trop simpliste. Nous allons ici étudier un //programme d'​entraînement//,​ un crackme. Nous verrons quelques bases théoriques et surtout pratiques d'une protection logicielle, ainsi que les moyens que nous avons à notre disposition pour les contourner.Dans cet article, nous nous concentrons sur un crackme dont je suis coupable, tiny-crackme,​ qui présente plusieurs aspects pratiques pour une étude, ainsi qu'une taille très réduite (< 800 octets). Nous nous y intéressons tout d'​abord du point de vue d'un reverse engineer, puis nous présentons l'​élaboration dudit crackme. Toute l'​étude est menée sur un PC sous Linux (Debian but who cares ?).
 +===== I. Looking for the key... =====
 +
 +On récupère tout d'​abord le crackme et on le teste :
 +<code bash>
 +kaya$ wget http://​nuxed.org/​code/​challenges/​tiny-crackme &>/​dev/​null
 +kaya$ chmod +x tiny-crackme
 +kaya$ file tiny-crackme
 +tiny-crackme:​ ELF 32-bit LSB executable, Intel 80386, version 1, statically linked, corrupted section header size
 +yanisto@kaya$ ls -l tiny-crackme
 +-rwxr-x--- ​ 1 yanisto yanisto 795 2004-08-29 03:50 tiny-crackme
 +yanisto@kaya$ ./​tiny-crackme
 +      Tiny_crackme - nisto'​s crackme #2
 +      ---------------------------------
 +
 +This one has a particularly small size...
 +Hope u'll get some fun with it.
 +
 +You can join me at yanisto@nuxed.org or on #​secdev@freenode.net
 +
 +Enter the Password :   ​1234567890
 +
 + Wrong password, sorry...
 +
 +yanisto@kaya$ 567890
 +bash: 567890: command not found
 +</​code>​
 +On remarque en premier lieu la toute petite taille de l'​exécutable... 795 octets. À première vue, le mot de passe comporte 4 caractères. On constate en effet que les suivants sont rejetés (cf. la répétition des caractères 567890 ci-dessus).\\
 +Ne connaissant pas le binaire, nous vérifions les symboles et autres informations contenues dans le header ELF. A priori, nous ne devrions pas en trouver beaucoup étant donné la petite taille de l'​exécutable (qui en général se rapporte davantage à un binaire strippé, c'​est-à-dire auquel on a enlevé toutes les informations non nécessaires à l'​exécution,​ symboles et autres). La commande file nous révèle d’autant plus d'​investigations que l'​en-tête ELF a l'air corrompu, d'où un comportement possiblement étrange des différents outils reposant sur la librairie BFD (binary file descriptor) comme gdb (cf. binutils-dev pour plus de détails) ou elfsh (http://​elfsh.devhell.org)
 +
 +<code bash>
 +kaya$ nm tiny-crackme
 +nm: tiny-crackme:​ File format not recognized
 +kaya$ elfls tiny-crackme
 +tiny-crackme:​ warning: invalid section header table offset.
 +tiny-crackme* (Intel 386)
 +Program header table entries: 1 (20 - 40)
 + 0 P rws     ​0 ​  31B 00200000
 +
 +kaya$ elfsh
 +         ​Welcome to The ELF shell 0.51b3 .::.
 +
 +         .::. This software is under the General Public License
 +         .::. Please visit http://​www.gnu.org to know about Free Software
 +
 +[ELFsh-0.51b3]$ load tiny-crackme
 +Segmentation fault
 +kaya$ objdump -x tiny-crackme
 +objdump: tiny-crackme:​ File format not recognized
 +</​code>​
 +Tentons maintenant les méthodes de debug usuelles :
 +<code bash>
 +kaya$ gdb tiny-crackme -q
 +"/​home/​yanisto/​tiny-crackme/​tiny-crackme":​ not in executable format:
 +File format not recognized
 +(gdb) r
 +Starting program:
 +No executable file specified.
 +Use the "​file"​ or "​exec-file"​ command.
 +(gdb) q
 +kaya$ strace ./​tiny-crackme
 +execve("​./​tiny-crackme",​ ["​./​tiny-crackme"​],​ [/* 67 vars */]) = 0
 +write(0, " ​  ​Tiny_crackme - nisto\'​s cra"​...,​ 244  Tiny_crackme - nisto'​s crackme #2
 +      ---------------------------------
 +
 +This one has a particularly small size...
 +Hope u'll get some fun with it.
 +
 +You can join me at yanisto@nuxed.org or on #​secdev@freenode.net
 +
 +Enter the Password :   ) = 244
 +ptrace(PTRACE_TRACEME,​ 0, 0x1, 0)       = -1 EPERM (Operation not permitted)
 +write(0, "Sorry but the process seems to b"..., 52Sorry but the process seems to be traced... Bye...
 +) = 52
 +_exit(0) ​                               = ?
 +</​code>​
 +Comme l'​essai n'est pas plus concluant, tentons d'​examiner d'​éventuelles chaînes de caractères :
 +<code bash>
 +kaya$ strings -a ./​tiny-crackme
 +{T?H
 +KxT?
 +ygg{t?
 +yT?AH
 +</​code>​
 +À ce point-là, on sait déjà que le binaire est :\\
 +
 +  * un ELF dont les en-têtes sont corrompus ;
 +  * crypté (au moins les chaînes de caractères) ;
 +  * protégé contre le traçage ;
 +  * programmé en ASM (de bonnes chances vu la taille de l'​exécutable) ;
 +  * protégé par un mot de passe de 4 caractères.
 +\\
 +Ne disposant pas d'IDA (et de fait pas de son plug-in d'​émulation),​ ni encore de The Perfect Emulator, le seul moyen qui nous reste pour étudier le crackme est le désassemblage.\\
 +La première chose à faire est donc de repérer le point d'​entrée de l'​exécutable :
 +<code bash>
 +kaya$ readelf -l tiny-crackme
 +Elf file type is EXEC (Executable file)
 +Entry point 0x200008
 +There are 1 program headers, starting at offset 32
 +
 +Program Headers:
 +  Type           ​Offset ​    ​VirtAddr ​  ​PhysAddr ​  ​FileSiz MemSiz ​ Flg Align
 +  LOAD           ​0x000000 0x00200000 0x00000001 0x0031b 0x0031b RWE 0x1000
 +</​code>​
 +Voilà donc les informations qu'il nous fallait pour désassembler le programme à partir de la bonne adresse. Le point d'​entrée du programme, c'​est-à-dire la première instruction exécutée se trouvera à l'​adresse mémoire **0x200008**.\\
 +En revanche, le fichier sera, quant à lui, chargé à partir de l'​adresse **0x00200000** (VirtAddr de l'​unique en-tête de programme), soit un décalage de 8 octets (**0x200008** - **0x00200000**) :
 +<code bash>
 + kaya$ ndisasm -V
 +NDISASM version 0.98.38 compiled Apr  6 2004
 +kaya$ ndisasm -au tiny-crackme -o 0x00200008 -e 0x08 |head -5
 +00200008 ​ B32A              mov bl,0x2a
 +0020000A ​ E931000000 ​       jmp 0x200040
 +0020000F ​ 0002              add [edx],al
 +00200011 ​ 0003              add [ebx],al
 +00200013 ​ 0001              add [ecx],al
 +</​code>​
 +La première instruction positionne le registre **BL** à **0x2a**, soit **42**, puis la suivante effectue un saut vers l'​adresse **0x200040**. Allons donc voir à cette adresse :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme -o 0x00200040 -e 0x40 |head -3
 +00200040 ​ E901000000 ​       jmp 0x200046
 +00200045 ​ B0E8              mov al,0xe8
 +00200047 ​ A5                movsd
 +</​code>​
 +En premier lieu, nous remarquons un //saut en milieu d'​instruction//​ : à l'​adresse **0x200046**,​ on ne voit aucune instruction pour le moment (on passe de l'​adresse **0x200045** à **0x200047**). Il s'agit juste d'une astuce utilisée par le code du crackme pour éviter que les instructions soient bien alignées et de fait que le code tombe //tout cuit// lors d'un désassemblage sauvage.\\
 +\\
 +En examinant directement le code situé à l'​adresse **0x200046**,​ on voit alors apparaître une nouvelle instruction :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme -o 0x00200046 -e 0x46 |head -1
 +00200046 ​ E8A5020000 ​       call 0x2002f0
 +kaya$ ndisasm -au tiny-crackme -o 0x2002f0 -e 0x02f0 |head -11
 +002002F0 ​ 90                nop
 +002002F1 ​ B84B002000 ​       mov eax,​0x20004b
 +002002F6 ​ 89C6              mov esi,eax
 +002002F8 ​ 89F7              mov edi,esi
 +002002FA ​ B9A5020000 ​       mov ecx,0x2a5
 +002002FF ​ C1E902 ​           shr ecx,0x2
 +00200302 ​ AD                lodsd
 +00200303 ​ 35F179543F ​       xor eax,​0x3f5479f1
 +00200308 ​ AB                stosd
 +00200309 ​ E2F7              loop 0x200302
 +0020030B ​ C3                ret
 +</​code>​
 +Après une série de sauts, nous nous retrouvons en **0x02002F0** (procédure Call). Ce dernier bloc d'​instructions est caractéristique d'un déchiffrement de code/​données contenu dans le binaire lui-même. En effet, nous voyons une boucle qui lit/​modifie/​réécrit la zone s'​étendant de l'​adresse **0x20004b** à **0x20004b+(0x2a5>>​2) = 0x2000f4**. La modification est par ailleurs toujours la même, c'est un XOR **0x3f5479f1**.\\
 +Une fois ce (premier ?) layer de cryptage passé, le binaire aura modifié **0xA9 dwords**, soit 169*4 = **676 octets**, autant dire qu'il était presque entièrement chiffré.\\
 +Écrivons un petit outil destiné à passer ce layer pour progresser :
 +<file c unciphered_layer1.c>​
 +void uncipher_dw(unsigned int *start, int length, unsigned int key)
 +{
 +    for (int i = 0; i < length; i++)
 +        *start++ ^= key;
 +}
 +
 +void operate(int fd)
 +{
 +    void *img;
 +    struct stat filestat;
 +
 +    fstat(fd, &​filestat);​
 +    img = mmap(0L, filestat.st_size,​ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
 +
 +    /* + 0x4b : la boucle de déchiffrement commence en 0x20004b */
 +    uncipher_dw(img + 0x4b, 0x2a5 >> 2, 0x3f5479f1);​
 +
 +    if (munmap(0L, filestat.st_size) != 0) {
 +        perror("​munmap"​);​
 +        exit - 1;
 +    }
 +}
 +</​file>​
 +
 +<code bash>
 +kaya$ gcc -Wall unciphered_layer1.c -o unciphered_layer1
 +kaya$ cp tiny-crackme tiny-crackme_work
 +kaya$ ./​unciphered_layer1 tiny-crackme_work
 +Applying unciphering process on tiny-crackme_work.
 +</​code>​
 +Regardons maintenant le code qui est apparu après le call de l'​adresse **0x200046**. En effet, on remarque que le déchiffrement commence à l'​adresse **0x20004B**,​ qui correspond exactement à l'​adresse de retour de la procédure, l'​instruction située juste après le call :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work -o 0x00200046 -e 0x46 |head -15
 +00200046 ​ E8A5020000 ​       call 0x2002f0
 +0020004B ​ B89E012000 ​       mov eax,​0x20019e
 +00200050 ​ BBF4000000 ​       mov ebx,0xf4
 +00200055 ​ C1EB02 ​           shr ebx,0x2
 +00200058 ​ 8B1592022000 ​     mov edx,​[0x200292]
 +0020005E ​ E859020000 ​       call 0x2002bc
 +00200063 ​ B99E012000 ​       mov ecx,​0x20019e
 +00200068 ​ BAF4000000 ​       mov edx,0xf4
 +0020006D ​ E828020000 ​       call 0x20029a
 +00200072 ​ B81A000000 ​       mov eax,0x1a
 +00200077 ​ 31C9              xor ecx,ecx
 +00200079 ​ 89CE              mov esi,ecx
 +0020007B ​ BA01000000 ​       mov edx,0x1
 +00200080 ​ CD80              int 0x80
 +00200082 ​ 29C3              sub ebx,eax
 +</​code>​
 +Après déchiffrement,​ nous désassemblons la même adresse que précédemment (le call). On peut se le permettre car le bytecode **E8A5020000** n'est pas modifié par le déchiffrement. Dans l'​absolu,​ il aurait fallu désassembler depuis **0x20004B** seulement, c'​est-à-dire l'​adresse de retour de la procédure de déchiffrement.\\
 +Nous voyons deux appels à une procédure par call puis une interruption système **(int 0x80)** qui admet pour arguments **(eax, ebx, ecx, edx, esi) = (0x1a, 0, 0, 1, 0)**, ce qui correspond à un appel à **ptrace()** (les détails viennent par la suite).
 +Examinons d'​abord la première procédure située en **0x2002bc** ​ :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work -o 0x00200046 -e 0x46 |head -6
 +00200046 ​ E8A5020000 ​       call 0x2002f0
 +0020004B ​ B89E012000 ​       mov eax,​0x20019e
 +00200050 ​ BBF4000000 ​       mov ebx,0xf4
 +00200055 ​ C1EB02 ​           shr ebx,0x2
 +00200058 ​ 8B1592022000 ​     mov edx,​[0x200292]
 +0020005E ​ E859020000 ​       call 0x2002bc
 +
 +kaya$ ndisasm -au tiny-crackme_work -o 0x2002bc -e 0x02bc |head -8
 +002002BC ​ 89C6              mov esi,eax
 +002002BE ​ 89F7              mov edi,esi
 +002002C0 ​ 89D9              mov ecx,ebx
 +002002C2 ​ AD                lodsd
 +002002C3 ​ 31D0              xor eax,edx
 +002002C5 ​ AB                stosd
 +002002C6 ​ E2FA              loop 0x2002c2
 +002002C8 ​ C3                ret
 +
 +kaya$ hexdump tiny-crackme_work -s 0x292 -n 4
 +0000292 c0da beef
 +</​code>​
 +Comme précédemment,​ il s'agit d'un layer de cryptage avec une fonction **xor** de clef **edx = [0x200292]** (i. e. le double word contenu à l'​adresse virtuelle **0x200292**,​ soit **0xbeefc0da**),​ qui s'​exécute sur les adresses de **0x20019e** (valeur de eax mise dans edi et esi) à **0x20019e+(0xf4>>​2)=0x2001a6** (car loop tant que le registre ecx n'est pas nul).\\
 +Nous n'​aurions qu'à adapter le premier programme de déchiffrement,​ mais nous attendons de vérifier qu'il ne s'agit pas de données plutôt que de code, car dans ce cas, il n'est peut-être pas nécessaire de décoder ces données maintenant.\\
 +Poursuivons avec la procédure située en **0x20029a** (**29A**, no comment ;)). Tout d'​abord,​ un saut en pleine instruction (on connaît maintenant le truc). Il suffit de désassembler à l'​adresse en question et tout se passe mieux.
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work -o 0x20029a -e 0x029a |head -3
 +0020029A ​ E901000000 ​       jmp 0x2002a0
 +0020029F ​ B031              mov al,0x31
 +002002A1 ​ C089C3B004CD80 ​   ror byte [ecx+0xcd04b0c3],​0x80
 +
 +kaya$ ndisasm -au tiny-crackme_work -o 0x2002a0 -e 0x02a0 |head -5
 +002002A0 ​ 31C0              xor eax,eax
 +002002A2 ​ 89C3              mov ebx,eax
 +002002A4 ​ B004              mov al,0x4
 +002002A6 ​ CD80              int 0x80
 +002002A8 ​ C3                ret
 +</​code>​
 +La deuxième procédure débute en fait réellement en **0x2002a0** : il s'agit d'une interruption système **(int 0x80)** avec pour arguments **(eax, ebx, ecx, edx) = (4, 0, 0x20019e, 0xf4)**, soit l'​appel système **write()** (cf. /​usr/​include/​asm/​unistd.h).\\
 +On sait par ailleurs que le prototype de la fonction write est : **ssize_t write(int fd, const void *buf, size_t count);**\\
 +Cette procédure affiche le message déchiffré par la première procédure (celle qui déchiffre des octets à partir de **0x20019**). Il s'agit en fait du message d'​invite. Vérifions cela rapidement ​ en modifiant quelques valeurs dans le programme :
 +<code bash>
 +unciphered_layer1.c :
 +kaya$ cp tiny-crackme_work tiny-crackme_work2
 +kaya$ cat unciphered_layer1.c | \
 + sed -e "​s/​uncipher_dw(img.*/​uncipher_dw(img + 0x19e, 0xf4 >> 2, 0xbeefc0da);/​g"​ \
 + > unciphered_layer2.c
 +kaya$ gcc unciphered_layer2.c -o unciphered_layer2
 +kaya$ ./​unciphered_layer2 tiny-crackme_work2
 +Applying unciphering process on tiny-crackme_work2.
 +
 +kaya$ hexdump -C tiny-crackme_work2 -s 0x19e -n 254
 +0000019e ​ 20 20 20 20 20 20 54 69  6e 79 5f 63 72 61 63 6b  |      Tiny_crack|
 +000001ae ​ 6d 65 20 2d 20 6e 69 73  74 6f 27 73 20 63 72 61  |me - nisto'​s cra|
 +000001be ​ 63 6b 6d 65 20 23 32 0a  20 20 20 20 20 20 2d 2d  |ckme #2.      --|
 +000001ce ​ 2d 2d 2d 2d 2d 2d 2d 2d  2d 2d 2d 2d 2d 2d 2d 2d  |----------------|
 +000001de ​ 2d 2d 2d 2d 2d 2d 2d 2d  2d 2d 2d 2d 2d 2d 2d 0a  |---------------.|
 +000001ee ​ 0a 54 68 69 73 20 6f 6e  65 20 68 61 73 20 61 20  |.This one has a |
 +000001fe ​ 70 61 72 74 69 63 75 6c  61 72 6c 79 20 73 6d 61  |particularly sma|
 +0000020e ​ 6c 6c 20 73 69 7a 65 2e  2e 2e 0a 48 6f 70 65 20  |ll size....Hope |
 +0000021e ​ 75 27 6c 6c 20 67 65 74  20 73 6f 6d 65 20 66 75  |u'll get some fu|
 +0000022e ​ 6e 20 77 69 74 68 20 69  74 2e 0a 0a 59 6f 75 20  |n with it...You |
 +0000023e ​ 63 61 6e 20 6a 6f 69 6e  20 6d 65 20 61 74 20 79  |can join me at y|
 +0000024e ​ 61 6e 69 73 74 6f 40 6e  75 78 65 64 2e 6f 72 67  |anisto@nuxed.org|
 +0000025e ​ 20 6f 72 20 6f 6e 20 23  73 65 63 64 65 76 40 66  | or on #secdev@f|
 +0000026e ​ 72 65 65 6e 6f 64 65 2e  6e 65 74 0a 0a 45 6e 74  |reenode.net..Ent|
 +0000027e ​ 65 72 20 74 68 65 20 50  61 73 73 77 6f 72 64 20  |er the Password |
 +0000028e ​ 3a 20 20 20 da c0 ef be  00 00 00 00 e9 01        |:   ​ÚÀï¾....é.|
 +0000029c
 +</​code>​
 +Les deux procédures situées avant l'​appel à **ptrace()** étant maintenant expliquées,​ revenons justement à notre **ptrace()** situé à l'​adresse **0x0200080** :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x00200072 -e 0x72 |head -15
 +00200072 ​ B81A000000 ​       mov eax,0x1a
 +00200077 ​ 31C9              xor ecx,ecx
 +00200079 ​ 89CE              mov esi,ecx
 +0020007B ​ BA01000000 ​       mov edx,0x1
 +00200080 ​ CD80              int 0x80
 +00200082 ​ 29C3              sub ebx,eax
 +00200084 ​ 85C0              test eax,eax
 +00200086 ​ 7411              jz 0x200099
 +00200088 ​ B902012000 ​       mov ecx,​0x200102
 +0020008D ​ B234              mov dl,0x34
 +0020008F ​ E806020000 ​       call 0x20029a
 +00200094 ​ E973020000 ​       jmp 0x20030c
 +00200099 ​ EB01              jmp short 0x20009c
 +0020009B ​ B053              mov al,0x53
 +0020009D ​ B996022000 ​       mov ecx,​0x200296
 +</​code>​
 +L'​appel à **ptrace()** se fait avec les registres **(eax, ebx, ecx, edx, esi) = (0x1a, 0, 0, 1, 0)**. La requête transmise à ptrace est donc **PTRACE_TRACEME**. Cela bloque tous les signaux, excepté **SIGKILL**,​ à destination du processus et un **SIGTRAP** est généré si le processus est //​ptracé()//​ par un processus externe, genre **strace** ou **gdb**, (**cf. ptrace(2)**).\\
 +Un test est effectué sur **eax**, valeur de retour de **ptrace()**,​ pour voir si le programme est tracé. Si eax n'est pas nul, le processus est considéré comme tracé. On appelle alors la procédure d'​affichage (**call 0x20029a**) vue auparavant en lui demandant d'​afficher **0x34** caractères depuis l'​adresse **0x102** :
 +<code bash>
 +kaya$ hexdump -C tiny-crackme_work2 -s 0x102 -n 52
 +00000102 ​ 53 6f 72 72 79 20 62 75  74 20 74 68 65 20 70 72  |Sorry but the pr|
 +00000112 ​ 6f 63 65 73 73 20 73 65  65 6d 73 20 74 6f 20 62  |ocess seems to b|
 +00000122 ​ 65 20 74 72 61 63 65 64  2e 2e 2e 20 42 79 65 2e  |e traced... Bye.|
 +00000132 ​ 2e 2e 0a 00
 +</​code>​
 +Enfin, on saute en **0x20030c**,​ qui nous renvoie encore une fois au milieu d'une instruction :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x0020030c -e 0x30c |head -3
 +0020030C ​ E901000000 ​       jmp 0x200312
 +00200311 ​ 3231                xor dh,[ecx]
 +00200313 ​ C089C3FEC0CD80 ​   ror byte [ecx+0xcdc0fec3],​0x80
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x00200312 -e 0x312 |head -15
 +00200312 ​ 31C0              xor eax,eax
 +00200314 ​ 89C3              mov ebx,eax
 +00200316 ​ FEC0              inc al
 +00200318 ​ CD80              int 0x80
 +0020031A ​ C3                ret
 +</​code>​
 +Cette suite d'​instruction positionne les registres eax et ebx respectivement à 1 et 0, puis déclenche une interruption 80 : cela correspond en fait à l'​appel système **exit() (eax = 1)** avec comme argument **0 (ebx = 0)**. On quitte misérablement le programme.\\
 +Revenons au cas où le processus n'est pas //​ptracé()//​ et que le registre eax vaut alors 0. On saute en **0x200099** puis à l'​intérieur d'une instruction **(0x20009c)**. Notons que le registre ebx reste nul dans ce cas :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x0020009c -e 0x9c |head -11
 +0020009C ​ 53                push ebx
 +0020009D ​ B996022000 ​       mov ecx,​0x200296
 +002000A2 ​ BA04000000 ​       mov edx,0x4
 +002000A7 ​ E8FE010000 ​       call 0x2002aa
 +002000AC ​ E818020000 ​       call 0x2002c9
 +002000B1 ​ 331D96022000 ​     xor ebx,​[0x200296]
 +002000B7 ​ 7413              jz 0x2000cc
 +002000B9 ​ B9E5002000 ​       mov ecx,​0x2000e5
 +002000BE ​ B21D              mov dl,0x1d
 +002000C0 ​ E8D5010000 ​       call 0x20029a
 +002000C5 ​ E942020000 ​       jmp 0x20030c
 +
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x002002aa -e 0x2aa |head -1
 +002002AA ​ E901000000 ​       jmp 0x2002b0
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x002002b0 -e 0x2b0 |head -6
 +002002B0 ​ 31C0              xor eax,eax
 +002002B2 ​ 89C3              mov ebx,eax
 +002002B4 ​ FEC3              inc bl
 +002002B6 ​ B003              mov al,0x3
 +002002B8 ​ CD80              int 0x80
 +002002BA ​ C3                ret
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x2002c9 -e 0x2c9 |head -13
 +002002C9 ​ E903000000 ​       jmp 0x2002d1
 +002002CE ​ B0C8              mov al,0xc8
 +002002D0 ​ 43                inc ebx
 +002002D1 ​ 31C0              xor eax,eax
 +002002D3 ​ 89C3              mov ebx,eax
 +002002D5 ​ B9DF020000 ​       mov ecx,0x2df
 +002002DA ​ C1E902 ​           shr ecx,0x2
 +002002DD ​ BE08002000 ​       mov esi,​0x200008
 +002002E2 ​ AD                lodsd
 +002002E3 ​ 01C3              add ebx,eax
 +002002E5 ​ E2FB              loop 0x2002e2
 +002002E7 ​ 81F36B040855 ​     xor ebx,​0x5508046b
 +002002ED ​ C3                ret
 +</​code>​
 +Ainsi, dans le cas où le processus ne serait pas ptracé, on met ebx sur la pile et nous voyons successivement les branchements suivants : 2 calls, 1 jump conditionnel (jz), un call puis un jump.
 +
 +  * **call 0x2002aa** :
 +**int_80h (3, 0, 0x200296, 4) = read(O, 0x200296, 4)** lit le mot de passe (4 caractères) et le stocke en **0x200296**.\\
 +  * **call 0x2002c9** :
 +fonction de type calcul CRC sur le binaire lui-même, à partir de l'​adresse **0x200008**.\\
 +  * **jz 0x2000cc** :
 +saut si **ebx xor [0x200296] == 0**, donc si **ebx xor password == 0** (voir ci-après).\\
 +  * **call 0x20029a** :
 +**write(0, 0x2000e5, 0x01d)** : on retrouve le message d'​erreur à l'​adresse **0x2000e5** :
 +<code bash>
 +kaya$ hexdump -C tiny-crackme_work2 -s 0x000e5 -n 30
 +  000000e5 ​ 0a 20 57 72 6f 6e 67 20  70 61 73 73 77 6f 72 64 |. Wrong password|
 +  000000f5 ​ 2c 20 73 6f 72 72 79 2e  2e 2e 0a 0a 00 53       |, sorry......S|
 +</​code>​
 +  * **jmp 0x20030c : exit(0)** : comme on connaît déjà.
 +Nous en déduisons qu'il convient d'​effectuer le saut conditionnel. Intéressons-nous donc à cette partie :
 +<code bash>
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x002000CC -e 0xCC |head -7
 +002000CC ​ 5B                pop ebx
 +002000CD ​ 85DB              test ebx,ebx
 +002000CF ​ 75E8              jnz 0x2000b9
 +002000D1 ​ B936012000 ​       mov ecx,​0x200136 ​ ;adresse du texte
 +002000D6 ​ BA68000000 ​       mov edx,​0x68 ​     ;longueur du texte
 +002000DB ​ E8BA010000 ​       call 0x20029a ​    ;​write(0,​ txt, len)
 +002000E0 ​ E927020000 ​       jmp 0x20030c
 +
 +kaya$ ndisasm -au tiny-crackme_work2 -o 0x002000b9 -e 0xb9 |head -4
 +002000B9 ​ B9E5002000 ​       mov ecx,​0x2000e5
 +002000BE ​ B21D              mov dl,0x1d
 +002000C0 ​ E8D5010000 ​       call 0x20029a
 +002000C5 ​ E942020000 ​       jmp 0x20030c
 +</​code>​
 +**ebx** avait été poussé sur la pile et était censé être nul (non ptracé). En le tirant de la pile, il ne change donc pas et reste nul, à moins que le binaire ait été patché plus tôt maladroitement.\\
 +Voyons le texte qui est alors affiché :
 +<code bash>
 +kaya$ hexdump -C tiny-crackme_work2 -s 0x00136 -n 110
 +00000136 ​ 2d 3e 20 53 75 63 63 65  73 73 20 21 21 20 43 6f  |-> Success !! Co|
 +00000146 ​ 6e 67 72 61 74 75 6c 61  74 69 6f 6e 73 2e 2e 2e  |ngratulations...|
 +00000156 ​ 0a 20 20 2d 3e 20 59 6f  75 20 63 61 6e 20 73 65  |.  -> You can se|
 +00000166 ​ 6e 64 20 6d 65 20 79 6f  75 72 20 73 6f 6c 75 74  |nd me your solut|
 +00000176 ​ 69 6f 6e 2f 63 6f 6d 6d  65 6e 74 73 20 61 74 20  |ion/​comments at |
 +00000186 ​ 74 68 65 20 61 62 6f 76  65 20 6d 61 69 6c 20 61  |the above mail a|
 +00000196 ​ 64 64 72 2e 2e 2e 0a 00  20 20 20 20 20 20        |ddr..... ​     |
 +000001a4
 +</​code>​
 +Il s'agit donc de refaire un calcul CRC et de nous assurer que le saut conditionnel soit validé, i. e. (**ebx xor password == 0**).\\
 +Le but de ce crackme n'​étant pas de fournir un keygen rapide, nous n'​optimiserons pas le générateur de clé, mais collerons plutôt à l'​algorithme afin de bien comprendre.
 +<file c keygen.c>​
 +char * alphabet = "​0123456789"​
 + "​abcdefghijklmnopqrstuvwxyz"​
 + "​ABCDEFGHIJKLMNOPQRSTUVWXYZ";​
 +
 +/* Reproduit le calcul de la fonction ​ 002002C9 */
 +void crc_dw(unsigned int *start, int length, unsigned int *key)
 +{
 +    for (int i = 0; i < length; i++)
 + *key += *start++;
 +    *key ^= 0x5508046b;
 +}
 +
 +/* Génère un mot de passe de 4 caractères */
 +unsigned int *put_pass (void *img, unsigned int index);
 +
 +int main(int argc, char **argv)
 +{
 +    void *img;
 +    struct stat filestat;
 +    int length, i, fd;
 +    unsigned int crc = 0, *pass;
 +    char * passwd;
 +    fd = open(argv[1],​ O_RDONLY);
 +
 +    length = strlen(alphabet);​
 +    fstat(fd, &​filestat);​
 +    img = mmap(0L, filestat.st_size,​ PROT_READ | PROT_WRITE,
 +      ​MAP_PRIVATE,​ fd, 0);
 +
 +    passwd = img + 0x0296;
 +
 +    for (i = 0; i < length^4; i++) {
 +     crc = 0;
 +     pass = put_pass(img,​ i);
 +     crc_dw(img + 0x08, 0x2df >> 2, &crc);
 +     if ((crc ^ *pass) == 0)
 +         fprintf(stdout,​ "​Password : 0x%08X <-> %c%c%c%c\n",​ *pass,
 +    ​*passwd,​ *(passwd+1),​ *(passwd+2),​ *(passwd+3));​
 +    }
 +
 +    if (munmap(0L, filestat.st_size) != 0) {
 + perror("​munmap"​);​
 + exit - 1;
 +    }
 +
 +    close(fd);
 +}
 +</​file>​
 +<code bash>
 +kaya$ gcc -Wall keygen.c -o keygen
 +
 +kaya$ ./keygen tiny-crackme_work2
 +Password : 0x6D303062 <-> b00m
 +Password : 0x6F303262 <-> b20o
 +Password : 0x69303462 <-> b40i
 +...
 +</​code>​
 +Et hop... Nous avons une liste de mots de passe valides. Bien entendu, nous aurions pu étendre cela à tous les caractères imprimables mais ce n'​était pas le but. Essayons en un :
 +<code bash>
 +kaya$ ./​tiny-crackme
 +Tiny_crackme - nisto'​s crackme #2
 +---------------------------------
 +...
 +Enter the Password :   b00m
 +-> Success !! Congratulations...
 +  -> You can send me your solution/​comments at the above mail addr...
 +</​code>​
 +Et voilà, le crackme est cracké :) .
 +===== II. Genèse d'un crackme... =====
 +Certes, cracker le crackme est amusant, mais nous continuons maintenant en montrant comment on le construit, c'​est-à-dire comment on met en place les différentes protections,​ par exemple, en modifiant le point d'​entrée du programme ou en désalignant certaines instructions.
 +==== Le programme source initial ====
 +Ici, nous considérons le programme dans son ensemble. Son rôle principal est de vérifier que le mot de passe saisi est valide, rien de plus. La première chose à remarquer concerne le header Elf, construit //à la main// (cf. section //ELF Headers//). On retrouve bien les 8 octets qui seront placés à l'​adresse **0x00200000**,​ ce qui nous donne bien un entry point à l'​adresse virtuelle **0x00200008** (champ e_entry de l'​en-tête,​ égal à _start). De plus, les instructions **mov** et **jmp** présentes dans cette mini procédure fixent des valeurs hexadécimales théoriquement non attribuables dans un en-tête ELF, ce qui induit des erreurs d'​interprétation avec certains outils construits sur BFD.\\
 +Enfin, on prend soin de mettre la valeur des flags du Program Header (**p_flags**) à **LECTURE + ECRITURE + EXECUTION (RWX)**, soit la valeur 4+2+1=7. En effet, on va avoir besoin d'​écrire le mot de passe et les instructions déchiffrées,​ de lire toute la mémoire pour calculer un CRC et quand même d'​exécuter le code.\\
 +On retrouve également dans les sources la protection anti-debug : l'​appel système 0x1a (**ptrace()**) est effectué et selon le résultat, on en déduit ce qu'il en est.
 +<code asm>
 +...
 +; Ce qui suit est issu de tiny.asm (site muppetlabs)
 +; A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
 +; A little bit adapted... ​  ​http://​nuxed.org/​code/​asm/​teensy.html
 +
 +BITS 32
 +                org   ​0x00200000  ​        ; Adresse Virtuelle ELF = 0x0200000
 +; ELF Headers
 +                db    0x7F, "​ELF" ​            ; e_ident
 +                db    1, 1, 1, 0
 +  _start:​  ​        ; Point d'​entrée initial
 +           mov     bl, 42
 +           jmp         ​_secstart ​ ; Saut vers le vrai point d'​entrée
 +
 +                db    0
 +                dw    2                       ; e_type
 +                dd    _start ​                 ; e_entry
 +                dd    phdr - $$               ; e_phoff
 +  phdr:         ​dd ​   1                       ; e_shoff
 +                ...
 +                dd    filesize ​               ; p_memsz
 +                dd    7                       ; p_flags = RWX
 +                dd    0x1000 ​                 ; p_align
 +
 +; Fin des headers ELF, début du programme...
 +
 +_secstart:
 + jmp       ​.unalign
 + db        0xB0                    ; octet pour désaligner
 +.unalign
 + call unstrap
 +
 +_begin:
 + ; Déchriffre puis affiche le message d'​invite
 + mov       eax, invite_msg
 + mov       ebx, invite_msg_len
 + shr       ebx, 2
 + mov       edx, [dword key]
 + call      uncipher
 +        ...
 +
 + ; Teste si le programme est ptracé
 + mov       eax, 0x1a
 + xor       ecx, ecx
 + mov       esi, ecx
 + mov       edx, 1
 + int       ​80h ​ ; appel système ptrace()
 + sub       ebx, eax
 + test      eax, eax
 + jz        get_answer ​         ; Saute si le programme n'est pas tracé
 +
 +traced:
 + mov       ecx, traced_txt ​    ; Procédure invoquée lorsque ptrace est détecté
 + mov       dl, traced_txt_len
 + call      print
 + jmp       exit
 +
 +get_answer:
 + jmp       short .unalign
 + db 0xB0
 +.unalign
 + push          ebx
 + ; Lit la réponse à la question (ie le mot de passe)
 +        ...
 + ; Vérifie la validité du mot de passe
 + call          crc
 + xor           ebx, [dword pass]
 + jz          ​won ​                    ; Si ebx == 0, c'est gagné
 +
 +wrong_pass
 + ; Sinon, affiche un message de mot de passe erroné...
 +        ...
 + jmp           exit
 + db          0x72, 0x36
 +
 +won:
 + ; Dernière chose, on vérifie que la valeur sur la pile (ebx) est bien à 0... pop  ​        ebx
 + test          ebx, ebx
 + jnz           ​wrong_pass
 +        mov          ecx, won_txt
 + mov          edx, won_txt_len
 + call  ​        print
 + jmp          exit
 +
 +; Data
 +...
 +;invite_msg          ​db ​   " ​    ​Tiny_crackme - nisto'​s crackme #2", 10
 +;          ​db ​   " ​     ---------------------------------",​10,​10
 +;                    ...
 +invite_msg ​          ​dd ​ 0x9ecfe0fa,​0x93c2e0fa,​0x93c2edf7,​0x93c2edf7,​0x93c2edf7
 +     ​  ​  ​  ​        ​dd ​ 0x93c2edf7,​0x93c2edf7,​0x93c2edf7,​0x93c2edf7,​0xb4c2edf7
 +                     ...
 +       ​  ​        ​dd ​ 0x9ecfe0e0
 +
 +key   dd          ​0xbeefc0da
 +pass   dd          0
 +
 +; -------------------------------------------------
 +; -------------- ​  ​Standard functions -------------
 +; -------------------------------------------------
 +print:
 + ...
 +read:
 + ...
 +
 +; Déchiffrement du message d'​invite
 +uncipher:
 +        ...
 +
 +; Calcul d'un CRC 32 bits
 +crc:
 +        ...
 +
 +; déchiffre les instructions du programme
 +unstrap:
 + ret  ​        ; Ne pas exécuter maintenant
 + mov           eax, _begin
 +         ...
 + ret
 +
 +; quitte le programme
 +exit:
 +   ...
 +end:
 +   ret
 +</​code>​
 +On remarque bien les techniques supplémentaires utilisées :
 +
 +  * **jmp [short] .unalign / db 0xXX** : les instructions ne sont plus alignées durant un désassemblage sauvage ;
 +  * calcul de CRC, mais nous y reviendrons plus loin ;
 +  * layer de cryptage.
 +
 +Les deux dernières ont un même mode de fonctionnement. Elles utilisent des lectures successives du code (ou de données), y appliquent une fonction (**ADD**, **XOR**...) puis éventuellement remplacent la valeur lue par celle calculée (dans le cas du layer).
 +
 +==== Le chiffrement ====
 +Pour chiffrer, on utilise les instructions **STOSx** et **LODSx**, respectivement commandes d'​écriture et de lecture d'​adresse. Un bon exemple valant mieux qu'un long discours, voici un prototype général de layer de cryptage :
 +<code asm>
 +_layer:
 + mov esi,​ INPUT_ADDRESS
 + mov edi,​ OUTPUT_ADDRESS
 + mov ecx,​ LENGTH ; (/4 if lodsD, /2 if lodsW)
 +.loop
 + lodsd ;
 +Lit la valeur en [INPUT_ADDRESS] et la stocke en EAX.
 + xor eax, 0xdeadbeef ;​
 +pplique une fonction XOR en 32 bits sur EAX (car lodsD)
 + stosd ;
 +Ecrit la valeur de EAX en [OUTPUT_ADDRESS]
 + loop .loop ;​
 +Boucle LENGTH fois.
 +</​code>​
 +En supposant que le flag **DF** soit nul (il précise le sens incr/​décrémentation des adresses pour **lods** et **stos**), dans cette boucle, esi est incrémenté de 4 (car on est en lodsD = 4 octets) à chaque lecture. De la même manière, chaque écriture incrémente **edi** de 4.\\
 +On aura donc lu le code/les données s'​étendant de **INPUT_ADDRESS** à **INPUT_ADDRESS+LENGTH** en appliquant un **'XOR 0xdeadbeef'​** à chaque bloc de 32 bits et le résultat sera stocké de **OUTPUT_ADDRESS** à **OUTPUT_ADDRESS+LENGTH**. Pour un layer de cryptage banal, sans décalage de données, **INPUT_ADDRESS == OUTPUT_ADDRESS**.\\
 +Dans notre programme, cela est appliqué une fois pour le message d'​invite (procédure uncipher avec la clé 0xbeefc0da),​ mais aussi pour l'​ensemble des instructions (procédure **unstrap**).
 +
 +Ce qui donne, une fois le layer de cryptage appliqué
 +
 +La structure du programme étant maintenant en place, il reste à chiffrer les instructions et les données. Pour cela, on crée un outil qui lit l'​exécutable précédent 32 bits par 32 bits à partir de l'​adresse souhaitée, en appliquant à chaque reprise notre opération de chiffrement,​ soit **XOR 0x3f5479f1**. Le résultat obtenu est placé dans le corps de la source et l'​instruction ret est remplacée par un nop (no operation) dans la procédure **unstrap** : elle sera donc désormais exécutée intégralement,​ juste après le point d'​entrée et les instructions seront déchiffrées en mémoire.
 +<code asm>
 +BITS 32
 +                org     ​0x00200000
 +                db      0x7F, "​ELF" ​            ; e_ident
 +                ...                             ; suite normale des sections
 +
 +; Début du code
 +_secstart:
 + jmp .unalign
 + db 0xB0
 +.unalign
 + ; Déchiffrons le code
 + call unstrap
 +_begin:
 + ; Ajout du bytecode chiffré calculé précédemment
 + dd      0x1F55E749, 0x3FA0C2F1, 0xD49579F1, 0xAD41F2F3,
 +        dd      0xD75459F3, 0x3F547BA8, 0x1F55E748, 0x3FA0C3F1,
 +        [...]
 + dd      0x2D972CF9,
 +
 + ; A cause de l'​alignement,​ nous ajoutons cette partie à la main
 + db 0x23
 +
 +        ...
 +
 + ; Routine de déchiffrement
 +        ; XOR 32b de clé aléatoire : 0x3f5479f1
 +unstrap:
 + nop   ; remplace le ret de la version précédente
 + mov eax, _begin
 + mov esi, eax
 + mov edi, esi
 + mov ecx, unstrap-_begin
 + shr ecx, 2
 +.lewp
 + lodsd
 + xor eax, 0x3f5479f1
 + stosd
 + loop .lewp
 + ret
 +
 +exit:
 + ...
 +</​code>​
 +==== Oui mais, comment stocke-t-on le(s) mot(s) de passe ? ====
 +Où et comment a-t-on stocké la (les) clé(s) valide(s) ? Pour comprendre cela, nous allons détailler la procédure de CRC :
 +<code asm>
 + crc:
 +        jmp     ​.unalign
 +        db      0xB0, 0xC8, 0x43
 +.unalign
 +        xor     eax, eax
 +        mov     ebx, eax
 +        mov     ecx, .end-_start
 +        shr     ecx, 2
 +        mov     esi, _start
 +.lewp
 +        lodsd
 +        add     ebx, eax
 +        loop    .lewp
 +.end  ​ ; Fin du pseudo CRC (ebx = X).
 +        xor     ebx, 0x5508046B
 +        ret
 +</​code>​
 +En fait, c'est la valeur **0x5508046B** qui fixe le mot de passe. Voyons de plus près comment obtenir cette valeur en fonction d'un mot de passe que l'on souhaite voir fonctionner.\\
 +Une boucle est tout d'​abord effectuée (nous l'​appellerons //pseudo CRC//) puis le **XOR** est appliqué avec une valeur très particulière.\\
 +En fait, le **CRC** s'​étend de **_start** à **.end**. Ce calcul est effectué sur cette partie, à un instant t postérieur à l'​entrée du mot de passe (le read a lieu avant le call crc) et une fois le message d'​invite affiché (donc déchiffré). Pour le calcul de la valeur **0x5508046B**,​ il faut donc charger le programme en mémoire, déchiffrer l'​invite,​ stocker à sa place le mot de passe voulu (ex : "​**b00m**"​ à l'​adresse **0x200296**),​ puis calculer ce pseudo CRC (noté X par la suite). Une fois cette valeur obtenue, la comparaison sera effectuée avec le mot de passe saisi. On ne peut donc effectuer directement la comparaison (car X contient déjà le mot de passe) : il faut donc " retirer " le mot de passe de X pour obtenir la valeur à mettre dans le code, soit **0x5508046B = X xor b00m**.
 +On peut se permettre de faire cela UNIQUEMENT parce que le pseudo CRC ne parcourt pas la zone où est stockée l'​instruction **XOR ebx, 0x5508046B** (le code est en effet situé après **crc.end**).\\
 +En pratique, j'​avais remplacé en dur (dans le code ASM) les valeurs précédentes (invite en clair, mot de passe,...) dans une copie destinée au calcul du pseudo CRC pour obtenir la valeur désirée.
 +===== Conclusion =====
 +J'​espère que cette incursion dans le monde des crackmes vous aura intéressés. Nous sommes venus à bout de cet exemple à l'aide d'​outils simples (éditeur hexa
 ++ disassembleur + compilateur C).\\
 +L'​étude nous a toutefois été facilitée car le programme a été rédigé en assembleur directement et c'est donc un style assez bref. Cela n'​aurait pas du tout été pareil avec un langage plus haut niveau comme du C : il y aurait eu davantage de " pollution " et d'​appel de procédures de la glib (printf...).\\
 +Si d'​aventure vous souhaitiez m'​envoyer des remarques ou corrections,​ n'​hésitez pas à me contacter.
 +
 +==== Remerciements ====
 +Merci à Tof pour son soutien, Micky pour son sofa, à Belek, May et à Nicolas Brulez.
 +
 +==== Références ====
 +  * [[http://​nuxed.org/​code/​challenges/​|Nisto'​s linux crackmes et sources]]
 +  * [[http://​www.rennes.supelec.fr/​sstic/​articles/​SSTIC03-Vanegue_Roy-Reverse_Intel_ELF.pdf|Julien Vanegue : Reverse Engineering des systèmes ELF/Intel]]
 +  * [[http://​www.fravia.com/​others.htm|Fravia]]
 +  * [[http://​www.muppetlabs.com/​~breadbox/​|Muppetlabs]]
  
misc/crackme_linux.txt · Dernière modification: 2011/10/31 15:55 (modification externe)