iCTF 2011 > Challenge 29 - Reverse 800
Le challenge 29 était le plus dur, avec le nombre maximum de points. Contrairement aux autres, l'exécutable est un PE Windows (reverse1.exe), mais il n'y a pas de soucis pour le faire tourner sous wine. Comme je n'ai jamais vraiment parlé de Windows au long du site, je vais m'épancher un peu plus sur ce reverse.
Le début de l'exécutable est une fonction main sur terminal classique, le point d'entrée de celle-ci est à 0x401000. Après l'initialisation des variables, une première fonction est appellée à 0x401274 :
push ebx push ecx push edx push esi push 30h pop eax mov edx, fs:[eax] ; edx => PEB xor eax, eax mov edx, [edx+0Ch] ; edx => peb loader data mov edx, [edx+1Ch] mov ecx, edx ; ecx = edx => first initialized module mov esi, [edx+20h] ; esi => BaseDll string pointer mov ebx, [esi] ; ebx = 2 first bytes of ustring or ebx, 61206120h cmp ebx, 6165616Bh jnz short loc_4012A0 mov eax, [edx+8] ; edx => adresse de base du module jmp short loc_4012A6 mov edx, [edx] cmp edx, ecx jnz short loc_401288 ; jump out si pas trouvé pop esi pop edx pop ecx pop ebx retn
Cette fonction prend d'abord fs:[0x30], qui n'est autre que le pointeur vers le PEB (Process Environement Block). Un coup d'oeil aux diverses documentations qu'on peut trouver, et on voit que le champ à PEB+12 pointe vers PEB_LDR_DATA qui contient notamment les listes chaînées des différents modules chargés par l'exécutable.
Vous pouvez jeter un coup d'oeil à cet article d'Ivanlef0u si vous ne trouvez pas de référence pour ces structures, les APIs officielles étant volontairement très incomplètes.
Le champ à PEB_LDR_DATA+0x1c est le pointeur vers la première entrée de la liste des modules par ordre d'initialisation. Ensuite, la fonction parcourt les entrées de cette liste dans edx et fait une vérification sur les 2 premiers bytes de la chaîne de caractère correspondant au nom de la DLL du module. Vous l'aurez compris, cette vérification tombe juste pour une DLL commençant par KE ou ke. Si une telle DLL est trouvée, la fonction retourne son adresse de base, 0 sinon. Le but est donc simplement de trouver l'adresse de base de kernel32.
En continuant dans notre fonction main, on a une lecture de 30 bytes sur l'entrée standard, qui vérifie que les bytes lus sont de l'hexa minuscule. Ensuite, appel d'une nouvelle fonction inconnue 0x40134a :
push ebx push edx xor eax, eax xor edx, edx rdtsc mov ebx, [esp+8+arg_0] mov [ebx], edx mov ebx, [esp+8+arg_4] mov [ebx], eax pop edx pop ebx retn
Un classique de l'anti-debugging, qui effectue l'instruction rdtsc pour retrouver le compteur de cycles. La partie haute se trouve dans edx est placée dans le premier argument, la partie base dans eax est placée dans le second. Il y aura donc probablement un deuxième appel à cette fonction pour vérifier que la différence entre les deux mesures ne dépasse pas un certain seuil (ce qui permet de vérifier qu'il n'y a pas de breakpoint ou action anormale qui fait perdre du temps). On trouve en effet un deuxième appel un peu plus loin dans la fonction main, à 0x40111A :
call getRdtsc pop ecx pop ecx mov eax, [ebp+highrdtsc2] sub eax, [ebp+highrdtsc1]
Une différence est faite entre les bytes de poids fort du rdtsc. A priori, cette différence doit donc être nulle et il faudra patcher / modifier les valeurs retournées. Cette vérification protège donc le code entre les deux rdtsc du debug, il doit donc y avoir quelque chose d'intéressant. Immédiatement après le premier appel, on a en effet un autre appel de fonction inconnue 0x4012AB. Pas mal de paramètres sont pushés avant l'appel : GetProcAddress, LoadLibraryW, la valeur de retour de getKernel32, et 0xa36dc676.
Quand on rencontre une fonction plus compliquée et légèrement obfusquée comme ça, le mieux reste d'essayer de comprendre le résultat sans aller dans le détail. Avec les paramètres, on se doute que la fonction veut avant tout trouver une adresse intéressante en mémoire. On place donc un breakpoint et on voit que l'adresse retournée se trouve dans kernel32.dll, on peut donc analyser le code qui s'y trouve :
push ebp mov eax, large fs:18h mov eax, [eax+30h] mov ebp, esp pop ebp movzx eax, byte ptr [eax+2] retn
N'importe qui a déjà touché à de l'anti-debugging Windows reconnait assez vite la fonction : IsDebuggerPresent(). Elle cherche l'adresse du TIB à fs:0x18, en déduit l'adresse du PEB à fs:[0x30] et retourne la valeur du champ isBeingDebugged. Dans le cas où on n'avait pas d'idée de ce que représentait cette adresse, on peut utiliser les symboles de WinDbg qui aide beaucoup dès qu'on rentre dans les API Windows. Par la suite, il nous suffit de mettre un breakpoint à la fin de IsDebuggerPresent() et de mettre eax à 0.
La fonction se termine ensuite à 0x40114a par un push eax, ret, ce qui nous emmène à 0x40F1B0 où nous exécutons entre autre les snippets suivants :
movz eax, 11h jmp short loc_40F1BA mov eax, fs:[eax+7] cmp eax, offset unk_400000 jnz short loc_40F1C7 mov ebx, 12h mov eax, [eax+ebx+1Eh] cmp eax, 200h jnz short loc_40F1E5 mov ebx, 7 movzx eax, byte ptr [eax+2]
Même si légèrement obfusqué, on reconnait encore une suite fs:[0x18] + 0x30 + 2 qui va rechercher la valeur de isBeingDebugged directement. Il faut donc de nouveau nettoyer eax ici. On revient ensuite dans la suite de la fonction main à 0x40144b, qui va essentiellement effectuer un strtoul byte à byte sur la clé entrée, mettre le tout sur la pile et appeller la fonction 0x4042ED qui elle même appelle 0x4043E5 :
pop esi mov ebx, esi add esi, 62h call sub_40433E push esi push edi mov esi, [ebp+8] lea edi, [ebp-18h] mov ecx, 18h rep movsb pop edi pop esi mov eax, [ebp+0Ch] push eax lea eax, [ebp-18h] push eax call loc_404357 add esp, 8 call sub_40433E pop esi pop edx pop ecx pop ebx mov esp, ebp pop ebp retn
On est en présence de deux sous-fonctions, 0x40433E qui est appellée au début et à la fin et 0x404357 qui est appellée au milieu. En regardant la première :
xor ecx, ecx mov eax, esi mov edx, ecx and edx, 0Fh mov dl, [ebx+edx] xor [eax+ecx], dl inc ecx cmp ecx, 0A41Bh jl short loc_404342 retn
On a une boucle xor de base et on voit donc que la fonction parent 0x4043E5 fait basiquement de l'unpack / execute / repack, ce qui signifie qu'il n'est pas possible de se servir d'un dump mémoire à la fin de l'exécution. En allant voir la fonction unpackée, on voit qu'elle contient simplement des pushs et un call vers une nouvelle fonction d'unpacking exactement semblable. En premier lieu, je me suis dit que j'allais donc mettre un hardware breakpoint sur le buffer qui contient notre clé pour voir quand il est utilisé et je me rends compte que l'instruction rep movsb que l'on voit au milieu de la fonction unpack/repack transmet notre buffer de fonction en fonction.
Au bout de la 30e fonction en pas à pas, on se dit que ça ne va pas être possible, d'autant que le segment qui contient ces fonctions est assez grand. Comme les fonctions sont semblables, on se dit qu'on peut trouver un motif à exploiter de manière intelligente, et c'est le cas : toutes les fonctions exécutées sont distantes d'exactement 113 bytes.
On essaye donc d'approximer le nombre de fonctions à unpacker. La première fonction est à 0x404357, on mets donc un hardware breakpoint à la 100e, 200e, 300e et 400e fonction unpackée (respectivement 0x406f7b, 0x409b9f, 0x40c7c7 et 0x40f3e7). Il est important d'utiliser un hardware breakpoint puisque le code est amené à être modifié. A coup de F9, on voit que les deux premiers breakpoints sont bien exécutés mais pas le 3e, on a donc entre 200 et 300 fonctions. On réessaye avec 220, 240, 260 et 280 et on voit qu'on a entre 240 et 260 fonctions. Dernier essai avec 245, 250, 255. On arrive au breakpoint 255 (0x40b3e6) :
mov eax, [ebp+8] cmp dword ptr [eax], 27149A52h jnz short loc_40B427 cmp dword ptr [eax+4], 8124A07h jnz short loc_40B427 cmp dword ptr [eax+8], 451CCAB9h jnz short loc_40B427 cmp dword ptr [eax+0Ch], 91827364h jnz short loc_40B427 cmp dword ptr [eax+10h], 45913FDCh jnz short loc_40B427 cmp dword ptr [eax+14h], 0B1CC6357h jnz short loc_40B427 mov eax, [ebp+0Ch] mov dword ptr [eax], 1 mov esp, ebp pop ebp retn
On n'aura même pas eu à faire quelques fonctions pas à pas car on tombe directement sur la fonction de comparaison de l'entrée et on peut donc facilement en déduire le numéro de banque :
$ wine reverse1.exe Enter key: 529a1427074a1208b9ca1c4564738291dc3f91455763ccb1 Bank account: 2526390575284-60846167886
Comme dernière remarque je dirais que s'amuser avec l'anti-debugging est bien gentil, mais dans une épreuve rapide comme l'iCTF, utiliser des plugins comme IDA Stealth permet de faire exactement la même chose, plus facilement, et sans patcher le code ou ajouter de breakpoints.
trop vrai ici.