Vytvoření vlastního Shellcodu

V minulých článcích jsme se zabývali limitací prostoru a velmi omezenou dostupnou sadou znaků. Zmíněná omezení, na první pohled, celkem efektivně bránily k použití volně dostupných shellcodů – vygenerovaných např. pomocí nástroje msfvenom. V dnešním článku se (opět) nebudeme zaměřovat na obcházení bezpečnostních ochran, ale popíšeme si způsob, jak vytvořit vlastní shellcode.

Prerekvizity:

  • Základní znalost tvorby exploitů
  • Základní znalost stacku, registrů, assembler instrukcí a opcodes
  • Základní znalost práce s nástrojem WinDbg

Systémové volání

Prakticky v každém shellcodu se setkáme s nějakým druhem systémového volání (syscalls). Jedná se o sadu funkcí, které poskytují „rozhraní“ mezi kernelem operačního systému a uživatelským prostorem – rozhraní tedy umožňuje přístup k funkcím operačního systému používané např. pro synchronizaci vláken, čtení / zápis souboru, správu síťových spojení atd., aniž by byl kompromitován / ohrožen samotný operační systém.

Účel shellcodu je ve své podstatě provést libovolné operace (nejčastěji navázat zpětné spojení), které nejsou součástí původní logiky aplikace – po úspěšné exploitaci zranitelnosti, typicky typu Buffer Overflow. K provolání syscalls se využívají tzv. Windows Native API skrz knihovnu ntdll.dll, bohužel však velmi často chybí jakákoliv dokumentace. Další možností, jak provolávat „funkce kernelu“ je prostřednictvím Windows API, které jsou exportovány v rámci načtených DLL knihoven v paměti daného procesu – nejprve potřebnou funkci lokalizujeme a následně ji můžeme v shellcodu využít.

Knihovna kernel32.dll, která je prakticky vždy v paměti procesu načtena (exportuje základní funkce vyžadované většinou procesů), obsahuje funkce LoadLibraryA – umožňuje načíst libovolnou knihovnu, a GetProcessAddress – vrátí adresu exportované knihovní funkce. Zmíněná knihovna je tedy velmi vhodný kandidát při tvorbě shellcodu. Nejprve však musíme zjistit její „base adresu“ – běžně používaná metoda, kterou si v článku ukážeme, spoléhá na Process Environmental Block (PEB) strukturu.

PEB Struktura

Datová struktura PEB je alokovaná operačním systémem pro každý proces a její ukazatel se nachází (v případě 32-bit) na offsetu 0x30 ve struktuře Thread Environment Block (TEB). Pointer adresy TEB struktury je uložen ve FS registru.

0:004> dt nt!_TEB @$teb
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : (null) 
   +0x02c ThreadLocalStoragePointer : 0x00e17280 Void
   +0x030 ProcessEnvironmentBlock : 0x008ff000 _PEB
...

0:004> dt nt!_PEB 0x008ff000
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00fa0000 Void
   +0x00c Ldr              : 0x77675d80 _PEB_LDR_DATA
...

Struktura PEB obsahuje spoustu informací, nás nejvíce zajímá _PEB_LDR_DATA, neboť obsahuje tři spojové seznamy obsahující informaci o načtených modulech, které jsou namapovány v paměti procesu.

0:004> dt _PEB_LDR_DATA 0x77675d80
ntdll!_PEB_LDR_DATA
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0xdf4d90 - 0xe04078 ]
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0xdf4d98 - 0xe04080 ]
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0xdf4cb8 - 0xe04088 ]
   +0x024 EntryInProgress  : (null) 
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null)

0:004> dt _LIST_ENTRY (0x77675d80 + 0x1c)
ntdll!_LIST_ENTRY
 [ 0xdf4cb8 - 0xe04088 ]
   +0x000 Flink            : 0x00df4cb8 _LIST_ENTRY [ 0xdf26c8 - 0x77675d9c ]
   +0x004 Blink            : 0x00e04088 _LIST_ENTRY [ 0x77675d9c - 0xe036e8 ]

Pole Flink a Blink představují následující, respektive předchozí „záznam“ ve spojovém seznamu. Navíc, struktura _LIST_ENTRY v _PEB_LDR_DATA je součástí větší struktury _LDR_DATA_TABLE_ENTRY_, která již obsahuje informace, které potřebujeme – název modulu a „base adresu“ (začátek struktury _LDR_DATA_TABLE_ENTRY se nachází na zápornému offsetu 0x10):

0:004> dt _LDR_DATA_TABLE_ENTRY (0x00df4cb8 - 0x10)
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0xdf4f08 - 0xdf4d90 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xdf4f10 - 0xdf4d98 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0xdf26c8 - 0x77675d9c ]
   +0x018 DllBase          : 0x77550000 Void
   +0x01c EntryPoint       : (null) 
   +0x020 SizeOfImage      : 0x1a4000
   +0x024 FullDllName      : _UNICODE_STRING "C:\WINDOWS\SYSTEM32\ntdll.dll"
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"
...

Oficiální dokumentace uvádí, že _UNICODE_STRING struktura má proměnou Buffer obsahující ukazatel na příslušný řetězec znaků na offsetu 0x04, pro potřeby shellcodu název DLL knihovny tedy začíná na offsetu 0x30 (0x2c + 0x04).

Shellcode Assembly

Pro vytvoření shellcodu použijeme Keystone Framework a knihovnu CTypes, díky čemuž se spustí kód přímo v paměti procesu python.exe prostřednictvím Windows API callů. Python skript využije framework a knihovnu k:

  • Přeměně ASM kódu do opcodes
  • Alokaci paměti pro shellcode
  • Zkopírování shellcodu do paměti
  • Spuštění shellcodu v paměti

V následujících řádcích si ukážeme jak realizovat výše popsaný způsob s PEB strukturou k nalezení „base adresy“ knihovny kernel32.dll. Nejprve vytvoříme kostru skriptu:

import ctypes, struct
from keystone import *

# proměnná obsahující ASM instrukce
ASMCODE = (
)

try:
    # inicializace enginu v X86-32bit módu
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    
    # kompilace instrukcí a uložení jako bytové pole
    encoding, count = ks.asm(ASMCODE)
    print("Pocet instrukci: %d" % count)

    sh = b""
    for e in encoding:
        sh += struct.pack("B", e)
    shellcode = bytearray(sh)

    # alokace paměti, zkopírování shellcodu do nově alokované paměti a spuštění
    ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),ctypes.c_int(len(shellcode)),ctypes.c_int(0x3000),ctypes.c_int(0x40))
    buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
    ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),buf,ctypes.c_int(len(shellcode)))

    # pozastavení běhu programu k připojení procesu do WinDbg
    input("Pro pokracovani stiskni libovolnou klavesu")
    th = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),ctypes.c_int(0),ctypes.c_int(ptr),ctypes.c_int(0),ctypes.c_int(0),ctypes.pointer(ctypes.c_int(0)))
    ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(th), ctypes.c_int(-1))

except KsError as e:
    print("ERROR: %s" %e)
    count = e.get_asm_count()
    if count is not None:
        print("asmcount = %u" %e.get_asm_count())

Následně do proměnné ASMCODE začneme přidávat kód shellcodu. Na začátek vložíme instrukci int3 (softwarový breakpoint), který nám usnadní hledání shellcodu v paměti – není potřeba vypisovat adresu alokované paměti a nastavovat breakpoint ručně při každém spuštění skriptu. Poté uložíme do registru EBP hodnotu registru ESP a odečteme „libovolný“ offset, který „nerozbije“ zásobník.

" start:                     "
"   int3                    ;" 
"   mov ebp, esp            ;" 
"   sub esp, 0x200          ;"

Funkce find_kernel32 nejprve vynuluje registr ECX, který využije společně s offsetem 0x30 k uložení ukazatele na strukturu PEB do registru ESI. Dále je hodnota registru ESI posunuta o offset 0x0C (_PEB_LDR_DATA) a na závěr o dalších 0x1C, díky čemuž registr ESI obsahuje ukazatel na InInitializationOrderModuleList.

" find_kernel32:             "
"   xor ecx, ecx            ;" 
"   mov esi,fs:[ecx+0x30]   ;" # &PEB
"   mov esi,[esi+0x0C]      ;" # PEB->Ldr
"   mov esi,[esi+0x1C]      ;" # PEB->Ldr.InInitOrderModuleList

Druhá funkce next_module slouží k nalezení knihovny kernel32.dll. Nejprve se do registru EBX uloží „base adresa“ modulu, do registru EDI jméno modulu a do registru ESI ukazatel na další záznam v seznamu. Instrukce CMP porovnává, zda na 24. pozici jména je ukončovací NULL (porovnání délky řetězce). Délka názvu hledané knihovny kernel32.dll je 12 znaků, ale řetězec je uložen v UNICODE formátu (každý znak bude reprezentován typu WORD) a tudíž délka řetězce v UNICODE je 24. Pokud není nalezen hledaný řetězec, podmíněný skok znovu provolá funkci next_module a provede porovnání na následujícím záznamu. Při vyhledávání knihovny kernel32.dll využíváme faktu, že seznam InInitializationOrderModuleList uchovává záznamy v pořadí, v kterém byly načteny, tudíž první název se správnou délkou bude vždy knihovna kernel32.dll (načítá se mezi prvními).

" next_module:               " 
"   mov ebx, [esi+8h]       ;" # InInitOrderModuleList.base_address
"   mov edi, [esi+20h]      ;" # InInitOrderModuleList.module_name
"   mov esi, [esi]          ;" # InInitOrderModuleList.flink (další záznam)
"   cmp [edi+12*2], cx      ;" 
"   jne next_module         ;" 
"   ret                     ;"

Nalezení „base adresy“ knihovny kernel32.dll je dobrý začátek, dalším krokem je vyhledání a použití API, které modul exportuje. Pro ukázku zvolíme TerminateProcess symbol („symbol“ v tomto kontextu znamená jméno funkce a adresa v paměti), aby bylo možné náš shellcode legitimně ukončit, procházením Export Address Table (EAT) knihovny.

Každá knihovna, která exportuje funkce má tzv. Export Directory Table (EDT) obsahující základní informace o symbolech – Relative Virtual Address (RVA) funkcí, RVA jméno, atd. Pro rozlišení symbolů využijeme vztahu mezi následujícími třemi poli v EDT struktuře: AddressOfFunctions, AddressOfNames, a AddressOfNameOrdinals. K nalezení symbolu podle jména využijeme pole AddressOfNames. Každý symbol má unikátní jméno a index, pokud nalezneme požadovaný symbol na indexu i, můžeme stejný index i použít i v poli AddressOfNameOrdinals. Záznam na indexu i v poli AddressOfNameOrdinals obsahuje hodnotu, která bude sloužit jako nový index v poli AddressOfFunctions, díky čemuž nalezneme relativní adresu paměti hledané funkce. Připočtením „base adresy“ knihovny získáme plně funkční Virtual Memory Address (VMA).

Do shellcodu přidáme tři nové funkce, které vyhledají potřebný symbol: find_function, find_function_loop a find_function_finished. Po nalezení „base adresy“ knihovny kernel32.dll instrukcí PUSHAD vložíme hodnoty registrů na zásobník. Do registru EAX vložíme „base adresu“ knihovny, ke které přičteme offset 0x3C (offset k PE header). Abychom získali RVA z Export Directory Table přičteme k předchozí hodnotě další offset 0x78 a uložíme do registru EDI, následně přičteme „base adresu“, a tím získáme VMA.

" find_function:             "
"   pushad                  ;" 
"   mov eax, [ebx+0x3c]     ;" # Offset k PE Signature
"   mov edi, [ebx+eax+0x78] ;" # Export Table Directory RVA
"   add edi, ebx            ;" # Export Table Directory VMA

Do registru ECX uložíme hodnotu NumberOfNames, která udává počet exportovaných symbolů přidáním offsetu 0x18 (registr ECX bude sloužit jako počitadlo). Do registru EAX nejprve uložíme pole AddressOfNames (offset 0x20) a následně přičteme „base adresu“. Na závěr uložíme nalezenou funkci na zásobník s využitím registru EBP (na začátku jsme do tohoto registru uložili ukazatel na zásobník).

"   mov ecx, [edi+0x18]     ;" # NumberOfNames
"   mov eax, [edi+0x20]     ;" # AddressOfNames RVA
"   add eax, ebx            ;" # AddressOfNames VMA
"   mov [ebp-4], eax        ;"

Jak již bylo řečeno, registr ECX slouží jako počítadlo, pokud dosáhne hodnoty NULL (požadovaný symbol nebyl nalezen), provede se podmíněný skok do funkce find_function_finished. Zároveň se registr ECX využije jako index v poli AddressOfNames – jelikož každá položka v poli je typu DWORD, je potřeba index vynásobit 4. Následně do registru ESI uložíme VMA symbolu připočtením „base adresy“.

" find_function_loop:        " 
"   jecxz find_function_finished ;" 
"   dec ecx                 ;" 
"   mov eax, [ebp-4]        ;" # AddressOfNames VMA
"   mov esi, [eax+ecx*4]    ;" # Symbol RVA
"   add esi, ebx            ;" # VMA aktuálního symbolu

" find_function_finished:    " 
"   popad                   ;" # Obnova registrů
"   ret                     ;"

Hashovací funkce

K nalezení požadovaného symbolu – v našem případě TerminateProcess – nebudeme využívat délku řetězce (jako v případě nalezení knihovny kernel32.dll), nýbrž osvědčenou jednoduchou hashovací funkci. Hashovací funkce převede řetězec (název symbolu) na unikátní DWORD. Do shellcodu přidáme další tři funkce: compute_hash, compute_hash_again a compute_hash_finished.

Ve funkci compute_hash nejprve vynulujeme registr EAX a EDX (instrukcí CDQ), na závěr provoláním instrukce CLD dojde k vymazání příznaku DF v registru EFLAGS. Provedení této instrukce způsobí, že všechny operace s řetězci zvýší indexové registry, což jsou ESI (kde je uložen název symbolu) a/nebo EDI.

" compute_hash:              " 
"   xor eax, eax            ;" 
"   cdq                     ;" 
"   cld                     ;"

Následuje funkce compute_hash_again, kde se nejprve provolá instrukce LODSB – do registru AL se načte jeden bajt z paměti, na kterou ukazuje registr ESI, a poté automaticky inkrementuje nebo dekrementuje registr podle příznaku DF. Další instrukce TEST ověří, zda jsme již na konci řetězce (název symbolu) a případně se provede podmíněny skok do funkce compute_hash_finished. Tato funkce neobsahuje žádné instrukce, slouží pouze k ukončení hashování. Pokud není proveden podmíněný skok následuje instrukce ROR, která posune bity registru EDX o 13 bitů doprava. Na závěr připočteme hodnotu registru EAX (obsahuje bajt názvu daného symbolu) do registru EDX a provedeme skok do funkce comute_hash_again. Tato funkce představuje smyčku, která projde každý bajt názvu symbolu a přidá jej do „akumulátoru“ (registr EDX) po provedení rotace bitů doprava. Jakmile narazíme nakonec jména symbolu, registr EDX bude obsahovat unikátní čtyř bajtový hash.

" compute_hash_again:        " 
"   lodsb                   ;" # Načtení bajtu z ESI do AL
"   test al, al             ;" 
"   jz compute_hash_finished;" # Pokud je příznak nastavený, jsme na konci řetězce
"   ror edx, 0x0d           ;" # Rotace o 13 bitů doprava
"   add edx, eax            ;" # Přidaní dalšího bajtu do akumulátoru
"   jmp compute_hash_again  ;" # 

" compute_hash_finished:     "

Nejjednodušší způsob zjištění, jakou hodnotu hash bude vlastně mít, je naprogramovat stejnou hashovací funkci a spustit ji z příkazové řádky, případně vyčtením hodnoty v debuggeru za běhu programu (není součástí článku).

K porovnání, zda se hash v registru EDX shoduje s požadovanou hodnotou přidáme další funkci find_function_compare. Pokud se hash bude shodovat, můžeme též rovnou využít index uložený v registru ECX v poli AddressOfNameOrdinals a následně získat RVA, respektive VMA dané funkce.

Terminate Process

Nejprve se podíváme na parametry, které funkce TerminateProcess očekává:

BOOL TerminateProcess(
  [in] HANDLE hProcess,
  [in] UINT   uExitCode
);

Parametr uExitCode je výstupní kód, kdy hodnota 0 značí úspěšné ukončení běhu a parametr hProcess je „handler“ na proces, který má být ukončen. U druhého zmíněno parametru využijeme „pseudo-handleru“, kdy hodnota -1 reprezentuje aktuální proces.

Nejdříve je potřeba upravit funkci start. Hodnota hashe názvu symbolu TerminateProcess je 0x78b5b983. Před zavoláním funkce find_function vložíme zmíněnou hodnotu na zásobník, aby bylo snadné ji později dohledat a porovnávat s vypočítaným hashem názvu symbolu (ve funkci compute_hash_again). Následně na zásobník vložíme oba parametry funkce TerminateProcess, tedy 0 a -1. Poslední přidaná instrukce provede nepřímé volání TerminateProcess prostřednictvím registru EAX, který bude obsahovat VMA dané funkce.

" start:                     "
"   int3                    ;" 
"   mov ebp, esp            ;" 
"   sub esp, 0x200          ;" 
"   call find_kernel32      ;" 
"   push 0x78b5b983         ;" # TerminateProcess hash
"   call find_function      ;" 
"   xor ecx, ecx            ;" 
"   push ecx                ;" # uExitCode
"   push 0xffffffff         ;" # hProcess
"   call eax                ;" # call TerminateProcess

Funkce find_function_compare nejprve provede porovnání zda aktuálně vypočítaný hash názvu symbolu se shoduje s dříve vloženou hodnotou na zásobník (0 x78b5b983). Offset 0x24 se bude lišit podle počtu provedených PUSH / POP instrukcí – přesnou hodnotu lze získat za běhu programu z debuggeru. Pokud se hodnota neshoduje, provede se podmíněný skok do funkce find_function_loop (další záznam v poli AddressOfNames). Jakmile najdeme správný záznam, nejprve z Export Directory Table na offsetu 0x24 zjistíme VMA pole AddressOfNameOrdinals a připočteme „base adresu“. Následně využijeme stejný index (registr ECX) vynásobený 2 – neboť každá položka je typu WORD a uložíme výsledek do registru CX, čímž získáme daný záznam z pole AddressOfNameOrdinals. Následně do registru EDX vložíme nejprve RVA pole AddressOfFunctions (offset 0x1c z Export Directory Table), po přičtení „base adresy“ získáme VMA. Posledním krokem je získaní adresy hledané funkce (v našem případě TerminateProcess) s využitím nového indexu (stále registry ECX) v poli AddressOfFunctions a přepsání hodnoty registru EAX na zásobníku (původně zazálohovaný pomocí instrukce PUSHAD).

" find_function_compare:     " #
"   cmp edx, [esp+0x24]     ;" # Porovná hash s hodnotou na zásobníku
"   jnz find_function_loop  ;" # Pokud se neshoduje, provede se skok - find_function_loop
"   mov edx, [edi+0x24]     ;" # AddressOfNameOrdinals RVA
"   add edx, ebx            ;" # AddressOfNameOrdinals VMA
"   mov cx, [edx+2*ecx]     ;" 
"   mov edx, [edi+0x1c]     ;" # AddressOfFunctions RVA
"   add edx, ebx            ;" # AddressOfFunctions VMA
"   mov eax, [edx+4*ecx]    ;" # RVA funkce
"   add eax, ebx            ;" # VMA funkce
"   mov [esp+0x1c], eax     ;" # Přepsání registru EAX na zásobníku (z PUSHAD)

Nyní připojíme skript s shellcodem k debuggeru, abychom zkontrolovali, zda vše funguje správně.

Nalezení knihovny kernel32.dll a ověření, že se adresa shoduje se systémovou:

0:004> u @eip Ld
0153001f 31c9            xor     ecx,ecx
01530021 648b7130        mov     esi,dword ptr fs:[ecx+30h]
01530025 8b760c          mov     esi,dword ptr [esi+0Ch]
01530028 8b761c          mov     esi,dword ptr [esi+1Ch]
0153002b 8b5e08          mov     ebx,dword ptr [esi+8]
0153002e 8b7e20          mov     edi,dword ptr [esi+20h]
01530031 8b36            mov     esi,dword ptr [esi]
01530033 66394f18        cmp     word ptr [edi+18h],cx
01530037 75f2            jne     0153002b
01530039 c3              ret
0153003a 60              pushad
0153003b 8b433c          mov     eax,dword ptr [ebx+3Ch]
0153003e 8b7c0378        mov     edi,dword ptr [ebx+eax+78h]
0:004> bp 01530039
0:004> g
Breakpoint 1 hit
eax=031efb34 ebx=75a60000 ecx=00000000 edx=01530000 esi=00babc50 edi=00ba2470
eip=01530039 esp=031ef8d8 ebp=031efadc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
01530039 c3              ret
0:004> r @ebx
ebx=75a60000
0:004> du @edi
00ba2470  "KERNEL32.DLL"
0:004> lm m kernel32
Browse full module list
start    end        module name
75a60000 75b50000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\wkernel32.pdb\072DDECD0AE548D4D8D92DBF651598441\wkernel32.pdb

Ověření, zda registr EAX odkazuje na symbol TerminateProcess:

0:004> u @eip L30
...
01530083 8944241c        mov     dword ptr [esp+1Ch],eax
01530087 61              popad
01530088 c3              ret
...
0:004> bp 01530087
0:004> g
Breakpoint 2 hit
eax=75a79910 ebx=75a60000 ecx=00000592 edx=75af2ed8 esi=75affb2b edi=75af2eb0
eip=01530087 esp=031ef8b4 ebp=031efadc iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
01530087 61              popad
0:004> u @eax
KERNEL32!TerminateProcessStub:
75a79910 8bff            mov     edi,edi
75a79912 55              push    ebp
75a79913 8bec            mov     ebp,esp
75a79915 5d              pop     ebp
75a79916 ff250c15ae75    jmp     dword ptr [KERNEL32!_imp__TerminateProcess (75ae150c)]
75a7991c cc              int     3
75a7991d cc              int     3
75a7991e cc              int     3

Zavolání symbolu TerminateProcess s předanými parametry:

...
0:004> t
eax=75a79910 ebx=75a60000 ecx=00000000 edx=01530000 esi=00babc50 edi=00ba2470
eip=0153001d esp=031ef8d0 ebp=031efadc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
0153001d ffd0            call    eax {KERNEL32!TerminateProcessStub (75a79910)}
0:004> dds @esp L2
031ef8d0  ffffffff
031ef8d4  00000000
0:004> p
WARNING: Step/trace thread exited
...

Jak jsme právě ověřili, po doběhnutí našeho shellcodu je proces řádně ukončen. V dnešním článku jsme si ukázali základy, jak vytvářet shellcode, metodu PEB struktury k nalezení libovolného načteného modulu (DLL knihovny), nalezení adresy paměti exportované funkce a její provolání včetně předání potřebných parametrů.