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]]