Catégories
ECW 2022 Reverse Write-ups

ECW 2022 – UEFI

Dans ce second write-up de l’ECW 2022, nous allons regarder le challenge UEFI. Encore une fois je ne participais pas au CTF mais ce challenge était pour moi le plus intéressant en plus d’être relativement facile, avec 10 solves en deux semaines.

Énoncé

Some friend of you developped the ‘next-generation’ UEFI-based password protected system. Find the valid password.

Auteur : valkheim

Fichier du challenge

Première approche

On commence par télécharger l’archive du challenge et extraire son contenu.

$ 7z e uefi.7z
[...]
Everything is Ok

Folders: 1
Files: 5
Size:       50097524
Compressed: 1237629
$ ls
disk.img  OVMF_CODE.fd  OVMF_VARS.fd  readme  run.sh  uefi  uefi.7z
$ rm -r uefi uefi.7z
$ cat readme 
# Challenge de reverse UEFI

Pour lancer le challenge dans QEMU : `./run.sh`

On se retrouve donc avec plusieurs fichiers dont un readme qui nous indique qu’il faut lancer le script “run.sh” pour lancer le challenge dans QEMU. On peut commencer par le lancer.

Exécution du script (QEMU)

On se retrouve donc avec une application qui nous demande un mot de passe.

Environnement UEFI

Voyons à quoi le script servant à lancer le challenge ressemble.

qemu-system-x86_64 \ 
  -cpu qemu64 \
  -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
  -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
  -drive format=raw,file=disk.img,if=virtio \
  -net none \
  -nographic \
  -serial mon:stdio \
  -monitor telnet::45454,server,nowait

On y trouve donc comme prévu une commande QEMU. Il est intéressant de voir 3 paramètres -drive, soit 3 “disques”. On va chercher à en savoir pus sur ceux-ci.

$ file OVMF_* disk.img 
OVMF_CODE.fd: data
OVMF_VARS.fd: data
disk.img:     DOS/MBR boot sector; partition 1 : ID=0xee, start-CHS (0x0,0,2), end-CHS (0x5,213,6), startsector 1, 93749 sectors, extended partition table (last)

On ne trouve pas d’information ici sur les fichiers “OVMF_CODE.fd” et “OVMF_VARS.fd”, alors on peut aller se renseigner sur internet. Plusieurs documentations différentes sont à notre disposition (j’ai utilisé ce whitepaper) et on apprend que le projet OVMF, pour “Open Virtual Machine Firmware”, est un sous-projet de l’Intel EFI Development Kit II (EDK2) qui permet le support de l’UEFI (Unified Extensible Firmware Interface, le remplaçant du BIOS), notamment pour les machines virtuelles x64. Pour résumer, c’est un firmware qui supporte l’UEFI.

Le fichier “disk.img” est identifié comme MBR (Master Boot Record), c’est en effet le disque dur à proprement parler. Ce fichier contiendra donc entre autres la table des partitions et la routine de démarrage. C’est donc aussi ici que l’on devrait donc trouver notre “OS” ou toute application lancée au boot par le firmware UEFI OVMF.

On peut donc essayer de binwalk le disk.img pour trouver l’application EFI qui contient la vérification du mot de passe.

PS : binwalk -e (extract) n’extrait pas le PE, on peut pour cela utiliser l’option –dd=”.*”).

$ binwalk --dd=".*" disk.img 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
1788416       0x1B4A00        Microsoft executable, portable (PE)
1792965       0x1B5BC5        Unix path: /home/valkheim/workspace/ecw_uefi/edk2/MdePkg/Library/BasePrintLib/PrintLibInternal.c
1793612       0x1B5E4C        Unix path: /home/valkheim/workspace/ecw_uefi/edk2/MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.c

$ cd _disk.img.extracted/
$ file 1B4A00 
1B4A00: MS-DOS executable PE32+ executable (EFI application) x86-64, for MS Windows
$ mv 1B4A00 efi_app.exe

On se retrouve alors avec une application EFI en x86_64 que l’on va pouvoir reverse. On note également deux paths qui sont bons à retenir : un path vers “BasePrintLib/PrintLibInternal.c” de edk2 ainsi qu’un path ver “UefiBootServicesTableLib/UefiBootServicesTableLib.c”, faisant également partie d’EDK2.

Reverse du loader

On peut alors ouvrir l’application EFI dans IDA, tout en ouvrant à coté le code source du projet EDK2 (https://github.com/tianocore/edk2/).

La fonction qui nous intéresse sera évidemment l’entry point de l’application qui est une fonction ayant pour signature “ModuleEntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)”. Cette fonction est divisable en 2 parties distinctes : initialisation et code.

Partie 1 – Initialisation

Le début de cette fonction correspond relativement à la fonction “UefiBootServicesTableLibConstructor” de la lib “UefiBootServicesTableLib”. (https://github.com/tianocore/edk2/blob/master/MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.c). On peut renommer et typer les variables et fonctions (comme “assert_failed” que l’on devine meme si l’on ne prendra pas ici le temps d’explorer en détails le code de la “PrintLibInternal” car cela ne nous sert pas) pour plus de clarté.

Déut de la fonction ModuleEntryPoint, une fois typé correctement

On a ici plusieurs structures intéressantes. On peut s’aider de la documentation UEFI (https://uefi.org/sites/default/files/resources/UEFI_Spec_2_7.pdf) pour les comprendre : EFI_SYSTEM_TABLE contient entre autres des pointeurs vers les structures EFI_RUNTIME_SERVICES et EFI_BOOT_SERVICES. Ces dernières contiennent globalement des pointeurs sur fonctions utilisables par toutes les applications EFI : elles forment donc une partie de l’API UEFI.

Partie 2 – Code

Maintenant que les pointeurs vers les différentes structures nécessaires ont été récupérés, on rentre dans la partie fonctionnelle du code. Encore une fois, on peut tout typer et renommer pour gagner en clarté.

Code de la fonction, variables typées et renommées

Pour ce qui est du decompress protocol, ça pouvait ne pas être évident à trouver mais tout est dans la spécification UEFI encore une fois.

Spécification : EFI_DECOMPRESS_PROTOCOL

Pour résumer cette fonction va globalement jouer le role de loader en décompressant du code (que j’ai appelé ROM ici ¯\_(ツ)_/¯ ), qui fait une taille de 936 octets. Il va ensuite XORer le code décompressé avec 0x71 puis l’exécuter grâce aux fonctions LoadImage() et StartImage().

Récupération de la ROM

Notre objectif suivant va donc être de récupérer la ROM qui est décompressée puis XORée au runtime. Pour cela, on se renseigne sur le format de compression EFI et on trouve la structure ici.

typedef struct {
    EFI_COMMON_SECTION_HEADER    CommonHeader;
    UINT32                       UncompressedLength;
    UINT8                        CompressionType;
} EFI_COMPRESSION_SECTION;

Il nous faut donc également la structure EFI_COMMON_SECTION_HEADER que l’on peut trouver ici.

typedef struct {
    UINT8               Size[3];
    EFI_SECTION_TYPE    Type;
} EFI_COMMON_SECTION_HEADER;

On peut alors ajouter ces structures dans IDA et appliquer la structure EFI_COMPRESSION_SECTION sur notre ROM compressée et chercher à quoi correspondent les différentes valeurs des champs.

Structure EFI_COMPRESSION_SECTION appliquée sur la ROM

On voit que la taille réelle de la ROM annoncée dans le header est 0x3A0 soit 928 (936 – 8 bytes de header aligné), que la taille décompressée annoncée est 2048 octets et que c’est une compression LZMA classique.

On va alors exporter la ROM au format binaire pour pouvoir la décompresser à la main. Pour cela, sélectionner toute la ROM dans la section .data et appuyer sur Shift+E (pour Export).

Exportation de la ROM au format binaire

On vérifie que la ROM est bien complète et que tout s’est bien passé.

$ xxd COMPRESSED_ROM.bin | head
00000000: a003 0000 0008 0000 0328 6b99 f787 569a  .........(k...V.
00000010: f516 f8be 3d61 190b 4b73 4a1a d030 6b94  ....=a..KsJ..0k.
00000020: 900d 8032 0970 14a1 4d18 baf8 4d2e 8e8a  ...2.p..M...M...
00000030: 5f37 8b46 477e ff3c 562b 8890 8f36 bb93  _7.FG~.<V+...6..
00000040: 74bd 3b9f 3c3a 3b81 8c78 1e0d 372e e163  t.;.<:;..x..7..c
00000050: cce1 9119 a467 f7bd 411d 98f2 bcaf 53f0  .....g..A.....S.
00000060: f59d efc4 1736 d236 94ee d0c6 8e98 92b2  .....6.6........
00000070: bb7f 35ab 5bf6 bb96 8d8c dfb5 cf5f df91  ..5.[........_..
00000080: cfbb df0f d9bc bc3f 1f7e afd6 ca3d fab8  .......?.~...=..
00000090: b7d3 22bb 11a9 dca6 af30 1d93 793e 7ce8  .."......0..y>|.
julien@julien-$ xxd COMPRESSED_ROM.bin | tail
00000310: 9d92 ae69 3f25 7b42 1db3 3ca2 87d5 5d40  ...i?%{B..<...]@
00000320: 5e22 f78d 38eb dd52 21d3 3c63 df1a 6e87  ^"..8..R!.<c..n.
00000330: 6ea9 c52d 1b62 94fc b35e c977 a017 c093  n..-.b...^.w....
00000340: ac4d 9234 79c1 7e18 531c 65d5 10f2 89b1  .M.4y.~.S.e.....
00000350: 805d 70c7 3e16 2e14 287a 4257 54fc 49a0  .]p.>...(zBWT.I.
00000360: 02dc 12e3 083d 61bf 8e3e eb11 6643 f8c6  .....=a..>..fC..
00000370: d143 b238 70a1 6ddf 4283 e817 79df d7e6  .C.8p.m.B...y...
00000380: 9ee9 fe01 e81c 233c 49b4 af01 7815 7067  ......#<I...x.pg
00000390: 3218 b144 aeec 3fe2 5c22 2c53 d6c8 ed05  2..D..?.\",S....
000003a0: 7ec4 3fd5 afff c000                      ~.?.....
julien@julien-$ cat COMPRESSED_ROM.bin | wc -c
936

Parfait. Maintenant on va chercher un outil pour nous permettre de décompresser cette ROM et on peut par exemple utiliser ce wrapper Python : https://github.com/linearregression/python-eficompressor, disponible via pip.

$ python3 -q
>>> import EfiCompressor
>>> dir(EfiCompressor)
['FrameworkCompress', 'FrameworkDecompress', 'UefiCompress', 'UefiDecompress', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> compressed_ROM = open("COMPRESSED_ROM.bin", "rb").read()
>>> decompressed_ROM = EfiCompressor.UefiDecompress(compressed_ROM, len(compressed_ROM))
>>> decompressed_ROM
<memory at 0x7f89dfe4da00>
>>> dir(decompressed_ROM)
['__class__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'c_contiguous', 'cast', 'contiguous', 'f_contiguous', 'format', 'hex', 'itemsize', 'nbytes', 'ndim', 'obj', 'readonly', 'release', 'shape', 'strides', 'suboffsets', 'tobytes', 'tolist', 'toreadonly']
>>> decompressed_ROM.hex()
'3c2b09717071717175718d7e8e8e818e8f8e71717170818e3171717171717171717171[...]717171717171717171717171717171717171'

On peut ensuite sortir Cyberchef pour XORer le résultat avec 0x71 et télécharger le résultat.

ROM décompressée, XORée avec 0x71

EFI byte code

Une fois le résultat téléchargé, on se retrouve une nouvelle fois avec une application EFI, mais cette fois-ci sous la forme d’une DLL en EFI byte code et non pas en x86-64.

l’EFI byte code (ou EBC) est du code exécuté dans une VM de sorte à avoir des instructions UEFI exécutables sur n’importe quelle architecture de CPU (x86_64, RISC-V, IA32…). Il existe une version 32 ainsi qu’une version 64 bitset et le format des fichiers binaires exécutables en EBC sera toujours le format PE32 (ou PE32+ si 64 bits) de Windows. La spécification peut se trouver ici : https://uefi.org/specs/UEFI/2.10/22_EFI_Byte_Code_Virtual_Machine.html.

Setup

Pour analyser cet exécutable j’ai pour ma part utilisé IDA Pro mais il existe des désassembleurs comme Spore et même un processor Ghidra (que je n’ai pas réussi à faire marcher). L’entry point ressemble à ça.

Entry point du programme EBC

Comme on l’a vu précédemment, le point d’entrée d’une application EFI a pour signature “EfiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)”. On peut donc typer celui-ci et on sait alors que notre “arg_8” est un pointeur vers la SystemTable. La structure de la SystemTable et toutes les structures dont nous aurons besoin n’étant pas disponibles, il va falloir les créer nous-même dans IDA.

Fort heureusement, certains ont déjà fait ça avant nous et il existe un plugin EFI pour IDA : https://github.com/snare/ida-efiutils. Le plugin a 10 ans et a été développé en Python2 donc on va abandonner l’idée de s’en servir mais on peut quand même récupérer le fichier “structs.idc” qui contient un script qui va créer les structures EFI_TABLE_HEADER, EFI_BOOT_SERVICES, EFI_RUNTIME_SERVICES et EFI_SYSTEM_TABLE.

Chargement du script “structs.idc”

On peut ensuite appliquer les structures EFI_SYSTEM_TABLE et EFI_BOOT_SERVICES dans le code et commencer à y voir clair.

Structures appliquées dans le code

Il nous manque cependant certains call de fonctions : CALL32Exa [R1+8] dans le premier bloc, CALL32Exa [R1] dans le 3eme etc. Pour savoir quelles fonctions sont appelées ici, nous avons besoin de savoir ce que sont les champs ConIn et ConOut, et IDA peut nous donner cette information si on regarde la définition de la structure EFI_SYSTEM_TABLE.

Définition de ConIn et ConOut

Il nous faut donc définir les structures EFI_SIMPLE_TEXT_INPUT_PROTOCOL et EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL. Encore une fois, on les trouve dans la spécification UEFI.

typedef struct {
    EFI_INPUT_RESET      Reset;
    EFI_INPUT_READ_KEY   ReadKeyStroke;
    EFI_EVENT            WaitForKey;
} EFI_SIMPLE_TEXT_INPUT_PROTOCOL;

typedef struct {
    EFI_TEXT_RESET                Reset;
    EFI_TEXT_STRING               OutputString;
    EFI_TEXT_TEST_STRING          TestString;
    EFI_TEXT_QUERY_MODE           QueryMode;
    EFI_TEXT_SET_MODE             SetMode;
    EFI_TEXT_SET_ATTRIBUTE        SetAttribute;
    EFI_TEXT_CLEAR_SCREEN         ClearScreen;
    EFI_TEXT_SET_CURSOR_POSITION  SetCursorPosition;
    EFI_TEXT_ENABLE_CURSOR        EnableCursor;
    SIMPLE_TEXT_OUTPUT_MODE       *Mode;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

On peut alors importer ces structures dans IDA (par soucis de facilité on peut remplacer chaque type par un void*, ce sont en fait des pointeurs sur fonction).

Reverse du code

On a maintenant tous les éléments et on peut commencer à comprendre ce qu’il se passe exactement. Pour cela, on va séparer le gros bloc de code principal en plusieurs sous-parties et faire attention à chaque bloc, l’un après l’autre.

Deux premiers blocs du code

Les deux premiers blocs sont très simples : le programme affiche la chaîne de caractères “Password:” et R2 est mis à 0 (on verra par la suite que c’est le “char counter”).

Suite du code

La suite du code va servir à lire une entrée utilisateur : reset du flux d’entrée, définition d’un event pour l’entrée utilisateur, lecture du caractère reçu, stockage de ce caractère dans un buffer que j’ai appelé “INPUT_BUFFER” et reset du flux d’entrée.

Bloc de code qui affiche les astérisques à chaque caractère entré

Ensuite, pour chaque caractère entré, une astérisque est affichée (on ce souvient de ce comportement observé lorsque l’on avait exécuté lancé le challenge au début). On rentre ensuite dans la partie importante du code : la comparaison avec le mot de passe attendu.

Fin du code

Ici, on voit que l’on commence par récupérer le “char_counter” dans R2, précédemment sauvegardé (push) au début de la fonction. On récupère aussi le caractère entré par l’utilisateur dans R4 : il faut bien garder en tête que l’on gère les chaînes de caractères en Unicode !

Ensuite vient la partie la “plus compliquée” du code mais qui reste simple une fois décomposée : globalement, on va effectuer un ROT4 sur chaque caractère entrée par l’utilisateur et comparer le résultat au caractère correspondant dans le flag (qui est donc chiffré par ROT4 dans le binaire) qui se trouve dans le buffer que j’ai nommé “DATA_BUFFER” (en fait une chaîne de caractères Unicode) à partir de l’index 128*2 == 256 et qui a une taille de 24 octets.

Pour ce qui est du junk code, on voit que les deux embranchements du saut conditionnel à la fin du bloc mènent tous les deux au même endroit, donc on peut passer outre ce bloc qui est surement là juste pour déstabiliser le reverseur.

Résolution du challenge

Pour résoudre le challenge, on va commencer par récupérer le “DATA_BUFFER” qui est donc une chaîne de caractère UTF-16. On peut le faire en utilisant la commande strings.

$ strings ROM.bin --encoding=l
x0cV$2ekF2Qizv6^oyq^pUHKUgFj1Jd__V4LKW45H3R3__QvN3@sMwGeWw0VKBYFzRbviq6u#7RA9ArnM8XDIEEvHQ&HGT@Sv&LUZdb4BF6%2_4dci33595^VZQeoji^z^ucPVhc#&cT6#NH0^97O$7WqofM3pHpyMsY4WeTtS&eeNwq466kV6__GHG7e&S&ReuO353pv^UppLd5*$5!TD__nipgduZdxzv#oDWd&DFNzVWAmO_7jEH38DGb%dkAA?SwABE[>up/[_,`/[nqh/vy4PrhsulP%wMNpg&4cRY7S8x^!Veptn9kK__P8D3j41V%qktB7i_L&ViJdr1%#P&Dhy4C3H
Congratulations!
Password: 

Dans cette chaîne on peut alors sélectionner notre flag chiffré grâce à python.

>>> DATA_BUFFER = "x0cV$2ekF2Qizv6^oyq^pUHKUgFj1Jd__V4LKW45H3R3__QvN3@sMwGeWw0VKBYFzRbviq6u#7RA9ArnM8XDIEEvHQ&HGT@Sv&LUZdb4BF6%2_4dci33595^VZQeoji^z^ucPVhc#&cT6#NH0^97O$7WqofM3pHpyMsY4WeTtS&eeNwq466kV6__GHG7e&S&ReuO353pv^UppLd5*$5!TD__nipgduZdxzv#oDWd&DFNzVWAmO_7jEH38DGb%dkAA?SwABE[>up/[_,`/[nqh/vy4PrhsulP%wMNpg&4cRY7S8x^!Veptn9kK__P8D3j41V%qktB7i_L&ViJdr1%#P&Dhy4C3H"
>>> DATA_BUFFER[128*2:128*2+24]
'A?SwABE[>up/[_,`/[nqh/vy'

il faut ensuite appliquer un ROT4 sur le résultat et on récupère le flag (comme souvent, j’ai pour ça utilisé Cyberchef par flemme).

Récupération du flag avec Cyberchef

J’espère que vous avez trouvé ce challenge aussi intéressant que moi, c’était pour moi le meilleur de l’ECW 2022.