Logmein

Le challenge Logmein est une épreuve proposée par l’équipe RIT Competitive Cybersecurity Club (RC3) lors du CTF du 20 Novembre 2016.

Logmein est un binaire linux à reverse, commençons ;)

Sommaire

  • Reconnaissance du binaire
  • Désassemblage
  • Récupération du flag

Reconnaissance du binaire

La commande file va nous permettre de reconnaître le type de fichier

$ file logmein
logmein: 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]=c8f7fb137d9be24a19eb4f10efc29f7a421578a7, stripped

Nous sommes en face d’un exécutable 64 bits pour Linux, essayons de l’exécuter ;)

$ ./logmein
Welcome to the RC3 secure password guesser.
To continue, you must enter the correct password.
Enter your guess: password
Incorrect password!

Le but du challenge est de retrouver le mot de passe pour s’authentifier. Nous allons utiliser gdb pour examiner le binaire.

$ gdb logmein

Essayons de désassembler la fonction main du programme.

Désassemblage

(gdb) disas main
No symbol table is loaded. Use the "file" command

Pas de chance le binaire n’a pas été compilé avec les symboles, essayons

(gdb) info file
file type elf64-x86-64.
Entry point: 0x400530
0x0000000000400238 - 0x0000000000400254 is .interp
0x0000000000400254 - 0x0000000000400274 is .note.ABI-tag
0x0000000000400274 - 0x0000000000400298 is .note.gnu.build-id
0x0000000000400298 - 0x00000000004002b4 is .gnu.hash
0x00000000004002b8 - 0x0000000000400360 is .dynsym
0x0000000000400360 - 0x00000000004003c4 is .dynstr
0x00000000004003c4 - 0x00000000004003d2 is .gnu.version
0x00000000004003d8 - 0x0000000000400408 is .gnu.version_r
0x0000000000400408 - 0x0000000000400420 is .rela.dyn
0x0000000000400420 - 0x0000000000400498 is .rela.plt
0x0000000000400498 - 0x00000000004004b2 is .init
0x00000000004004c0 - 0x0000000000400520 is .plt
0x0000000000400520 - 0x0000000000400528 is .plt.got
0x0000000000400530 - 0x0000000000400892 is .text
0x0000000000400894 - 0x000000000040089d is .fini
0x00000000004008a0 - 0x0000000000400993 is .rodata
0x0000000000400994 - 0x00000000004009d8 is .eh_frame_hdr
0x00000000004009d8 - 0x0000000000400b0c is .eh_frame
0x0000000000600e10 - 0x0000000000600e18 is .init_array
0x0000000000600e18 - 0x0000000000600e20 is .fini_array
0x0000000000600e20 - 0x0000000000600e28 is .jcr
0x0000000000600e28 - 0x0000000000600ff8 is .dynamic
0x0000000000600ff8 - 0x0000000000601000 is .got
0x0000000000601000 - 0x0000000000601040 is .got.plt
0x0000000000601040 - 0x0000000000601050 is .data
0x0000000000601050 - 0x0000000000601058 is .bss

Cette commande nous permet d’afficher le point d’entrée et les différentes sections du binaire. La section .text contient le code du programme.

La commande disas nous permet de désassembler le code machine entre deux adresses.

(gdb) disas 0x400530,0x400892
xor ebp,ebp
mov r9,rdx
pop rsi
mov rdx,rsp
and rsp,0xfffffffffffffff0
push rax
push rsp
mov r8,0x400890
mov rcx,0x400820
mov rdi,0x400630
call 0x4004f0 <__libc_start_main@plt>

On peut remarquer un appel à la fonction __libc_start_main dont le prototype est décrit ci-dessous :

int __libc_start_main(
	int *(main) (int, char * *, char * *),
	int argc,
	char * * ubp_av,
	void (*init) (void),
	void (*fini) (void),
	void (*rtld_fini) (void),
	void (* stack_end)
);

Cette fonction permet d’initialiser le processus et lance la fonction main avec les bons arguments. En assembleur 64 bits les arguments de fonction sont transmis par les registres. RDI contiendra le premier argument, RSI le deuxième, ensuite RDX, RCX, R8, R9, XMM0–7. A partir de cela on peut trouver l’adresse de la fonction main 0x400630 transmis par RDI.

(gdb) disas 0x400630,0x400892
push   rbp
mov    rbp,rsp
sub    rsp,0x90
movabs rdi,0x4008d8
mov    DWORD PTR [rbp-0x4],0x0
mov    rax,QWORD PTR ds:0x4008b0 ; charge dans rax le QWORD pointé par ds:0x4008b0
mov    QWORD PTR [rbp-0x20],rax  ; place à l'adresse rbp-0x20 le contenu rax
mov    rax,QWORD PTR ds:0x4008b8 ; 8 octets plus loin
mov    QWORD PTR [rbp-0x18],rax  ; 8 octets plus loin 
mov    cx,WORD PTR ds:0x4008c0
mov    WORD PTR [rbp-0x10],cx
mov    rax,QWORD PTR ds:0x4008d0
mov    QWORD PTR [rbp-0x28],rax
mov    DWORD PTR [rbp-0x2c],0x7
mov    al,0x0
call   0x4004e0 <printf@plt>

Examinons les différentes zones mémoires,

(gdb) x/s 0x4008d0
0x4008d0: "harambe"
(gdb) x/s 0x4008c0
0x4008c0: "6"
(gdb) x/s 0x4008b8
0x4008b8: "L*.?+6/46"
(gdb) x/s 0x4008b0
0x4008b0: ":\"AL_RT^L*.?+6/46"
(gdb) x/s 0x4008d8
0x4008d8: "Welcome to the RC3 secure password guesser.\n"

Gardons à l’esprit que nous avons une chaîne certainement chiffrée en 0x4008b0 et une certainement la clé de chiffrement en 0x4008d0. De plus la taille de la clé (7) se trouve en rbp-0x2c. En continuant un peu plus loin nous obtenons

movabs rdi,0x40094b               ; arg1 de scanf "%32s"
lea    rsi,[rbp-0x50]             ; arg2 de scanf soit l'adresse du buffer qui va réceptionner ce que l'on entre , buff
mov    DWORD PTR [rbp-0x64],eax
mov    al,0x0
call   0x400500 <__isoc99_scanf@plt>
lea    rdi,[rbp-0x20]             ; mais dit donc je connais cette zone mémoire ;) (voir au dessus mov QWORD PTR [rbp-0x20],rax), c'est l'adresse de la chaîne chiffrée
lea    rsi,[rbp-0x50]             ; buff
mov    QWORD PTR [rbp-0x70],rdi
mov    rdi,rsi                    ; arg1 de strlen, buff
mov    DWORD PTR [rbp-0x74],eax
call   0x4004d0 <strlen@plt>      ; strlen(buff)
mov    rdi,QWORD PTR [rbp-0x70]
mov    QWORD PTR [rbp-0x80],rax
call   0x4004d0 <strlen@plt>  ; strlen(cipher) 
mov    rsi,QWORD PTR [rbp-0x80]
cmp    rsi,rax
jae    0x400700                   ; strlen(buff) >= strlen(cipher)
call   0x4007c0                   ; bad password et exit

Après cette petite analyse on remarque que le programme compare la chaîne entrée avec la chaîne chiffrée ce qui nous donne une information sur la taille du mot de passe, celui-ci fait 17 caractères (taille de la chaîne chiffrée) ou plus (jae jump if above or equal).

En continuant un peu plus loin on tombe sur une boucle à partir de l’adresse 0x400707, et là ça se corse un petit peu.

mov    DWORD PTR [rbp-0x54],0x0  ; i = 0
condition_boucle:
lea    rdi,[rbp-0x50]            ; buff
movsxd rax,DWORD PTR [rbp-0x54]  ; chargement de i dans rax
mov    QWORD PTR [rbp-0x88],rax  ; compteur1 = i = 0
call   0x4004d0 <strlen@plt>     ; strlen(buff)
mov    rdi,QWORD PTR [rbp-0x88]  ; chargement du compteur dans rdi
cmp    rdi,rax
jae    0x4007ac                  ; si compteur1 >= strlen(buff)  call 0x4007f0 = printf("bad password") et exit 
lea    rdi,[rbp-0x20]            ; adresse de la chaine chiffrée
movsxd rax,DWORD PTR [rbp-0x54]  ; chargement de i dans rax
mov    QWORD PTR [rbp-0x90],rax  ; compteur2 = i = 0
call   0x4004d0  <strlen@plt>    ; strlen(cipher)
mov    rdi,QWORD PTR [rbp-0x90]  ; compteur2
cmp    rdi,rax
jb     0x400754         ; si compteur2 < strlen(cipher) 
call 0x4007c0                    ;  = bad password et exit
interne:
movsxd rax,DWORD PTR [rbp-0x54]      ; charger compteur1 dans rax
mov    cl,BYTE PTR [rbp+rax*1-0x20]  ; cipher[compteur1] 
mov    BYTE PTR [rbp-0x55],cl        ; caractere_1 = cipher[compteur1]
mov    eax,DWORD PTR [rbp-0x54]      ; chargement de i dans eax
cdq
idiv   DWORD PTR [rbp-0x2c]           ; eax % [rbp-0x2c] % 7  eax modulo 7 (taille de "harambe") 
movsxd rsi,edx
mov    cl,BYTE PTR [rbp+rsi*1-0x28]   ; rsi est utilisé comme index, à [rbp-0x28] nous avons "harambe"
mov    BYTE PTR [rbp-0x56],cl         ; caractere_2 = key[compteur]
movsx  edx,BYTE PTR [rbp-0x55]        ; caractere_1
movsx  edi,BYTE PTR [rbp-0x56]        ; caractere_2
xor    edx,edi                        ; caractere_1 XOR caractere_2
mov    cl,dl                          
mov    BYTE PTR [rbp-0x57],cl         ; c = caractere_1 XOR caractere_2
movsxd rsi,DWORD PTR [rbp-0x54]       ; chargement de i dans rsi
movsx  edx,BYTE PTR [rbp+rsi*1-0x50]  ; rbp-0x50  chaine entrée (buff) , mychar = buff[i]
movsx  edi,BYTE PTR [rbp-0x57]        ; c
cmp    edx,edi                       
je     0x400799                       ; si c == mycar
call   0x4007c0                       ; bad password and et exit
continue:
jmp    0x40079e                       
mov    eax,DWORD PTR [rbp-0x54]       ; i
add    eax,0x1                        ; i+1
mov    DWORD PTR [rbp-0x54],eax       ; i = i + 1
jmp    0x400707 

En conclusion cette boucle déchiffre les caractères de la chaîne chiffrée et les compare avec ceux qui composent notre chaîne.

Récupération du flag

Maintenant réalisons le script python qui va nous faire le boulot :

key="harambe"
cipher=":\"AL_RT^L*.?+6/46"
decode=""
for i in range(0,len(cipher)):
     decode+= chr( ord(cipher[i]) ^ ord(key[i%len(key)]) )
print(decode)

Et voilà on peut valider le challenge avec le flag trouvé ;P

$ ./logmein
Welcome to the RC3 secure password guesser.
To continue, you must enter the correct password.
Enter your guess: RC3-2016-XORISGUD
You entered the correct password!
Great job!

Références