Writeup: FlareOn 2021: 007 - spel

1. TLDR

spel graph

2. Dane wejściowe

Plik z zadaniem znajduje się tutaj. Hasło: flare.

Przedmiotem zadania jest plik spel.exe

3. Analiza wstępna

Po uruchomieniu zobaczyłem komunikat o błędzie:

Spel Window

Czyżby z aplikacją było coś nie tak?

Myślałem że coś jest nie tak z setupem mojego środowiska. Może miałem problem z dźwiękiem? Brak dostępu do jakiegoś urządzenia? Nie ta wersja systemu? Podczas debuggowania aplikacji okazało się, że proces po zamknięciu okna dalej działa, hmmmm…..

W zdeasemblowanym kodzie widziałem dużo wywołań z klasy CWinApp świadczących o tym, że kod aplikacji zostały napisany z wykorzystaniem biblioteki Microsoft Foundation Class (MFC). Poniżej przykład listy funkcji z tej biblioteki użytych w aplikacji spel.exe:

CWinApp

Przejrzałem listę importów w poszukiwaniu wywołań:

CreateThread
CreateProcessInternalW
VirtualAlloc
VirtualProtect

W trakcie tej czynności zauważyłem nietypową (przynajmniej w mojej opinii) funkcję VirutalAllocExNuma

Próba zlokalizowania fragmentu kodu wywołującego tą funkcję (w widoku grafu kodu) zakończyła się niepowodzeniem:

VirutalAllocExNumaXRef

Zlokalizowanie funkcji VirutalAllocExNuma w widoku listingu pozwoliło na ujawnienie interesującego fragmentu kodu. Moją uwagę zwrócił długi ciag rozkazów mov układający dane w pamięci, który został zakończony ułożeniem literału “flare”, a następnie alokacja pamięci, jej skopiowanie oraz wykonanie danych jako kod:

ShellCode-Pre

4. Analiza szczegółowa

Używając x64dbg wykonałem kod do breakpointa ustawionego na VirutalAllocExNuma, który zaalokował adres 0x260000, i po wywołaniu funkcji memmove, zobaczyłem pod tym adresem potencjalny shellcode. Było chyba dobrze, pierwszy rozkaz to call (E8):

ShellCode-x64

Używając Process Hackera zrzuciłem cały obszar pamięci:

ShellCode-x64

W celu ograniczenia rozmiaru dumpu do fragmentu, który miał jakiekolwiek znaczenie dla shellcodu, otworzyłem zrzut pamięci w HxD. Szybko zauważyłem że tuż obok shellcodu znajduje się plik PE, który wyodrębiłem i bez analizy shellcodu założyłem, że jest on przez niego ładowany.

Stage-02-PE

Ładowany plik okazał się być plikiem DLL:

Stage-02-dll

Po załadowaniu dll do narzędzia IDA, zauwayżyłem dwie ciekawe rzeczy:

  1. DLL nie miała żadnych exportów oprócz DllEntryPoint.
  2. Funkcja DLL używała bardzo długiej tablicy bajtów, którą przekazywała do nienzanej mi funkcji. Bardzo długiej tablicy, która była kolejnym plikiem PE.

Stage-03-PE

Wyodrębniłem zatem zagnieżdżony plik:

> emit spel-stage2.dll | carve-pe > spel-stage3.dll

Kolejny, ładowany plik okazał się być plikiem DLL:

Stage-03-dll

Analiza statyczna kodu doprowadziła mnie do kilku spostrzeżeń:

  1. Funkcje są dynamicznie importowane
  2. Nazwy importowanych funkcji i bibliotek są zacieminone
  3. Wszystkie stringi są zaciemnione: zaszyfrowane xorem, którego klucz każdorazowo znajdował się kilka instrukcji po załadowaniu szyfrogramu do pamięci. Na rysnku poniżej zaznaczono odpowiedno ładowanie szyfrogramu oraz odszyfrowanie:

String-xor

5. Deobfuskacja

Nazwy bibliotek były mapowane z liczba całkowitych, skonstruowanie typu wyliczeniowego ułatwiło analizę takiej metody zaciemiania kodu:

Library-enum

Sama obfuskacja nazw ładowanych funkcji nie stanowiła problemu, dzięki skorzystaniu z bazy HashDb oraz wtyczki do środowiska IDA

Obfuskację stringów rozwiązałem poniższym skryptem:

import idautils
import idc
import ida_ua
import ida_bytes
import idaapi
import struct
import malduck

def get_ciphertext(ea):
    input = list()
    for x in range(0,15):
        if ida_ua.ua_mnem(ea) == 'mov' and idc.get_numbered_type_name(idc.get_operand_type(ea, 1))=='DWORD':
            value = idc.get_operand_value(ea, 1)
            if value!=0:
                value = struct.pack("<I", value)
                input.append(value)
        else:
            break
        ea = ida_bytes.next_head(ea, ida_ida.cvar.inf.max_ea)
    return (ea,bytes().join(input))
    

def get_key(ea):                 
    while ida_ua.ua_mnem(ea) != 'xor' or idc.get_numbered_type_name(idc.get_operand_type(ea, 1))!='DWORD':
        ea = ida_bytes.next_head(ea, ida_ida.cvar.inf.max_ea)
    key = idc.get_operand_value(ea, 1)
    key = struct.pack("<I", key)
    return (ea,bytes(key))
    

ea = idc.here()
ea, ciphertext = get_ciphertext(ea)
ea, key = get_key(ea)

plaintext = malduck.xor(key, ciphertext)
plaintext_utf8 = plaintext.decode("UTF-8")
plaintext_utf16 = plaintext.decode("UTF-16")

print(f"key:{key}")
print(f"ciphertext:{ciphertext}")
print(f"plaintext_utf8:{plaintext_utf8}")
print(f"plaintext UTF-16:{plaintext_utf16}")

Dalsza analiza kodu programu pozwoliła zauważyc, że program korzystał z biblioteki bcrypt oraz dostarczanej przez niego implementacji algorytmu AES-128. W ten sposób ładował i odszyfrywował fragment jeden z resource’ów o id 128:

findresource

6. Analiza payloadu

Zawartość tego resource’a odzyskałem narzędziem CFF Explorer, na czerwono zaznaczono faktyczny szyfrogram:

resource-128

Wykorzystanym kluczem był jeden ze stringów:

string-key

IV stanowił ciąg: 80808080808080808080808080808080

Odszyforwałem zatem szyfrogram korzystając z Binary Refinery

$ emit 80808080808080808080808080808080D7FB7E628DAB8765CD7185CE530F5A8C2D8A4537124B791D40DA768626D3D372 | hex | aes --mode cbc -i cut::16 d41d8cd98f00b204e9800998ecf8427e

l3rlcps_7r_vb33eehskc3

Po odszyfrowaniu ciąg był ponownie zaciemnany xorem oraz zmieniana była kolejność znaków:

plaintext2reg

7. Odczytanie flagi

Wystarczyło zignorować szyfr strumieniowy oraz odczytać prawidłową kolejność znaków:

p = "l3rlcps_7r_vb33eehskc3"
indexes = [12,13,6,8,7,6,5,1,0,3,4,17,15,20,19,21,2,10,16,11,14,2]
t=[]
for i in indexes:
	t.append(p[i])

t = ''.join(t)
print(t)

Flaga:

b3s7_sp3llcheck3r_ev3r@flare-on.com