== Une solution du Challenge SSTIC 2017 == --[ Introduction Ce rapport est une description des étapes menant à la résolution du Challenge SSTIC 2017. --[ Electronic Flash $ wget http://static.sstic.org/challenge2017/Bittendo_email_leak_by_shadowbrokers.zip $ unzip Bittendo_email_leak_by_shadowbrokers.zip Archive: Bittendo_email_leak_by_shadowbrokers.zip inflating: Bittendo_email_leak_by_shadowbrokers/firmware.eml Le fichier téléchargé est une archive ZIP contenant un e-mail. L'ouverture de ce dernier peut facilement s'effectuer avec un client mail (ex: thunderbird). Une fois l'e-mail ouvert, des instructions précisent la prochaine étape : l'extraction des données d'une Flash NAND à partir de l'enregistrement d'un analyseur logique. $ cat firmware.eml ... Bonjour, La chaîne de production est prête pour produire votre nouvelle console de jeux. Nous avons programmé un premier modèle avec le firmware que vous nous avez fourni. Avant de lancer la production, merci de valider que le firmware écrit sur ce premier modèle est bien celui que vous souhaitez déployer sur l'ensemble de vos consoles. Pour cela, et afin de ne pas faire d'erreur, nous avons enregistré avec un analyseur logique la programmation de la mémoire flash NAND, vous trouverez en pièce jointe cet enregistrement. Nous utilisons le logiciel sigrok, vous pouvez récupérer le firmware avec le décodeur parallèle sur le bus de données. Nous utilisons uniquement l'interface graphique "pulseview", mais il me semble qu'il y aussi des bindings python et une interface CLI, qui vous permettront certainement d'automatiser l'extraction. Dans l'attente de votre validation. Cordialement, John ... Le premier fichier joint "NAND_pinout.jpg" représente le "pinout" de la NAND. Le second fichier est la trace en elle-même au format sigrok. Le format est plutôt simple. Il s'agit d'une archive ZIP composée des fichiers contenant les données enregistrées par l'analyseur logique, préfixés par "logic-1-" ainsi que des fichiers metadata et version. $ file NAND_FLASH_writes_no_OOB_5MHz.sr NAND_FLASH_writes_no_OOB_5MHz.sr: Zip archive data, at least v2.0 to extract $ unzip NAND_FLASH_writes_no_OOB_5MHz.sr extracting: version inflating: metadata inflating: logic-1-1 inflating: logic-1-2 inflating: logic-1-3 inflating: logic-1-4 inflating: logic-1-5 inflating: logic-1-6 inflating: logic-1-7 ... La lecture du fichier "metadata" nous informe sur le nom des 12 sondes (probes) définies. On note la différence entre le nombre de sondes et la valeur de "total probes" qui est à 16. [global] sigrok version=0.4.0 [device 1] capturefile=logic-1 total probes=16 samplerate=5 MHz probe1=I/O-0 probe2=I/O-1 probe3=I/O-2 probe4=I/O-3 probe5=I/O-4 probe6=I/O-5 probe7=I/O-6 probe8=I/O-7 probe9=CLE probe10=ALE probe11=WE probe12=RE unitsize=2 Les documentations techniques sur les mémoires NAND donnent des informations sur les sigles associés aux différents pins. CLE = Command Latch enable ALE = Address latch enable WE = Write enable RE = Read Enable I/O[7:0] = Data bus Afin de manipuler plus facilement les données, les fichiers logic-1-* ont été concaténés dans l'ordre dans un fichier logic.bin. En affichant le début des données, un alignement sur 16 bits parait évident (ce qui correspond à la variable "total probes" définie dans le fichier de métadata). $ hd logic.bin | head -n 50 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0113e2f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 |................| 0113e300 00 04 00 04 00 04 00 0c 00 0c 00 0c 00 0c 00 0c |................| 0113e310 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c ff 0c ff 0c |................| 0113e320 ff 0d ff 0d ff 09 ff 09 ff 0d ff 0d ff 0c ff 0c |................| 0113e330 ff 0c ff 0c ff 0c ff 0c ff 0c ff 0c ff 0c ff 0c |................| * 0113e370 ff 0c ff 0c 90 0c 90 0c 90 0d 90 0d 90 09 90 0d |................| 0113e380 90 0d 90 0c 90 0c 90 0e 90 0e 00 0e 00 0e 00 0a |................| 0113e390 00 0a 00 0e 00 0e 00 0c 00 0c 00 0c 00 0c 00 0c |................| 0113e3a0 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c |................| * 0113e430 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c 00 0c 53 0c |..............S.| 0113e440 53 0c 53 04 53 04 53 0c 53 0c 53 0c 53 0c 53 0c |S.S.S.S.S.S.S.S.| 0113e450 53 0c 53 04 53 04 53 0c 53 0c 53 0c 54 0c 54 0c |S.S.S.S.S.S.T.T.| La présence d'un LUM est visible juste après : 0113e560 20 04 20 04 20 0c 20 0c 20 0c 4c 0c 4c 0c 4c 04 | . . . . .L.L.L.| 0113e570 4c 04 4c 0c 4c 0c 4c 0c 4c 0c 55 0c 55 0c 55 04 |L.L.L.L.L.U.U.U.| 0113e580 55 04 55 0c 55 0c 55 0c 4d 0c 4d 0c 4d 04 4d 04 |U.U.U.U.M.M.M.M.| ... 0113e640 52 04 52 04 52 0c 52 0c 52 0c 52 0c 7d 0c 7d 0c |R.R.R.R.R.R.}.}.| 0113e650 7d 04 7d 04 7d 0c 7d 0c 7d 0c 00 0c 00 0c 00 04 |}.}.}.}.}.......| LUM{x.g215WiPCR} Chaque échantillon peut être représenté de la manière suivante : 0 8 16 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ |IO0|IO1|IO2|IO3|IO4|IO5|IO6|IO7|CLE|ALE|WE |RE | | | | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ En prenant en compte que l'envoi d'une commande ou adresse s'effectue sur le front montant de WE avec respectivement CLE ou ALE au niveau haut, il est possible d'écrire un code pour extraire la séquence de commande envoyée à la mémoire NAND : cmd=ff cmd=90 cmd=60 cmd=d0 cmd=80 addr a0=0 a1=0 addr=0 block=0 cmd=10 count=8192 cmd=80 addr a0=0 a1=0 addr=0 block=1 cmd=10 count=8192 cmd=80 addr a0=0 a1=0 addr=0 block=2 cmd=10 count=8192 cmd=80 ... cmd=80 addr a0=0 a1=0 addr=0 block=255 cmd=10 count=8192 cmd=60 cmd=d0 cmd=80 addr a0=0 a1=0 addr=8 block=0 cmd=10 count=8192 cmd=80 addr a0=0 a1=0 addr=8 block=1 cmd=10 count=8192 cmd=80 addr a0=0 a1=0 addr=8 block=2 La taille des blocks est de 8192 octets. L'écriture n'étant pas séquentielle, on procède alors à l'extraction des données en prenant en compte l'adresse de chaque block. Le fichier obtenu est une partition FAT. $ file nand.dd nand.dd: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", Media descriptor 0xf8, sectors/track 63, heads 255, sectors 67584 (volumes > 32 MB) , FAT (32 bit), sectors/FAT 520, serial number 0xf0fa166b, unlabeled L'utilisation de fls (sleuthkit) nous indique que cette partition contient les fichiers challenges.zip et challenges.zip.md5. Un dernier fichier "effacé" contient le deuxième LUM de l'épreuve. $ fls nand.dd r/r 5: challenges.zip r/r 8: challenges.zip.md5 r/r * 10: lum.txt v/v 1064195: $MBR v/v 1064196: $FAT1 v/v 1064197: $FAT2 d/d 1064198: $OrphanFiles $ icat nand.dd 10 LUM{AsPBdVWz95y} Une vérification du md5 nous confirme que nous avons le bon fichier. $ icat nand.dd 5 > challenges.zip $ icat nand.dd 8 > challenges.zip.md5 $ md5sum challenges.zip 3977e2084331bb1c52abb115a332c6cc challenges.zip $ cat challenges.zip.md5 3977e2084331bb1c52abb115a332c6cc challenges.zip Une fois le fichier challenges.zip extrait, on découvre une arborescence web incluant une machine virtuelle openrisc émulée en javascript. Cette interface va servir de socle aux trois prochaines étapes comme nous l'indique le fichier README.txt : $ cat README.txt Bonjour, Voici la suite du challenge, les épreuves suivantes se dérouleront dans un navigateur. Pour fonctionner, ce dossier doit être propulsé par un serveur web, par exemple avec python SimpleHTTPServer : $ python -m SimpleHTTPServer 8000 Pour travailler plus confortablement tu peux te connecter à la machine virtuelle fonctionnant dans ton navigateur en SSH. Une passerelle Websocket<->Interface TAP est nécessaire pour fournir du réseau à la machine virtuelle. Le projet «go-websockproxy» [1] permet de créer cette passerelle, il remplit également la fonction de serveur web. Le binaire nécessite des privilèges élevés (les capabilities CAP_NET_RAW et CAP_NET_ADMIN ainsi que le droit d'exécuter la commande "ip" avec ces capabilities) pour créer une interface TAP : sudo go-websockproxy --listen-address=127.0.0.1:8090 --static-directory=$CHALLENGE_DIR --tap-ipv4=10.42.42.1/30 Ensuite il ne reste plus qu'à ouvrir ton navigateur à l'adresse http://127.0.0.1:8090/main.html. Lorsque la machine virtuelle a fini de démarrer, il est possible de se connecter en SSH : ssh user@10.42.42.2, le mot de passe est "sstic". [1] https://github.com/gdm85/go-websockproxy Les fichiers composants les différentes parties sont présents dans le répertoire /challenges de la machine virtuelle. Les deux premières peuvent ne pas être résolues dans l'ordre. Cependant leur résolution est necessaire au déverouillage de la troisième. --[ Don't let him escape L'épreuve se présente sous la forme de trois fichiers. $ cd /challenges/dont_let_him_escape $ ls bpf.py mini-libc.so server.py En les analysant, on se rend vite compte qu'il va falloir comprendre le fonctionnement d'un filtre eBPF contenu dans le fichier server.py. En fonction des paquets reçus ce filtre définit un état qui sera testé par le script python : state = bpf_map_lookup(table_fd) print payload if state == 1: lum = payload print("Good job!, you find a LUM : "+lum) try: sys.path.append("/challenges/tools/") from tee_client import add_lum add_lum(lum) except: pass elif state == 2: key = payload[4:36] print("Good job!, key is : "+key) try: sys.path.append("/challenges/tools/") from tee_client import add_key add_key(key) except: pass L'état un donne un LUM, l'état deux la clé. Le filtre est compressé en zlib et encodé en base64. Quelques commandes python permettent de retrouver le code eBPF : $ hd bpf.dec |head 00000000 bf 16 00 00 00 00 00 00 b7 07 00 00 00 00 00 00 |................| 00000010 28 00 00 00 0c 00 00 00 55 00 d0 04 00 08 00 00 |(.......U.......| 00000020 30 00 00 00 17 00 00 00 55 00 ce 04 11 00 00 00 |0.......U.......| 00000030 28 00 00 00 10 00 00 00 bf 08 00 00 00 00 00 00 |(...............| 00000040 30 00 00 00 0e 00 00 00 bf 09 00 00 00 00 00 00 |0...............| 00000050 28 00 00 00 24 00 00 00 55 00 c8 04 39 05 00 00 |(...$...U...9...| 00000060 18 01 00 00 f8 ff ff ff 00 00 00 00 00 00 00 00 |................| 00000070 0f 18 00 00 00 00 00 00 67 09 00 00 02 00 00 00 |........g.......| 00000080 57 09 00 00 3c 00 00 00 1f 98 00 00 00 00 00 00 |W...<...........| 00000090 7b 9a e8 ff 00 00 00 00 07 09 00 00 16 00 00 00 |{...............| ... A ce stade, il est nécessaire de désassembler le code. Plusieurs outils permettent de désassembler le BPF. L'utilisation d'ubpf, agrémenté de quelques modifications pour décoder les instructions non gérées et afficher un offset absolu pour les sauts nous donne le source : 0 mov r6, r1 1 mov r7, 0x0 2 ldabsh r0, [0xc] 3 jne r0, 0x800, 1236 4 ldabsb r0, [0x17] 5 jne r0, 0x11, 1236 6 ldabsh r0, [0x10] 7 mov r8, r0 8 ldabsb r0, [0xe] 9 mov r9, r0 10 ldabsh r0, [0x24] 11 jne r0, 0x539, 1236 12 lddw r1, 0xfffffff8 13 14 add r8, r1 15 lsh r9, 0x2 16 and r9, 0x3c 17 sub r8, r9 18 stxdw [r10-24], r9 19 add r9, 0x16 20 mov r1, 0x0 21 stxdw [r10-8], r1 22 lddw r1, 0x3 23 24 mov r2, r10 25 add r2, 0xfffffff8 26 call 0x1 27 jne r0, 0x0, 29 ... Après analyse, il apparait que pour passer à l'état un le code attend un datagramme UDP avec pour port destination 1337. Le contenu des données est comparé à LUM{BvWQEdCrMfA}. Deux lignes de scapy permettent de vérifier que nous avons le bon paquet : p = IP(dst="127.0.0.1")/UDP(dport=1337)/Raw("LUM{BvWQEdCrMfA}") send(p) Le deuxième paquet doit-être envoyé après celui contenant le LUM (l'état doit être à un pour activer la vérification). Le code est ici beaucoup plus long. La plus grande partie étant dédiée à la conversion hexadécimale de la clé. Plusieurs opérations sont effectuées sur les quatres DWORD de la clé, ces derniers sont ensuite comparés aux DWORD composant la chaine "SSTIC CHALL 2017". Les transformations opérés sur les deux premiers DWORD sont relativement simples (addition, multiplication, division, xor, not, etc.) Les deux derniers DWORD sont eux élevés à la puissance 257 modulo 0x8a46a52d et 0xdc6c88e1 respectivement. Ce qui fait penser à une sorte de signature RSA très simple sur 32 bits. La factorisation des deux modules nous le prouve : ces derniers sont issus de deux facteurs premiers. Le calcul de phi ainsi que de l'inverse de 257 est alors trivial. Le code suivant inverse les différentes opérations appliquées sur les DWORD et retrouve la clé : #!/usr/bin/python ''' m1 = 43019 * 53927 phi(m1) = (43019-1) * (53927-1) = 2319788668 inv 257 mod 2319788668 = 713086789 m2 = 64969 * 56921 phi(m2) = (64969-1) * (56921-1) = 3697978560 inv 257 mod 3697978560 = 3108028673 ''' inv_m1 = 713086789 inv_m2 = 3108028673 l1 = 0x42765751 l2 = 0x45644372 m1 = 0x8a46a52d m2 = 0xdc6c88e1 r1 = 0x53535449 r2 = 0x43204348 r3 = 0x204c4c41 r4 = 0x37313032 s = 45 a = (r1^(s*0x706f6c79)^~l1) & 0xffffffff b = ((r2-0x6d6f7266/s)^~l2) & 0xffffffff c = (pow(r3, inv_m1, m1)^~l1) & 0xffffffff d = (pow(r4, inv_m2, m2)^~l2) & 0xffffffff print "KEY{%08x%08x%08x%08x}"%(a,b,c,d) Ce qui donne la clé : KEY{2d4ceda2fa2a0e08fc360b55291de7c9} Il ne reste plus qu'a ajouter la ligne suivante dans le script scapy afin de confirmer la solution : p = IP(dst="127.0.0.1")/UDP(dport=1337)/Raw("KEY{2d4ceda2fa2a0e08718fe186291de7c9}") send(p) NOTE: Il est possible pour le premier des DWORD correspondant à la "signature RSA" de partir d'une valeur congrue (mais donc non contenue dans l'anneau) appartenant à l'espace [0:2^32] et menant au même résultat après exponentiation. Ce qui explique la note présente sur la page du challenge. Après calcul nous obtenons la fausse clé : 2d4ceda2fa2a0e08718fe186291de7c9. --[ Riscy zones Cette partie est composés de quatre fichiers: $ cd /challenges/riscy_zones $ ls password_is_00112233445566778899AABBCCDDEEFF.txt.encrypted secret.lzma.encrypted TA.elf.signed trustzone_decrypt Un petit test permet de confirmer que le fichier password_is//.encrypted se déchiffre bien avec la clé 00112233445566778899AABBCCDDEEFF. Le but du challenge devient évident, découvrir la clé de déchiffrement du fichier secret.lzma.encrypted. La première idée est d'analyser le code du fichier trustzone_decrypt. $ readelf -a trustzone_decrypt ELF Header: Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, big endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: OpenRISC 1000 Version: 0x1 Entry point address: 0x26dc Start of program headers: 52 (bytes into file) Start of section headers: 6752 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 7 Size of section headers: 40 (bytes) Number of section headers: 23 Section header string table index: 20 ... Quelques recherches montrent qu'il n'existe actuellement quasiment aucun désassembleur pour l'architecture openrisc. Cependant le code étant relativement court l'outil objdump présent dans la toolchain fera l'affaire. L'analyse montre que la vérification du mot de passe et le chiffrement des block est faite via des IOCTL sur le device /dev/sstic. Il faut donc découvrir le code noyau gérant le périphérique /dev/sstic. Ce qui nous mène au désassemblage du noyau vmlinux.bin avec objdump. Après localisation du code grace aux parties basses des offsets de chaines de caractères, on découvre un code épuré vérifiant les paramètres et procédant à un l.sys 0, ce qui est étonnant. Les appels systèmes utilisant d'habitude l.sys 1. Il est inutile de chercher dans le système en lui même le code de gestion de cet appel, celui-ci est intercepté par l’émulateur en javascript. Après le passage du fichier à js-beautify. On remarque que la gestion des clés ainsi que l'algorithme de HMAC sont gérés par le javascript. Un LUM peut-être facilement récupéré en envoyant un IOCTL correspondant à la commande CMD_GET_TA_LUM. LUM{gdN8.D*@+UV} Afin de comprendre le fonctionnement global du déchiffrement il devient nécessaire de désassembler le fichier TA.elf.signed. Le code javascript permet d'identifier que ce fichier est compilé pour une architecture RISC V également émulée. N'ayant pas de désassembleur RISC V à disposition, objdump s'impose encore une fois. La compilation de la toolchain RISC V effectuée, on passe à l'analyse du code. Le principe est le suivant : Les 112 premiers octets du fichier sont déchiffrés en AES ECB à l'aide de la clé SSTIC_AES_KEY : "___SSTIC_2017___" Ces octets correspondent à un block ascii contenant le HMAC-SHA256 du mot de passe permettant le déchiffrement du fichier. Celui-ci est comparé au HMAC du mot de passe passé en paramètre. S'il correspond, le mot de passe est alors utilisé pour déchiffrer les octets suivant du fichier via un algorithme custom. La clé de HMAC (SSTIC_PASSWORD_HMAC_KEY) est: "LUM{0qvYH:QI?K6}" HMAC contenus dans les fichiers chiffrés: password_is_00112233445566778899AABBCCDDEEFF.txt.decrypted ==BEGIN PASSWORD HMAC== 4bcea2ecf77bda18804b395a5b0a81c9c745b5add567073c3afb4ec5e7a478fa ==END PASSWORD HMAC== secret.lzma.encrypted ==BEGIN PASSWORD HMAC== 504861089d10a8d5900cdf3bad962004282e73c20d2d5ab95b67ae6b3356748b ==END PASSWORD HMAC==' L'algorithme de chiffrement de block doit être compris afin de remonter jusqu'à la clé. Il est possible à partir d'un clair connu de remonter au stream généré par la clé et d'inverser l'algorithme suivant: mask_a = 0xadaaa9aa; mask_b = 0x52555655; a = (data[1] & mask_b) | (data[3] & mask_a); b = ROR32(a, (n + 23) & 31); s[15] = (B3(b) + 65)&0xff; s[14] = (B2(b) + s[15] + 72)&0xff; s[13] = (B1(b) + s[14] + 79)&0xff; s[12] = (B0(b) + s[13] + 86)&0xff; a = (data[0] & mask_b) | (data[2] & mask_a); b = ROR32(a, (n + 19) & 31); s[11] = (B3(b) + s[12] + 93)&0xff; s[10] = (B2(b) + s[11] + 100)&0xff; s[9] = (B1(b) + s[10] + 107)&0xff; s[8] = (B0(b) + s[9] + 114)&0xff; a = (data[1] & mask_a) | (data[3] & mask_b); b = ROR32(a, (n + 17) & 31); s[7] = (B3(b) + s[8] + 121)&0xff; s[6] = (B2(b) + s[7] + 128)&0xff; s[5] = (B1(b) + s[6] + 135)&0xff; s[4] = (B0(b) + s[5] + 142)&0xff; a = (data[0] & mask_a) | (data[2] & mask_b); b = ROR32(a, (n + 13) & 31); s[3] = (B3(b) + s[4] + 149)&0xff; s[2] = (B2(b) + s[3] + 156)&0xff; s[1] = (B1(b) + s[2] + 163)&0xff; s[0] = (B0(b) + s[1] + 170)&0xff; D'après son nom, le fichier secret.lzma.encrypted est un fichier compressé en lzma. Une hypothèse très forte peut-être faite sur les 14 premiers octets du fichier : 5D00008000FFFFFFFFFFFFFFFF00. Reste à bruteforcer les deux manquants. La condition d'arrêt étant la correspondance du HMAC SHA256 du mot de passe candidat avec celui présent dans l'entête du fichier chiffré. Un petit programme en C permet d'effectuer le brute force en moins d'une seconde. $ ./search_key hmac=504861089d10a8d5900cdf3bad962004282e73c20d2d5ab95b67ae6b3356748b Key found: 5921cd9fd3a82bd9244ece5328c6c95f Le déchiffrement produit un fichier lzma qui après décompression donne un fichier JPG. Le dernier LUM se trouve en ROT13 dans les données EXIF. $ exif secret.jpg EXIF tags in 'secret.jpg' ('Intel' byte order): --------------------+---------------------------------------------------------- Tag |Value --------------------+---------------------------------------------------------- Image Description |Congratulations : YHZ{+g%Yi.vzG8Z} X-Resolution |72 Y-Resolution |72 Resolution Unit |Inch Exif Version |Exif Version 2.1 FlashPixVersion |FlashPix Version 1.0 Color Space |Internal error (unknown value 65535) --------------------+---------------------------------------------------------- Ce qui donne : LUM{+t%Lv.imT8M} KEY{5921cd9fd3a82bd9244ece5328c6c95f} --[ Unstable machines Suite à la validation des deux précédentes étapes, un fichier est apparu dans le répertoire /challenge/unstable_machines $ ls unstable.machines.exe $ file unstable.machines.exe unstable.machines.exe: PE32 executable (GUI) Intel 80386, for MS Windows Enfin un fichier PE ! On peut alors ouvrir IDA. Le programme possède une interface à la Windows, ressources + dispatch loop. A l'exécution l'interface est rudimentaire, et l'on remarque rapidement une machine à état controlée par les "check buttons" disposés en oblique. +-+ |0| +-+ +-+ |1| +-+ +-+ |2| +-+ +-+ |3| +-+ ... jusqu'à 6. Afin d'incrémenter la variable d'état, la séquence de click sur ces boutons doit être la suivante : 4,3,5,1,6,3,0,2,0,6,2 Certains états procèdent à des opérations sur une variable. L'état 10 atteint, cette dernière contient un LUM : LUM{2KREDvn3OPf} Arrivé à ce stade, la textbox devient visible et il est possible d'entrer un mot de passe afin de "Sauver le grand proton !". Le challenge est bien défini : trouver le mot de passe. Rapidement on note la présence d'une VM (machine virtuelle). Appelée en .text:0040163A, elle effectue des opérations sur le mot de passe récupéré à l'aide de GetDlgItemTextA. Le résultat est ensuite comparé à une valeur prédéfinie. L'analyse des handlers de la VM n'est pas trop difficile malgré les quelques pièges laissés de part et d'autre afin de tromper les décompilateurs (facilement repérable car utilisant quasi systématiquement la valeur 0x42 :). On note au passage que les valeurs des registres de la VM sont encodées avant leur stockage en mémoire. Il est ensuite utile de coder un desassambleur pour obtenir une vision du fonctionnement de la VM. 05571c00 -> 05572000 : code (400) 05572000 -> 05572100 : stack (100) 05572100 -> 05573200 : data (1100) 00000000: mov r0, 5572100 00000005: add r0, 1000 0000000a: call e 0000000c: jmp ef 0000000e: mov r1, r0 00000010: push 5b1c63ed (a6fbb82f) 00000015: push 7ebb8eb3 (835c8279) 0000001a: push b8ab7606 (4d8ca9c4) 0000001f: push 8a06fff9 (7c210c33) 00000024: push f0a21668 (15860aa2) 00000029: push 962e5304 (6c09bec6) 0000002e: push 36644553 (cbc3d999) 00000033: push 040375a0 (fe24a95a) 00000038: mov r0, 8 0000003d: do_chksum 0000003e: mov [r1], r0 00000040: push e9b2165a (1c960aa0) ... 000000f4: getpixel 000000f5: je r1, 78, 13b 000000fa: je r1, fe, 14b 000000ff: je r1, 33, 169 00000104: je r1, 12, 19b 00000109: je r1, a1, 1a6 0000010e: je r1, af, 1c7 00000113: je r1, 8e, 1e9 00000118: je r1, 13, 20b 0000011d: je r1, bb, 22d 00000122: je r1, 7c, 2d9 00000127: je r1, 32, 277 0000012c: je r1, 2f, 2a7 00000131: je r1, dc, 2f8 00000136: je r1, 59, 252 ... On remarque alors une chose intéressante : la VM embarque une autre VM. Le bytecode de cette seconde VM est contenu dans certains bits de poids faible du fichier image des ressources. Reste à extraire ce bytecode et à analyser les nouveaux handlers pour developper un nouveau desassembleur. Un LUM est présent dans le bytecode de la seconde VM : LUM{C1UAidv_pzJ} $ hd bytecode.bin 00000000 78 1c 00 33 1c 04 a1 9c 00 fe 04 1c af 04 04 bb |x..3............| 00000010 56 47 fe 0c 00 78 20 01 af 04 20 bb ed 11 fe 10 |VG...x ... .....| 00000020 00 78 14 00 78 18 00 33 18 40 a1 57 00 fe 04 10 |.x..x..3.@.W....| 00000030 85 08 04 32 00 e5 fe 20 00 af 20 10 fe 04 14 5b |...2... .. ....[| 00000040 08 00 2f 96 48 fe 24 00 af 24 14 13 20 24 af 0c |../.H.$..$.. $..| 00000050 20 7c 88 4b af 14 00 fe 04 0c 47 08 04 32 be 66 | |.K......G..2.f| 00000060 fe 20 00 af 20 0c fe 04 14 78 08 0b 2f ae 58 fe |. .. ....x../.X.| 00000070 24 00 af 24 14 13 20 24 af 10 20 78 20 01 af 18 |$..$.. $.. x ...| 00000080 20 12 5d 80 fe 08 0c fe 04 1c af 04 04 59 d6 d3 | .]..........Y..| 00000090 fe 08 10 78 20 01 af 04 20 59 d0 d6 78 20 01 af |...x ... Y..x ..| 000000a0 1c 20 12 a2 80 dc 84 d6 4c 55 4d 7b 43 31 55 41 |. ......LUM{C1UA| 000000b0 69 64 76 5f 70 7a 4a 7d 4d f6 b0 08 8e 8a af d5 |idv_pzJ}M.......| 000000c0 71 f5 35 78 99 3e fa e2 de 77 9a cc b5 89 81 4e |q.5x.>...w.....N| Code de la seconde VM : 0 : [78 1c 00] mov r7, 0 3 : [33 1c 04] cmp r7, 4 (set zf [30]=1 if equal) 6 : [a1 9c 00] jz a5 9 : [fe 04 1c] mov r1, r7 c : [af 04 04] add r1, r1 f : [bb 56 47] mov r0, data[r1] 12 : [fe 0c 00] mov r3, r0 15 : [78 20 01] mov r8, 1 18 : [af 04 20] add r1, r8 1b : [bb ed 11] mov r0, data[r1] 1e : [fe 10 00] mov r4, r0 21 : [78 14 00] mov r5, 0 24 : [78 18 00] mov r6, 0 27 : [33 18 40] cmp r6, 40 (set zf [30]=1 if equal) 2a : [a1 57 00] jz 84 ... En prenant du recul sur l'ensemble du code obtenu, on peut identifier certaines particularités : - 64 tours - un handler effectuant l'opération (a << 4) ^ (a >> 5) - des blocks de 64 bits (2 DWORD) - une clé de 128 bits (4 DWORD) L'algorithme de chiffrement XTEA correspond. Cependant les entrées/sorties ne correspondent toujours pas à ce qui est observé, même après avoir relevé le delta de 0xd2d413b3 différent du delta 0x9e3779b9 habituellement utilisé par XTEA. En cause, un second thread exécuté quand la valeur de y (qui correspond en fait au registre PC de la deuxième VM) passe à -1. Ce qui arrive en fin de chiffrement. L'analyse du code de ce thread relève l'exécution d'un code en ROP 64 bits modifiant la valeur de la sortie. Le passage de 32 à 64 bits est effectué au début de la chaine de ROP grâce à un retf sur le sélecteur de segment 0x33 (Heavens Gate). Le pivot du ROP est à l'adresse 0x401363. Juste avant, une fonction effectue un XOR des DWORD allant de 0x414100 à 0x414240 avec la valeur 0xF4B02F4B. Cette plage mémoire sera utilisé en tant que pile après le pivot. Un LUM est présent dans la chaine : LUM{+zhVQqJy03q} Compilation des gadgets 64bits : pop rdx pop rcx not rdx bswap rcx pop rax mov eax, [rax] mov r8, [rax] xor r8,rcx add r8,rcx mov [rax],r8 add rax,8 mov r8, [rax] xor r8,rdx bswap r8 sub r8,rcx mov [rax],r8 add rax,8 mov r8, [rax] bswap r8 rol rcx,0xd ror r8,cl mov [rax],r8 add rax,8 mov r8, [rax] xor r8,rcx ror rdx,15h xchg rdx,rcx ror r8,cl add r8,rdx mov [rax],r8 Une fois recodé en python et ajouté au code de chiffrement XTEA, le comportement du code est correctement reproduit en utilisant la clé récupérée en mémoire : 9bc6ac9a8a1da3895ccdb3a554cea5b0. Mais même l'algorithme inversé, un problème subsiste : la clé obtenue n'est pas composée de caractères ASCII. Bien qu'étrange, l'injection de celle-ci en mémoire engendre bien le résultat attendu. Vient alors une longue et douloureuse recherche de l'erreur. C'est en modifiant le fichier exécutable afin d'appliquer automatiquement la clé binaire considéré comme valide que le problème se fit moins mystérieux : la clé n'est plus valide si le processus n'est pas en mode Debug. Un anti-debug est donc présent et change le comportement. En analysant chaque partie de code, il s'avère que certains bits de la clé sont modifiés. Le résultat produit par le déchiffrement est donc différent. clé_debug = 9bc6ac9a8a1da3895ccdb3a554cea5b0 clé_valide = 9bc6ac9a8a1da3891ccdb2a554cea5b0 ^ ^ On procède une nouvelle fois au déchiffrement avec la nouvelle clé pour obtenir: 3f691f3d6eb60b343c931c22e0baa92f NOTE: Le fonctionnement de l'anti-debug m'a été divulgué après la résolution du challenge (stack overflow et modification du handler de gestion du cookie). Durant l'épreuve je n'avais que localisé le problème grâce notamment à la différence entre la valeur du troisième DWORD calculée grâce au désassemblage de la VM et celle obtenue en mémoire. --[ LabyQRinth La dernière clé validée, un fichier final.txt est déchiffré. $ cat final.txt Une dernière petite étape! cefec06 b a e 0 cb519c4 6 9 434 a4d 12 660e 4 4 4 d94 f 9 bf f 02 b a f 287 4 f 01d 1 2 65 0 0 65 7 183 6 a fd6 b 7 e7 5 a 5 d d4b c f 6 7 a 6 e 6 3 3 c56b0b4 0 8 3 9 9 0 4 5 c d186a57 60 3 8c a 9 2 6 214 9c b 20 d e80 65f 422 f e95 b3 6 1 16e 8 a 49eb 157 2 d a 96d5 f d 9 2d f4 5 a d 6d2 a b55 cf36d6b657658a8abeca0 fc 8 a 73 1 5 3 c 2c 7 5 0 3 a4 0 2c58 86 1 f 2 56 6 041 a e 8 3f 3791 7 4 6558bab a e13 d 4 16 0 f1 27 2 c 90 8829bb1 f8 431 4b3 7e c 9 26 5e5 70e c 9 35 d61 e 9 3 a c2f c f 7dc 26 ad 0 4d b f4 a938ac9a7 3 7 cc e 92 431 6 6 d 4c5 8 f a 0da9 a0f 23 6 a3 8d deQRypt(a e6 8 8b 66 24 6 92 c e ce80 5ba a c d 2 5bd 81e52f c 3 c 8f f 3d 6 d 2 228caa6 e2 90 6 8b3ab 5 4c973); a 8 d 96f9 b e 2b7e 0 294 e 7 0 44 0 2061d2 9 0 40c 9 fb 44 10 91bcc4 2 44 3 829 5 1 c0 4 8 6e f 08d e b 07 2b a6b 0 a8c7 fd0ca91 26ad 6 9 3 a 9 Le QR code est évident. On écrit un petit script python qui convertit celui-ci en image afin de pouvoir le scanner facilement. Le résultat du scan nous donne : "Please use this Nibble ADD key: 5571C2017" Le titre de l'épreuve étant "LabyQRinth" il s'agit d'un labyrinth. En supposant l'entrée est indiquée par "deQRypt(" et la sortie par ");", on relève les nibbles formant le chemin le plus court : ace80e6fcca1776558babe2c51d6b657658a8abcf973d1bb5c938ac9da252fd4c973 L'application de la clé se fait en soustrayant chacun de ces nibbles avec ceux de la clé. (la clé étant une ADD KEY le déchiffremnt s'effectue en faisant un SUB). On obtient alors l'adresse e-mail : WwLnWZkEAeMjPaoKEs5K7rld@sstic.org --[ Conclusion Pour conclure, un excellent challenge, très orienté reverse. Merci aux concepteurs. --[ LUM, KEY & e-mail electronic_flash LUM{x.g215WiPCR} LUM{AsPBdVWz95y} dont_let_him_escape LUM{BvWQEdCrMfA} KEY{2d4ceda2fa2a0e08fc360b55291de7c9} riscy_zones LUM{gdN8.D*@+UV} LUM{0qvYH:QI?K6} LUM{+t%Lv.imT8M} KEY{5921cd9fd3a82bd9244ece5328c6c95f} unstable_machines LUM{2KREDvn3OPf} LUM{C1UAidv_pzJ} LUM{+zhVQqJy03q} KEY{3f691f3d6eb60b343c931c22e0baa92f} LabyQRinth WwLnWZkEAeMjPaoKEs5K7rld@sstic.org