/ reverse

NDH2K16 - lol so obfuscated

Ce challenge est issu de la nuit du hack 2016, dans la catégorie crack me, le but est de retrouver le mot de passe de validation d'un ELF 64 bits.
Le programme vérifie que la chaine passé en paramètre correspond bien au flag.

Jetons un coup d'oeil en profondeur :

radare2 lol_so_obsfucated
>>aa
>>pdf @ main

On y trouve plusieurs choses intéressantes.
Une longue chaîne est hardcodée au début du main

0x00400629    c6042402       mov byte [rsp], 2
0x0040062d    c644240111     mov byte [rsp + 1], 0x11        ; [0x11:1]=0
0x00400632    c64424020a     mov byte [rsp + 2], 0xa         ; [0x2:1]=76
0x00400637    c644240353     mov byte [rsp + 3], 0x53        ; [0x53:1]=0 ; 'S'
0x0040063c    c64424045c     mov byte [rsp + 4], 0x5c        ; [0x5c:1]=0 ; '\'
0x00400641    c644240505     mov byte [rsp + 5], 5           ; [0x5:1]=1
0x00400646    c644240654     mov byte [rsp + 6], 0x54        ; [0x54:1]=0 ; 'T'
0x0040064b    c644240760     mov byte [rsp + 7], 0x60        ; [0x60:1]=248 ; '`'
0x00400650    c64424083b     mov byte [rsp + 8], 0x3b        ; [0x3b:1]=0 ; ';'
0x00400655    c644240971     mov byte [rsp + 9], 0x71        ; [0x71:1]=0 ; 'q'
0x0040065a    c60700         mov byte [rdi], 0
0x0040065d    c644240a61     mov byte [rsp + 0xa], 0x61      ; [0x61:1]=1 ; 'a'
0x00400662    c644240b6c     mov byte [rsp + 0xb], 0x6c      ; [0x6c:1]=0 ; 'l'
0x00400667    c644240c28     mov byte [rsp + 0xc], 0x28      ; [0x28:1]=192 ; '('
0x0040066c    c644240d27     mov byte [rsp + 0xd], 0x27      ; [0x27:1]=0 ; '''
0x00400671    c644240e79     mov byte [rsp + 0xe], 0x79      ; [0x79:1]=0 ; 'y'
0x00400676    c644240f3f     mov byte [rsp + 0xf], 0x3f      ; [0x3f:1]=0 ; '?'
0x0040067b    c644241073     mov byte [rsp + 0x10], 0x73     ; [0x73:1]=0 ; 's'
0x00400680    c644241173     mov byte [rsp + 0x11], 0x73     ; [0x73:1]=0 ; 's'
0x00400685    c64424122a     mov byte [rsp + 0x12], 0x2a     ; [0x2a:1]=0 ; '*'
0x0040068a    c644241361     mov byte [rsp + 0x13], 0x61     ; [0x61:1]=1 ; 'a'
0x0040068f    c644241462     mov byte [rsp + 0x14], 0x62     ; [0x62:1]=0 ; 'b'
0x00400694    c644241501     mov byte [rsp + 0x15], 1        ; [0x15:1]=0
...
...

Une deuxieme chaine hardcodée

0x00400729    bfa8094000     mov edi, str.lwskdhgkjsqnvkjwxchzeUBVWCXKJBNVWXCKJBGGG ; "lwskdhgkjsqnvkjwxchzeUBVWCXKJBNVWXCKJBGGG" @ 0x4009a8

Une fonction "encrypt"

0x0040072e    e81d010000     call sym.encrypt ;sym.encrypt()

et une comparaison qui, en fonction du retour va nous renvoyez "you're right", ou "you're wrong"

0x0040073a    e891feffff     call sym.imp.strcmp ;sym.imp.strcmp()
0x0040073f    85c0           test eax, eax
0x00400741    89c3           mov ebx, eax
0x00400743    750c           jne 0x400751                  
0x00400745    bf97094000     mov edi, str.You_re_right.     ; "You're right." @ 0x400997
0x0040074a    e841feffff     call sym.imp.puts ;sym.imp.puts()
0x0040074f    ebbb           jmp 0x40070c                  
0x00400751    bf89094000     mov edi, str.You_re_wrong.     ; "You're wrong." @ 0x400989

la fonction encrypt prend 2 paramètres :

  • la chaine "lwskdhgkjsqnvkjwxchzeUBVWCXKJBNVWXCKJBGGG"
  • l'input de l'utilisateur

un scénario assez commun, on chiffre l'input de l'utilisateur pour le comparer avec une version chiffrée du flag, il va donc falloir jeter un coup d'oeil de plus près à la fonction encrypt.
la fonction nous renvoie une version chiffré de notre input, en passant en paramètre une suite continu de caractère on peut remarquer quelque chose d'intéressant :

./lol_so_obfuscated aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | grep --color "13 27 9 3 6 15 9"

13 27 9 3 6 15 9 3 8 26 10 5 18 24 19 5 28 30 23 12 8 60 31 40 30 60 5 47 4 39 8 63 9 48 18 56 19 48 22 48 22 27 13 31 21 16 25 31 21 30 12 28 19 4 14 5 19 10 8 1 26 30 42 9 62 8 42 19 57 18 49 30 41 31 38 4 46 5 38 0 38 0 13 27 9 3 6 15 9 3 8 26 10 5 18 24 19 5 28 30 23 12 8 60 31

le chiffrement est cyclique, ce qui me fait tout de suite penser à du xor !
examinons la fonction encrypt :

radare2 lol_so_obfuscated 
[0x00400764]> aa
[0x00400764]> pdf @ sym.encrypt

on y voit que la fonction encrypt va afficher chaque caractère après chiffrement avec __printf_chk

bon, le xor me parait une bonne piste on peut vérifier ! avec notre chaîne "aaaa...." et notre présumée clé "lwskd...."

>>>print ord('a')^ord('l')
13

ok ça match pour le premier caractère ! le xor semble être une bonne piste, cependant...

>>>print ord('a')^ord('w')
22

les ennuis commencent !
ok..c'est ici que toute la magie se fait :

0x00400897    324c1d00       xor cl, byte [rbp + rbx]
0x0040089b    4863d2         movsxd rdx, edx
0x0040089e    41324c1500     xor cl, byte [r13 + rdx]
0x004008a3    884c1d00       mov byte [rbp + rbx], cl

un double xor ? c'est parti pour un peu de debugging avec gdb

gdb lol_so_obfuscated
db-peda$ b *0x00400897
Breakpoint 1 at 0x400897
gdb-peda$ r aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

notre premier xor

=> 0x400897 <encrypt+71>:	xor    cl,BYTE PTR [rbp+rbx*1+0x0]
gdb-peda$ x/x $rbp+$rbx
0x7fffffffea91:	0x6161616161616161
gdb-peda$ print $cl
$2 = 0xd
0xd ^0x61 = 0x6c

ok donc on xor notre caractère avec ...0xd ? 0xd soit 13 en décimal, exactement la valeur de notre précédent xor !
le deuxieme xor

=> 0x40089e <encrypt+78>:	xor    cl,BYTE PTR [r13+rdx*1+0x0]
gdb-peda$ print $cl
$3 = 0x6c  <== le retour de notre premier xor !
gdb-peda$ x/x $r13+$rdx
0x4009a9:	0x77 <=== le caractere "w" de notre clé !

donc pour récapituler :

  1. le premier caractere sera xoré tout simplement
  2. ensuite le programme va xorer chaque caractere avec le caractere trouvé précédemment, et enfin xorer ce résultat avec la clé

notre input a été xoré ! maintenant regardons la partie du programme qui s'occupe de vérifier cette input

=> 0x40073a <main+314>:	call   0x4005d0 <strcmp@plt>
Guessed arguments:
arg[0]: 0x7fffffffea90 --> 0x3090f0603091b0d
arg[1]: 0x7fffffffe640 --> 0x6054055c530a1102

on reconnait :

  1. arg[0] notre chaine xorée en little endian
  2. arg[1] la chaine qui est construite au début du binaire !

donc on a l'algo et le résultat attendu ! un peu de script et ça sera réglé !

key="lwskdhgkjsqnvkjwxchzeUBVWCXKJBNVWXCKJBGGG"
flagxor=[0x2,0x11,0xa,0x53,0x5c,0x5,0x54,0x60,0x3b,0x71,0x61,0x6c,0x28,0x27,0x79,0x3f,0x73,0x73,0x2a,0x61,0x62,0x1,0x25,0x4a,0x79,0x5b,0x36,0x1c,0x67,0x41,0x3c,0x59,0x3a,0x50,0x76,0xe,0x74,0x2,0x27,0x3]

flag="n"
print len(flagxor)
for i in range(1,len(flagxor)):
    flag+=chr(flagxor[i-1]^ord(key[i])^flagxor[i])
    
print flag

ndh2k16_19ac2d414c11f6f9da5a1d3342e304bc