Pour l'édition 2014 de la Nuit du Hack, j'ai réalisé une petite épreuve de reverse pour le wargame public, voici la solution que j'ai rédigé.
Le binaire avec les sources est téléchargeable ici : mach2.tar.gz
Le challenger trouve un fichier fat.bin
$ ls -la fat.bin -rwxr-xr-x 1 tlk staff 1524189 25 mai 17:45 fat.bin
Le fichier est executable. L'un des premiers réflexes est de lancer la commande file
et strings
pour essayer d'identifier le format de fichier.
$ file fat.bin fat.bin: compiled Java class data, version 72.0 $ strings fat.bin Nuit du hack 2014 - Mac Pwnage :) sU:/ ypbuk ... __PAGEZERO __TEXT __text __TEXT __symbol_stub __TEXT __stub_helper __TEXT __cstring __TEXT __unwind_info __TEXT __DATA __nl_symbol_ptr __DATA __la_symbol_ptr __DATA __common __DATA __WOW __beautiful __WOW __LINKEDIT /usr/lib/dyld /usr/lib/libSystem.B.dylib @^[] /home/tlk strcmp usage : %s <flag> HOME 50585be4e3159a71c874c590d2ba12ec This is the flag ;) Nope. @dyld_stub_binder @_dlopen @_dlsym @_fclose @_fopen$UNIX2003 @_fwrite$UNIX2003 @_printf @_puts _mh_execute_header Agetenv Estrcmp Jmain Ohandle Tblblbl libSystem.B __mh_execute_header _blblbl _getenv _handle _main _strcmp _dlopen $ xxd fat.bin | head 0000000: cafe babe 0000 0048 004e 7569 7420 6475 .......H.Nuit du 0000010: 2068 6163 6b20 3230 3134 202d 204d 6163 hack 2014 - Mac 0000020: 2050 776e 6167 6520 3a29 2020 2020 2020 Pwnage :) 0000030: 1aaf 1cc0 81bd 6af2 9a73 553a 2f9d 7d03 ......j..sU:/.}. 0000040: c715 6c8d 91dd 1ab8 b8e9 b623 5eaf 3f92 ..l........#^.?. 0000050: 3ee2 8242 b068 3640 0c50 fc7a c3ef 332a >..B.h6@.P.z..3* 0000060: 9379 7062 756b 8b49 5589 0b82 ef2a 681b .ypbuk.IU....*h. 0000070: 9e4b d773 1c66 846a a3fa f6e9 12c2 bde9 .K.s.f.j........ 0000080: 46fb b6bd 2ceb f310 e862 b78c 7883 f8a1 F...,....b..x... 0000090: 217d d2ec 0211 a32e 9456 84be 5130 ebbf !}.......V..Q0..
La commande file
indique que le fichier est une classe java compilée. Pourtant en regardant les chaines de caractères du fichier, on ne peut penser qu'à un fichier executable : printf, puts, main, strcmp, … Après quelques recherches sur internet ( __mh_execute_header par exemple), on se dit que le fichier ne peut être qu'un fichier Mach-O. Une personne qui a un mac sous la main peut facilement le vérifier.
$ ./fat.bin usage : ./fat.bin <flag> $ ./fat.bin toto Nope.
Une seule chose ne semble pas correspondre : le magic number du fichier. Après quelques recherches, on se rend en fait compte que le fichier est un universal binary. Ce même format de fichier semble corrompu, on peut essayer d'écrire un petit script python afin d'essayer d'identifier ce qui cloche.
#!/usr/bin/env python from struct import unpack with open('fat.bin', 'rb') as f: f.seek(0, 2) fsize = f.tell() f.seek(0) magic, narchs = unpack('>II', f.read(8)) print "magic : 0x%08x" % magic print "narchs : 0x%08x" % narchs for i in range(narchs): cputype, cpusubtype, offset, size, align = unpack('>IIIII', f.read(4*5)) print "[+] arch :" print "\t- cputype : 0x%08x" % cputype print "\t- cpusubtype : 0x%08x" % cpusubtype print "\t- offset : 0x%08x" % offset print "\t- size : 0x%08x" % size print "\t- align : 0x%08x" % align print "\t- valid ? : %s" % ("YES" if offset < fsize else "NO")
magic : 0xcafebabe narchs : 0x00000048 [+] arch : - cputype : 0x004e7569 - cpusubtype : 0x74206475 - offset : 0x20686163 - size : 0x6b203230 - align : 0x3134202d - valid ? : NO ... [+] arch : - cputype : 0x00000007 - cpusubtype : 0x00000003 - offset : 0x00001000 - size : 0x000042bc - align : 0x00000000 - valid ? : YES
Une seule architecture semble être valide, autant au niveau des valeurs de cputype
, cpusubtype
que de l'offset
dans le fichier. Essayons d'extraire cette architecture.
$ dd if=fat.bin of=fat-extract.bin bs=1 skip=4096 count=17084 17084+0 records in 17084+0 records out 17084 bytes transferred in 0.079635 secs (214529 bytes/sec) $ file fat-extract.bin fat-extract.bin: Mach-O i386 executable
Nice! Un fichier Mach-O 32 bits ! Let's start IDA !
L'éxecutable contient encore tous les noms de fonction … La fonction main
commence par vérifier si le bon nombre d'argument est passé au programme. Si un flag est bien passé en paramètre, la fonction getenv(“HOME”)
commence par être appelé puis strcmp(argv[1], “50585be4e3159a71c874c590d2ba12ec”)
.
__text:00001E6C mov esi, esp __text:00001E6E mov [esi+4], edx __text:00001E71 mov [esi], eax __text:00001E73 call _strcmp __text:00001E78 cmp eax, 0
50585be4e3159a71c874c590d2ba12ec
semble être le flag, mais il n'en est rien. En vérifiant ce que fait la fonction strcmp
, on se rend compte qu'elle cherche à résoudre une nouvelle fonction strcmp
depuis un handler sur une dylib puis à appeler cette dernière.
__text:00001DC7 mov [esp], eax ; handle __text:00001DCA mov [esp+4], esi ; symbol __text:00001DCE call _dlsym __text:00001DD3 mov [ebp+var_10], eax __text:00001DD6 mov eax, [ebp+var_10] __text:00001DD9 mov ecx, [ebp+var_8] __text:00001DDC mov edx, [ebp+var_C] __text:00001DDF mov [esp], ecx __text:00001DE2 mov [esp+4], edx __text:00001DE6 call eax
En cherchant un peu, on se rend compte qu'en fait ce handler est initialisé dans la fonction getenv
. Cette dernière commence par xorer la section WOWbeautiful
du binaire avec la clef \x4F\xFF\x9F\x13\x20\x37
. Les 64 premiers octets sont utilisés comme nom de fichier, le reste des données est écrit dans ce fichier. Une fois le fichier extrait dans /tmp
, le handler est initialisé avec un appel à la fonction dlopen
.
#!/usr/bin/env python from struct import unpack with open('fat.bin', 'rb') as f: f.seek(0x3000) # __WOW__beautiful section data = f.read(0x10c4) KEY = "\x4f\xff\x9f\x13\x20\x37" ret = "" for i in range(len(data)): ret += chr(ord(data[i]) ^ ord(KEY[i%len(KEY)])) print "name : %s" % ret[:64] with open('fat-dylib.dylib', 'wb') as f: f.write(ret[64:])
$ python fat-lib.py name : /tmp/Ndh2k14.dylib $ file fat-dylib.dylib fat-dylib.dylib: Mach-O i386 dynamically linked shared library
Allons voir ce que fait la fonction strcmp
de cette lib :)
Une fois de plus, le programme semble utiliser un xor. Il xor notre argv[1] avec 4 octets : \xBA\xD0\xC0\xDE
, puis compare la chaine obtenue avec la séquence \x86\x9d\xa1\xbd\x8a\xa3\x90\xa9\xd4\xe3\xa4\xe4\x93\xee
. Il suffit de xorer cette chaine avec la clef de 4 octets pour obtenir le flag !
#!/usr/bin/env python KEY = "\xBA\xD0\xC0\xDE" data = "\x86\x9d\xa1\xbd\x8a\xa3\x90\xa9\xd4\xe3\xa4\xe4\x93\xee" ret = "" for i in range(len(data)): ret += chr(ord(data[i]) ^ ord(KEY[i%len(KEY)])) print "Flag : %s" % ret
$ python wp-dat.py Flag : <Mac0sPwn3d:)> $ ./fat.bin "<Mac0sPwn3d:)>" This is the flag ;)