User Tools

Site Tools


rev2_angr

This is an old revision of the document!


Zadatak s Hacknite platforme - rev2 - rješenje pomoću alata Angr

Prije nego što pročitate ovaj članak, preporuča writeup rješenja istog zadatka s alatom Ghidra:

CTF writeup - rev2-Ghidra

Angr je napredan i moćan alat koji se koristi za analizu binarnih izvršnih datoteka, gdje se može koristiti za reverzno inženjerstvo, automatizirano generiranje exploita, otkrivanje ranjivosti i druge namjene. Može izvršavati i statičku i dinamičku analizu, gdje je statička analiza slična kao ona koju pružaju Ghidra, Binary Ninja i slični alati, dok je dinamička analiza Angr alata njegova najjača strana.

Angr pruža mogućnost tehnike simboličnog izvršavanja (symbolic execution), koja spada pod dinamičku analizu. Simbolično izvršavanje može tretirati neki korisnički unos kao neodređenu simboličnu varijablu, umjesto definirane vrijednosti, te potom tijekom rješavanja, na svakom grananju (npr. if statement), zabilježi uvjete nad simboličkom varijablom definirane za ulaz u svaku granu, i tako gradi moguće putanje izvršavanja programa. Kada nađe željenu putanju u programu, unazad riješi sve uvjete nad simboličkom varijablom, koji su potrebni da bi se program izvršio tom putanjom.

Npr. simbolička varijabla “password” za korisnički unos u zadatku rev2, kada se dođe do prvog uvjeta koji provjerava duljinu varijable, je li jednaka 21, Angr zabilježi

grana 1:  len(password) = 21  
grana 2:  len(password) != 21  

Nakon toga na provjeri prvog znaka korisničkog unosa zabilježi,

grana 1.1:   password[0] + 146 == 213
grana 1.2:   password[0] + 146 != 213

I na takav način izvršava program, bez da definira vrijednost simbolične varijable password, sve dok ne dođe do željene grane, koja se može definirati kao grana u kojoj se ispiše string “Correct!”. Tada zna koji skup uvjeta nad varijablom password mora biti zadovoljen da bi se izvršila grana koja ispisuje “Correct!”, te rješava taj skup uvjeta i nalazi potrebnu početnu vrijednost varijable password, da bi se program izvršio željenim putem, koji će na kraju ispisati “Correct!”.

Efektivno, Angr alatu je samo potrebno objasniti kojoj varijabli mora naći specifičnu vrijednost i koja je željena putanja izvršavanja programa, a Angr alat sam rješava ostalo.

U nastavku je prikazana jednostavna Angr skripta koja rješava ovaj zadatak.

import angr
import claripy

# Otvori projekt
p = angr.Project("./rev2", auto_load_libs=False)

# Inicijalno stanje
state = p.factory.full_init_state()

# Main, instruction address prve instrukcije u mainu
state.regs.rip = 0x004017e0

# postavi base pointer
state.regs.rbp = state.regs.rsp

# Simbolicna varijabla - user input, duljina 21 bajt
password = claripy.BVS("password",8*21)


# preskoci fgets, manualno postavi
# simbolicnu varijablu password kao unos za fgets
@p.hook(0x0040186f, length = 5)
def skip_fgets(state):
    state.memory.store(state.regs.rsp + 0x40,password)
    state.regs.rax = state.regs.rsp + 0x40

# Preskoci poziv strcspn funkciju, kako ne bi
# nesto krivo iz memorije procitala, 
# rucno postavi njezin return kao 0x15, odnosno 21
# (potrebna duljina korisnickog unosa)
@p.hook(0x00401887, length = 5)
def skip_strcspn(state):
    state.regs.rax = 0x15

# Stvori simulation manager nad objektom p (rev2) 
# "namjesti" ga s definiranim inicijalnim stanjem
sm = p.factory.simgr(state)

# Pronađi putanju u kojoj će se izvršiti poziv funkcije koja ispisuje ("Correct!")
# 0x00401914 - instruction address poziva funkcije koja ispisuje "Correct!"
sm.explore(find = 0x00401914)

if sm.found:
    # Ispiši pronađeno rješenje
    print("Rješenje:", sm.found[0].solver.eval(password,cast_to = bytes))

else:
    print("Nema rješenja!")

Preporučuje se korištenje Python virtual environmenta (venv) za instalaciju Angr paketa i pokretanje ove skripte.

Napravite novi direktorij, u njega stavite rev2 izvršnu datoteku i Python Angr skriptu, potom izvršite naredbe za postavljanje virtualnog okruženja:

python -m venv .
source ./bin/activate
pip install angr claripy
python angrSolveScript.py

Posljednjom naredbom se pokreće skripta (umjesto angrSolveScript.py stavite ime svoje skripte).

Pokretanjem ove skripte, Angr nalazi rješenje zadatka.

 Slika 1 - Rješenje zadatka s pomoću Angr-a

Skripta ima komentare u kojima su objašnjenja koda, no još će dodatno biti objašnjeno kako se pronađe početna adresa i željena adresa za pronalazak, kao i dvije hook funkcije.

Na liniji koda 11,

state.regs.rip = 0x004017e0

Ova adresa je adresa prve instrukcije koja se izvršava u main funkciji, i prikazano je kako se može pronaći s pomoću Ghidre na slici ispod

 Slika 2 - adresa prve instrukcije unutar main funkcije

Na liniji koda 41,

sm.explore(find = 0x00401914)

Postavlja se adresa instrukcije do koje se želi da Angr nađe put, ova adresa je adresa puts funkcije koja ispisuje “Correct!”, što je ispis u slučaju kada je korisnički unos ispravan.

Na slici ispod je prikazano kako se može pronaći s pomoću Ghidre.

 Slika 3 - adresa instrukcije kojom se poziva ispis "Correct!" stringa

Druga hook funkcija koja preskače strcspn funkciju definirana na liniji 31

@p.hook(0x00401887, length = 5)
def skip_strcspn(state):
	state.regs.rax = 0x15

Se postavlja, jer strcspn funkcija čita iz memorije, no kada se koristi simbolično izvršavanje, ono što bi se pročitalo iz memorije može biti neispravno definirano, zato se “ručno” postavlja koji je željeni return te funkcije i sam poziv funkcije se preskače.

Funkcije vraćaju return vrijednost preko RAX registra, mi unaprijed znamo da ova funkcija vraća duljinu unesenog korisničkog unosa i da je željena duljina korisničkog unosa 21, odnosno 0x15 hex, zato se ovo ovom linijom postavlja željena return vrijednost

state.regs.rax = 0x15

Prvi argument hook funkcije

0x00401887

je adresa instrukcije koja poziva izvršavanje te funkcije, na slici ispod je prikazano kako je pronađena.

 Slika 4 - adresa instrukcije koja poziva strcspn funkciju

Drugi argument hook funkcije,

length = 5

je duljina bajtova koja se se preskače, odnosno na kolikom offsetu nakon što se izvrši hook na definiranoj adresi se nastavlja izvršavanje.

Adresa instrukcije odmah nakon adrese instrukcije poziva strscpn funkcije, je

0x0040188c

kao što se vidi na slici iznad (MOV instrukcija, odmah nakon CALL instrukcije koja se preskače)

0x0040188c - 0x00401887 = 5

Zato je length argumentu postavljena vrijednost 5.

Preostala hook funkcija se koristi kako bi se preskočio fgets, koji čita iz stdin (standard input), kojim se zapravo unosi korisnički unos, što je u ovom kontekstu zapravo definirana simbolična varijabla password u Angr skripti, koja se želi postaviti umjesto korisničkog unosa preko stdin.

@p.hook(0x0040186f, length = 5)
def skip_fgets(state):
	state.memory.store(state.regs.rsp + 0x40,password)
	state.regs.rax = state.regs.rsp + 0x40

Argumenti hook funkcije su isti kao i u prethodnom objašnjenju, u slici ispod je prikazano gdje je definirana adresa instrukcije koja poziva izvršavanje fgets funkcije.

 Slika 5 - adresa instrukcije koja poziva fgets funkciju

No dio koda koji još nije jasan je sadržaj funkcije

state.memory.store(state.regs.rsp + 0x40,password)
state.regs.rax = state.regs.rsp + 0x40

Fgets funkcija inače uzima korisnički unos iz stdin i sprema ga u buffer čija je adresa poslana kao prvi argument fgets funkcije. No pri simboličnom izvršavanju, to nije poželjno, nego je potrebno postaviti simboličnu varijablu “password” umjesto onoga što bi inače bio korisnički unos.

state.memory.store(arg1,arg2)

simulira dio fgets funkcije koja korisnički unos sprema u buffer čija je adresa dana kao prvi argument. Ovaj kod će spremiti ono dano argumentom 2 na mjesto definirano argumentom 1.

Jasno je da se simbolična varijabla password želi spremiti umjesto korisničkog unosa, pa je ona drugi argument ovog poziva, no kako pronaći adresu buffera u koji fgets funkcija sprema unos?

Prvi argument je user_input, podcrtan plavom bojom na prethodnoj slici.

Pregledom koda u prikazu dekompajlirane main funkcije vidi se linija:

user_input = input_buffer;

Također, prije poziva funkcije, funkciji se prvi argument prosljeđuje preko RDI registra.

Ova adresa se može ili pronaći praćenjem što je bilo postavljeno u RDI registar prije poziva fgets funkcije, ili samo pronalaskom input_buffer adrese.

Na slici ispod, prikazana su oba načina.

 Slika 6 - input_buffer adresa

Vidi se da se odmah prije poziva fgets funkcije u RDI registar kojim se prosljeđuje prvi argument, odnosno input_buffer postavlja vrijednost RBX registra, koji je ranije definiran, gdje se vidi relativna adresa RSP+0x40.

Također dvoklikom na input_buffer u prikazu dekompajliranog koda, označenog crvenom bojom s desne strane, se odmah prikazuje instrukcija označena crvenom bojom na vrhu slike, u kojoj se vidi relativna adresa RSP+0x40.

Zato se ovom linijom

state.memory.store(state.regs.rsp + 0x40,password)

efektivno postavlja simbolična varijabla password u input_buffer.

Nakon što se fgets funkcija uspješno izvrši, ona vraća pointer na buffer u koji je upisan unos, odnosno pointer na input_buffer u ovom slučaju. Pošto funkcija vraća return rezultat preko RAX registra, samo se postavlja vrijednost RAX registra na relativnu adresu input_buffera.

state.regs.rax = state.regs.rsp + 0x40

Ako vas Angr alat zanima i želite naučiti više, u nastavku je prikaz Angr skripte koja također rješava ovaj zadatak, ali na još brži i efikasniji način.

import angr
import claripy
import logging

logging.getLogger('angr.sim_manager').setLevel(logging.DEBUG)

p = angr.Project("./rev2", auto_load_libs=False)

state = p.factory.entry_state(addr = 0x004017e0,
                            add_options = {
                                angr.options.LAZY_SOLVES,
                                angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY
                                })


state.regs.rbp = state.regs.rsp

password = claripy.BVS("password",8*21)

@p.hook(0x0040185b, length = 5)
def skip_print(state):
    return

@p.hook(0x0040186f, length = 5)
def skip_fgets(state):
    state.memory.store(state.regs.rsp + 0x40,password)
    state.regs.rax = state.regs.rsp + 0x40

@p.hook(0x00401887, length = 5)
def skip_strcspn(state):
    state.regs.rax = 0x15

sm = p.factory.simgr(state)

sm.explore(find = 0x0040190d ,avoid = 0x004018d0) 

if sm.found:
    print("Rješenje:", sm.found[0].solver.eval(password,cast_to = bytes))

else:
    print("Nema rješenja!")

Ova skripta ne radi full_init_state, nego odmah počinje iz main funkcije, koristi “LAZY_SOLVES” opciju koja dodatno optimizira simbolično izvršavanja u ovom slučaju (nije uvijek primjenjiva), također eksplicitno definira koju putanju izbjegavati (definirana “avoid” argumentom postavljenim na adresu funkcije koja ispisuje “Wrong password!), osim željene putanje i također preskače izvršavanje print funkcije.

rev2_angr.1763645265.txt.gz · Last modified: 2025/12/01 11:40 (external edit)

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki