Dynamická analýza

Dynamická analýza je technika reverse engineeringu, při které program spouštíme a sledujeme jeho chování za běhu. Na rozdíl od statické analýzy tak pracujeme s reálným execution flow, pamětí a registry.

Cílem je například:

  • sledovat, jak program zpracovává vstup
  • identifikovat rozhodovací body (podmínky)
  • manipulovat běh programu

Nástroje

Pro dynamickou analýzu se používají debuggery. V této části budeme pracovat s GDB rozšířeným o GEF (GDB Enhanced Features).

GEF přidává:

  • lepší výpis registrů
  • přehlednější práci s pamětí
  • užitečné zkratky pro reversing

Další velmi užitečné nástroje:

strace

$ strace ./program

Sleduje, jak program komunikuje s jádrem (syscalls).

ltrace

$ ltrace ./program

Zobrazuje volání knihovních funkcí (např. strcmp, printf, open).

Základy debugování

Spuštění programu v GDB:

$ gdb ./program

Spuštění programu:

gef➤  run

Breakpointy

gef➤  break main
gef➤  break strcmp

next vs step

gef➤  next   # přeskočí funkce
gef➤  step   # vstoupí do funkce

Registry

gef➤  info registers

Na Linuxu (x86-64, System V ABI):

  • rdi = 1. argument
  • rsi = 2. argument
  • rax = návratová hodnota

Výpis funkcí

gef➤  info functions

Disassemble

gef➤  disassemble main

Umožní zobrazit assembler kód funkce, což je základ pro pochopení větvení programu.

Ukázka: strcmp

Program často používá funkci strcmp pro porovnání vstupu:

if (strcmp(vstupUzivatele, "Klic_2026_x7!") == 0) {
    puts("ACCESS GRANTED");
} else {
    puts("Wrong password!");
}

Můžeme nastavit breakpoint na strcmp a program spustit:

gef➤  break strcmp
gef➤  run

Po zastavení programu zkontrolujeme registry. Na architektuře x86-64 se argumenty funkce předávají přes registry, přičemž:

  • první argument je v registru rdi
  • druhý argument je v registru rsi

Ukázkový výstup může vypadat například takto:

gef➤  info registers
rax            0x0                 0x0
rbx            0x7fffffffe1d8      0x7fffffffe1d8
rcx            0x7ffff7e2e6a0      0x7ffff7e2e6a0 <strcmp>
rdx            0x0                 0x0
rsi            0x555555556004      0x555555556004
rdi            0x7fffffffe0f0      0x7fffffffe0f0
rbp            0x7fffffffe160      0x7fffffffe160
rsp            0x7fffffffe0d8      0x7fffffffe0d8
rip            0x7ffff7e2e6a0      0x7ffff7e2e6a0 <strcmp>

Samotné registry nám řeknou, kde se argumenty nacházejí. Obsah řetězců následně přečteme z paměti.

Čtení stringů z paměti

Obsah paměti lze zobrazit například takto:

gef➤  x/s $rdi
0x7fffffffe0f0: "test"

gef➤  x/s $rsi
0x555555556004: "Klic_2026_x7!"

To nám umožní přímo vidět:

  • uživatelský vstup
  • očekávanou hodnotu, se kterou se porovnává

Další možností je dump paměti po bytech:

gef➤  x/20bx $rdi
0x7fffffffe0f0: 0x74 0x65 0x73 0x74 0x00 0x41 0x41 0x41
0x7fffffffe0f8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffe100: 0x00 0x00 0x00 0x00

Tento přístup je užitečný zejména tehdy, když data nejsou čistý string, ale například pole bajtů, binární buffer nebo částečně zakódovaná hodnota.

Jumping

Debugger umožňuje změnit execution flow programu. To znamená, že můžeme ručně přeskočit část kódu a pokračovat od jiné instrukce.

Například po analýze disassemblovaného kódu zjistíme, že adresa 0x401234 odpovídá větvi, která vypisuje ACCESS GRANTED. Poté lze provést:

gef➤  jump *0x401234

Tím se změní instrukční pointer (rip) a program začne vykonávání od zadané adresy.

Jumping je užitečný například tehdy, když:

  • chceme přeskočit neúspěšnou větev programu
  • chceme otestovat konkrétní blok kódu bez splnění všech podmínek
  • chceme rychle ověřit, co daná větev dělá

Je však potřeba opatrnost: skok do nesprávného místa může narušit stav programu. Pokud cílový blok očekává určité registry, zásobník nebo inicializovaná data, program může spadnout nebo se chovat nepředvídatelně.

Patching

Patching znamená úpravu instrukcí programu. Lze jej provádět buď dočasně za běhu, nebo trvale přímo v binárce.

Dočasný patch v debuggeru

Jednoduchý příklad je přepsání instrukce pomocí NOP (No Operation), čímž efektivně odstraníme část kódu:

gef➤  set {char}0x401220 = 0x90

Instrukce 0x90 na x86 odpovídá NOP. Pokud takto přepíšeme například podmíněný skok nebo část kontroly, program může pokračovat bez validace.

Praktický význam

Patching se používá například tehdy, když chceme:

  • odstranit kontrolu hesla
  • obejít podmíněný skok
  • změnit chování programu při testování
  • ověřit hypotézu o významu konkrétní instrukce