Nezávisle na zvolenom programovacom jazyku, výsledné programy sú komplexným súborom pravidiel, ktoré jednoznačne určujú počítaču čo má "robiť". Exploitovanie programov je spôsob, ako povedať počítaču čo má robiť, aj napriek tomu, že bol práve bežiaci program vytvorený, aby zabránil danému spôsobu. Nakoľko program dokáže robiť len to, k čomu bol vytvorený, bezpečnostné diery sú výsledkom chýb alebo nepozorností vziknutých vo fáze písana zdrojového kódu programu.
Väčšina exploitov využíva pre útok tzv. memory corruption. Sem môžeme zaradiť exploitovacie techniky ako pretečenie zásobníka (buffer overflow) alebo zneužitie formátovacích reťazcov (format strings exploit). V ďaľších častiach si oba spôsoby názorne predvedieme. Využitím týchto techník sleduje útočník jediný cieľ, a to získať kontrolu nad vykonávacím tokom cieľového programu tým, že ho prinúti vykonať prepašovaný kód v pamäti počítača.
Samozrejme, aby sme mohli začať skúmať programy a hľadat ich zraniteľnosti, musíme disponovať určitými vedomosťami a zručnosťami. V prvom rade je potrebná znalosť vyššieho programovacieho jazyka ( v našom seriály sa zameriame na jazyk C ) a tiež istá znalosť nižšieho programovacieho jazyka, assemblera ( zameráme sa na inštrukčné sady procesorov x86 ). Pracovať budeme v prostredi operačného systému založenom na GNU/Linux, ku práci budeme potrebovať kompilátor (gcc), debugger (gdb), použijeme aj nástroj objdump. V neposlednom rade budeme potrebovať aj prostredie perlu, ktorý využijeme na zjednodušenie vstupných parametrov pre naše zraneniteľné programy. Samozrejmosťou je editor, odporúčam Vim s konfigurákom Cream, ktorý je vhodný aj pre úplných začiatočníkov.
Najprv si napíšeme veľmi jednoduchý program, ktorý následne prejdeme objdumpom a debuggerom.
#include <stdio.h> int main() { int i; for(i=0;i<10;i++) puts("Hello World!"); return 0; }
Program 10 krát vypíše reťazec Hello World! a oznámi (return 0;), že prebehol bez chýb. Uložme si ho ako hello.c Program teraz skompilujeme.
$ gcc -o hello hello.c
Prostredníctvom prepínača -o určíme výstupný súbor kompilácie. Po spustení programu dostaneme nasledujúci výstup.
$ ./hello
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Pozrime sa na strojový kód, do ktorého bola funkcia main() preložená. Použijeme príkaz objdump
, ktorého výstup cez pipe bude vstupom príkazu grep
. Nakoľko sa budeme venovať procesorom x86, objdumpu prepínačom -M
oznámime, aby pre výstup použil Intel syntax. Defaultne je použitá AT&T syntax.
$ objdump -M intel -D hello | grep -A20 main.:
080483a4 <main>:
80483a4: 8d 4c 24 04 lea ecx,[esp+0x4]
80483a8: 83 e4 f0 and esp,0xfffffff0
80483ab: ff 71 fc push DWORD PTR [ecx-0x4]
80483ae: 55 push ebp
80483af: 89 e5 mov ebp,esp
80483b1: 51 push ecx
80483b2: 83 ec 14 sub esp,0x14
80483b5: c7 45 f8 00 00 00 00 mov DWORD PTR [ebp-0x8],0x0
80483bc: eb 10 jmp 80483ce <main+0x2a>
80483be: c7 04 24 b0 84 04 08 mov DWORD PTR [esp],0x80484b0
80483c5: e8 0a ff ff ff call 80482d4 <puts@plt>
80483ca: 83 45 f8 01 add DWORD PTR [ebp-0x8],0x1
80483ce: 83 7d f8 09 cmp DWORD PTR [ebp-0x8],0x9
80483d2: 7e ea jle 80483be <main+0x1a>
80483d4: b8 00 00 00 00 mov eax,0x0
80483d9: 83 c4 14 add esp,0x14
80483dc: 59 pop ecx
80483dd: 5d pop ebp
80483de: 8d 61 fc lea esp,[ecx-0x4]
80483e1: c3 ret
Čísla v šestnástkovej sústave na ľavej strane výpisu predstavujú pamäťové adresy. Pamäť je súborom bajtov, pričom každý z nich ma svoju vlastnú adresu. Ku každému z týchto bajtov sa pristupuje na základe jeho adresy a v tomto prípade CPU pristupuje k tejto časti pamäti, aby získal inštrukcie, ktoré tvoria skompilovaný program. Čisla v ďalšom stĺpci sú inštrukcie v strojovom kóde pre x86 procesor. Samozrejme sú len reprezentáciou binárnych čísiel, ktorým CPU skutočne rozumie. Inštrukcie na pravej strane sú v assemblery pre x86 procesory. Inštrukcie assemblera majú priamy vzťah jedna ku jednej k príslušej inštrukcii strojového kódu. Procesory majú aj sadu špeciálnych premenných, ktoré nazývame registre. Ukážeme si ich pomocou debuggera. Debuggery sa využívajú na prechádzanie skompilovanými programami, na preskúmanie pamäte a prehľad registrov procesora. Príkazom echo "set dis intel" > ~/.gdbinit
nastavíme, že GDB bude pri svojom behu používať Intel syntax. Alternatívnym riešením je po každom spustení vykonať (gdb) set dis intel
$ gdb -q hello
(gdb) break main
Breakpoint 1 at 0x80483b2
(gdb) run
Starting program: /1/hello
Breakpoint 1, 0x080483b2 in main ()
Current language: auto; currently asm
(gdb) info registers
eax 0xbfd5d564 -1076505244
ecx 0xbfd5d4e0 -1076505376
edx 0x1 1
ebx 0xb7f20ff4 -1208872972
esp 0xbfd5d4c4 0xbfd5d4c4
ebp 0xbfd5d4c8 0xbfd5d4c8
esi 0x8048400 134513664
edi 0x80482f0 134513392
eip 0x80483b2 0x80483b2 <main+14>
eflags 0x200282 [ SF IF ID ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) quit
The program is running. Exit anyway? (y or n) y
Nastavili sme si breakpoint na funkciu main(). Následne GDB spustí program, zastaví sa pri breakpointe a vypíše informácie o registroch.Prvé štyri registre EAX(accumulator), ECX(counter), EDX(data) a EBX(base) sú nazývané General Purpose Registers(všeobecné registre). Používajú sa na viaceré účely, ale hlavne ako dočasné premenné pre CPU vo fáze vykonávania strojových inštrukcií. Nasledujúce štyri registre ESP(stack pointer), EBP(base pointer), ESI(source index) a EDI(destination index) sú tiež General Purpose Registers. Sú známe aj ako smerníky a indexy. Prvé dva sa nazývajú smerníky, pretože sú v nich uložené 32bit adresy, ktoré ukazujú na umiestnenie v pamäti. Zvyšné dva registre ukazujú odkiaľ sa má čítať a kam sa má zapisovať, čiže technicky sú to tiež smerníky. EIP(instruction pointer) register ukazuje na momentálnu inštrukciu, ktorú procesor číta. EFLAGS register sa skladá z niekoľkých bit-flagov, ktoré sa používajú na porovnávanie a segmentáciu pamäte. Teraz sa trochu zameriame na samotný assembler. Nie som s ním velký kamarát, ale pokúsim sa ho trošku ozrejmiť, nakoľko porozumenie inštrukcií je dôležité pri odhaľovaní zraniteľností. Intel syntax sa riadi štýlom inštrukcia <cieľ>, <zdroj>. Cieľ a zdroj sú buď register, adresa pamäte alebo hodnota. Napríklad inštrukcia mov presunie hodnotu zo zdroja do cieľa, sub bude odčítavať atď. Uvediem si zopár príkladov.
80483af: 89 e5 mov ebp,esp
// inštrukcia mov presunie ESP do EBP80483b1: 51 push ecx
// push uloží ECX do stacku(utriedený zoznam prvkov na princípe LIFO)80483b2: 83 ec 14 sub esp,0x14
// a následne sub odčíta 14 z ESP a výsledok uloží do ESP80483ce: 83 7d f8 09 cmp DWORD PTR [ebp-0x8],0x9
// cmp porovná DWORD hodnotu z EBP mínus 8 s číslom 980483d2: 7e ea jle 80483be <main+0x1a>
// ak je hodnota menšia alebo rovná 9 (jump less then equal),vykonávanie skočí na inštrukciu v 0x80483be80483d4: b8 00 00 00 00 mov eax,0x0
// ak hodnota nie je <=9, presunie sa hodnota 0 do EAX
Použijeme debugger, aby sme si predchádzajúce vedomosti vyskúšali v praxi. Prepínač -g použitý s GCC umožní pridať extra informácie pre debugger, ktoré umožnia debuggeru prístup k zdrojovému kódu, ktorý príkazom (gdb) list
môžeme dať vypísať. Tento príkaz nebudem demonštrovať, vyskúšajte si ho sami.
$ gcc -g -o hello hello.c
$ gdb -q hello
(gdb) disassemble main
Dump of assembler code for function main:
0x080483a4 <main+0>: lea ecx,[esp+0x4]
0x080483a8 <main+4>: and esp,0xfffffff0
0x080483ab <main+7>: push DWORD PTR [ecx-0x4]
0x080483ae <main+10>: push ebp
0x080483af <main+11>: mov ebp,esp
0x080483b1 <main+13>: push ecx
0x080483b2 <main+14>: sub esp,0x14
0x080483b5 <main+17>: mov DWORD PTR [ebp-0x8],0x0
0x080483bc <main+24>: jmp 0x80483ce <main+42>
0x080483be <main+26>: mov DWORD PTR [esp],0x80484b0
0x080483c5 <main+33>: call 0x80482d4 <puts@plt>
0x080483ca <main+38>: add DWORD PTR [ebp-0x8],0x1
0x080483ce <main+42>: cmp DWORD PTR [ebp-0x8],0x9
0x080483d2 <main+46>: jle 0x80483be <main+26>
0x080483d4 <main+48>: mov eax,0x0
0x080483d9 <main+53>: add esp,0x14
0x080483dc <main+56>: pop ecx
0x080483dd <main+57>: pop ebp
0x080483de <main+58>: lea esp,[ecx-0x4]
0x080483e1 <main+61>: ret
End of assembler dump.
(gdb) break main
Breakpoint 1 at 0x80483b5: file hello.c, line 4.
(gdb) run
Starting program: /1/hello
Breakpoint 1, main () at hello.c:4
4 for(i=0;i<10;i++)
(gdb) info register eip
eip 0x80483b5 0x80483b5 <main+17>
(gdb)
Breakpoint sme nastavili na funkciu main() a následne vypíšeme hodnotu EIP, ktorá obsahuje adresu, ktorá ukazuje na zvýraznenú inštrukciu. Inštrukcie nachádzajúce sa pred týmto miestom, sú známe ako prológ a sú generované kompilátorom na vymedzenie pamäťového miesta pre zvyšné lokálne premenné funkcie. Debugger poskytuje priamy spôsob preskúmania pamäte prostredníctvom príkazu x. Tento príkaz umožňuje pozrieť sa na určitú adresu v pamäti. Potrebuje dva argumenty - akým spôsobom má zobraziť informácie a miesto v pamäti na preskúmanie. Formáty sú nasledovné: o (zobraziť v osmičkovej sústave), x (zobraziť v 16kovej sústave), u (zobraziť v unsigned 10kovej sústave), t (zobraziť v binárnej sústave). Vyzerá to nasledovne:
(gdb) x/o $eip
0x80483b5 : 076042707
(gdb) x/x $eip
0x80483b5 : 0x00f845c7
(gdb) x/u $eip
0x80483b5 : 16270791
(gdb) x/t $eip
0x80483b5 : 00000000111110000100010111000111
(gdb)
Pred formátom výpisu možeme použiť číslo na preskúmanie jednotiek na danej adrese. Defaultná veľkosť jednotky je 4bajtová jednotka nazývaná word. Veľkosť zobrazených jednotiek môžeme zmeniť pridaním písmena na koniec formátu: b (bajt), h (halfword, 2bajty), w (word, 4bajty), g (tzv. giant, 8bajtov). Termín word sa používa aj na označenie 2bajtových hodnôt a termín DWORD sa používa na označenie 4bajtových hodnôt. Ukážka na príklade:
(gdb) x/8xb $eip
0x80483b5 <main+17>: 0xc7 0x45 0xf8 0x00 0x00 0x00 0x00 0xeb
(gdb) x/8xh $eip
0x80483b5 <main+17>: 0x45c7 0x00f8 0x0000 0xeb00 0xc710 0x2404 0x84b0 0x0804
(gdb) x/8xw $eip
0x80483b5 <main+17>: 0x00f845c7 0xeb000000 0x2404c710 0x080484b0
0x80483c5 <main+33>: 0xffff0ae8 0xf84583ff 0xf87d8301 0xb8ea7e09
Teraz si výpis pozorne prejdite predtým, ako začnete čítať ďalej. Všimli ste si niečo zaujímavé? Prvý príkaz zobrazí prvých osem bajtov, čím väčšie jednotky zobrazujeme, tým je logicky viac údajov. Prvý výpis obsahuje prvé dva bajty 0xc7 a 0x45. Ale keď si pozrieme halfword na tej iste adrese, dostaneme hodnotu 0x45c7, čiže s prehodenými bajtmi. Podobne si túto reverziu môžeme všimnút pri 4bajtovej hodnote 0x00f845c7. Je to spôsobené tým, že hodnoty na x86 procesoroch sú uložené v tzv. little endian bajtovom usporiadaní, čo znamená, že najmenej významný bajt, alebo least significant byte, je uložený na najnižšej adrese. Na záver prvého dielu sa ešte trošku pohráme s debuggerom. Ten nám umožňuje použitím formátovacieho znaku i vypísať obsah pamäte ako inštrukciu assemblera:
(gdb) x/i $eip
0x80483b5 <main+17>: mov DWORD PTR [ebp-0x8],0x0
Ak si pozriete znova výstup objdumpu, zistíte, že 7bajtov, na ktoré EIP ukazuje, je príslušná inštrukcia assemblera v strojovom kóde. Inštrukcia presunie hodnotu 0 do pamäte, ktorá sa nachádza na adrese uloženej v EBP mínus 8. Presne tu je uložená naša premenná i z funkcie main(). Táto inštrukcia v podstate vynuluje premennú i pre cyklus.
(gdb) info register ebp
ebp 0xbf9758e8 0xbf9758e8
(gdb) print $ebp-8
$1 = (void *) 0xbf9758e0
(gdb) x/xw $1
0xbf9758e0: 0xb7f6b250
EBP obsahuje adresu 0xbf9758e8 a inštrukcia zapíše do hodnoty o 8 nižšej 0xbf9758e0. Príkaz print uloží výsledok operácie do dočasnej premennej $1, ku ktorej môžeme neskôr pristupovať. Posuňme sa ďalej v našom programe. Príkaz nexti vypíše ďalšiu inštrukciu. CPU prečíta inštrukciu v EIP, vykoná ju a posunie EIP na ďalšiu inštrukciu.
(gdb) nexti
0x080483bc 4 for(i=0;i<10;i++)
(gdb) x/dw $1
0xbf9758e0: 0
// dôkaz o vynulovaní miesta v pamäti ebp-8
Prejdime si ďalšie inštrukcie, z ktorých nám pomaly bude jasnejšia postupnosť a logika inštrukcií.
(gdb) x/12i $eip
0x80483bc <main+24>: jmp 0x80483ce <main+42>
0x80483be <main+26>: mov DWORD PTR [esp],0x80484b0
0x80483c5 <main+33>: call 0x80482d4 <puts@plt>
0x80483ca <main+38>: add DWORD PTR [ebp-0x8],0x1
0x80483ce <main+42>: cmp DWORD PTR [ebp-0x8],0x9
0x80483d2 <main+46>: jle 0x80483be <main+26>
0x80483d4 <main+48>: mov eax,0x0
0x80483d9 <main+53>: add esp,0x14
0x80483dc <main+56>: pop ecx
0x80483dd <main+57>: pop ebp
0x80483de <main+58>: lea esp,[ecx-0x4]
0x80483e1 <main+61>: ret
Nasledujúca inštrukcia jmp je tzv. unconditional jump na adresu 0x80483ce. Na danej adrese máme inštrukciu cmp, ktorá porovnáva našu premennú i s číslom 9. Nasleduje podmienka "jump if less or equal". Ak je podmienka i<=9 splnená, skočí na adresu 0x80483be, ktorá sa nachádza za fixným jumpom. Inštrukcia call zavolá puts() funkciu, add pripočíta k adrese uloženej v ebp-8 hodnotu 1 a následne sa ide opäť porovnávať. Ak podmienka nebude splnená, vykoná sa inštrukcia mov eax,0x0, ktorá presunie hodnotu 0 do EAX na ukončenie programu. Zvyšné inštrukcie sú tzv. epilóg, ktorý zruší kroky, ktoré vykonal prológ (sub esp,0x14/add esp,0x14 ; push ecx/pop ecx atď).
Nakoľko vieme,že na adrese, ktorá sa porovnáva s 9 je uložená 0, EIP by mala ukazovať na 0x80483be po vykonaní ďalšich dvoch inštrukcií.
(gdb) nexti
0x080483d2 4 for(i=0;i<10;i++)
(gdb) nexti
5 puts("Hello World!");
(gdb) info register eip
eip 0x80483be 0x80483be <main+26>
(gdb) x/i $eip
0x80483be : mov DWORD PTR [esp],0x80484b0
Inštrukcia mov zapíše adresu 0x80484b0 do pamäťovej adresy v ESP, ale kam ESP ukazuje, zistíme nasledovne:
(gdb) info register esp
esp 0xbf9758d0 0xbf9758d0
ESP momentálne ukazuje na adresu 0xbf9758d0, takže po vykonaní inštrukcie mov sa tam zapíše adresa 0x80484b0. Je len na nás, aby sme zistili, prečo je táto adresa zaujímavá.
(gdb) x/8xb 0x80484b0
0x80484b0: 0x48 0x65 0x6c 0x6c 0x6f 0x20 0x57 0x6f
(gdb) x/8ub 0x80484b0
0x80484b0: 72 101 108 108 111 32 87 111
Ak sa pozrieme do ASCII tabuľky, zistíme, že naše bajty korešpondujú s konkrétnymi znakmi. Použijeme ďalšie formátovacie znaky c, ktorý vyhľadá bajt v ASCII tabuľke a s, ktorý nám zobrazí celý reťazec:
(gdb) x/8cb 0x80484b0
0x80484b0: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 87 'W' 111 'o'
(gdb) x/s 0x80484b0
0x80484b0: "Hello World!"
Výstup príkazu nám ukazuje, že na adrese 0x80484b0 sa nachádza reťazec Hello World!.
V druhej časti seriálu, ktorý bude menej nezáživný, sa budeme zaoberať segmentáciou pamäte a pripravíme sa na techniky exploitovania, ktoré si predstavíme v tretej časti seriálu.
Nie prilis jednoduche veci, podane dost zrozumitelnym jazykom ...
Vdaka !
Základy assembleru získať veľmi ľahko, návody sú všade. Ak ťa zaujíma napr. reverzné inžinierstvo, tak si snáď ochotný obetovať trochu času assembleru bez toho, aby bol ten tutorial na linuxos
Dobrym introm do GNU debugeru je zase tato kniha.
Inak musim napisat, ze mnou uvedene linky su omnoho zrozumitelnejsie ako tento clanok.
Jenom bych doplnil, že se nejedná o ukazatel na aktuálně prováděnou instrukci, ale na tu následující po aktuálně prováděné!
PS: sedem * štyri, takový těžký počty v ochraně proti spamu ... co třeba jedna + jedna?
Nie, instrikcia mov kopiruje.