Register for your free account! | Forgot your password?

Go Back   elitepvpers > Blogs > link
You last visited: Today at 23:32

  • Please register to post and access all features, it's quick, easy and FREE!

Advertisement



Rate this Entry

Funktionen hooken in x64 asm

Posted 02/23/2012 at 02:30 by link
Updated 03/25/2012 at 22:48 by link
Tags asm, furry, hooking, porn, x64

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)
Code:
//mov edi,edi
  push ebp
  mov ebp,esp
//sub esp,4
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:
Code:
mov rax,1234567812345678h
jmp rax
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:
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
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 :)
Posted in Uncategorized
Views 764 Comments 0 Email Blog Entry
« Prev     Main     Next »
Total Comments 0

Comments

 

All times are GMT +1. The time now is 23:37.


Powered by vBulletin®
Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
SEO by vBSEO ©2011, Crawlability, Inc.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Support | Contact Us | FAQ | Advertising | Privacy Policy | Terms of Service | Abuse
Copyright ©2025 elitepvpers All Rights Reserved.