AlexCTF est un CTF en ligne proposé par le MSP TechClub qui s’est déroulé en février 2017. Catalyst est la 3ième épreuve de la catégorie Reverse.
Sommaire
- Reconnaissance
- Faire sauter la restriction
- Trouver le username
- Trouver password
Reconnaissance
Dans un premier temps on remarque, avec la commande file, que le fichier est un binaire compilé en 64 bits pour linux.
$ file catalyst
catalyst: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3c4646c45da147f57cfa3fe0b9f1022d84fbe85f, stripped
On observe un chargement presque interminable
$ ./catalyst
Mais que fait ce chargement ?
C’est parti désassemblons le binaire ;)
$ gdb catalyst
gdb-peda$ disas main
No symbol table is loaded. Use the "file" command.
Pas de symbole, méthode B trouver l’entrypoint :
gdb-peda$ info file
Symbols from "catalyst".
Local exec file:
`catalyst`, file type elf64-x86-64.
Entry point: 0x400780
[...]
0x0000000000400570 - 0x0000000000400690 is .rela.plt
0x0000000000400690 - 0x00000000004006a7 is .init
0x00000000004006b0 - 0x0000000000400780 is .plt
0x0000000000400780 - 0x0000000000401032 is .text
0x0000000000401034 - 0x000000000040103d is .fini
[...]
Maintenant on peut désassembler de l’entrypoint jusqu’à la fin du code, soit de 0x400780 à 0x401032.
gdb-peda$ disas 0x400780,0x401032
Dump of assembler code from 0x400780 to 0x401032:
xor ebp,ebp
mov r9,rdx
pop rsi
mov rdx,rsp
and rsp,0xfffffffffffffff0
push rax
push rsp
mov r8,0x401030
mov rcx,0x400fc0
mov rdi,0x400d93
call QWORD PTR [rip+0x201846] # 0x601ff0
...
Le programme fait un premier call, c’est l’appel à libc_start_main comme dans tout programme compilé avec gcc. En 64 bits les paramètres sont passés par les registres. Le premier paramètre de cette fonction est l’adresse de la fonction main, placé dans le registre EDI.
gdb-peda$ disas 0x400d93,0x401032
Dump of assembler code from 0x400d93 to 0x401032:
push rbp
mov rbp,rsp
sub rsp,0x20
mov edi,0x3e8
call 0x400720 <malloc@plt>
mov QWORD PTR [rbp-0x10],rax
mov edi,0x3e8
call 0x400720 <malloc@plt>
mov QWORD PTR [rbp-0x18\],rax
mov edi,0x0
call 0x400710 <time@plt>
mov edi,eax
call 0x400700 <srand@plt>
mov edi,0x401088
[...]
mov edi,0x401890
call 0x4006d0 <puts@plt>
mov edi,0x4018b0
mov eax,0x0
call 0x4006f0 <printf@plt>
On remarque 2 appel à malloc qui alloue chacun 1000 octets, les adresses sont respectivement stockée à [ebp-0x10] et [ebp-0x18].
Ensuite le programme initialise le générateur de nombre aléatoire (srand) avec le temps actuel (time).
La fonction puts est appelée plusieurs fois pour dessiner “ALEXCTF” en couleur, puis printf affiche la chaine situé à 0x4018b0, “Loading”.
call 0x400730 <fflush@plt>
mov DWORD PTR [rbp-0x4],0x0
---------jmp 0x400ea5
| ---->call 0x400770 <rand@plt>
| | mov esi,eax
| | mov edx,DWORD PTR [rbp-0x4]
| | mov eax,edx
| | add eax,eax
| | add eax,edx
| | lea ecx,[rax+0x1]
| | mov eax,esi
| | cdq
| | idiv ecx
| | mov eax,edx
| | mov edi,eax
| | call 0x400760 <sleep@plt>
| | mov edi,0x2e
| | call 0x4006c0 <putchar@plt>
| | mov rax,QWORD PTR [rip+0x20122f] # 0x6020c8 <stdout>
| | mov rdi,rax
| | call 0x400730 <fflush@plt>
| | add DWORD PTR [rbp-0x4],0x1
---|---> cmp DWORD PTR [rbp-0x4],0x1d
|---- jle 0x400e67
Voici ci-dessus la boucle de chargement. A chaque tour de boucle le programme tire un nombre aléatoire qu’il divise par une certaine valeur. Le résultat est passé à la fonction sleep et un point (0x2e) est affiché à chaque tour de boucle. Les opérations sont aux moins répétées 29 fois (0x1d). Le programme nous ralentis dans notre travail.
Faire sauter la restriction
Nous allons donc “patcher” l’instruction cmp DWORD[rbp-0x4],0x1d par cmp DWORD[rbp-0x4],0x00 afin de diminuer l’attente.
Ouvrons le binaire avec hexedit et changeons les octets de l’instruction à l’adresse 0x400ea5
$ objdump -h catalyst
catalyst: format de fichier elf64-x86-64
Sections :
Idx Nom Taille VMA LMA Fich off Algn
...
12 .text 000008b2 0000000000400780 0000000000400780 00000780 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
L’offset de l’instruction dans le fichier se calcul de la manière suivante : 0x780 + (0x400ea5 - 0x400780) = 0xea5 Et voici les octets qui compose l’instruction, modifier le 1D en 00.
Trouver le username
Quelques pas plus loin dans le code,
mov edi,0x4018b8 ; "Username: "
mov eax,0x0
call 0x4006f0 printf
mov rax,QWORD PTR [rbp-0x10]
mov rsi,rax
mov edi,0x4018c3 ; "%s"
mov eax,0x0
call 0x400740 __isoc99_scanf
mov edi,0x4018c6 ; "Password: "
mov eax,0x0
call 0x4006f0 printf
mov rax,QWORD PTR [rbp-0x18]
mov rsi,rax
mov edi,0x4018c3 ; "%s"
mov eax,0x0
call 0x400740 __isoc99_scanf
mov edi,0x4018d1 ; "Loggin in"
mov eax,0x0
call 0x4006f0 printf
[...]
Le code ci-dessus gère la saisie du nom d’utilisateur et celle du mot de passe. On retrouve nos deux buffers alloués précédemment [rbp-0x10] et [rbp-0x18] qui sont en paramètre des appels à scanf. [rbp-0x10] fait référence au buffer qui contiendras le nom d’utilisateur et [rbp-0x18] au mot de passe. Ensuite on tombe sur la même boucle de chargement qu’il faudra patcher à nouveau.
Le plus intéressant se trouve à la fin de la fonction main:
mov rax,QWORD PTR [rbp-0x10]
mov rdi,rax
call 0x400c9a
mov rax,QWORD PTR [rbp-0x10]
mov rdi,rax
call 0x400cdd
mov rax,QWORD PTR [rbp-0x10]
mov rdi,rax
call 0x4008f7
mov rdx,QWORD PTR [rbp-0x18]
mov rax,QWORD PTR [rbp-0x10]
mov rsi,rdx
mov rdi,rax
call 0x400977
mov rdx,QWORD PTR [rbp-0x18]
mov rax,QWORD PTR [rbp-0x10]
mov rsi,rdx
mov rdi,rax
call 0x400876
mov eax,0x0
leave
ret
De nombreux appels prennent en paramètre le mot de passe, le nom d’utilisateur ou les deux. La première fonction (0x400c9a) semble contrôler la taille mais rien d’intéressant concernant le nom d’utilisateur. Cependant dans la fonction suivante (0x400cdd), les instructions cmp nous mettent la puce à l’oreille ;).
push rbp
mov rbp,rsp
sub rsp,0x30
mov QWORD PTR [rbp-0x28],rdi ; username
mov rax,QWORD PTR [rbp-0x28]
mov QWORD PTR [rbp-0x8],rax
mov rax,QWORD PTR [rbp-0x8] ; username
mov eax,DWORD PTR [rax] ; username[0:4]
mov eax,eax
mov QWORD PTR [rbp-0x10],rax ; x = username[0:4]
mov rax,QWORD PTR [rbp-0x8] ; username
add rax,0x4 ; username + 4
mov eax,DWORD PTR [rax] ; username[4:8]
mov eax,eax
mov QWORD PTR [rbp-0x18],rax ; y = username[4:8]
mov rax,QWORD PTR [rbp-0x8] ; username
add rax,0x8 ; username + 8
mov eax,DWORD PTR [rax] ; username[8:12]
mov eax,eax
mov QWORD PTR [rbp-0x20],rax ; z = username[8:12]
mov rax,QWORD PTR [rbp-0x10] ; x
sub rax,QWORD PTR [rbp-0x18] ; x - y
mov rdx,rax ; (x - y)
mov rax,QWORD PTR [rbp-0x20] ; z
add rax,rdx ; z + (x - y)
cmp rax,0x5c664b56 ; z + (x - y) == 0x5c664b56
jne 0x400d7c <dead>
mov rdx,QWORD PTR [rbp-0x10] ; x
mov rax,QWORD PTR [rbp-0x20] ; z
add rdx,rax ; x + z
mov rax,rdx ; (x + z)
add rax,rax ; (x + z) * 2
add rdx,rax ; (x + z) * 3
mov rax,QWORD PTR [rbp-0x18] ; y
add rdx,rax ; (x + z) * 3 + y
movabs rax,0x2e700c7b2
cmp rdx,rax ; (x + z) * 3 + y == 0x2e700c7b2
jne 0x400d7c <dead>
mov rax,QWORD PTR [rbp-0x18] ; y
imul rax,QWORD PTR [rbp-0x20] ; y * z
mov rdx,rax
movabs rax,0x32ac30689a6ad314
cmp rdx,rax ; y * z == 0x32ac30689a6ad314
je 0x400d90 <quit>
dead:
mov edi,0x401069 ; "invalid username or password"
call 0x4006d0 <puts@plt>
mov edi,0x0
call 0x400750 <exit@plt>
quit:
nop
leave
ret
Après analyse on se retrouve avec 3 équations à 3 inconnue à résoudre, z + (x - y) = 1550207830 (0x5c664b56) (x + z) * 3 + y = 12465522610 (0x2e700c7b2) y * z = 3651346623716053780 (0x32ac30689a6ad314) Un petit tour sur dcode et on trouve x=1635017059 —> 0x61746163 —> ‘atac’ y=1953724780 —> ‘tsyl’ z=1868915551 —> ‘oec_’ Soit en remettant dans le bon ordre : catalyst_ceo
Trouver le mot de passe
Maintenant on peut s’attaquer à la fonction qui gère le check du mot de passe (0x400977), celle-ci commence par une boucle qui test si les caractères saisis sont bien compris dans certains intervalles, mais le plus intéressant vient après :
push rbp
mov rbp,rsp
push rbx
sub rsp,0x38
mov QWORD PTR [rbp-0x38],rdi ; nom d'utilisateur
mov QWORD PTR [rbp-0x40],rsi ; mot de passe
mov rax,QWORD PTR [rbp-0x38]
mov QWORD PTR [rbp-0x20],rax ; nom d'utilisateur
mov rax,QWORD PTR [rbp-0x40]
mov QWORD PTR [rbp-0x28],rax ; mot de passe
mov DWORD PTR [rbp-0x14],0x0
[... code de la boucle ...]
mov rax,QWORD PTR [rbp-0x20] ; username
mov edx,DWORD PTR [rax] ; username[0:4]
mov rax,QWORD PTR [rbp-0x20] ; username
add rax,0x4 ; username + 4
mov eax,DWORD PTR [rax] ; username[4:8]
add edx,eax ; username[0:4] + username[4:8]
mov rax,QWORD PTR [rbp-0x20] ; username
add rax,0x8 ; username + 8
mov eax,DWORD PTR [rax] ; username[8:12]
add eax,edx ; username[0:4] + username[4:8] + username[8:12]
mov edi,eax
call 0x400700 srand ; srand(username[0:4] + username[4:8] + username[8:12])
mov rax,QWORD PTR [rbp-0x28] ; password
mov ebx,DWORD PTR [rax] ; password[0:4]
call 0x400770 rand ; rand()
sub ebx,eax ; password[0:4] - rand()
mov eax,ebx
cmp eax,0x55eb052a ; password[0:4] - rand() == 0x55eb052a
je 0x400a9b ----------Good------
mov edi,0x401069 |
call 0x4006d0 puts |
mov edi,0x0 |
call 0x400750 exit |
mov rax,QWORD PTR [rbp-0x28] |
add rax,0x4 <------------- ; password + 4
mov ebx,DWORD PTR [rax] ; password[4:8]
call 0x400770 rand ; rand()
sub ebx,eax ; password[4:8] - rand()
mov eax,ebx
cmp eax,0xef76c39 ; password[4:8] - rand() == 0xef76c39
je 0x400ac9 ----------Good------
mov edi,0x401069 |
call 0x4006d0 puts |
mov edi,0x0 |
call 0x400750 exit |
mov rax,QWORD PTR [rbp-0x28] |
add rax,0x8 <---------------- ; password + 8
[...]
La fonction srand va initialiser le générateur de nombre aléatoire avec la graine : x + y + z (valeurs trouvées précédemment), ensuite le programme tire un nombre aléatoire le soustrait avec une partie du mot de passe et compare le résultat avec 0x55eb052a dans le premier cas, 0xef76c39 dans le deuxième etc … Mais comme srand est toujours initialiser avec la même, les nombres tirés par rand sont toujours les mêmes : Le petit programme suivant va nous permettre de trouver la séquence de nombre et en conséquence le mot de passe.
#include
#include
int main(int argc,char** argv)
{
srand((unsigned int)(1953724780 + 1635017059 + 1868915551));
unsigned int r = rand();
r+= 0x55eb052a;
printf("%04.4x\n",r);
r = rand();
r+= 0xef76c39;
printf("%04.4x\n",r);
r = rand();
r+= 0xcc1e2d64;
printf("%04.4x\n",r);
r = rand();
r+= 0xc7b6c6f5;
printf("%04.4x\n",r);
r = rand();
r+= 0x26941bfa;
printf("%04.4x\n",r);
r = rand();
r+= 0x260cf0f3;
printf("%04.4x\n",r);
r = rand();
r+= 0x10d4caef;
printf("%04.4x\n",r);
r = rand();
r+= 0xc666e824;
printf("%04.4x\n",r);
r=rand();
r+= 0xfc89459c;
printf("%04.4x\n",r);
r=rand();
r+= 0x2413073a;
printf("%04.4x\n",r);
return 0;
}
Il ne reste plus qu’a convertir les résultats :
56534c73 VSLs
76345170 v4Qp
4763334b Gc3K
38577957 8WyW
5a694136 ZiA6
77676768 wggh
6a42484c jBHL
4339786d C9xm
56707352 VpsR
6a676747 jggG
Ce qui nous donne sLSVpQ4vK3cGWyW86AiZhggwLHBjmx9CRspVGggj :
Et paf un flag :D