Abfangen von .NET / CLR-Funktionen

Manchmal ist es bei der Entwicklung von Software erforderlich, zusätzliche Funktionen in vorhandene Anwendungen einzubetten, ohne den Quellcode der Anwendungen zu ändern. Darüber hinaus existieren die Anwendungen selbst häufig nur in kompilierter Binärform ohne Quellcode. Eine weithin bekannte Methode zur Lösung dieses Problems ist die sogenannte. "Spleißen" ist eine Methode zum Abfangen von Funktionen durch Ändern des Codes der Zielfunktion. Normalerweise werden beim Spleißen die ersten Bytes der Zielfunktion an andere Adressen verschoben, und der unbedingte Sprungbefehl (jmp) zur Ersetzungsfunktion wird an ihre ursprüngliche Stelle geschrieben. Da das Spleißen Speicheroperationen auf niedriger Ebene erfordert, wird es in Assembler und C / C ++ ausgeführt.

Die Spleißmethode zum Abfangen von API-Funktionen in Windows ist im Internet und in verschiedenen Literaturstellen ausführlich beschrieben. Die Einfachheit dieses Abfangens wird durch die folgenden Faktoren bestimmt:
  1. Die Zielfunktion ist statisch - sie ist sofort im Speicher des geladenen Moduls vorhanden.
  2. Die Adresse der Zielfunktion ist einfach zu ermitteln (über die Modul- Exporttabelle oder die Funktion GetProcAddress ).

Die Implementierung von Ersatzfunktionen in C / C ++ beim Abfangen von API-Funktionen ist die beste Option, da die Windows-API, wie Sie wissen, in der Sprache C implementiert ist und Ersatzfunktionen dieselben Konzepte wie ersetzte Funktionen verwenden können.

Mit dem Aufkommen der .NET-Technologie hat sich die Situation grundlegend geändert. Dynamisch verknüpfte Bibliotheken, die für .NET erstellt wurden, enthalten keine statischen Funktionen mehr (Funktionen werden dynamisch auf der Grundlage von IL-Zwischensprachenbefehlen generiert). Infolgedessen ist es schwierig, die Adresse im Speicher vorherzusagen, an der Funktionen nach der dynamischen Kompilierung (JIT-Kompilierung) platziert werden, und den Moment der JIT-Kompilierung selbst zu verfolgen. Darüber hinaus ist es ohne zusätzlichen Aufwand nicht möglich, die .NET-Funktion als Ersatzfunktion zu verwenden, da sie nicht statisch ist und nicht in C / C ++ implementiert ist.

In diesem Artikel wird ein Algorithmus beschrieben, mit dessen Hilfe Sie .NET-Funktionen durch Funktionen ersetzen können, die ebenfalls in der .NET-Umgebung entwickelt wurden. Um den gegebenen Algorithmus zu verstehen, müssen wir uns mit der Implementierung von CLR (Common Language Runtime) .NET befassen. Bei der Beschreibung der Implementierung der CLR werden einige Details vereinfacht, um das Verständnis des allgemeinen Wesens nicht zu erschweren.

1. Methodenaufrufmethoden in der CLR


In der CLR ist jede Funktion (Methode) ein Satz von AWL-Befehlen, und alle Informationen dazu werden in den Metadaten des Moduls gespeichert. Beim Laden eines Moduls für jede seiner Klassen erstellt das CLR-System eine MethodTable- Tabelle mit Informationen zu den Methoden der Klasse. Jede Klassenmethode wird durch eine MethodDesc- Struktur beschrieben , deren eines Feld die Adresse der kompilierten Methode im Speicher enthält (wenn die Methode JIT-kompiliert ist), und das andere Feld enthält den Index in der MethodTable- Tabelle , der die Adresse des Adapters (Thunk) angibt, dessen Inhalt sich während der Ausführung in ändert abhängig davon, ob die Methode kompiliert ist oder nicht.



Zu Beginn (vor der JIT-Kompilierung) fungiert eine der vier sogenannten Übergänge als Adapter. Vorcodierte CLR-Adapter:StubPrecode, FixupPrecode, RemotingPrecode или NDirectImportPrecode. Поскольку последний переходник используется только для вызова API-функций Windows, которые можно перехватить и напрямую, то его мы рассматривать не будем.

Основной задачей каждого из precode-переходников является передача адреса структуры MethodDesc,
определяющей используемый метод, внутренней функции ThePreStub (ThePreStubAMD64 для платформы x64, на рисунке отмечена как Stub), которая выполняет следующие задачи:
  1. JIT-компиляция метода, идентифицируемого структурой MethodDesc;
  2. установка указателя в структуре MethodDesc на сгенерированный native-код;
  3. Schreiben Sie den Adapter so um, dass er einen unbedingten Sprung (jmp) zum generierten nativen Code ausführt.
  4. Ausführung des generierten nativen Codes.

Als Ergebnis des anfänglichen Aufrufs der Zielmethode wird nicht nur der Methodencode generiert und ausgeführt, sondern auch der Inhalt des Adapters wird geändert, was bei nachfolgenden Methodenaufrufen zu einem direkten Aufruf des generierten nativen Codes führt.

Jede von der Common Language Runtime aufgerufene .NET-Methode durchläuft die Adresse in der MethodTable- Tabelle der Klassenmethoden. Die CLR bietet jedoch die Möglichkeit, eine Methode aus einer nicht verwalteten C / C ++ - Umgebung aufzurufen. Um dies zu tun, verwenden Sie die folgenden Funktionen: GetFunctionPointer Klasse Runtime und GetFunctionPointerForDelegate Klasse Marshal . Die von diesen Funktionen zurückgegebenen Adressen sind auch Adapteradressen, von denen bereits erwähnt werden kannStubPrecode , FixupPrecode und RemotingPrecode . Durch den initialen Aufruf der Methode wird diese kompiliert und ausgeführt, mit einem anschließenden Aufruf wird ein direkter Übergang zum generierten Code durchgeführt. Gleichzeitig ist es für uns wichtig, dass für eine nicht kompilierte Methode, wenn sie sowohl über die Methodentabelle als auch über die von den genannten Funktionen zurückgegebenen Zeiger aufgerufen wird, die interne Funktion ThePreStub aufgerufen wird .

2. CLR-Adapter vorkodieren


Betrachten wir nun die CLR-Precode-Adapter separat und geben an, wie wir, wenn wir nur den Binärcode des Adapters selbst kennen, die Adresse der diesem Adapter zugeordneten MethodDesc- Struktur und die Adresse der internen ThePreStub- Funktion bestimmen können (dies wird in Zukunft nützlich sein). Außerdem geben wir an, wie die Adresse des generierten Codes im angegebenen Adapter nach der JIT-Kompilierung ermittelt werden soll.
  1. StubPrecode . Bei seiner Erstellung wird der Wert der Adresse der MethodDesc- Strukturdirekt vom CLR-Systemin den angegebenen Adaptereingebettet (als direkter Wert im Assembler-Befehl). Der Adaptercode ist nur von der Hardwareplattform und nicht von der CLR-Version abhängig. Für verschiedene Hardwareplattformen hat es die folgende Form:
    x86:    
            mov eax, pMethodDesc 
            mov ebp, ebp         
            jmp ThePreStub       
    x64: 
            mov r10, pMethodDesc 
            jmp ThePreStub       
            

    Daher wird die Adresse der MethodDesc- Struktur an die ThePreStub- Funktion im eax-Register (für x86) oder r10 (für x64) übergeben. Während der Speicheranalyse kann die angegebene Adresse bei Offset 1 (für x86) oder 2 (für x64) des Adapters unter Berücksichtigung der Prozessorkapazität eindeutig gelesen werden. Die Adresse der ThePreStub- Funktion kann berechnet werden, indem der relative Versatz, der im letzten jmp-Befehl enthalten ist, mit der Adresse des Abschlusses des angegebenen Befehls addiert wird.

    Nach Abschluss der JIT-Kompilierung wird die Übergangsadresse von der Adresse der ThePreStub- Funktion durch die Adresse des generierten Codes ersetzt, und der Inhalt des Adapters wird wie folgt:
    x86: 
            mov eax, pMethodDesc 
            mov ebp, ebp         
            jmp NativeCode       
    x64: 
            mov r10, pMethodDesc 
            jmp NativeCode       
            

    Die Methode zum Ermitteln der Adresse des generierten Codes nach der JIT-Kompilierung ist dieselbe wie die Methode zum Ermitteln der Adresse der ThePreStub- Funktion vor der JIT-Kompilierung.

  2. FixupPrecode . Der angegebene Adapter wurde entwickelt, um die Speichernutzung zu optimieren. Auf allen Hardwareplattformen sind 8 Byte erforderlich , was weniger als die Größe des StubPrecode- Adapters entspricht(12 Byte für x86 und 16 Byte für x64). Der Adaptercode für alle Hardwareplattformen und CLR-Versionen lautet wie folgt:

            call PrecodeFixupThunk  
            db 0x5E                
            db MethodDescChunkIndex 
            db PrecodeChunkIndex    
    или
            call PrecodeFixupThunk  
            db 0xСС                 
            db MethodDescChunkIndex 
            db PrecodeChunkIndex    
            

    Bei Verwendung von FixupPrecode-Adaptern erfüllt die CLR die folgenden beiden Anforderungen:

    1. Adapterspezifische MethodDesc- Strukturen werden in fortlaufenden MethodDescChunk- Speicherblöcken kombiniert :

    2. FixupPrecode -perehodniki auch auf einen kontinuierlichen Speicherblock kombiniert in der genannten Adaptereinheit nach dem Verschlusssystem eingebettet ist CLR Basisadresse pMethodDescChunkBase Strukturen MethodDesc in der Speichereinheit MethodDescChunk :

      call PrecodeFixupThunk       
      db ?                         
      db MethodDescChunkIndex      
      db PrecodeChunkIndex         
      ...                          
      call PrecodeFixupThunk       
      db ?                         
      db MethodDescChunkIndex      
      db 2                         
      call PrecodeFixupThunk       
      db ?                         
      db MethodDescChunkIndex      
      db 1                         
      call PrecodeFixupThunk       
      db ?                         
      db MethodDescChunkIndex      
      db 0                         
      dd pMethodDescChunkBase (x86)
      dq pMethodDescChunkBase (x64)
                 


    Mit einer solchen Speicherorganisation Strukturadresse MethodDesc für einen bestimmten Adapter FixupPrecode durch die folgende Formel gegeben ist:

    Adresse MethodDesc = pMethodDescChunkBase + MethodDescChunkIndex * sizeof (void *),

    wo die Basis - Offset ( pMethodDescChunkBase ) an der folgenden Adresse extrahiert wird:

    Adresse pMethodDescChunkBase = Adresse FixupPrecode + 8 + PrecodeChunkIndex * 8

    und MethodDescChunkIndex und PrecodeChunkIndex sind Byte-Werte, die in PrecodeFixupThunk eingebettet sind .

    Der Wert der Adresse der MethodDesc- Struktur durch die CLR wird im optionalen Adapter PrecodeFixupThunk berechnet , der im Singular vorhanden ist und nur zum Berechnen und Übergeben der angegebenen Adresse an ThePreStub im Register eax (für x86) oder r10 (für x64) vorgesehen ist. Hier ist der Code für den PrecodeFixupThunk- Adapter für verschiedene Hardwareplattformen.
    x86: 
            pop    eax	                                            
            push   esi	                                            
            push   edi	                                            
            movzx  esi, byte ptr  [eax + 0x2]	                    
            movzx  edi, byte ptr  [eax + 0x1]	                    
            mov    eax, dword ptr [eax + esi * 8 + 0x3]             
            lea    eax, [eax + edi * 4] 			                
            pop    edi	                                            
            pop    esi	                                            
            jmp    dword ptr [g_dwPreStubAddr] (для CLR 2.0)        
            jmp    ThePreStub                  (для CLR 4.0 и выше)                  
    x64: 
            pop    rax	                                            
            movzx  r10, byte ptr  [rax + 0x2]	                    
            movzx  r11, byte ptr  [rax + 0x1]	                    
            mov    rax, qword ptr [rax + r10 * 8 + 0x3]	            
            lea    r10, [rax + r11 * 8] 			                
            jmp    ThePreStub                                       
            

    Die Adresse der internen ThePreStub- Funktion mithilfe des FixupPrecode- Adapters kann in zwei Schritten berechnet werden:

    1. Berechnen Sie die Adresse des PrecodeFixupThunk- Adapters, indem Sie den relativen Offset, der im ersten Befehl des Call FixupPrecode- Adapters enthalten ist, zur Abschlussadresse des angegebenen Befehls hinzufügen .
    2. Berechnen Sie für alle Plattformen mit Ausnahme von CLR 2.0 x86 die ThePreStub- Adresse, indem Sie den relativen Offset, der im letzten jmp-Befehl des PrecodeFixupThunk- Adapters integriert ist, mit der Beendigungsadresse des angegebenen Befehls addieren .
    3. Extrahieren Sie für die CLR 2.0 x86-Plattform die ThePreStub- Adresse in die Adresse, die im letzten jmp-Befehl enthalten ist (indirekte Adressierung über die interne Variable g_dwPreStubAddr ).


    Nach Abschluss der JIT-Kompilierung im FixupPrecode- Adapter wird der erste Aufrufbefehl durch den Befehl jmp ersetzt, wobei die Übergangsadresse von der Adresse des PrecodeFixupThunk- Adapters zur Adresse des generierten Codes ersetzt wird. Wenn dem ersten Befehl außerdem Byte 0x5E folgt, wird es durch Byte 0x5F ersetzt (die angegebenen Bytes sind ein Indikator für das Vorhandensein oder Fehlen einer JIT-Kompilierung, Byte 0xCC bedeutet keine Information). Nach dem Austausch sieht der Inhalt des Adapters folgendermaßen aus:

            jmp NativeCode         
            db 0x5E                
            db MethodDescChunkIndex
            db PrecodeChunkIndex   
    или
            jmp NativeCode          
            db 0xСС                 
            db MethodDescChunkIndex 
            db PrecodeChunkIndex    
            

    Nach der JIT-Kompilierung wird die Adresse des generierten Codes berechnet, indem der relative Versatz, der im ersten jmp-Befehl enthalten ist, zur Abschlussadresse des angegebenen Befehls hinzugefügt wird.

  3. RemotingPrecode . Der angegebene Adapter wird beim Aufrufen von Methoden von Objekten verwendet, die möglicherweise in einer anderen Anwendungsdomäne vorhanden sind. Der Adaptercode lautet wie folgt:
    x86: 
            mov eax, pMethodDesc        
            nop                         
            call PrecodeRemotingThunk   
            jmp ThePreStub              
    x64: 
            test rcx,rcx
            je Local
            mov rax, qword ptr [rcx]
            mov r10, ProxyAddress
            cmp rax, r10
            je Remote
    Local:  mov rax, ThePreStub
            jmp rax
    Remote: mov r10, pMethodDesc
            mov rax, RemotingCheck
            jmp rax
            

    Wie beim StubPrecode- Adapter wird beim Erstellen in RemotingPrecode der Wert der Adresse der MethodDesc- Struktur direkt vom CLR-System eingebettet (als direkter Wert im Assembler-Befehl). Der angegebene Wert kann bei Offset 1 (für x86) und 37 (für x64) extrahiert werden. Die Adresse der ThePreStub- Funktion ergibt sich aus der Addition des relativen Offsets im letzten Befehl jmp mit der Abschlussadresse des angegebenen Befehls (für x86) oder des direkten Offsets von 25 (für x64).

    Bei Objekten, die nicht zu anderen Domänen gehören, wird die Übergangsadresse nach der JIT-Kompilierung durch die Adresse der ThePreStub- Funktion ersetztIn Bezug auf die Adresse des generierten Codes ist das Verfahren zum Bestimmen der Adresse des generierten Codes nach dem Durchführen der JIT-Kompilierung dasselbe wie das Verfahren zum Bestimmen der Adresse der ThePreStub-Funktion vor dem Durchführen der JIT-Kompilierung. Bei Objekten, die zu anderen Domänen gehören, ändert sich nach der JIT-Kompilierung der Hauptteil des RemotingPrecode- Adapters nicht. Der Einfachheit halber wird die Option, RemotingPrecode für Objekte zu verwenden, die nicht zur Anwendungsdomäne gehören, nicht in Betracht gezogen .

3. ThePreStub- Funktion


Wie bereits erwähnt, bewirkt die interne Funktion von ThePreStub Folgendes:
  1. JIT-Kompilierung der durch die MethodDesc-Struktur identifizierten Methode;
  2. Setzen eines Zeigers in der MethodDesc-Struktur auf den generierten nativen Code;
  3. Schreiben Sie den Adapter so um, dass er einen unbedingten Sprung (jmp) zum generierten nativen Code ausführt.
  4. Ausführung des generierten nativen Codes.

In allen Versionen der CLR und der Hardwareplattformen wird die ThePreStub- Funktion auf Hardwareebene in der CLR implementiert, indem die interne PreStubWorker- Funktion aufgerufen und anschließend die Steuerung (über den Befehl jmp) an die von der angegebenen Funktion zurückgegebene Adresse übertragen wird. Der Vollständigkeit halber hier der Code für die ThePreStub- Funktion für verschiedene Plattformen.

ThePreStub-Funktionscode (x64)
CLR 4.6 и выше:
        push        r15  
        push        r14  
        push        r13  
        push        r12  
        push        rbp  
        push        rbx  
        push        rsi  
        push        rdi  
        sub         rsp,68h  
        mov         qword ptr   [rsp+0B0h],rcx  
        mov         qword ptr   [rsp+0B8h],rdx  
        mov         qword ptr   [rsp+0C0h],r8  
        mov         qword ptr   [rsp+0C8h],r9  
        movdqa      xmmword ptr [rsp+ 20h],xmm0  
        movdqa      xmmword ptr [rsp+ 30h],xmm1  
        movdqa      xmmword ptr [rsp+ 40h],xmm2  
        movdqa      xmmword ptr [rsp+ 50h],xmm3  
        lea         rcx,[rsp+68h]  
        mov         rdx,r10  
        call        PreStubWorker  
        movdqa      xmm0,xmmword ptr [rsp+20h]  
        movdqa      xmm1,xmmword ptr [rsp+ 30h]  
        movdqa      xmm2,xmmword ptr [rsp+ 40h]  
        movdqa      xmm3,xmmword ptr [rsp+ 50h]  
        mov         rcx,qword ptr    [rsp+0B0h]  
        mov         rdx,qword ptr    [rsp+0B8h]  
        mov         r8,qword ptr     [rsp+0C0h]  
        mov         r9,qword ptr     [rsp+0C8h]  
        add         rsp,68h  
        pop         rdi  
        pop         rsi  
        pop         rbx  
        pop         rbp  
        pop         r12  
        pop         r13  
        pop         r14  
        pop         r15  
        jmp         rax  
CLR 4.0:
        lea     rax, [rsp + 0x08]
        push    r10
        push    r15
        push    r14
        push    r13
        push    r12
        push    rbp
        push    rbx
        push    rsi
        push    rdi
        push    rax
        sub     rsp, 0x78
        mov     qword ptr   [rsp + 0xD0], rcx
        mov     qword ptr   [rsp + 0xD8], rdx
        mov     qword ptr   [rsp + 0xE0], r8
        mov     qword ptr   [rsp + 0xE8], r9
        movdqa  xmmword ptr [rsp + 0x20], xmm0
        movdqa  xmmword ptr [rsp + 0x30], xmm1
        movdqa  xmmword ptr [rsp + 0x40], xmm2
        movdqa  xmmword ptr [rsp + 0x50], xmm3
        lea     rcx, qword ptr [rsp + 0x68]
        call    PreStubWorker
        movdqa  xmm0, xmmword ptr [rsp + 0x20]
        movdqa  xmm1, xmmword ptr [rsp + 0x30]
        movdqa  xmm2, xmmword ptr [rsp + 0x40]
        movdqa  xmm3, xmmword ptr [rsp + 0x50]
        mov     rcx, qword ptr    [rsp + 0xD0]
        mov     rdx, qword ptr    [rsp + 0xD8]
        mov     r8 , qword ptr    [rsp + 0xE0]
        mov     r9 , qword ptr    [rsp + 0xE8]
        nop     
        add     rsp, 0x80
        pop     rdi
        pop     rsi
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        pop     r10
        jmp     rax
CLR 2.0:
        lea    rax, [rsp + 0x08]	          
        push   r10	
        push   r15	
        push   r14	
        push   r13	
        push   r12	
        push   rbp	
        push   rbx	
        push   rsi	
        push   rdi	
        push   rax	
        sub    rsp, 0x78	
        mov    qword ptr   [rsp + 0xD0], rcx		  
        mov    qword ptr   [rsp + 0xD8], rdx	  
        mov    qword ptr   [rsp + 0xE0], r8	  
        mov    qword ptr   [rsp + 0xE8], r9	  
        movdqa xmmword ptr [rsp + 0x20], xmm0
        movdqa xmmword ptr [rsp + 0x30], xmm1
        movdqa xmmword ptr [rsp + 0x40], xmm2
        movdqa xmmword ptr [rsp + 0x50], xmm3
        call   PrestubMethodFrame::GetMethodFrameVPtr
        mov    qword ptr [rsp + 0x68], rax	
        mov    rax, qword ptr [s_gsCookie]
        mov    qword ptr [rsp + 0x60], rax	
        call   GetThread
        mov    r12, rax	
        mov    rdx, qword ptr [r12 + 0x10]	
        mov    qword ptr [rsp + 0x70], rdx	
        lea    rcx, [rsp + 0x68]	
        mov    qword ptr [r12 + 0x10], rcx	
        call   PreStubWorker
        mov    rcx, qword ptr [r12 + 0x10]	
        mov    rdx, qword ptr [rcx + 0x08]	
        mov    qword ptr [r12 + 0x10], rdx
        movdqa xmm0, xmmword ptr [rsp + 0x20]
        movdqa xmm1, xmmword ptr [rsp + 0x30]
        movdqa xmm2, xmmword ptr [rsp + 0x40]
        movdqa xmm3, xmmword ptr [rsp + 0x50]
        mov    rcx, qword ptr    [rsp + 0xD0]	  
        mov    rdx, qword ptr    [rsp + 0xD8]	  
        mov    r8 , qword ptr    [rsp + 0xE0]	  
        mov    r9 , qword ptr    [rsp + 0xE8]	  
        nop	
        add    rsp, 0x80	  
        pop    rdi	
        pop    rsi	
        pop    rbx	
        pop    rbp	
        pop    r12
        pop    r13	
        pop    r14	
        pop    r15	
        pop    r10	
        jmp    rax
    


ThePreStub-Funktionscode (x86)
CLR 4.6 и выше:
        push    ebp  
        mov     ebp,esp  
        push    ebx  
        push    esi  
        push    edi  
        push    ecx  
        push    edx  
        mov     esi,esp  
        push    eax  
        push    esi  
        call   PreStubWorker
        pop     edx  
        pop     ecx  
        pop     edi  
        pop     esi  
        pop     ebx  
        pop     ebp  
        jmp     eax  
CLR 4.0:
        push   ebp	
        mov    ebp, esp	
        push   ebx	
        push   esi	
        push   edi	
        push   ecx	
        push   edx	
        push   eax	
        sub    esp, 0x0C	
        lea    esi, [esp + 0x04]	
        push   esi	
        call   PreStubWorker
        add    esp, 0x10	
        pop    edx	
        pop    ecx	
        pop    edi	
        pop    esi	
        pop    ebx	
        pop    ebp	
        jmp    eax	
CLR 2.0:
        push   eax
        push   edx
        push   PrestubMethodFrame::'vftable'
        push   ebp	
        push   ebx	
        push   esi	
        push   edi	
        lea    esi, [esp + 0x10]
        push   dword ptr [esi + 0x0C]
        push   ebp	
        mov    ebp, esp	
        push   ecx	
        push   edx	
        mov    ebx, dword ptr fs:0x0E34
        mov    edi, dworp ptr [ebx + 0x0C]
        mov    dword ptr [esi + 0x04], edi
        mov    dword ptr [ebx + 0x0C], esi
        push   cookie
        push   esi
        call   PreStubWorker
        mov    dword ptr [ebx + 0x0C], edi
        mov    ecx, dword ptr [esi + 0x08]
        mov    dword ptr [esi + 0x08], eax
        mov    eax, ecx
        add    esp, 0x04	
        pop    edx	
        pop    ecx	
        mov    esp, ebp	
        pop    ebp	
        add    esp, 0x04	
        pop    edi	
        pop    esi	
        pop    ebx	
        pop    ebp	
        add    esp, 0x08	
        ret    
    


Bei Kenntnis der Binärstruktur der Precode-Adapter kann die Adresse der ThePreStub- Funktion wie folgt bestimmt werden:
  1. Определим произвольный статический метод CLR (можно даже его сделать пустым), запретив inline-встраивание и предварительную компиляцию:

    public delegate void EmptyDelegate();
    [MethodImplAttribute(
     MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
    public static void Empty() {}
        

  2. Создадим и заблокируем в памяти делегат метода и определим адрес, возвращаемый функцией RuntimeMethodHandle.GetFunctionPointer:

    EmptyDelegate function = Empty; 
    GCHandle gc = GCHandle.Alloc(function);
    IntPtr methodPtr = function.Method.MethodHandle.GetFunctionPointer();
        

  3. Если команды по адресу methodPtr совпадают с образцом переходника StubPrecode, то следует воспользоваться способом вычисления адреса функции ThePreStub из пункта 1 раздела 2. Если же команды по полученному адресу совпадают с образцом переходника FixupPrecode, то следует воспользоваться способом вычисления адреса функции ThePreStub из пункта 2 раздела 2.

  4. Отменить блокировку памяти делегата метода:

    gc.Free();
        



4. PreStubWorker- Funktion


Die PreStubWorker- Funktion führt die folgenden Aktionen aus:
  1. JIT-Kompilierung der durch die MethodDesc-Struktur identifizierten Methode;
  2. Setzen eines Zeigers in der MethodDesc-Struktur auf den generierten nativen Code;
  3. Schreiben Sie den Adapter so um, dass er einen unbedingten Sprung (jmp) zum generierten nativen Code ausführt.
  4. Geben Sie die ThePreStub- Funktion der Adresse des geänderten Adapters zurück.

Die PreStubWorker- Funktion hat die folgende C-Deklaration (gemäß der CLR-Quelle):

для CLR 4.6 и выше: void* __stdcall PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD);
для CLR ниже 4.6:   void* __stdcall PreStubWorker(PrestubMethodFrame *pPFrame);
    

Используя этот факт, листинги кода функции ThePreStub, а также то, что функции ThePreStub в регистрах eax (для x86) и r10 (для x64) передается значение адреса MethodDesc, можно определить, как функция PreStubWorker получает внутри себя доступ к значению MethodDesc:
  1. для CLR 4.6 (и выше) указанное значение извлекается из второго переданного функции параметра;
  2. для CLR ниже 4.6 платформы x86 значение находится по смещению 8 структуры, адресуемой параметром pPFrame;
  3. для CLR ниже 4.6 платформы x64 значение находится адресу, на 16 байтов меньше значения адреса, расположенного по смещению 16 структуры, адресуемой параметром pPFrame.

Wenn Sie die Adresse der internen ThePreStub- Funktion kennen und die obigen Auflistungen des Codes verwenden, können Sie den Algorithmus für die Berechnung der Adresse der internen PreStubWorker- Funktion angeben , ohne feste Offsets innerhalb der ThePreStub- Funktion zu verwenden (die sich, wie Sie sehen, mit jeder neuen Version der CLR ändern):
  1. Für x86- und x64-Plattformen (mit Ausnahme von CLR 2.0) ergibt sich die angegebene Adresse aus dem Hinzufügen des relativen Offsets, der in den Aufrufbefehl integriert ist, der in der Funktion ThePreStub eindeutig ist , zur Abschlussadresse des angegebenen Befehls.
  2. Bei x64 CLR 2.0 ist die angegebene Adresse das Ergebnis des Addierens des relativen Offsets, der in den Aufrufbefehl integriert ist, dem der Befehl lea vorausgeht, mit der Endadresse des Aufrufbefehls.

Sie können die erforderlichen Aufrufbefehle während der Ausführung finden, wenn Sie über einen integrierten Disassembler verfügen, der die Codes und Größen von Befehlen zur Laufzeit bestimmen kann.

5. Der Abfangalgorithmus


Zusammenfassend können wir die folgenden Möglichkeiten zum Abfangen von .NET-Funktionen vorschlagen:
  1. Ruft die Adresse der Ersetzungsmethode mit einem Aufruf von RuntimeMethodHandle.GetFunctionPointer ab .
  2. Wenn die ersetzte Methode bereits JIT-kompiliert ist, suchen Sie die Adresse im Speicher des generierten nativen Codes und fangen Sie die angegebene Adresse ab, um die Ersetzungsmethode auszuführen.
  3. Wenn die ersetzte Methode noch nicht JIT-kompiliert ist, dann
    1. berechnen Sie die Adresse seiner MethodDesc- Struktur ;
    2. Berechnen Sie die Adresse und fangen Sie die PreStubWorker- Funktion so ab, dass die ursprüngliche Implementierung in der ersetzten PreStubWorker- Methode aufgerufen wird.
    3. Fügen Sie der ersetzten PreStubWorker- Funktion zusätzliche Logik für den Fall hinzu, dass die Funktion die MethodDesc- Adresse verwendet , die mit der erforderlichen Adresse übereinstimmt. In diesem Fall müssen Sie nach dem Aufrufen der ursprünglichen Implementierung die Adresse der generierten nativen Methode abrufen und die empfangene Adresse abfangen, um die Ersetzungsmethode auszuführen.


Nach alledem erfordern die obigen Absätze des Algorithmus keine detaillierten Erläuterungen mit Ausnahme der Absätze 2 und 3.1.

In Abschnitt 2 wird die Adresse eines real generierten nativen Codes (ohne Adapter) ermittelt. Der folgende Algorithmus basiert auf der Kenntnis der Binärstruktur der von der CLR-Umgebung generierten Adapter und berechnet die angegebene Adresse (oder gibt NULL zurück, wenn keine JIT-Kompilierung vorhanden ist).
  1. Rufen Sie die Adresse einer .NET-Methode ab, indem Sie RuntimeMethodHandle.GetFunctionPointer aufrufen .
  2. Если команды по полученному адресу совпадают с образцом переходника StubPrecode или RemotingPrecode, то извлечь адрес скомпилированного кода, как описано в п.1 и п.3 раздела 2. Если указанный адрес совпадает с адресом функции ThePreStub, то JIT-компиляция метода не проводилась и следует вернуть NULL. В противном случае вернуть адрес скомпилированного кода.
  3. До тех пор пока текущий адрес не совпадает с адресом функции ThePreStub выполнять следующее:
    1. если текущий адрес указывает на команду jmp, то перейти на адрес назначения для команды jmp;
    2. Andernfalls überprüfen Sie die Zieladresse des Aufrufbefehls, wenn die aktuelle Adresse auf den Aufrufbefehl verweist. Wenn es gleich dem PrecodeFixupThunk- Adapter ist (der Fall des FixupPrecode- Adapters vor der JIT-Kompilierung), geben Sie NULL zurück. Andernfalls geben Sie die Adresse zurück, an der sich der Aufrufbefehl befindet (oder die Zieladresse für den Aufrufbefehl).
    3. Andernfalls geben Sie die aktuelle Adresse zurück.

  4. Gibt NULL zurück, seit die Adresse der ThePreStub- Funktion erreicht wurde .

In Abschnitt 3.1 wird die Adresse einer MethodDesc- Struktur für eine nicht kompilierte Methode definiert. Der folgende Algorithmus basiert auf der Kenntnis der Binärstruktur der von der CLR-Umgebung generierten Adapter und berechnet die angegebene Adresse (oder NULL in einigen Fällen mit einer JIT-Kompilierung).
  1. Holen Sie die Adresse von .NET-Methode durch Aufruf RuntimeMethodHandle.GetFunctionPointer .
  2. Wenn die Befehle an der empfangenen Adresse mit dem Beispieladapter StubPrecode oder RemotingPrecode übereinstimmen , berechnen Sie die Adresse der MethodDesc- Struktur , wie in Abschnitt 2 Absätze 1 und 3 beschrieben.
  3. Gehen Sie folgendermaßen vor, bis die aktuelle Adresse mit der Adresse der ThePreStub- Funktion übereinstimmt :
    1. Wenn die aktuelle Adresse auf den Befehl jmp verweist, überprüfen Sie das Byte unmittelbar nach dem Befehl jmp. Wenn es sich um 0x5F handelt ( FixupPrecode- Fall nach JIT-Kompilierung), berechnen Sie die Adresse der MethodDesc- Struktur , wie in Abschnitt 2, Abschnitt 2 beschrieben. Andernfalls wechseln Sie zur Adresse für den Befehl jmp.
    2. Andernfalls überprüfen Sie die Zieladresse des Aufrufbefehls, wenn die aktuelle Adresse auf den Aufrufbefehl verweist. Wenn es dem PrecodeFixupThunk- Adapter entspricht (der Fall des FixupPrecode- Adapters vor der JIT-Kompilierung), berechnen Sie die Adresse der MethodDesc- Struktur , wie in Abschnitt 2, Abschnitt 2 beschrieben. Andernfalls geben Sie NULL zurück.
    3. Andernfalls geben Sie NULL zurück.

  4. Rückgabe NULL (das angegebene Element muss nicht erreichbar sein).

6. Fazit


Die Leistung des obigen Algorithmus wurde wiederholt in der Praxis (einschließlich industrieller Entwicklungen) auf verschiedenen Versionen von .NET und Hardwareplattformen getestet. Darauf aufbauend wurde die .NET-Bibliothek entwickelt, mit der das Abfangen von .NET-Funktionen recht einfach wird. Hier ist ein Beispiel für die Verwendung von Interception mithilfe der entwickelten Bibliothek.

Angenommen, Sie möchten die Open- Funktion der SqlConnection- Klasse abfangen . Dann kann der Abfangcode bei Verwendung der entwickelten Bibliothek wie folgt in C # aussehen:

public static class HookedConnection
{
    public static RTX.NET.HookHandle OpenHandle;
    [MethodImplAttribute(MethodImplOptions.NoInlining)]
    public static void Open(SqlConnection connection)
    {
        // вывести строку соединения
        Console.WriteLine(connection.ConnectionString); 
        // вызвать базовую функцию
        OpenHandle.Call(connection); 
    } 
} 
    

Hier enthält die Variable OpenHandle einen Deskriptor, mit dem Sie die Implementierung der ersetzten Funktion aufrufen können und der durch die Zuweisung einer Interception initialisiert wird:

using (ConnectionEntry entry = new ConnectionEntry()) 
{ 
    Test(); 
}
    

wobei die ConnectionEntry- Klasse die sogenannte ist "Interception Manager":

public class ConnectionEntry : RTX.NET.HookDispatcher, RTX.NET.IHookLoadHandler
{
    // обрабатываемые типы
    public virtual string[] GetTypes() 
    { 
        // указать класс для перехватываемых методов
        return new string[] { "System.Data.SqlClient.SqlConnection"}; 
    }
    // обработчик загрузки типов
    public virtual void OnLoad(RTX.NET.HookDispatcher dispatcher, Type type)
    {
        // перехватить методы
        HookedConnection.OpenHandle = HookOpen(dispatcher, type); 
    }
    private RTX.NET.HookHandle HookOpen(
        RTX.NET.HookDispatcher dispatcher, Type targetType)
    {
        // указать имя и тип параметров метода
        string name = "Open"; Type[] types = Type.EmptyTypes; 
        // указать атрибуты метода
        BindingFlags flags = BindingFlags.Public | 
        BindingFlags.Instance | BindingFlags.InvokeMethod; 
        // выполнить перехват
        return dispatcher.Install(targetType, name, 
            typeof(HookedConnection), name, flags, types
        ); 
    }
}
    

Dann beim Ausführen der Testfunktion

public static void Test()
{
    SqlConnection connection = new SqlConnection(); 
    connection.ConnectionString = @"Server=(localdb)\v11.0;" + 
        @"AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;"; 
    connection.Open (); 
    connection.Close(); 
}
    

Die folgende Meldung wird in der Konsole angezeigt:

Server=(localdb)\v11.0;AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;
    

Jetzt auch beliebt: