FR EN

Challenge 29 - Reverse 800

<< Challenge 30 - Reverse 500

Convicts service >>

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.

<< Challenge 30 - Reverse 500

Convicts service >>

1 message

  1. Anonyme 18/01/13 15:06

    trop vrai ici.

Votre message