PWNing 2017 – Rozwiązanie zadania RE150 “Military grade algorithm” za pomocą Manticore

Początek listopada przyniósł kolejną świetną konferencję –** **Security PWNing Conference 2017. W tym roku miałem przyjemność współtworzyć z zespołem P4 konkurs CTF organizowany w ramach tej konferencji. Poniżej zaprezentuję rozwiązanie mojego zadania z RE, wycenianego na 150 punktów. W trakcie trwania konkursu rozwiązało je dwanaście osób.

Clue zadania był szereg odwracalnych, lecz najzwyczajniej w świecie upierdliwych do ręcznego liczenia operacji (dodawanie, odejmowanie, mnożenie oraz xor). Zadanie to było modelowym problemem do zaprzęgnięcia Symbolic Execution, co ninejszym uczynię z pomocą bardzo interesującego frameworka Manticore.

Reverse engineering binarki

Poza enigmatycznym opisem zadania: “Dane Cyberlandii chroni nowy autorski algorytm o sile “military grade”. Czy podołasz zadaniu i go złamiesz?“, otrzymujemy niewielką binarkę w formacie ELF:

a@b:~/Downloads$ file re3 
re3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=28f523af671d756a9abb946580d040a60acb486e, not stripped

Wrzućmy ją do IDA’y i zajrzyjmy do main():

Logika programu wygląda prosto: od użytkownika pobierana jest flaga za pomocą funkcji fgets i przekazywana do funkcji check_all po zdekompilowaniu wyglądająca w następujący sposób:

Widać wyraźnie, że każdej literze flagi “przypisana” jest funkcja sprawdzająca konkretną literę. Poniżej listing funkcji check_flag_17:

Skoro już wiemy z jakim problemem przyjdzie się nam zmierzyć, przystępujemy do działania 🙂

Łączymy kropki: RE + Manticore

Pierwszym problemem do pokonania jest pobieranie danych do programu – musimy w jakiś sposób zasymulować wprowadzanie danych przez użytkownika za pomocą skryptu. Z racji tego, że mamy pełną kontrolę nad wykonaniem kodu, możemy “przeskoczyć” niewygodny fragment kodu za pomocą ręcznego ustawienia rejestru RIP.

Ze screenshota można odczytać, że pierwsza instrukcja asma przed wyciętym fragmentem znajduje się pod adresem 0x40104E, a ostatnia pod 0x40106B. Z boku zapisujemy sobie również, że wywołanie funkcji check_all() jest pod adresem 0x401072.

Pierwszą rzeczą, którą musimy zrobić jest hook adresu 0x40104E i wpisanie w RIP wartości 0x40106B. Drugą jest zauważenie (albo statycznie, albo w debuggerze), że pod adresem 0x40106F w rejestrze RDI znajduje się adres wpisywanej flagi (oznaczony czerwoną strzałką).

Fragment kodu odpowiedzialny za tą operacje:

from manticore import Manticore

flag = None
m = Manticore('re3')

# "Przeskok" nad funkcją fgets()
@m.hook(0x40104E)
def hook(state):
    state.cpu.EIP = 0x40106B

# Pobranie adresu z rejestru RDI i stworzenie bufora dla symbolic execution wraz z zapisem danych do niego
@m.hook(0x401072)
def hook(state):
    global flag
    flag = state.cpu.RDI
    buffer = state.new_symbolic_buffer(34)
    state.cpu.write_bytes(flag, buffer)

Skoro mamy już załatwioną sprawę funkcji gets() oraz adresu bufora w którym przechowywana jest flaga, nie pozostaje nic innego jak kazać Manticore samodzielnie rozwiązać zadanie.

W tym celu musimy znaleźć adres końca funkcji wywołującej po kolei funkcje do sprawdzania poszczególnych znaków flagi, czyli check_all(). A konkretniej: nteresuje nas fragment przed instrukcją ret i oczywiście leave też 😉 :

Wiedząc, że jest to adres 0x40102B możemy w tym miejscu rozpocząć rozwiązywanie flagi za pomocą poniższego fragmentu kodu:

from manticore import Manticore

flag = None
m = Manticore('re3')

# "Przeskok" nad funkcją fgets()
@m.hook(0x40104E)
def hook(state):
    state.cpu.EIP = 0x40106B

# Pobranie adresu z rejestru RDI i stworzenie bufora dla symbolic execution wraz z zapisem danych do niego
@m.hook(0x401072)
def hook(state):
    global flag
    flag = state.cpu.RDI
    buffer = state.new_symbolic_buffer(34)
    state.cpu.write_bytes(flag, buffer)

# Rozwiązanie flagi w miejscu końca funkcji check_all()
@m.hook(0x40102B)
def hook(state):
    flag = ''.join(map(chr, state.solve_buffer(flag_buffer, 34)))
    print(flag)
    state.abandon()

# Porzucenie aktualnego stanu, tak aby nie rozwiązywać w nieskończoność: hook dla funkcji exit()
def exit_hook(state):
    state.abandon()

W poprzednim kroku “przemyciłem” też ważny fragment kodu – obsłużenie wywołania funkcji exit(), które zakończy operacje na poszukiwanym buforze. W tym celu musimy zrobić hooka na wszystkie wywołania funkcji exit(). Niestety nie jest to zapewnione z automatu i trzeba o tym pamiętać, aby w końcu dostać upragnioną flagę 😉

Informację pod jakimi adresami jest wywoływana funkcja exit() możemy znaleźć za pomocą polecenia: objdump -d re3 | grep exit

a@b:~/Downloads$ objdump -d re3 | grep exit
  4004e8:	e8 73 00 00 00       	callq  400560 <exit@plt+0x10>
0000000000400550 <exit@plt>:
  400685:	e8 c6 fe ff ff       	callq  400550 <exit@plt>
  4006b8:	e8 93 fe ff ff       	callq  400550 <exit@plt>
  4006ea:	e8 61 fe ff ff       	callq  400550 <exit@plt>
  40071a:	e8 31 fe ff ff       	callq  400550 <exit@plt>
  40074c:	e8 ff fd ff ff       	callq  400550 <exit@plt>
  400782:	e8 c9 fd ff ff       	callq  400550 <exit@plt>
  4007b5:	e8 96 fd ff ff       	callq  400550 <exit@plt>
  4007eb:	e8 60 fd ff ff       	callq  400550 <exit@plt>
  400824:	e8 27 fd ff ff       	callq  400550 <exit@plt>
  40085e:	e8 ed fc ff ff       	callq  400550 <exit@plt>
  400894:	e8 b7 fc ff ff       	callq  400550 <exit@plt>
  4008cd:	e8 7e fc ff ff       	callq  400550 <exit@plt>
  400908:	e8 43 fc ff ff       	callq  400550 <exit@plt>
  400941:	e8 0a fc ff ff       	callq  400550 <exit@plt>
  40097a:	e8 d1 fb ff ff       	callq  400550 <exit@plt>
  4009b0:	e8 9b fb ff ff       	callq  400550 <exit@plt>
  4009e2:	e8 69 fb ff ff       	callq  400550 <exit@plt>
  400a1b:	e8 30 fb ff ff       	callq  400550 <exit@plt>
  400a4b:	e8 00 fb ff ff       	callq  400550 <exit@plt>
  400a84:	e8 c7 fa ff ff       	callq  400550 <exit@plt>
  400ab7:	e8 94 fa ff ff       	callq  400550 <exit@plt>
  400af6:	e8 55 fa ff ff       	callq  400550 <exit@plt>
  400b2b:	e8 20 fa ff ff       	callq  400550 <exit@plt>
  400b64:	e8 e7 f9 ff ff       	callq  400550 <exit@plt>
  400b9a:	e8 b1 f9 ff ff       	callq  400550 <exit@plt>
  400bcd:	e8 7e f9 ff ff       	callq  400550 <exit@plt>
  400c03:	e8 48 f9 ff ff       	callq  400550 <exit@plt>
  400c41:	e8 0a f9 ff ff       	callq  400550 <exit@plt>
  400c7a:	e8 d1 f8 ff ff       	callq  400550 <exit@plt>
  400cad:	e8 9e f8 ff ff       	callq  400550 <exit@plt>
  400ce7:	e8 64 f8 ff ff       	callq  400550 <exit@plt>
  400d23:	e8 28 f8 ff ff       	callq  400550 <exit@plt>
  400d5d:	e8 ee f7 ff ff       	callq  400550 <exit@plt>

Parsowanie tego outputu możemy dodać do naszego skryptu w następujący sposób:

from manticore import Manticore

flag = None
m = Manticore('re3')

# Parsowanie outputu z objdump, celem wyciągnięcia adresów wywołania funkcji exit()
def get_exits():
    def addr(line):
        return int(line.split()[0][:-1], 16)

    exits_disasm = check_output("objdump -d re_symbolic | grep exit", shell=True)
    exits = [addr(line) for line in exits_disasm.split('\n')[2:-1]]
    for e in exits:
        yield e


# "Przeskok" nad funkcją fgets()
@m.hook(0x40104E)
def hook(state):
    state.cpu.EIP = 0x40106B

# Pobranie adresu z rejestru RDI i stworzenie bufora dla symbolic execution wraz z zapisem danych do niego
@m.hook(0x401072)
def hook(state):
    global flag
    flag = state.cpu.RDI
    buffer = state.new_symbolic_buffer(34)
    state.cpu.write_bytes(flag, buffer)

# Rozwiązanie flagi w miejscu końca funkcji check_all()
@m.hook(0x40102B)
def hook(state):
    flag = ''.join(map(chr, state.solve_buffer(flag_buffer, 34)))
    print(flag)
    state.abandon()

# Porzucenie aktualnego stanu, tak aby nie rozwiązywać w nieskończoność: hook dla funkcji exit()
def exit_hook(state):
    state.abandon()

# Dodanie hooków na wszyskie wywołania funkcji exit()
for index, exit in enumerate(get_exits()):
    m.add_hook(exit, exit_hook)

Ostatnim krokiem jest skonfigurowanie workerów obiektu Manticore i uruchomienie analizy:

from manticore import Manticore

flag = None
m = Manticore('re3')

# Parsowanie outputu z objdump, celem wyciągnięcia adresów wywołania funkcji exit()
def get_exits():
    def addr(line):
        return int(line.split()[0][:-1], 16)

    exits_disasm = check_output("objdump -d re_symbolic | grep exit", shell=True)
    exits = [addr(line) for line in exits_disasm.split('\n')[2:-1]]
    for e in exits:
        yield e


# "Przeskok" nad funkcją fgets()
@m.hook(0x40104E)
def hook(state):
    state.cpu.EIP = 0x40106B

# Pobranie adresu z rejestru RDI i stworzenie bufora dla symbolic execution wraz z zapisem danych do niego
@m.hook(0x401072)
def hook(state):
    global flag
    flag = state.cpu.RDI
    buffer = state.new_symbolic_buffer(34)
    state.cpu.write_bytes(flag, buffer)

# Rozwiązanie flagi w miejscu końca funkcji check_all()
@m.hook(0x40102B)
def hook(state):
    flag = ''.join(map(chr, state.solve_buffer(flag_buffer, 34)))
    print(flag)
    state.abandon()

# Porzucenie aktualnego stanu, tak aby nie rozwiązywać w nieskończoność: hook dla funkcji exit()
def exit_hook(state):
    state.abandon()

# Dodanie hooków na wszyskie wywołania funkcji exit()
for index, exit in enumerate(get_exits()):
    m.add_hook(exit, exit_hook)

# "Gadatliwość" Manticore
m.verbosity = 0
# 1 worker = 1 core w CPU
m.workers = 4
# Wewnętrzna optymalizacja działania Manticore
m.should_profile = True
# Uruchomienie analizy
m.run()

Skrypt po około minucie działania na VM (4 wirtualne procesory) wyświetli nam poszukiwaną flagę:

a@b:~/Downloads$ python re3_solver.py 
pwn{symbolic_execution_4_the_win}