Shellcode: PEB i adres bazowy modułu kernel32.dll

Pisząc jakieś shellkody lub inne tego typu paskudztwa napotyka się na problem interakcji z systemem lub jego API. Aby cokolwiek zrobić sensownego wymagany jest dostęp do kilku kluczowych funkcji znajdujących się w kernel32.dll, będących niejako kluczem do świata systemu. Takimi funkcjami są oczywiście LoadLibrary/GetModuleHandle, GetProcAddress, itp… Mając dostęp do tych funkcji możemy zrobić praktycznie wszystko i wykorzystać dowolny kod z innych modułów.

Nawet jeśli chce się wstrzyknąć tylko dll-kę importującą niezbędne i używane przez siebie rzeczy, to wymagany jest jakiś sensowny kod loadera, który także musi zostać wstrzyknięty do procesu. A ten loader jakimś cudem musi znać adres bazowy modułu kernel32 i offset do funkcji LoadLibrary, aby móc bezproblemowo załadować wstrzykiwany moduł do danego procesu.

O ile w czasach pre-Vista/Win7 i pre-ASLR, kiedy to moduły systemowe zawsze ładowane był w to samo miejsce w przestrzeni adresowej procesu, albo były randomizowane per reboot systemu, to adresy tych samych funkcji w różnych procesach były takie same. W takich wypadach wystarczyło, aby to caller (wstrzykujący) pobrał standardowymi metodami adres do danej funkcji z własnej przestrzeni i taki użył we wstrzykiwanym kodzie.

Czasy się zmieniały, dodawane nowe rozwiązania i zabezpieczenia miały teoretycznie na celu ukrócić takie zabawy. Ale w praktyce to za wiele się nie zmieniło, no może prócz potrzeby stworzenia większego fragmentu kodu ;)

Istnieje kilka metod determinacji adresu bazowego (ImageBase) pod którym w pamięci znajduje się kod modułu. Najbardziej popularnym sposobem jest skorzystanie z danych zawartych w strukturze PEB procesu. Można tam znaleźć pełną listę załadowanych do przestrzeni adresowej procesu modułów.

Dane te zawarte są w strukturze PEB_LDR_DATA używane przez funkcje loadera (Ldr*) w ntdll.

0:003> dt _PEB
ntdll!_PEB
   ...
   +0x00c Ldr              : Ptr32 _PEB_LDR_DATA
   ...
 
0:003> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr32 Void
   +0x00c InLoadOrderModuleList : _LIST_ENTRY
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY
   +0x024 EntryInProgress  : Ptr32 Void
   +0x028 ShutdownInProgress : UChar
   +0x02c ShutdownThreadId : Ptr32 Void

Istotne są tutaj dwukierunkowe listy LIST_ENTRY, zawierające, w odpowiedniej kolejności, odwołania (wskaźniki) do danych modułu opisanych strukturą LDR_MODULE (windbg widzi je jako LDR_DATA_TABLE_ENTRY).

0:003> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x008 InMemoryOrderLinks : _LIST_ENTRY
   +0x010 InInitializationOrderLinks : _LIST_ENTRY
   +0x018 DllBase          : Ptr32 Void
   +0x01c EntryPoint       : Ptr32 Void
   +0x020 SizeOfImage      : Uint4B
   +0x024 FullDllName      : _UNICODE_STRING
   +0x02c BaseDllName      : _UNICODE_STRING
   +0x034 Flags            : Uint4B
   +0x038 LoadCount        : Uint2B
   +0x03a TlsIndex         : Uint2B
   +0x03c HashLinks        : _LIST_ENTRY
   +0x03c SectionPointer   : Ptr32 Void
   +0x040 CheckSum         : Uint4B
   +0x044 TimeDateStamp    : Uint4B
   +0x044 LoadedImports    : Ptr32 Void
   +0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
   +0x04c PatchInformation : Ptr32 Void
   +0x050 ForwarderLinks   : _LIST_ENTRY
   +0x058 ServiceTagLinks  : _LIST_ENTRY
   +0x060 StaticLinks      : _LIST_ENTRY
   +0x068 ContextInformation : Ptr32 Void
   +0x06c OriginalBase     : Uint4B
   +0x070 LoadTime         : _LARGE_INTEGER

Do którejś wersji (bodajże XP/Vista) niektóre (interesujące) systemowe moduły były ładowane w takiej samej kolejności, czyli były dostępne pod tym samym offsetem (indeksem) na liście. Wygaldało to mniej więcej tak:

; XP/pre-Win7
  LoadOrder: proc, ntdll, kernel32, ...
MemoryOrder: proc, ntdll, kernel32, ...
  InitOrder: ntdll, kernel32, ...

I z tego faktu korzystało dosyć dużo kodu. W 7-mce pojawił się kernelbase.dll, doszło do przetasowań i trochę się namieszało. Ciekawy jestem jak to wygląda pod tym właśnie systemem. Sprawdźmy na 32-bitowym Kalkulatorze ;)

0:003> !peb
PEB at 7efde000
    ...
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList: 005c53d8 . 005e97b0
    Ldr.InLoadOrderModuleList:           005c5348 . 005e97a0
    Ldr.InMemoryOrderModuleList:         005c5350 . 005e97a8
            Base TimeStamp                     Module
          b60000 4ce7979d Nov 20 10:40:45 2010 C:\Windows\SysWOW64\calc.exe
        77cf0000 58bf8715 Mar 08 05:22:45 2017 C:\Windows\SysWOW64\ntdll.dll
        75690000 58bf87ba Mar 08 05:25:30 2017 C:\Windows\syswow64\kernel32.dll
        77420000 58bf87bb Mar 08 05:25:31 2017 C:\Windows\syswow64\KERNELBASE.dll
    ...

Korzystając z makra !list można w windbg „przelecieć” po liście LIST_ENTRY. Trzeba pamiętać, że poszczególne elementy listy wskazują docelowo na „swojego” rodzaju listę zawartą w strukturze LDR_DATA_TABLE_ENTRY, dlatego trzeba odliczyć offset, aby dostać się na początek danych tejże struktury.

Początek listy InLoadOrderModuleList zawiera moduły:

0:003> !list -t ntdll!_LIST_ENTRY.Flink -x "dt _LDR_DATA_TABLE_ENTRY @$extret" 005c5348
ntdll!_LDR_DATA_TABLE_ENTRY
   ...
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\SysWOW64\calc.exe"
   +0x02c BaseDllName      : _UNICODE_STRING "calc.exe"
   ...
 
ntdll!_LDR_DATA_TABLE_ENTRY
   ...
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\SysWOW64\ntdll.dll"
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"
   ...
 
ntdll!_LDR_DATA_TABLE_ENTRY
   ...
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\syswow64\kernel32.dll"
   +0x02c BaseDllName      : _UNICODE_STRING "kernel32.dll"
   ...
 
ntdll!_LDR_DATA_TABLE_ENTRY
   ...
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\syswow64\KERNELBASE.dll"
   +0x02c BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   ...

Lista InMemoryOrderModuleList:

0:003> !list -t ntdll!_LIST_ENTRY.Flink -x "dt _LDR_DATA_TABLE_ENTRY @$extret-8" 005c5350
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "calc.exe"
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "kernel32.dll"
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   ...

Lista InInitializationOrderModuleList:

0:003> !list -t ntdll!_LIST_ENTRY.Flink -x "dt _LDR_DATA_TABLE_ENTRY @$extret-10" 005c53d8
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   ...
   +0x02c BaseDllName      : _UNICODE_STRING "kernel32.dll"
   ...

Podobnie wygląda to w 64-bitowej wersji systemu.

Aby upewnić się w jakiej kolejności ładowane są te moduły, należy zerknąć do kodu inicjalizującego proces w user-space. Dzieje się to w LdrpInitializeProcess (ntdll.dll), tam można znaleźć ciekawe fragmenty kodu przedstawione poniżej. Wynika z nich, że najpierw tworzona i wypełniana jest struktura LDR_DATA_TABLE_ENTRY dla bieżącego procesu i za pomocą funkcji LdrpInsertDataTableEntry dodawana jest do listy InLoadOrderModuleList i InMemoryOrderModuleList. Nastepnie to samo dzieje się dla znajdującego się już w pamięci modułu ntdll. A nieco później ładowane są standardowymi funkcjami, moduły kernel32 i kernelbase.

; UNICODE_STRING defs for module names in _LdrpInitializeProcess@8 body
...
.text:7DEB66FC _NtDllName      dw 12h 
.text:7DEB66FE                 dw 14h
.text:7DEB6700 off_7DEB6700    dd offset aNtdll_dll_0  ; "ntdll.dll"
...
.text:7DEB670C word_7DEB670C   dw 18h
.text:7DEB670E                 dw 1Ah
.text:7DEB6710                 dd offset aKernel32_dll ; "kernel32.dll"
...
.text:7DEF14C0 word_7DEF14C0   dw 1Ch
.text:7DEF14C2                 dw 1Eh
.text:7DEF14C4                 dd offset aKernelbase_dll ; "kernelbase.dll"
...
; create and insert LDR_MODULE for proc
.text:7DEB6419     call _LdrpAllocateDataTableEntry@4 ; LdrpAllocateDataTableEntry(x)
.text:7DEB641E     mov  _LdrpImageEntry, eax
...
.text:7DEB64DA     push eax
.text:7DEB64DB     mov  [eax+30h], ecx              ; LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
.text:7DEB64DE     call _LdrpInsertDataTableEntry@4 ; LdrpInsertDataTableEntry(x)
...
; create and insert LDR_MODULE for ntdll
.text:7DEB64E9     call _LdrpAllocateDataTableEntry@4 ; LdrpAllocateDataTableEntry(x)
.text:7DEB64EE     mov  esi, eax
...
.text:7DEB6597     mov  eax, dword ptr ds:_NtDllName
.text:7DEB659C     mov  [esi+2Ch], eax              ; LDR_DATA_TABLE_ENTRY.BaseDllName
.text:7DEB659F     mov  eax, ds:off_7DEB6700
.text:7DEB65A4     push esi
.text:7DEB65A5     mov  [esi+30h], eax              ; LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
.text:7DEB65A8     call _LdrpInsertDataTableEntry@4 ; LdrpInsertDataTableEntry(x)
...
.text:7DEB65B5     mov  _LdrpNtDllDataTableEntry, esi
...
; load kernel32.dll module
.text:7DEB6665     push eax
.text:7DEB6666     mov  esi, offset word_7DEB670C
.text:7DEB666B     push esi
.text:7DEB666C     push 0
.text:7DEB666E     push 0
.text:7DEB6670     call _LdrLoadDll@16  ; LdrLoadDll(x,x,x,x)
...
; load kernelbase.dll module
.text:7DEF137C     push eax
.text:7DEF137D     push offset word_7DEF14C0
.text:7DEF1382     push 0
.text:7DEF1384     push 0
.text:7DEF1386     call _LdrLoadDll@16  ; LdrLoadDll(x,x,x,x)
...

To by potwierdzało otrzymane wyżej zawartości list podejrzane debuggerem. O ile kolejność ładowania i mapowania do pamięci jest stała i odzwierciedla rzeczywistość, to kolejność inicjalizacji jest inna. A to dlatego, że kernel32 zależy od kernelbase, więc jego inicjalizacja będzie „odpalana” wtedy, gdy inne zależności będą już „gotowe”.

W ntdll z Windows 8 i 10 można znaleźć podobny kod. Nie spodziewałbym się aby to się szybko zmieniło, ale wykonałem testy i wszyło to samo co pod 7-mką. Teoretycznie można więc nadal bazować na kolejności danych w listach.

; post-XP/Win7+
  LoadOrder: proc, ntdll, kernel32, kernelbase, ...
MemoryOrder: proc, ntdll, kernel32, kernelbase, ...
  InitOrder: ntdll, kernelbase, kernel32, ...

W większości implementacji na jakie trafiałem, metoda opierająca się na kolejności, bazowała na kolejności inicjalizacji modułów (lista InInitializationOrderModuleList). Tej właśnie, która jest zależna poniekąd od implementacji, i której zawartość na dowolnym systemie ciężko jednoznacznie przewidzieć (vide hooki antiwirusowe i inne moduły inicjalizowane przy starcie procesu lub manipulujące tą listą).

Dlatego jeśli już chce się korzystać z tej metody, to ma to jakiś sens tylko na pozostałych listach, wtedy kernel32 będzie 3 modułem. Taki kod można zapisać w kilku prostych linijkach:

bits 32
mov eax, [fs:0x30]		; PEB
mov eax, [eax + 0x0C]	; PEB->Ldr
mov eax, [eax + 0x0C]	; 1st module - PEB->Ldr.InLoadOrderModuleList.Flink
mov eax, [eax]			; 2nd module - Module.InLoadOrderLinks.Flink
mov eax, [eax]			; 3rd module - Module.InLoadOrderLinks.Flink
mov eax, [eax + 0x18]	; kernel32 base address - Module.DllBase

Do testów i zabawy, lepiej użyć jakieś prostej aplikacji, aby potwierdzić poprawne działanie. Ja sobie napisałem coś takiego:

__declspec(naked)
HMODULE FindKernel32Handle() {
 
	__asm {
		mov eax, fs:[0x30]		// PEB
		mov eax, [eax + 0x0C]	// PEB->Ldr
		mov eax, [eax + 0x0C]	// 1st entry (InLoadOrderModuleList)
		mov eax, [eax]			// 2nd entry
		mov eax, [eax]			// 3rd entry
		mov eax, [eax + 0x18]	// kernel32 base address
		ret
	};
}
 
int main() {
 
	HMODULE mod1 = GetModuleHandleA("kernel32.dll");
	HMODULE mod2 = FindKernel32Handle();
 
	printf("%p %p %d", mod1, mod2, mod1 == mod2);
	return 0;
}

Niestety __asm nie jest dostępny dla x64 projektów VS, więc eksperymentuję na x86. Ale kod dla x64 jest podobny. Jedyną różnicą są offsety do danych pól w strukturach, wynikające z różnych rozmiarów wskaźników.

bits 64
mov rax, [gs:0x60]		; PEB
mov rax, [rax + 0x18]	; PEB->Ldr
mov rax, [rax + 0x10]	; 1st module - PEB->Ldr.InLoadOrderModuleList.Flink
mov rax, [rax]			; 2nd module - Module.InLoadOrderLinks.Flink
mov rax, [rax]			; 3rd module - Module.InLoadOrderLinks.Flink
mov rax, [rax + 0x30]	; kernel32 base address - Module.DllBase

Po asemblacji w nasm-ie sformatowany kod binarny gotowy do użycia:

//
// find_kernel32_x86
//
// \x64\xA1\x30\x00\x00\x00\x8B\x40\x0C\x8B
// \x40\x0C\x8B\x00\x8B\x00\x8B\x40\x18\
//
const unsigned char fk32x86[] = {
	0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x40, 0x0C, 0x8B,
	0x40, 0x0C, 0x8B, 0x00, 0x8B, 0x00, 0x8B, 0x40, 0x18
};
 
//
// find_kernel32_x64
//
// \x65\x48\x8B\x04\x25\x60\x00\x00\x00\x48
// \x8B\x40\x18\x48\x8B\x40\x10\x48\x8B\x00
// \x48\x8B\x00\x48\x8B\x40\x30
//
const unsigned char fk32x64[] = {
	0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0x48,
	0x8B, 0x40, 0x18, 0x48, 0x8B, 0x40, 0x10, 0x48, 0x8B, 0x00,
	0x48, 0x8B, 0x00, 0x48, 0x8B, 0x40, 0x30
};

Kod ten bez problemu powinien działać od Windowsa w wersji XP do najnowszych 10-tek. Poniżej XP, na przykład na 2000 albo jakimś starym NT mogą być małe problemy. Tam bodajże kernel32 nie jest pod stałym indeksem. Nie mniej nie mam obecnie możliwości sprawdzenia tego. Ale czy ktoś potrzebuje aż taką kompatybilność?

W miedzy czasie odkryłem też jeszcze inny, dodatkowy problem. Trzeba jednak wziąć pod uwagę pewien fakt, że w przypadku aplikacji .NET-owych, pomimo tego, że proces jest inicjalizowany podobnie jak zwykły x86/x64, to kolejność ładowanych modułów jest zabużona. W czasie mapowania modułu głównego (prawdopodobnie) system ładuje bootstrapową .NET-ową bibliotekę mscoree.dll, będącą rzeczywistym entry-pointem aplikacji w świat dot-net-u.

ModLoad: 00a00000 00a08000   SimpleNetApp.exe
ModLoad: 77d50000 77eb9000   ntdll.dll
ModLoad: 68200000 68256000   C:\Windows\SYSTEM32\MSCOREE.DLL
ModLoad: 77820000 77920000   C:\Windows\system32\KERNEL32.dll
ModLoad: 75630000 75709000   C:\Windows\system32\KERNELBASE.dll

A, że moduł ten jest ładowany w czasie mapowania, które występuje przed załadowaniem modułu kernel32 i kernelbase kolejność w listach ulega drobnej zmianie.

; .net process
  LoadOrder: proc, ntdll, mscoree, kernel32, kernelbase, ...
MemoryOrder: proc, ntdll, mscoree, kernel32, kernelbase, ...
  InitOrder: ntdll, kernelbase, kernel32, mscoree, ...

Teraz mój prosty, krótki i piękny kod nie zadziała, bo 3-cim modułem nie jest spodziewany kernel32, ale dot-net-owe jądro mscoree. Trzeba znaleźć inne, bardziej uniwersalne rozwiązanie, z ewentualną iteracją całej listy, czego bardzo chciałem uniknąć.

Niektórzy sobie z takimi problemami (różna kolejność modułów na różnych systemach i procesach) radzili poprzez iterowanie po liście, aż do napotkania modułu o nazwie długiej na 12 znaków. Co niekoniecznie zawsze musi zwrócić poprawny wynik. Mogą przecież pojawić się tuż przed poszukiwanym modułem inne z nazwą o podobnej długości i już będzie zonk.

Ja, jeśli musiałbym iść tą drogą, to wolałbym jednak porównać także pełną nazwę tych 12 znakowych modułów, aby mieć pewność, że znalazłem dokładnie to czego szukałem. Taki kod nie byłby skomplikowany, ale nieco dłuższy:

	; push "kernel32.dll" onto stack
	push 0x4c4c442e			; .DLL
	push 0x32334c45			; EL32
	push 0x4e52454b			; KERN
 
	mov eax, fs:[0x30]		; PEB
	mov ebx, [eax + 0x0C]	; PEB->Ldr
	add ebx, 0x0C			; ptr to PEB->Ldr.InLoadOrderModuleList.Flink
 
next:
	mov ebx, [ebx]			; Module.InLoadOrderModuleList.Flink
	movzx ecx, word ptr [ebx + 0x2c]	; Module.BaseDllName.Length
	cmp ecx, 12 * 2			; Length == 12 unicode
	jne next				; jmp to next if not
 
	mov edi, [ebx + 0x30]	; Module.BaseDllName.Buffer
	mov esi, esp			; kernel32.dll string on stack
	shr ecx, 1				; length in chars (ecx/2)
 
name:
	lodsb					; load char of kernel32 string
	mov ah, [edi]			; load char of module name
	cmp ah, 'a'				; char is upper?
	jl noup					; if yes go to cmp
	sub ah, 0x20			; convert to uppercase
noup:
	cmp ah, al				; if char mismatch
	jne next				; jmp to next module
	add edi, 2				; next unicode char
	loop name				; unitl all chars checked
 
	mov eax, [ebx + 0x18]	; Module.DllBase
	add esp, 12				; clean stack

Pominąłem w nim sprawdzanie, czy przypadkiem nie przeleciałem już po całej liście i nie zaczęła się ponowna iteracja. Nie jest to potrzebne w tym przypadku, bo zakładam (co w rzeczywistości musi być prawdą), że poszukiwany kernel32 gdzieś w tej liście się znajduje.

Dodanie takowego sprawdzania wraz z przekazywaniem pointera na ciąg znaków z nazwą szukanego modułu pozwoli stworzyć odpowiednik funkcji GetModuleHandle. Funkcja może się przydać przy szukaniu dowolnego modułu zmapowanego do przestrzeni adresowej danego procesu.

HMODULE FindModuleHandle(const WCHAR* Name, SIZE_T Length) {
 
	PEB_LDR_DATA* Ldr = NtCurrentTeb()->ProcessEnvironmentBlock->Ldr;
 
	LIST_ENTRY* Head = &Ldr->InMemoryOrderModuleList;
	LIST_ENTRY* Entry = Head->Flink;
 
	while (Entry != Head) {
 
		LDR_DATA_TABLE_ENTRY* Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
 
		if (Module->BaseDllName.Length == Length && wcsicmp(Module->BaseDllName.Buffer, Name) == 0)
				return reinterpret_cast<HMODULE>(Module->DllBase);
 
		Entry = Entry->Flink;
	}
 
	return nullptr;
}

Pod różnymi wersjami systemu i w różnych procesach, nazwy i ścieżki do plików modułów są zapisane różną wielkością liter, czasem małymi, a czasem dużymi. Dlatego ewentualne porównania należy uodpornić na wielkość liter w nazwach, co uczyniłem w powyższych kodach.

Niestety case-insensitive uniemożliwia mi porównywanie stringów z nazwami w blokach po 4 bajty na raz, co przy długościach będących wielokrotnością słowa/dwusłowa danej architektury jest często wykorzystywane. Większość dobrych bibliotek standardowych lub kompilatorów potrafi nieźle zoptymalizować typowe operacje na blokach pamięci, jak na przykład kopiowanie czy porównywanie, często jednym zamachem operując na 4 lub 8 bajtach danych, a nawet na 16/32/64 bajtach przy wykorzystaniu SSE/AVX.

W wielu softach, szczególnie tych złośliwych, często unika się zapisywania wprost w kodzie i pamięci, czy na stosie, nazw importowanych w run-time funkcji i modułów. Zamiast tego stosuje się proste funkcje haszujące i porównuje wynik z zapianym skrótem. Poniekąd, aby uniemożliwić lub jakoś utrudnić wykrycie, analizę przez rożnego rodzaju oprogramowanie zabezpieczające, a z drugiej strony by przyspieszyć poszukiwanie funkcji, gdy trzeba wielokrotnie przeszukiwać zbiór dostępnych (eksportowanych) funkcji (chociaż chyba niekoniecznie, jeśli gdzieś nie zapiszemy sobie listy wyliczonych hash-y).

Natrafiłem kilka razy w takich kodach na bardzo prostą funkcję haszującą ROR-13, co by świadczyło, że jest często wykorzystywana w różnych shellcodach i malware. Sumuje ona poszczególne znaki stringa i przesuwa cyklicznie w każdym kroku sumę o 13 bitów w prawo.

Implementacja takiej funkcji w języku C:

uint32_t ror13_hash(const char* name)
{
	uint32_t hash = 0;
	while (*name) {
		hash = (hash >> 13) | (hash << (32 - 13));
		hash += (uint32_t)*name++;
	}
	return hash;
}

Kod taki można zapisać w Perlu w prostym skrypcie:

#!/usr/bin/perl -w
# ROR-13 hash tool
 
use strict;
 
sub ror13 {
	my $n = shift;
	return ($n >> 13) | ($n << (32 - 13)) & 0xFFFF_FFFF;
}
 
sub hash {
	my $hash = 0;
	foreach my $char (unpack("C*", shift)) {
		$hash = ror13($hash) + $char;
	}
	return $hash;
}
 
while(<>) {
	$_ =~ s/^\s+|\s+$//g;	# trim
	printf("%08x\t%s\n", hash($_), $_);
}

Ułatwia on liczenie funkcji skrótu z większej ilości danych, po przekierowaniu strumienia:

C:\Users\malcom>type names.txt | perl hash.pl
6e2bca17        KERNEL32.DLL
7c0dfcaa        GetProcAddress

Teraz można przepisać poprzedni asemblerowy kod pozbywając się string a zrzuconego na stos, a zamiast niego wpakować 32-bitowego hasha w kodzie. Dla modułu kernel32.dll w wersji uppercase wynosi dokładnie 6e2bca17.

	mov eax, fs:[0x30]		; PEB
	mov ebx, [eax + 0x0C]	; PEB->Ldr
	add ebx, 0x0C			; ptr to PEB->Ldr.InLoadOrderModuleList.Flink
 
next:
	mov ebx, [ebx]			; Module.InLoadOrderModuleList.Flink
	movzx ecx, word ptr [ebx + 0x2c]	; Module.BaseDllName.Length
	cmp ecx, 12 * 2			; Length == 12 unicode
	jne next				; jmp to next if not
 
	shr ecx, 1				; length in chars (ecx/2)
	mov esi, [ebx + 0x30]	; Module.BaseDllName.Buffer
	xor edi, edi			; hash = 0
 
name:
	lodsw					; load wchar of name
	cmp al, 'a'				; char is upper?
	jl noup					; if yes go to cmp
	sub al, 0x20			; convert to uppercase
noup:
	movzx eax, al			; eax = al
	ror edi, 13				; ror13
	add edi, eax			; hash + char
	loop name				; unitl all chars read
 
	cmp edi, 0x6e2bca17		; hash == ror13(kernel32.dll)
	jne next				; jmp to next if not
 
	mov eax, [ebx + 0x18]	; Module.DllBase

W powyższym kodzie z uwagi na fakt, że operowałem na szukaniu standardowej nazwy systemowej, wyliczałem hasha z wersji ANSI nazwy (degradując w locie 2-bajtowy znak na jego 1-bajtowy odpowiednik). W przypadku szukania różnych niestandardowych (i niesystemowych) modułów, gdzie w systemie nazwy plików mogą przybrać dowolną formę (inne języki), lepiej zrobić to poprawnie. Wystarczy zmodyfikować kod, zastępując rejestr al na ax w instrukcji movzx eax, al.

Jedno z przedstawionych tutaj rozwiązań, jakie udało mi się zakodować i przetestować, będę musiał wkrótce wprowadzić w swoim syringe, zamiast obecnej implementacji używającej hard-kodowanych adresów funkcji pobranych w procesie wstrzykującym. Co jak wspomniałem na początku, na nowych systemach może nie działać poprawnie.

Wybór takiego a nie innego rozwiązania zależy głównie od potrzeb i celu jaki chce się osiągnąć. Gdy atakujemy specyficzny proces lub grupę procesów to znając ofiarę można wykorzystać dedykowane dla niej rozwiązanie. Dla zwykłych procesów w większości systemów wystarczy przedstawione wyżej kilka linijek (bajtów) kodu na złapanie 3-ego załadowanego modułu Dla nieco bardziej uniwersalnych zastosowań trzeba wybrać nieco bardziej rozbudowany kod…

Swoją drogą metoda operująca na PEB-ie jest najpopularniejsza i niezawodna, ale istnieje jeszcze kilka innych metod i sposobów na zlokalizowanie modułu kernel32 w przestrzeni adresowej procesu. Nie miałem z nimi styczności, ale widziałem różne propozycje, więc może kiedyś, w niedalekiej przyszłości, pobawię się nimi zobaczę co z tego wyniknie.

A, gdy już mam adres bazowy modułu kernel32.dll w interesującym mnie procesie, mogę przejść dalej, do szukania adresu funkcji GetProcAddress, ale o tym w następnym odcinku… ;)

Aktualizacja 12/04/2017 @ 23:00
Trochę przeredagowałem notatkę uwzględniając zmiany jakie odkryłem w związku z procesami dot-NET-owymi, przy których trochę zmienia się kolejność ładowania systemowych modułów.

2 przemyślenia nt. „Shellcode: PEB i adres bazowy modułu kernel32.dll”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *