Funktionen hooken in x64 asm
Als ich letztens dabei war, meinen AutoIt Decompiler nach x64 zu portieren, stand ich vor dem Problem, wie ich 64bit Code detouren soll.
Während dies in x86 Prozessen kein großes Problem darstellt, sieht das auf x64 nicht ganz so einfach aus:
x86
1. Funktionen fangen meist mit einem Prolog an (der je nach Compilereinstellung allerdings auch wegfallen kann)
push ebp und mov ebp,esp initialisieren den Stack-Frame für die momentane Funktion und belegen insgesamt schonmal 3 Bytes.
Die beiden auskommentierten Zeilen sind situationsabhängig. Die Letztere dient dem Platzschaffen für lokale Variablen und zur Ersteren komme ich noch.
2. Durch das Flat Memory Model und einer Datenbusgröße von 32bit kann man auf x86 mit einem relativen JMP-Befehl -2GB bis +2GB ansprechen, was eine Größe von 5 Bytes (0E9h plus Offset in der Größe eines DWORDs) ergibt. Der absolute JMP-Befehl (25FFh plus Adresse an der die Adresse steht, zu der gesprungen werden soll) kann über einen DWORD-Wert ebenso eine Adresse von 0 bis 4GB ansprechen.
Wenn man also nun eine Funktion umleiten möchte, braucht man einen JMP, der zu seiner eigenen Funktion springt, der wie wir wissen 5 Bytes einnimmt. Heißt, wir müssen 5 Bytes in der Zielfunktion sichern und mit einem relativen JMP überschreiben und in unserem Hook dann zum Schluss die gesicherten Befehle ausführen und können wieder zur originalen Funktion+5 springen und schon haben wir unser Ziel erreicht.
3. Bei fast allen API Funktionen kommt MS einem sogar entgegen und hat extra Platz geschaffen. So starten diese mit einem mov edi,edi, was an für sich nichts macht, außer 2 extra Bytes zu schaffen (2-Bytes NOP).
Nun gibt es zwei Möglichkeiten:
Entweder hat man mit den 2 Bytes und den 3 Bytes von oben 5 Bytes, also genau die Menge an Bytes, die man für einen JMP braucht, der den gesamten Adressraum ansprechen kann.
Oder, wie MS es eigentlich vorgesehen hat, man benutzt diese 2 Bytes für einen relativen -128 bis +127 JMP, der 5 Bytes zurückspringt, wo vor jeder API Funktion, die mit dem genannten mov edi,edi beginnt, wieder 5 extra Bytes in Form von NOPs oder INT3s sind, wo nun wieder ein relativer DWORD-JMP hinpasst, mit dem man jede Adresse ansprechen kann¹.
Ein paar Ausnahmen sind z.B. GetCommandLine, IsDebuggerPresent und GetEnvironmentStrings.
4. Der Grund dafür, dass oben steht, dass man nur einen 5-Byte großen JMP benötigt und schon hooken kann, ist einerseits, dass 5 Bytes nicht wirklich viel sind, sodass auch nicht viele Instruktionen gesichert und überschrieben werden müssen (Bei einer API Funktion muss man dann wie geschrieben nur den Prolog in einen JMP umändern und dann über push ebp und mov ebp,esp lediglich einen neuen Stack-Frame erstellen, wenn man wieder die originale Funktion aufrufen möchte), und andererseits, dass außer bei JMPs und CALLs x86 Code nicht mit relativen Offsets arbeitet.
So lässt sich der Befehl CALL [DWORD DS:403080] (FF15 80304000), der MessageBoxA indirekt über die IAT aufruft, an eine andere Stelle kopieren und funktioniert immernoch, da die angegebene absolute Adresse genauso auch im Opcode steht.
Also muss bis auf relative JMPs und CALLs nichts angepasst werden, was in x64 anders ist, mehr dazu weiter unten. Und da nur 5 Bytes überschrieben werden müssen und sehr viele Funktionen (und sowieso API Funktionen) erstmal mit einem Funktionsprolog anfangen, um den Stack zu initialisieren, kommt es selten vor, dass man einen solchen positionsabhängigen Befehl antrifft.
(Wenn doch, dann müsste man natürlich über das Offset im Opcode die eigentliche Adresse, die aufgerufen werden soll, und damit dann das neue Offset für die Stelle, wohin der Befehl gesichert wurde, berechnen und den Befehl anpassen.)
x64
1. Unter x64 gibt es keinen einheitlichen Funktionsprolog. Eine Funktion könnte z.B. damit anfangen, Platz auf dem Stack für lokale Variablen bzw. als Shadow Space, der zur Calling Convention von Win64 gehört, zu reservieren. Oder auch damit, Register zu pushen oder im Shadow Space zu sichern. Funktionen, die weder das eine, noch das andere benötigen, können auch direkt mit richtigem Code beginnen, der von Funktion zu Funktion natürlich immer anders aussieht.
2. Durch eine Datenbusgröße von 64bit lässt sich unmittelbar mit QWORDs (8 Bytes) arbeiten, da das x64 Prozessor Modell allerdings nicht von Grund auf erneuert wurde, sondern nur eine Art Erweiterung des x86 Modells ist (daher auch die Bezeichnung x86-64), baut auch das neue Instruction Set auf dem alten auf. So ist die einzige Instruktion, die mit vollen 64bit Werten arbeiten kann, die MOV-Instruktion. Der JMP-Befehl ist also immer noch 5 Bytes groß, was logischerweise bedeutet, dass man damit wieder -2GB bis +2GB ansprechen kann, was unter x64 jedoch nur einen Bruchteil des Adressraums darstellt.
Möchte man zu einer absoluten Adresse springen, kann man dies über einen kleinen Umweg machen:
Auf diese Weise kann man zu einer absoluten 64bit Adresse springen. Sie nimmt jedoch ganze 11 Bytes in Anspruch und stellt zugleich die kleinste Art einer 64bit Umleitung dar.
3. Wie in oben angesprochen gibt es in x64 Code keinen klaren Funktionsprolog, weshalb das Ganze etwas unorganisierter aussieht. API Funktionen fangen auch nichts mit NOPs wie mov edi,edi an und haben auch keine konstante Anzahl an NOPs oder INT3s vor sich.
4. Da fast jede Instruktion weiter mit 32bit Werten arbeitet, stellt sich einem die Frage, wie man den Speicher ansprechen soll, wenn die Executable eine 64bit ImageBase hat und somit direkt an eine 64bit Adresse alloziert wird.
Es werden zwar weiterhin fast nur DWORDs kodiert, dafür hat die Disp32-Adressierung nun eine Besonderheit und zwar wird mit ihr RIP-relativ adressiert (RIP ist der Instruction Pointer, sprich die aktuelle Adresse). Das bedeutet also, dass, wenn nur ein Displacement im ModR/M-Byte angegeben ist und diesem folgt, auch ein Bereich von ±2GB abgedeckt wird. Dies trifft auf jede Instruktion zu, die eine Disp32-Adressierung verwendet. Weiterhin sind JMPs und CALLs auch relativ zur aktuellen Position.
Fazit
Ein x64 Detour nimmt also mindestens 11 Bytes ein. Muss nun aber nicht weiter als 2GB gesprungen werden, reicht ja wie zu x86 Zeiten ein 5 Bytes JMP mit einem 32bit Displacement.
Glücklicherweise lässt sich bei VirtualAlloc der Adressbereich angeben, in dem alloziert werden soll, sodass man in der Nähe sozusagen ein Tor erstellen kann, zu dem über ein 5 Bytes-JMP gesprungen wird und das dann über ein 64bit JMP-Konstrukt zur eigenen Funktion weiterleitet³.
Dennoch besteht weiterhin das Problem, dass kein einheitlicher Funktionsprolog existiert und dadurch eine große Anzahl an unterschiedlichen Instruktionen direkt an erster Stelle der zu hookenden Funktion stehen kann, die unter Umständen angepasst werden müssen.
Lösung
Entweder selber eine Funktion schreiben, die Obengenanntes berücksichtigt, oder einfach eine fertige Hooking-Library³⁴ benutzen :)
Da ich aber FASM als Assembler verwende und FASM kein Linker ist, kann ich mit den .lib's nicht viel anfangen und eine Hooking Library in asm habe ich bisher nicht gefunden.
Ich wollte es mir dann einfach machen und die IAT hooken, statt mich mit Codeanpassungen herumplagen zu müssen, dafür hab ich mir folgende Funktion geschrieben:
Das Problem dabei ist nur, dass viele Packer auch die Import Table zerstören und die Imports manuell auflösen und so die IAT zur Laufzeit wiederherstellen.
Ich muss mir also demnächst mal eine eigene Hook-Funktion schreiben, die positionsabhängigen Code fixt.
Werde den Blogpost dann irgendwann updaten..
¹ Hot Patching
² Trampolines in x64
³ Minimalistic x86/x64 API Hooking Library
⁴ Powerful x86/x64 Mini Hook-Engine
PS: Hab einfach mal drauf los geschrieben, deswegen gibt es keine durchdachte Struktur :)
Während dies in x86 Prozessen kein großes Problem darstellt, sieht das auf x64 nicht ganz so einfach aus:
x86
1. Funktionen fangen meist mit einem Prolog an (der je nach Compilereinstellung allerdings auch wegfallen kann)
Code:
//mov edi,edi push ebp mov ebp,esp //sub esp,4
Die beiden auskommentierten Zeilen sind situationsabhängig. Die Letztere dient dem Platzschaffen für lokale Variablen und zur Ersteren komme ich noch.
2. Durch das Flat Memory Model und einer Datenbusgröße von 32bit kann man auf x86 mit einem relativen JMP-Befehl -2GB bis +2GB ansprechen, was eine Größe von 5 Bytes (0E9h plus Offset in der Größe eines DWORDs) ergibt. Der absolute JMP-Befehl (25FFh plus Adresse an der die Adresse steht, zu der gesprungen werden soll) kann über einen DWORD-Wert ebenso eine Adresse von 0 bis 4GB ansprechen.
Wenn man also nun eine Funktion umleiten möchte, braucht man einen JMP, der zu seiner eigenen Funktion springt, der wie wir wissen 5 Bytes einnimmt. Heißt, wir müssen 5 Bytes in der Zielfunktion sichern und mit einem relativen JMP überschreiben und in unserem Hook dann zum Schluss die gesicherten Befehle ausführen und können wieder zur originalen Funktion+5 springen und schon haben wir unser Ziel erreicht.
3. Bei fast allen API Funktionen kommt MS einem sogar entgegen und hat extra Platz geschaffen. So starten diese mit einem mov edi,edi, was an für sich nichts macht, außer 2 extra Bytes zu schaffen (2-Bytes NOP).
Nun gibt es zwei Möglichkeiten:
Entweder hat man mit den 2 Bytes und den 3 Bytes von oben 5 Bytes, also genau die Menge an Bytes, die man für einen JMP braucht, der den gesamten Adressraum ansprechen kann.
Oder, wie MS es eigentlich vorgesehen hat, man benutzt diese 2 Bytes für einen relativen -128 bis +127 JMP, der 5 Bytes zurückspringt, wo vor jeder API Funktion, die mit dem genannten mov edi,edi beginnt, wieder 5 extra Bytes in Form von NOPs oder INT3s sind, wo nun wieder ein relativer DWORD-JMP hinpasst, mit dem man jede Adresse ansprechen kann¹.
Ein paar Ausnahmen sind z.B. GetCommandLine, IsDebuggerPresent und GetEnvironmentStrings.
4. Der Grund dafür, dass oben steht, dass man nur einen 5-Byte großen JMP benötigt und schon hooken kann, ist einerseits, dass 5 Bytes nicht wirklich viel sind, sodass auch nicht viele Instruktionen gesichert und überschrieben werden müssen (Bei einer API Funktion muss man dann wie geschrieben nur den Prolog in einen JMP umändern und dann über push ebp und mov ebp,esp lediglich einen neuen Stack-Frame erstellen, wenn man wieder die originale Funktion aufrufen möchte), und andererseits, dass außer bei JMPs und CALLs x86 Code nicht mit relativen Offsets arbeitet.
So lässt sich der Befehl CALL [DWORD DS:403080] (FF15 80304000), der MessageBoxA indirekt über die IAT aufruft, an eine andere Stelle kopieren und funktioniert immernoch, da die angegebene absolute Adresse genauso auch im Opcode steht.
Also muss bis auf relative JMPs und CALLs nichts angepasst werden, was in x64 anders ist, mehr dazu weiter unten. Und da nur 5 Bytes überschrieben werden müssen und sehr viele Funktionen (und sowieso API Funktionen) erstmal mit einem Funktionsprolog anfangen, um den Stack zu initialisieren, kommt es selten vor, dass man einen solchen positionsabhängigen Befehl antrifft.
(Wenn doch, dann müsste man natürlich über das Offset im Opcode die eigentliche Adresse, die aufgerufen werden soll, und damit dann das neue Offset für die Stelle, wohin der Befehl gesichert wurde, berechnen und den Befehl anpassen.)
x64
1. Unter x64 gibt es keinen einheitlichen Funktionsprolog. Eine Funktion könnte z.B. damit anfangen, Platz auf dem Stack für lokale Variablen bzw. als Shadow Space, der zur Calling Convention von Win64 gehört, zu reservieren. Oder auch damit, Register zu pushen oder im Shadow Space zu sichern. Funktionen, die weder das eine, noch das andere benötigen, können auch direkt mit richtigem Code beginnen, der von Funktion zu Funktion natürlich immer anders aussieht.
2. Durch eine Datenbusgröße von 64bit lässt sich unmittelbar mit QWORDs (8 Bytes) arbeiten, da das x64 Prozessor Modell allerdings nicht von Grund auf erneuert wurde, sondern nur eine Art Erweiterung des x86 Modells ist (daher auch die Bezeichnung x86-64), baut auch das neue Instruction Set auf dem alten auf. So ist die einzige Instruktion, die mit vollen 64bit Werten arbeiten kann, die MOV-Instruktion. Der JMP-Befehl ist also immer noch 5 Bytes groß, was logischerweise bedeutet, dass man damit wieder -2GB bis +2GB ansprechen kann, was unter x64 jedoch nur einen Bruchteil des Adressraums darstellt.
Möchte man zu einer absoluten Adresse springen, kann man dies über einen kleinen Umweg machen:
Code:
mov rax,1234567812345678h jmp rax
3. Wie in oben angesprochen gibt es in x64 Code keinen klaren Funktionsprolog, weshalb das Ganze etwas unorganisierter aussieht. API Funktionen fangen auch nichts mit NOPs wie mov edi,edi an und haben auch keine konstante Anzahl an NOPs oder INT3s vor sich.
4. Da fast jede Instruktion weiter mit 32bit Werten arbeitet, stellt sich einem die Frage, wie man den Speicher ansprechen soll, wenn die Executable eine 64bit ImageBase hat und somit direkt an eine 64bit Adresse alloziert wird.
Es werden zwar weiterhin fast nur DWORDs kodiert, dafür hat die Disp32-Adressierung nun eine Besonderheit und zwar wird mit ihr RIP-relativ adressiert (RIP ist der Instruction Pointer, sprich die aktuelle Adresse). Das bedeutet also, dass, wenn nur ein Displacement im ModR/M-Byte angegeben ist und diesem folgt, auch ein Bereich von ±2GB abgedeckt wird. Dies trifft auf jede Instruktion zu, die eine Disp32-Adressierung verwendet. Weiterhin sind JMPs und CALLs auch relativ zur aktuellen Position.
Fazit
Ein x64 Detour nimmt also mindestens 11 Bytes ein. Muss nun aber nicht weiter als 2GB gesprungen werden, reicht ja wie zu x86 Zeiten ein 5 Bytes JMP mit einem 32bit Displacement.
Glücklicherweise lässt sich bei VirtualAlloc der Adressbereich angeben, in dem alloziert werden soll, sodass man in der Nähe sozusagen ein Tor erstellen kann, zu dem über ein 5 Bytes-JMP gesprungen wird und das dann über ein 64bit JMP-Konstrukt zur eigenen Funktion weiterleitet³.
Dennoch besteht weiterhin das Problem, dass kein einheitlicher Funktionsprolog existiert und dadurch eine große Anzahl an unterschiedlichen Instruktionen direkt an erster Stelle der zu hookenden Funktion stehen kann, die unter Umständen angepasst werden müssen.
Lösung
Entweder selber eine Funktion schreiben, die Obengenanntes berücksichtigt, oder einfach eine fertige Hooking-Library³⁴ benutzen :)
Da ich aber FASM als Assembler verwende und FASM kein Linker ist, kann ich mit den .lib's nicht viel anfangen und eine Hooking Library in asm habe ich bisher nicht gefunden.
Ich wollte es mir dann einfach machen und die IAT hooken, statt mich mit Codeanpassungen herumplagen zu müssen, dafür hab ich mir folgende Funktion geschrieben:
Code:
proc DetourFunc module,address,new_address
local old:DWORD,pad1:DWORD,mbi:MEMORY_BASIC_INFORMATION
push r12 r13 r14
sub rsp,8*(4+1)
mov r12,rdx
mov r13,r8
test rcx,rcx
jns .ok
xor ecx,ecx
call [GetModuleHandle]
xchg rax,rcx
.ok:
mov r11,rcx
mov r14,rcx
cmp word [r11],IMAGE_DOS_SIGNATURE
jnz .err
mov r8d,[r11+IMAGE_DOS_HEADER.e_lfanew]
add r14,r8
cmp dword [r14],IMAGE_NT_SIGNATURE
jnz .err
lea r9,[r14+IMAGE_NT_HEADERS64.OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_IAT*sizeof.IMAGE_DATA_DIRECTORY]
mov r8d,[r9+IMAGE_DATA_DIRECTORY.VirtualAddress]
mov r9d,[r9+IMAGE_DATA_DIRECTORY.Size]
test r8,r8
je .no_iat
lea r14,[r11+r8]
.loop:
cmp [r14+IMAGE_THUNK_DATA64.Function],r12
je .found
add r14,8
sub r9,8
jnz .loop
jmp .err
.no_iat:
mov r8d,[r14+IMAGE_NT_HEADERS64.OptionalHeader.DataDirectory.VirtualAddress+IMAGE_DIRECTORY_ENTRY_IMPORT*sizeof.IMAGE_DATA_DIRECTORY]
lea r9,[r11+r8]
.descriptors:
mov r8d,[r9+IMAGE_IMPORT_DESCRIPTOR.FirstThunk]
test r8,r8
je .err
lea r14,[r11+r8]
.thunks:
mov r8,[r14+IMAGE_THUNK_DATA64.Function]
test r8,r8
je .next
cmp r8,r12
je .found
add r14,8
jmp .thunks
.next:
add r9,sizeof.IMAGE_IMPORT_DESCRIPTOR
jmp .descriptors
.found:
mov r8d,sizeof.MEMORY_BASIC_INFORMATION
lea rdx,[mbi]
mov rcx,r14
call [VirtualQuery]
lea r9,[old]
mov r8d,PAGE_READWRITE
mov rdx,[mbi.RegionSize]
mov rcx,[mbi.BaseAddress]
call [VirtualProtect]
mov [r14],r13
lea r9,[old]
mov r8d,[old]
mov rdx,[mbi.RegionSize]
mov rcx,[mbi.BaseAddress]
call [VirtualProtect]
jmp .fin
.err:
xor eax,eax
.fin:
add rsp,8*(4+1)
pop r14 r13 r12
ret
endp
;defines
struct MEMORY_BASIC_INFORMATION
BaseAddress rq 1
AllocationBase rq 1
AllocationProtect rd 2
RegionSize rq 1
State rd 1
Protect rd 1
Type rd 2
ends
IMAGE_DOS_SIGNATURE = 'MZ'
IMAGE_NT_SIGNATURE = 'PE'
IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16
IMAGE_DIRECTORY_ENTRY_IMPORT = 1
IMAGE_DIRECTORY_ENTRY_IAT = 12
struct IMAGE_DOS_HEADER
e_magic rw 1
e_cblp rw 1
e_cp rw 1
e_crlc rw 1
e_cparhdr rw 1
e_minalloc rw 1
e_maxalloc rw 1
e_ss rw 1
e_sp rw 1
e_csum rw 1
e_ip rw 1
e_cs rw 1
e_lfarlc rw 1
e_ovno rw 1
e_res rw 4
e_oemid rw 1
e_oeminfo rw 1
e_res2 rw 10
e_lfanew rd 1
ends
struct IMAGE_FILE_HEADER
Machine rw 1
NumberOfSections rw 1
TimeDateStamp rd 1
PointerToSymbolTable rd 1
NumberOfSymbols rd 1
SizeOfOptionalHeader rw 1
Characteristics rw 1
ends
struct IMAGE_DATA_DIRECTORY
VirtualAddress rd 1
Size rd 1
ends
struct IMAGE_OPTIONAL_HEADER64
Magic rw 1
MajorLinkerVersion rb 1
MinorLinkerVersion rb 1
SizeOfCode rd 1
SizeOfInitializedData rd 1
SizeOfUninitializedData rd 1
AddressOfEntryPoint rd 1
BaseOfCode rd 1
ImageBase rq 1
SectionAlignment rd 1
FileAlignment rd 1
MajorOperatingSystemVersion rw 1
MinorOperatingSystemVersion rw 1
MajorImageVersion rw 1
MinorImageVersion rw 1
MajorSubsystemVersion rw 1
MinorSubsystemVersion rw 1
Win32VersionValue rd 1
SizeOfImage rd 1
SizeOfHeaders rd 1
CheckSum rd 1
Subsystem rw 1
DllCharacteristics rw 1
SizeOfStackReserve rq 1
SizeOfStackCommit rq 1
SizeOfHeapReserve rq 1
SizeOfHeapCommit rq 1
LoaderFlags rd 1
NumberOfRvaAndSizes rd 1
DataDirectory IMAGE_DATA_DIRECTORY
rb (IMAGE_NUMBEROF_DIRECTORY_ENTRIES-1)*sizeof.IMAGE_DATA_DIRECTORY
ends
struct IMAGE_NT_HEADERS64
Signature rd 1
FileHeader IMAGE_FILE_HEADER
OptionalHeader IMAGE_OPTIONAL_HEADER64
ends
struct IMAGE_IMPORT_DESCRIPTOR
union
Characteristics rd 1
OriginalFirstThunk rd 1
ends
TimeDateStamp rd 1
ForwarderChain rd 1
Name rd 1
FirstThunk rd 1
ends
struct IMAGE_THUNK_DATA64
union
ForwarderString rq 1
Function rq 1
Ordinal rq 1
AddressOfData rq 1
ends
ends
Ich muss mir also demnächst mal eine eigene Hook-Funktion schreiben, die positionsabhängigen Code fixt.
Werde den Blogpost dann irgendwann updaten..
¹ Hot Patching

² Trampolines in x64

³ Minimalistic x86/x64 API Hooking Library

⁴ Powerful x86/x64 Mini Hook-Engine

PS: Hab einfach mal drauf los geschrieben, deswegen gibt es keine durchdachte Struktur :)
Total Comments 0






