1. Die Theorie
1.1 Das PE-Format
1.2 Code-Injection
2. Umsetzung in die Praxis
2.1 Header manipulieren
2.2 Unsere Section bauen und in die DLL schreiben
0. Vorwort
In diesem Tutorial wollen wir unseren Code in eine fremde DLL bringen ohne dass sich diese gemappt im Arbeitsspeicher befindet. Wir sorgen dafür, dass unser Code als
angenommen und danach der originale EntryPoint aufgerufen wird, wenn wir es wollen. Das schöne hierbei ist, dass das für alle Prozesse gilt, die die DLL nutzen.Eigentlich wollte ich etwas mehr schreiben und alles detaillierter Erläutern, wusste aber nicht genau, was und befinde, dass es relativ gut so ist, wie es ist
Ich bin eben nicht so toll, wenn's um's Erklären geht und etwas unkoordiniert, was man sicher auch hier wiederfindet. Nichtsdestotrotz hoffe ich natürlich, dass es halbwegs lehrreich ist, für die, die es noch nicht kannten
Wenn euch etwas fehlt (das tut es sicherlich), schreibt es in den Thread oder mir per PN, dann füge ich es hinzu
Um meinen konfusen Ausführungen folgen zu können solltet ihr nebenbei Erklärungen zum
und zum
offen haben. Ich werde hier nicht auf jedes einzelne Feld der Strukturen eingehen, sondern werde nur wichtige benennen und ihr dürft sie euch dann im MSDN anschauen, tut mir leid Weiterhin möchte ich euch bitten, es zu entschuldigen, wenn ich zu oft das Wort "Header" benutze. Tut mir Leid und ich hoffe, dass es nicht allzu stark stört.
1. Die Theorie
1.1 Das PE-Format

1.2 Code-Injection
So, nun zur Theorie des Manipulierens. Der Plan ist ja unseren Code als Entrypoint verwenden zu lassen. Aber wo ist Platz für unseren Code? Wenn wir ihn einfach so an's Ende der DLL klatschen, wird er nicht gemappt, das ist ja nicht Sinn der Sache. Aber wenn wir unseren Code an die Code-Section klatschen und dessen Größe nur anpassen, müssen wir alle darauffolgenden Sections verändern, da sich deren Daten ja dann an anderer Stelle befinden. Die Lösung: Wir machen einfach eine neue Section, deren Daten (unser Code) wir an's Ende der DLL packen. Was muss dafür gemacht werden? Wir müssen einen neuen Section-Header einfügen, die Anzahl der Section, die im PE-Header stehen, um 1 erhöhen, den Entrypoint auf unsere Funktion in unserer Section verschieben und unsere Daten mitsamt dem veränderten Rest auf die Festplatte schreiben. Zudem müssen wir irgendwo den alten Entrypoint speichern. Hier schlage ich vor, dass wir uns eine Struct bauen, in der wir den alten Entrypoint und Daten für unsere Funktion unterbringen. Jetzt wird man sich sicher fragen: Welche Daten sollen warum dort rein? OK, wenn ihr einen String benutzt, nutzt ihr eigentlich nicht den String an sich, sondern ein Pointer auf ein Char-Array. Dieses Array liegt im kompilierten Programm aber in der Data-Section und ist für uns somit nur über umwege zu erreichen. Daher nutzen wir also die Struct, um solche Daten, dort unterzubringen.
Da wir schon bei Einschränkungen sind: Außer dieser Einschränkung gibt es noch weitere:
- die Funktion darf maximal 4KB an lokalen Variablen benutzen (für mehr kann man ja die Struktur nutzen). Dies kommt daher, dass ja eigentlich für die lokalen Variablen der Stackpointer um deren Größe reduziert wird (sub esp, <Größe der lokalen Variablen>). Werden aber mehr als 4KB lokaler Variablen gebraucht, wird ESP nicht direkt reduziert, sondern über eine spezielle Funktion, welche gecallt wird. In der DLL (bzw. im Prozess, der die DLL lädt) ist diese Funktion natürlich nicht da, oder an einem anderen Platz, weswegen der Call in's Leere geht.
- viele Compiler machen Buffer Security Checks, unter anderem um die Integrität des Stacks zu gewährleisten. Diese werden aber auch durch externe Funktionen erledigt, weswegen auch die wegmüssen (bei den meisten Compilern ist das der /Gz-Switch).
- alle API-Aufrufe, die nicht zur kernel32.dll oder user32.dll gehören, dürfen nicht direkt aufgerufen werden, da sich die Module, die die Funktionen beinhalten an unterschiedlichen Stellen befinden können. Daher müssen wir hier zuerst mit GetModuleHandle das Modul bekommen und dann mit GetProcessAddress die Adresse der Funktion, die wir dann aufrufen wollen (vergesst nicht, nur Strings aus unserer Struktur zu nehmen).
So also unsere Section beinhaltet erst unsere Daten-Struktur und dann unsere Funktion für den neuen EntryPoint. Aber wie können wir aus unserer Funktion auf die Datenstruktur (anfang der Section) zugreifen? Ich habe dieses Problem so gelöst, dass ich den relativen Pointer zur Section (und damit ja zur Datenstruktur) in das "LoaderFlags"-Feld des NTHeader.OptionalHeader geschrieben habe. Dieses Feld ist laut MSDN "obsolete", weswegen wir es deswegen einfach missbrauchen :P
2. Umsetzung in die Praxis
So jetzt erstmal zum vorgeplänkel unseres Programms. Wir müssen die Opfer-DLL einlesen (nicht mit LoadLibrary oder sonstwie mappen!). Wie, ist jedem selbst überlassen. Am Ende befindet sie sich jedenfalls im Arbeitsspeicher und wir haben einen Pointer, der auf das erste Byte der DLL zeigt.
2.1 Header manipulieren
So, um die Header zu manipulieren, müssen wir sie erstmal kriegen. Nach dem PE-Format befindet sich ganz am Anfang der DOS-Header, also ab dem ersten Byte. Wir können also gleich einen IMAGE_DOS_HEADER-Pointer aus dem DLL-Pointer machen. Dann nutzen wir das Feld e_lfanew, um an den NT-Header (= PE-Header) zu kommen. Natürlich müssen wir diesen Wert noch mit der Basis addieren, da es nur ein relativer Pointer ist.
Code:
IMAGE_NT_HEADER* pNTHeader = (IMAGE_NT_HEADER*)(((IMAGE_DOS_HEADER*)pDLL)->e_lfanew + (DWORD)pDLL);
Code:
IMAGE_SECTION_HEADER* pSectionHeaders = (IMAGE_SECTION_HEADER*)((DWORD)pNTHeader + 4 + pNTHeader->FileHeader.SizeOfOptionalHeader);
Code:
for (int i = 0; i < pNTHeader->FileHeader.NumberOfSections; i++)
{
if (dwVAofSection < (pSectionHeaders[i].VirtualAddress + pSectionHeaders[i].Misc.VirtualSize))
{
dwVAofSection = (pSectionHeaders[i].VirtualAddress + pSectionHeaders[i].Misc.VirtualSize) + 0x1000 - ((pSectionHeaders[i].VirtualAddress + pSectionHeaders[i].Misc.VirtualSize) % 0x1000);
}
}
Jetzt kommen wir zum "PointerToRawData" für unsere Section. Da wir unsere Section ja an's Ende der DLL packen wollen muss hier also die aktuelle Größe der DLL rein. Die einzige Einschränkung ist, dass PointerToRawData uns SizeOfRawData vielfaches des FileAlignment-Feldes des FileHeaders im NTHeader sein müssen. Aber das ist ja kein Problem für uns. Wenn die Größe der Datei nicht glatt durch den FileAlignment-Wert teilbar ist (ist bei mir noch nicht vorgekommen), schreiben wir eben noch ein kleines Buffer vor unserer Section in die Datei. Genauso schrieben wir ein kleines Buffer nach unserer Section in die Datei, wenn die Größe unserer Section nicht durch den FileAlignment-Wert teilbar ist (das kommt öfter vor).
Wir berechnen nach diesen vorgaben nun also die Größe unserer Section (SizeOfRawData und Misc.VirtualSize) und den "PointerToRawData":
Code:
DWORD dwSectionSize = dwFuncLength + sizeof(SomeData) + NTHeader->OptionalHeader.FileAlignment - (dwContentLength % NTHeader->OptionalHeader.FileAlignment);
Code:
if (dwSizeOfDLL % pNTHeader->OptionalHeader.FileAlignment == 0)
{
dwPointerToRawData = dwSizeOfDLL;
}
else
{
dwPointerToRawData = dwSizeOfDLL + pNTHeader->OptionalHeader.FileAlignment - (dwSizeOfDLL % pNTHeader->OptionalHeader.FileAlignment);
dwPreSectionBufferLength = dwPointerToRawData - dwSizeOfDLL;
}
Wichtig ist auch noch das "Characteristics"-Feld im Section Header. Da wir ja unseren Code ausführen, unsere Datenstruktur lesen und vielleicht auch zur Laufzeit bearbeiten müssen. Setzen wir also auch so unser Characteristics-Feld fest:
Code:
DWORD dwCharacteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_CODE;
Code:
IMAGE_SECTION_HEADER MyHeader; memcpy(MyHeader.Name, ".Jeoni", 8); MyHeader.Misc.VirtualSize = dwSectionSize; MyHeader.SizeOfRawData = dwSectionSize; MyHeader.VirtualAddress = dwVAofSection; MyHeader.PointerToRawData = dwPointerToRawData; // restliche Felder mit 0 füllen
Code:
if ((NTHeader->FileHeader.NumberOfSections + 1) * sizeof(IMAGE_SECTION_HEADER) <= NTHeader->OptionalHeader.SizeOfHeaders)
{
memcpy((void*)((DWORD)pSectionHeaders + ((NTHeader->FileHeader.NumberOfSections)*sizeof(IMAGE_SECTION_HEADER))), (void*)&MySectionHeader, sizeof(IMAGE_SECTION_HEADER));
}
Code:
SectionData.OriginalEntryPoint = NTHeader->OptionalHeader.AddressOfEntryPoint; NTHeader->OptionalHeader.LoaderFlags = dwVAofSection; NTHeader->OptionalHeader.AddressOfEntryPoint = dwVAofSection + sizeof(SomeData); NTHeader->FileHeader.NumberOfSections += 1; NTHeader->OptionalHeader.SizeOfImage += dwSectionSize; NTHeader->OptionalHeader.SizeOfCode += dwSectionSize; // da wir der Section ja Code-Characteristics gegeben haben
2.2 Unsere Section bauen und in die DLL schreiben
Ok, wir wollen also unsere Section schreiben. Mehr als unser Code und die Datenstruktur muss da ja eigentlich nicht rein. Aber wie bekommen wir unseren Code da rein? Entweder wir schreiben die gesamte Funktion mit ASM, und kopieren dann die Offsets da rein, oder wir machen uns eine Funktion in unserem Manipulator, kompilieren sie und fügen das dann zur Laufzeit ein. Aber wie kriegen wir die Länge unserer Funktion? Man könnte das Programm natürlich einmal kompilieren und dann mit einem Debugger nachschauen oder eine der disasm libs nutzen.
In diesem Tutorial bedienen wir uns aber eines kleinen Tricks (nicht schön, aber einfach): direkt nach unserer Funktion machen wir noch eine und rechnen dann einfach
Code:
DWORD dwFuncLength = &EndFunction - &NewEntryPoint;
bool _stdcall NewEntryPoint(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) // EntryPoint gemäß MSDN
{
// ...
}
__declspec(naked) void EndFunction(){}
So kriegen wir relativ genau die Länge unserer Funktion. Wichtig hierbei ist, dass in den Compileroptionen das Incremental Linking ausgestellt wird oder die beiden Funktionen als static erklärt werden. Sonst liefert der '&'-Operator nicht die Adresse der eigentlichen Funktion, sondern die eines Jumps zur Funktion. Und da die Jumps zu den Funktionen meist direkt hintereinander stehen, wird man dann hier immer 1 herausbekommen, was natürlich falsch ist.
OK, kommen wir mal kurz zu unserem Code bzw. was wir eigentlich machen wollen. Hier werde ich mal nur die Funktion MessageBeep(0) ausführen und danach den originalen EntryPoint aufrufen.
In die Datenstruktur kommt daher nur der relative Pointer zum originalen EntryPoint rein. Das ist also völlig ausreichend:
Code:
struct SomeData
{
DWORD OriginalEntryPoint;
}SectionData;
Aufgrunddessen sieht unsere Funktion wie folgt aus:
Code:
bool _stdcall NewEntryPoint(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
MessageBeep(0);
IMAGE_NT_HEADERS32* NTHeader = (IMAGE_NT_HEADERS32*)(((IMAGE_DOS_HEADER*)hModule)->e_lfanew + (DWORD)hModule);
SomeData* Data = (SomeData*)(NTHeader->OptionalHeader.LoaderFlags + (DWORD)hModule);
DLLEntry_t pOrigEntryPoint = (DLLEntry_t)(Data->OrginalEntryPoint + (DWORD)hModule);
return pOrigEntryPoint(hModule, ul_reason_for_call, lpReserved);
}
Kommen wir zum schreiben unserer Section. Da wir natürlich an die Richtlinien für die Größe unserer Section gebunden sind, allokieren wir erstmal Platz für unsere Section bevor wir sie schreiben:
Code:
void* pMySection = malloc(pMySectionHeader->SizeOfRawData);
Code:
memcpy(pMySection, &SectionData, sizeof(SomeData)); memcpy((void*)((DWORD)pMySection + sizeof(SomeData)), &NewEntryPoint, dwFuncLength;
Jetzt schreiben wir also das alte DLL-Buffer wieder in eine DLL auf die Festplatte, hängen unsere Section dran und sind fertig
Ähnlich könnt ihr es auch mit exportierten Funktionen der DLL machen. Dahingehend solltet ihr euch das "DataDirectory"-Feld des OptionalHeaders anschauen.







