IOLI Crackme
RetourDans ce billet, un peu de reverse engineering avec les challenges IOLI. Ces challenges se divisent en 3 catégories :
- bin-linux avec des exécutables ELF32
- Bin-win32 avec des exécutables PE32 x86
- bin_pocketPC avec des exécutables PE32 ARM
Je vais ici débuter par la partie Linux.
Pourquoi ? Parce que je suis véritablement nul dans le domaine du reverse engineering. J’ai donc trouvé plusieurs livres sur l’assembleur dont celui ci.
Ce livre écrit par Dennis Yurichev offre une base étonnamment complète sur le reverse engineering pour les machines ARM, x86 et MIPS (j’en avais déjà parlé dans un billet précédent). Pour ma part j’ai simplement dévoré la partie x86 car je suis débutant dans le domaine du reverse engineering (ou RE).
Avant de débuter, il est tout de même recommandé d’avoir de bonnes bases en langage de “bas niveau”, ici le C.
Note : Je parle de bas niveau en rapport aux langages modernes dont le paradigme ainsi que le typage est bien plus flexibles et accessibles que ceux du C.
Au cas où vous avez quelques lacunes, notre ami Dennis Yurichev propose un rappel de C assez bien écrit à cette adresse .
Pour le reste, je vous recommande chaudement de dévorer les articles de Hackndo qui expliquent avec une simplicitée enfantine le fonctionnement de l’assembleur x86 (32 bits malheureusement).
La description du challenge est la suivante :
======================= IOLI CRACKME v1.2 by pof =======================
[Goal]
Crack the executable files to accept any password
[Hints]
Level 0x00: strings is your friend
Level 0x01: 5274
Level 0x02: ((10*9)+(123*4))^2
Level 0x03: same password as in level 0x02
Level 0x04: 9+6=15 - try 69
Level 0x05: 9+7=16 but one is not enough - try 970
Level 0x06: same algorithm as in 5, but envp LOLO=1
Level 0x07: if you find this level too difficult, try the next
one and then come back
Level 0x08: same algorithm as in 0x06
[Tools]
strings
Displays printable strings in files
included in all linux distributions
http://sources.redhat.com/binutils/
HT Editor
File editor/viewer/analyzer for executables.
sudo apt-get install ht
http://hte.sourceforge.net/
radare
Complete toolchain for working with binary files
http://radare.nopcode.org/
IDA Pro
Interactive multi-processor disassembler and debuger
http://www.datarescue.com/idabase/
========================================================================
[EOF]
Pour ce billet, je souhaite affûter mes talents de reverse engineering, je vais donc éviter au maximum de patcher les exécutables et me concentrer sur la compréhension des algorithmes.
bin-linux
crackme0x00
Depuis Radare2, je lance une analyse complète avec aaa
.
Je liste ensuite les fonctions avec afl
et désassemble le main avec pdf @sym.main
:
Comme nous pouvons le voir, un call vers strcmp est fait à l’adresse 0x08048469
.
Radare2 étant assez futé, il trouve directement la chaîne de caractères str.250382
dont la valeur est :
[0x08048492]> psz @str.250382
250382
Testons :
[0x08048492]> ! ./crackme0x00
IOLI Crackme Level 0x00
Password: 250382
Password OK :)
crackme0x01
Même démarche qu’avant, je désassemble le main :
Je remarque une comparaison avec la valeur ` 0x149a à l’adresse
0x0804842b`:
[0x08048330]> ? 0x149a
hex 0x149a
octal 012232
unit 5.2K
segment 0000:049a
int32 5274
string "\x9a\x14"
binary 0b0001010010011010
fvalue: 5274.0
float: 0.000000f
double: 0.000000
trits 0t21020100
Essayons donc de saisir cette valeur :
IOLI Crackme Level 0x01
Password: 5274
Password OK :)
root@kali:~/IOLI/bin-linux#
crackme0x02
Je reprends le même principe avec une décompilation du main :
La partie comparaison semble un poil plus compliquée, lisons la :
mov dword [local_8h], 0x5a ; 90 8h = 90
mov dword [local_ch], 0x1ec ; 492 ch = 492
mov edx, dword [local_ch] edx = 492
lea eax, dword [local_8h] eax = &8h // eax = addresse de 8h
add dword [eax], edx 8h = 8h + 492 // [eax] est un passage par reference
mov eax, dword [local_8h] eax = 582
imul eax, dword [local_8h] eax = 582 * 582
mov dword [local_ch], eax ch = 338724
mov eax, dword [local_4h] eax = entrée clavier
cmp eax, dword [local_ch] si eax == 338724
La partie la plus complexe pour moi est celle-ci :
lea eax, dword [local_8h]
add dword [eax], edx
La première ligne charge l’adresse de la variable [local_8h]
dans le registre eax
et la deuxième place, dans la case mémoire pointée par eax
, le résultat de l’addition de l’espace mémoire pointée par eax
et la valeur de edx
. ( ici 90 + 492 ).
Les []
permettent de spécifier la valeur contenue dans un espace mémoire, un peu comme le *
permet de déréférencer un pointeur en C. Ainsi mov dword [local_ch], 0x1ec
déplace la valeur 0x1ec
dans la case mémoire pointée par l’adresse local_ch
.
Note : Radare remplace automatiquement les adresses mémoires par des noms pour en simplifier le décodage.
crackme0x03
Pour cette fois, je vais analyser le contenu de la fonction main
:
mov dword [local_8h], 0x5a ; 90 8h = 90
mov dword [local_ch], 0x1ec ; 492 ch = 492
mov edx, dword [local_ch] edx = ch = 492
lea eax, dword [local_8h] eax = &8h
add dword [eax], edx 8h = 8h + 492 = 582
mov eax, dword [local_8h] eax = 582
imul eax, dword [local_8h] eax = 582 * 582
mov dword [local_ch], eax ch = eax = 338724
mov eax, dword [local_ch] eax = ch = 338724
mov dword [local_4h_2], eax 4h_2 = eax = 338724
mov eax, dword [local_4h] eax = 4h
mov dword [esp], eax &esp = eax
call sym.test appel de la fonction test
Le programme fait globalement le même calcul que le précédent mais il rajoute une nouvelle fonction test
Note : J’ai dû lancer un debug avec GDB pour comprendre le fonctionnement de la fonction test. Pour cela j’ai mis des points d’arrêt a l’appel de la fonction test dans le main et à la fin du prologue de cette même fonction. En comparant les valeurs des différents offset vers ebp j’ai découvert que la fonction test était appelée avec les paramètres suivants :
test([val_scanf],338724)
Voici son désassemblage :
push ebp
mov ebp, esp
sub esp, 8
mov eax, dword [arg_8h]
cmp eax, dword [arg_ch]
,=< je 0x804848a
| mov dword [esp], str.Lqydolg_Sdvvzrug ; [0x80485ec:4]=0x6479714c ; "Lqydolg#Sdvvzrug$"
| call sym.shift
| jmp 0x8048496
`-> mov dword [esp], str.Sdvvzrug_RN ; [0x80485fe:4]=0x76766453 ; "Sdvvzrug#RN$$$#=,"
call sym.shift
leave
ret
Pour simplifier voici un pseudo-code :
int test(int a,int b)
{
if(a == b)
{
shift("Sdvvzrug#RN$$$#=,");
}
else
{
shift("Lqydolg#Sdvvzrug$")
}
}
int main(int argc, char **argv)
{
int a;
int b = 90;
int c = 492;
printf("IOLI Crackme Level 0x03\n");
printf("Password: ");
scanf("%d", &a);
b = b + c;
c = b * b;
test(a,338724);
}
Nous pourrions nous arrêter ici car l’entrée clavier est comparée avec 338724. Mais l’objectif est d’apprendre à lire du code assembleur.
La fonction test appelle elle-même une autre fonction, shift
:
push ebp
mov ebp, esp
sub esp, 0x98
mov dword [local_7ch], 0 7ch = 0
<Début de boucle>----------------------------------------------------------------------
mov eax, dword [arg_8h] eax = adresse de la string (argument de la fonction)
mov dword [esp], eax esp = eax
call sym.imp.strlen Taille de la string
cmp dword [local_7ch], eax (eax = 17) == (7ch = 0)
jae 0x8048450 si supérieur ou égal à zéro aller à 0x8048450
lea eax, dword [local_78h] adresse de 78h dans eax
mov edx, eax edx = eax
add edx, dword [local_7ch] edx = edx + 7ch
mov eax, dword [local_7ch] eax = 7ch
add eax, dword [arg_8h] eax = 8h (adresse de la string)
movzx eax, byte [eax] eax = première lettre de la string
sub al, 3 al = al - 3 (al = premier octet eax) décaler la lettre de 3
mov byte [edx], al edx = al (premier octet seulement)
lea eax, dword [local_7ch] charger addr 7ch dans eax
inc dword [eax] incrément de 7ch
jmp 0x8048424 retour à mov eax, dword [arg_8h]
<Fin de boucle>
lea eax, dword [local_78h] charge adresse de 78h dans eax
add eax, dword [local_7ch] eax = eax + valeur de 7ch
mov byte [eax], 0 eax = 0 (premier octet)
lea eax, dword [local_78h] eax = adresse de 78h
mov dword [local_4h], eax 4h = eax = adresse de 78h
mov dword [esp], 0x80485e8 pointeur vers %s pour printf
call sym.imp.printf appel de printf
leave
ret
Fort de nos infos, la fonction shift
décale chaque caractère de 3 valeurs. Ainsi les deux codes étranges de la fonction test
donnent ceci :
Lqydolg#Sdvvzrug$ ===> Invalid Password!
Sdvvzrug#RN$$$#=, ===> Password OK !!! :)
Donc pour résumer le mot de passe est encore une fois 338724
Note : J’ai décidé ici de ne pas exploiter la fonction de debugger de Radare2. J’utiliserais cette fonction dans le prochain crackme .
crackme0x04
Comme pour les fois précédentes, je débute par l’affichage de la fonction main
:
A l’adresse 0x08048559
se trouve un appel à la fonction sym.check
voyons voir à quoi elle ressemble :
Voici un pseudo-code de la fonction check
:
passlen = strlen(password);
if(passlen >= 0)
{
int addition = 0;
for(i=0; i <= passlen ; i++)
{
int val = atoi(password[i]);
if(val + addition == 15)
{
printf("Password OK !");
}
addition += val;
}
printf("Password Incorrect !");
}
else
{
printf("Password Incorrect !")
}
Il faut en effet que les valeurs du mot de passe additionnées donnent 15 :
root@kali:~/Challenges/IOLI# bin-linux/crackme0x04
IOLI Crackme Level 0x04
Password: 555
Password OK!
root@kali:~/Challenges/IOLI# bin-linux/crackme0x04
IOLI Crackme Level 0x04
Password: 12345
Password OK!
root@kali:~/Challenges/IOLI# bin-linux/crackme0x04
IOLI Crackme Level 0x04
Password: 96
Password OK!
root@kali:~/Challenges/IOLI# bin-linux/crackme0x04
IOLI Crackme Level 0x04
Password: 69
Password OK!
crackme0x05
Ici l’algorithme ressemble pas mal à celui que nous avons déjà décortiqué avant :
Il semble que cette fois, il faille avoir un mot de passe dont la somme des chiffres donne 16, néanmoins une nouvelle fonction fait son apparition :
Sans entrer dans les détails comme nous l’avons fait avant, nous allons faire une analyse plus globale du programme en s’aidant des parties remarquables de ce dernier ainsi que de son comportement.
La fonction parell
teste tout simplement si les nombres saisis sont pairs, voici une preuve :
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 745
Password Incorrect!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 754
Password OK!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 88
Password OK!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 853
Password Incorrect!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 835
Password Incorrect!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 8431
Password Incorrect!
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 8134
Password OK!
Note : J’ai l’impression que seul le dernier chiffre est vérifié, car seul les nombres finissant par un chiffre pair sont acceptés.
La partie intéressante de la fonction est la suivante :
mov eax dword[var_4h] <-- Déplacement de la valeur à tester dans EAX
and eax 1 <-- ET logique sur le nombre retourne 0 si pair, 1 si impair
test eax eax <-- équivalent a cmp eax 0
jne 0x800484c6 <-- Jump if Not Equal (ZF=0)
Pourquoi ce code bizarre ? Pour des raisons d’optimisation, le CPU effectue plus rapidement une opération basique comme ET plutôt qu’une comparaison (cmp).
CMP
effectue les opérations suivantes :
- Soustraction des deux opérandes
- Si le résultat est 0 , les valeurs sont égales
- Mise en place du drapeau
ZF
- Mise en place du drapeau
TEST
est plus subtile :
- Et logique sur les deux opérandes
- Si le résultat est 0 , les valeurs sont égales
- Mise en place du drapeau
ZF
- Mise en place du drapeau
Un nombre pair ET 1 donnera toujours zéro
Le drapeau ZF
est ensuite utilisé pour les sauts conditionnels. Dans notre cas, JNE saute vers l’adresse 0x800484c6
si ZF n’est pas levé (ZF=0).
Rappelez-vous que le CPU n’est qu’un tas de portes logiques miniatures
Il faut donc saisir un nombre dont la somme est égale à 16 et dont le dernier chiffre est pair.
crackme0x06
Cette fois, le nombre de fonctions change avec l’apparition de dummy
:
[0x08048400]> afl
0x08048000 3 72 -> 73 fcn.08048000
0x08048048 1 221 fcn.08048048
0x08048125 8 478 -> 479 fcn.08048125
0x08048303 1 93 fcn.08048303
0x08048360 1 23 sym._init
0x08048388 1 6 sym.imp.__libc_start_main
0x08048398 1 6 sym.imp.scanf
0x080483a8 1 6 sym.imp.strlen
0x080483b8 1 6 sym.imp.printf
0x080483c8 1 6 sym.imp.sscanf
0x080483d8 1 6 sym.imp.strncmp
0x080483e8 1 6 sym.imp.exit
0x08048400 1 33 entry0
0x08048424 3 33 fcn.08048424
0x08048450 6 47 sym.__do_global_dtors_aux
0x08048480 4 50 sym.frame_dummy
0x080484b4 6 102 sym.dummy
0x0804851a 7 110 sym.parell
0x08048588 6 127 sym.check
0x08048607 1 99 sym.main
0x08048670 4 99 sym.__libc_csu_init
0x080486e0 1 5 sym.__libc_csu_fini
0x080486e5 1 4 sym.__i686.get_pc_thunk.bx
0x080486f0 4 35 sym.__do_global_ctors_aux
0x08048714 1 26 sym._fini
Étant donné que le programme précédent ressemble beaucoup à celui-ci, je décide de comparer directement l’appel de la fonction check
avec celle du challenge précédent :
Il semble que dans le crackme06 à gauche) deux paramètres sont envoyés à la fonction check
contre seulement 1 dans le crackme05 (à droite).
Étant donné que je n’y comprends pas grand-chose, je vais lancer le programme et voir ce que contiennent ces variables en utilisant le mode debug de Radare2 :
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ radare2 -d crackme0x06
Process with PID 8840 started...
= attach 8840 8840
bin.baddr 0x08048000
Using 0x8048000
asm.bits 32
[0xf7f49c70]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 8840 8840
8840
[0xf7f49c70]> pdf @sym.check
/ (fcn) sym.check 127
| sym.check (int arg_8h, int arg_ch);
| ; var int local_dh @ ebp-0xd
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; arg int arg_8h @ ebp+0x8
| ; arg int arg_ch @ ebp+0xc
| ; var int local_4h_2 @ esp+0x4
| ; var int local_8h_2 @ esp+0x8
| ; CALL XREF from 0x0804865e (sym.main)
| 0x08048588 55 push ebp
| 0x08048589 89e5 mov ebp, esp
| 0x0804858b 83ec28 sub esp, 0x28 ; '('
| 0x0804858e c745f8000000. mov dword [local_8h], 0
| 0x08048595 c745f4000000. mov dword [local_ch], 0
| ; JMP XREF from 0x080485f7 (sym.check)
| .-> 0x0804859c 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8
| : 0x0804859f 890424 mov dword [esp], eax ; const char * s
| : 0x080485a2 e801feffff call sym.imp.strlen
| : 0x080485a7 3945f4 cmp dword [local_ch], eax ; [0x13:4]=-1 ; 19
| ,==< 0x080485aa 734d jae 0x80485f9
| |: 0x080485ac 8b45f4 mov eax, dword [local_ch]
| |: 0x080485af 034508 add eax, dword [arg_8h]
| |: 0x080485b2 0fb600 movzx eax, byte [eax]
| |: 0x080485b5 8845f3 mov byte [local_dh], al
| |: 0x080485b8 8d45fc lea eax, dword [local_4h]
| |: 0x080485bb 89442408 mov dword [local_8h_2], eax
| |: 0x080485bf c74424043d87. mov dword [local_4h_2], 0x804873d ;
| |: 0x080485c7 8d45f3 lea eax, dword [local_dh]
| |: 0x080485ca 890424 mov dword [esp], eax ; ...
| |: 0x080485cd e8f6fdffff call sym.imp.sscanf
| |: 0x080485d2 8b55fc mov edx, dword [local_4h]
| |: 0x080485d5 8d45f8 lea eax, dword [local_8h]
| |: 0x080485d8 0110 add dword [eax], edx
| |: 0x080485da 837df810 cmp dword [local_8h], 0x10 ; [0x10:4]=-1 ; 16
| ,===< 0x080485de 7512 jne 0x80485f2
| ||: 0x080485e0 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| ||: 0x080485e3 89442404 mov dword [local_4h_2], eax
| ||: 0x080485e7 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8
| ||: 0x080485ea 890424 mov dword [esp], eax
| ||: 0x080485ed e828ffffff call sym.parell
| `---> 0x080485f2 8d45f4 lea eax, dword [local_ch]
| |: 0x080485f5 ff00 inc dword [eax]
| |`=< 0x080485f7 eba3 jmp 0x804859c
| `--> 0x080485f9 c704244e8704. mov dword [esp], str.Password_Incorrect ;
| 0x08048600 e8b3fdffff call sym.imp.printf
| 0x08048605 c9 leave
\ 0x08048606 c3 ret
[0xf7f49c70]> dcu 0x080485cd ## Je demande à radare de s'arrêter avant l'appel de sscanf
Continue until 0x080485cd using 1 bpsize
IOLI Crackme Level 0x06
Password: 666
hit breakpoint at: 80485cd
[0x080485cd]> afvd ## J'affiche les valeurs des variables locale à la fonction
var local_8h = 0xffcd5310 0x00000006 .... edx
var local_ch = 0xffcd530c 0x00000001 ....
var arg_8h = 0xffcd5320 0xffcd5340 @S.. eax stack R W 0x363636 (666) --> ascii
var local_dh = 0xffcd530b 0x00000136 6...
var local_4h = 0xffcd5314 0x00000006 .... edx
var arg_ch = 0xffcd5324 0xffcd545c \T.. stack R W 0xffcd625f --> stack R W 0x4c5f5347 (GS_LIB=/home/p0pr0ck5/.fonts) --> ascii
Nous avons ici découvert une chose importante, lorsque nous codons en C sous linux voici à quoi ressemble une fonction main
:
int main(int argc, char **argv, char** envp)
{
*** du code ***
}
Comme nous le constatons, la variable envp
est en fait un pointeur de pointeur ( les deux petites étoiles). Lorsque nous avons affiché les variables courantes de la fonction dummy
nous avons trouvé ceci :
var arg_ch = 0xffcd5324 0xffcd545c \T.. stack R W 0xffcd625f --> stack R W 0x4c5f5347 (GS_LIB=/home/p0pr0ck5/.fonts) --> ascii
Cette variable contient plusieurs adresses mémoires, c’est potentiellement notre pointeur de pointeur envp
qui contient les variables d’environnements, affichons-la durant le debuggage :
:> pxw @0xffcd625f ## Affichage du contenu de ce que pointe arg_ch
0xffcd625f 0x4c5f5347 0x2f3d4249 0x656d6f68 0x7030702f GS_LIB=/home/p0p
0xffcd626f 0x6b633072 0x662e2f35 0x73746e6f 0x45444b00 r0ck5/.fonts.KDE
0xffcd627f 0x4c55465f 0x45535f4c 0x4f495353 0x72743d4e _FULL_SESSION=tr
0xffcd628f 0x4c006575 0x4f435f53 0x53524f4c 0x3d73723d ue.LS_COLORS=rs=
0xffcd629f 0x69643a30 0x3b31303d 0x6c3a3433 0x31303d6e 0:di=01;34:ln=01
0xffcd62af 0x3a36333b 0x303d686d 0x69703a30 0x3b30343d ;36:mh=00:pi=40;
0xffcd62bf 0x733a3333 0x31303d6f 0x3a35333b 0x303d6f64 33:so=01;35:do=0
0xffcd62cf 0x35333b31 0x3d64623a 0x333b3034 0x31303b33 1;35:bd=40;33;01
0xffcd62df 0x3d64633a 0x333b3034 0x31303b33 0x3d726f3a :cd=40;33;01:or=
0xffcd62ef 0x333b3034 0x31303b31 0x3d696d3a 0x733a3030 40;31;01:mi=00:s
0xffcd62ff 0x37333d75 0x3a31343b 0x333d6773 0x33343b30 u=37;41:sg=30;43
0xffcd630f 0x3d61633a 0x343b3033 0x77743a31 0x3b30333d :ca=30;41:tw=30;
0xffcd631f 0x6f3a3234 0x34333d77 0x3a32343b 0x333d7473 42:ow=34;42:st=3
0xffcd632f 0x34343b37 0x3d78653a 0x333b3130 0x2e2a3a32 7;44:ex=01;32:*.
0xffcd633f 0x3d726174 0x333b3130 0x2e2a3a31 0x3d7a6774 tar=01;31:*.tgz=
0xffcd634f 0x333b3130 0x2e2a3a31 0x3d637261 0x333b3130 01;31:*.arc=01;3
0xffcd635f 0x2e2a3a31 0x3d6a7261 0x333b3130 0x2e2a3a31 1:*.arj=01;31:*.
0xffcd636f 0x3d7a6174 0x333b3130 0x2e2a3a31 0x3d61686c taz=01;31:*.lha=
Je continue à debug la fonction dummy
, voici son désassemblage :
/ (fcn) sym.dummy 102
| sym.dummy (int arg_ch);
| ; var int local_8h_2 @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; arg int arg_ch @ ebp+0xc
| ; var int local_4h_2 @ esp+0x4
| ; var int local_8h @ esp+0x8
| ; CALL XREF from 0x08048547 (sym.parell)
| 0x080484b4 55 push ebp
| 0x080484b5 89e5 mov ebp, esp
| 0x080484b7 83ec18 sub esp, 0x18
| 0x080484ba c745fc000000. mov dword [local_4h], 0
| 0x080484c1 8b45fc mov eax, dword [local_4h]
| 0x080484c4 8d1485000000. lea edx, dword [eax*4]
| 0x080484cb 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x080484ce 833c0200 cmp dword [edx + eax], 0
| 0x080484d2 743a je 0x804850e
| 0x080484d4 8b45fc mov eax, dword [local_4h]
| 0x080484d7 8d0c85000000. lea ecx, dword [eax*4]
| 0x080484de 8b550c mov edx, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x080484e1 8d45fc lea eax, dword [local_4h]
| 0x080484e4 ff00 inc dword [eax]
| 0x080484e6 c74424080300. mov dword [local_8h], 3
| 0x080484ee c74424043887. mov dword [local_4h_2], str.LOLO ; "LOLO"
| 0x080484f6 8b0411 mov eax, dword [ecx + edx]
| 0x080484f9 890424 mov dword [esp], eax ; size_t n
| ;-- eip:
| 0x080484fc e8d7feffff call sym.imp.strncmp
| 0x08048501 85c0 test eax, eax
| 0x08048503 75bc jne 0x80484c1
| 0x08048505 c745f8010000. mov dword [local_8h_2], 1
| 0x0804850c eb07 jmp 0x8048515
| 0x0804850e c745f8000000. mov dword [local_8h_2], 0
| ; JMP XREF from 0x0804850c (sym.dummy)
| 0x08048515 8b45f8 mov eax, dword [local_8h_2]
| 0x08048518 c9 leave
\ 0x08048519 c3 ret
Je remarque un inc
, cela montre la présence d’une boucle dont le compteur est local_4h
, Je décide donc de lancer le programme et d’en suivre le fonctionnement dans la fonction dummy
:
radare2 -d ./crackme0x06
[0x080483d0]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[0x080483d0]>pdf @sym.parell
***
Je passe les détails
***
[0x080483d0]> V # passage en mode visuel + 3 x 'p' pour se mettre dans la bonne vue
:> dcu 0x08048547
Continue until 0x08048547 using 1 bpsize
IOLI Crackme Level 0x06
Password: 6666
eip 0x080484fc eflags 1I oeax 0xffffffff
| ,==< 0x080484d2 743a je 0x804850e ;[1]
| |: 0x080484d4 8b45fc mov eax, dword [local_4h]
| |: 0x080484d7 8d0c85000000. lea ecx, dword [eax*4]
| |: 0x080484de 8b550c mov edx, dword [arg_ch] ; [0xc:4]=-1 ; 12
| |: 0x080484e1 8d45fc lea eax, dword [local_4h]
| |: 0x080484e4 ff00 inc dword [eax]
| |: 0x080484e6 c74424080300. mov dword [local_8h], 3
| |: 0x080484ee c74424043887. mov dword [local_4h_2], str.LOLO ; "LOLO"
| |: 0x080484f6 8b0411 mov eax, dword [ecx + edx]
| |: 0x080484f9 890424 mov dword [esp], eax ; size_t n
| |: ;-- eip:
| |: 0x080484fc e8d7feffff call sym.imp.strncmp
| |: 0x08048501 85c0 test eax, eax
| |`=< 0x08048503 75bc jne 0x80484c1 ;[3]
| | 0x08048505 c745f8010000. mov dword [local_8h_2], 1
| |,=< 0x0804850c eb07 jmp 0x8048515 ;[4]
| `--> 0x0804850e c745f8000000. mov dword [local_8h_2], 0
| | ; JMP XREF from 0x0804850c (sym.dummy)
| `-> 0x08048515 8b45f8 mov eax, dword [local_8h_2]
| 0x08048518 c9 leave
\ 0x08048519 c3 ret
Je décide d’afficher l’état de la variable au fur et à mesure des tours de boucles :
## Je demande à radare de continuer jusqu'à la ligne suivant STRCMP
:> dcu 0x08048501
Continue until 0x08048501 using 1 bpsize
hit breakpoint at: 8048501
## J'affiche le contenu de EAX et de la variable arg_ch
:> pxw @eax
0xffd9826d 0x435f534c 0x524f4c4f 0x73723d53 0x643a303d LS_COLORS=rs=0:d
****
:> pxw @0xffd98257
0xffd98257 0x54554c43 0x5f524554 0x4d5f4d49 0x4c55444f CLUTTER_IM_MODUL
****
## Je demande à radare de continuer jusqu'à la ligne suivant STRCMP
:> dcu 0x08048501
Continue until 0x08048501 using 1 bpsize
hit breakpoint at: 8048501
## J'affiche le contenu de EAX et de la variable arg_ch
:> pxw @eax
0xffd9826d 0x435f534c 0x524f4c4f 0x73723d53 0x643a303d LS_COLORS=rs=0:d
****
:> pxw @0xffd98257
0xffd98859 0x5353454c 0x534f4c43 0x752f3d45 0x622f7273 LESSCLOSE=/usr/b
****
Comme nous le voyons, la boucle récupère toutes les variables d’environnement et les compare à LOLO
, testons :
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ export LOLO=1
p0pr0ck5@WORKSTATION:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x06
IOLI Crackme Level 0x06
Password: 88
Password OK!
La partie liée à la chaîne
LOLO
n’est que pure déduction, mais l’appel àstrcmp
laisse à penser que c’est une comparaison entre ces deux éléments.
crackme0x07
Cette fois, le challenge se corse un peu avec des fonctions réécrites portant des noms proches de celles de la libc
. Voici un graphique des appels :
Je décide donc de comprendre le fonctionnement de chaque fonction à l’aide de Radare2 en débutant par sub.strlen_5B9
:
int addition = 0;
for(i=0; i <= passlen ; i++)
{
int val = atoi(password[i]);
addition += val;
}
Même chose que sur les précédents crackmes, nous procédons à l’addition des caractères convertis en int
.
Mais ce n’est pas tout :
## Comparaison de la somme précédente avec 16 (0x10)
0x0804860b 837df810 cmp dword [local_8h], 0x10
## Si non égale, sauter à l'adresse 0x8048623
0x0804860f 7512 jne 0x8048623
## Préparation des arguments
0x08048611 8b450c mov eax, dword [arg_ch]
0x08048614 89442404 mov dword [local_4h_2], eax
0x08048618 8b4508 mov eax, dword [arg_8h]
0x0804861b 890424 mov dword [esp], eax
## Appel de sub.sscanf_542
0x0804861e e81fffffff call sub.sscanf_542
Dans cette partie de code, la valeur additionnée est comparée au chiffre 16. Si la valeur correspond, le code de la fonction sub.sscanf_542
est appelé et appelle directement sub.LOLO_4b4
Une fois dans la fonction, un premier test a lieu :
0x080484e6 c74424080300. mov dword [esp + 8], 3 0x080484ee c7442404a887. mov dword [esp + 4], str.LOLO 0x080484f6 8b0411 mov eax, dword [ecx + edx]
0x080484f9 890424 mov dword [esp], eax
0x080484fc e8d7feffff call sym.imp.strncmp
C’est en fait le même test que la dernière fois qui vérifie la présence de la variable d’environnement LOLO
. Une fois de retour dans sub.sscanf_542
, un autre test qui m’est assez familier :
0x08048585 8b45fc mov eax, dword [ebp - 4] 0x08048588 83e001 and eax, 1 0x0804858b 85c0 test eax, eax 0x0804858d 7521 jne 0x80485b0 ;[8]
C’est le fameux test de parité du nombre. Suite à ce dernier, nous obtenons ceci :
0x08048598 c70424c58704. mov dword [esp], str.Password_OK 0x0804859f e814feffff call sym.imp.printf 0x080485a4 c70424000000. mov dword [esp], 0 0x080485ab e838feffff call sym.imp.exit
C’est simplement le bout de code affichant la validation du mot de passe. Donc pour résumer, il nous faut un mot de passe répondant aux prérequis suivants :
- Composés de chiffres dont l’addition donne 16
- Composé de chiffres pairs
Et pour finir la variable d’environnement LOLO
doit être mise en place. Je teste :
p0pr0ck5@p0pr0ck5-W0rK5t4t10n:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ export LOLO=1
p0pr0ck5@p0pr0ck5-W0rK5t4t10n:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x07
IOLI Crackme Level 0x07
Password: 88
Password OK!
J’ai découvert après coup la raison pour laquelle les fonctions ne retournaient pas des noms cohérents. C’est parce que le binaire est stripped
et que ses symboles ont été retirés :
p0pr0ck5@p0pr0ck5-W0rK5t4t10n:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ rabin2 -I ./crackme0x07
arch x86
binsz 4780
bintype elf bits 32 canary false class ELF32 crypto false endian little havecode true intrp /lib/ld-linux.so.2 lang c linenum false lsyms false machine Intel 80386 maxopsz 16 minopsz 1
nx true
os linux pcalign 0 pic false relocs false
relro partial
rpath NONE
static false
stripped true <------------------------ICI !!!
subsys linux
va true
Crackme0x08
Suite à la lecture rapide de la structure du code, je découvre que le code est exactement le même que dans le challenge précédent. Seule différence, l’exécutable n’est pas stripped
et les symboles sont donc disponibles.
En testant je confirme mon hypothèse :
p0pr0ck5@p0pr0ck5-W0rK5t4t10n:~/Documents/Reverse-eng/IOLI-crackme/bin-linux$ ./crackme0x08
IOLI Crackme Level 0x08
Password: 32
Password Incorrect!
Conclusion
Radare2 est un outil d’une puissance redoutable. Il dispose de fonctionnalités aussi utiles que variées et je compte bien continuer à m’en servir pour le développement d’exploits en remplacement (ou pas) de GDB-peda.
Il existe également une version graphique de Radare2 du nom de Cutter mais elle ne permet pour le moment pas de faire de debug complet. Je préfère donc continuer à utiliser la version CLI.
Sources
Retour