Иногда при разработке программного обеспечения требуется встроить дополнительную функциональность в уже существующие приложения без модификации исходного текста приложений. Более того, зачастую сами приложения существуют только в скомпилированном бинарном виде без наличия исходного текста. Широко известным способом решения указанной задачи является т.н. “сплайсинг” – метод перехвата функций путем изменения кода целевой функции. Обычно при сплайсинге первые байты целевой функции перемещаются по другим адресам, а на их исходное место записывается команда безусловного перехода (jmp) на замещающую функцию. Поскольку сплайсинг требует низкоуровневых операций с памятью, то он осуществляется с использованием языка ассемблера и С/C++, что также накладывает определенные ограничения на реализацию замещающих функций – они обычно также реализованы на С/C++ (реже на ассемблере).

Метод сплайсинга для перехвата API-функций в Windows широко описан в Интернете и в различных литературных источниках. Простота указанного перехвата определяется следующими факторами:
  1. целевая функция является статической – она сразу присутствует в памяти загруженного модуля;
  2. адрес целевой функции легко определить (через таблицу экспорта модуля или функцию GetProcAddress).

Реализация замещающих функций на C/C++ при перехвате API-функций является оптимальным вариантом, поскольку Windows API реализовано, как известно, на языке C, и замещающие функции могут оперировать теми же понятиями, что и заменяемые.

С появлением технологии .NET ситуация коренным образом изменилась. Динамически подключаемые библиотеки, созданные для .NET, уже не содержат статических функций (функции генерируются динамически на основе команд промежуточного языка IL). Как следствие этого, сложно предсказать адрес в памяти, по которому будут размещаться функции после динамической компиляции (JIT-компиляции), а также отследить сам момент JIT-компиляции. Кроме того, без дополнительных усилий в качестве замещающей функции невозможно использовать .NET-функцию, поскольку та сама не является статической и не реализуется на языке C/C++.

В указанной статье будет описан алгоритм, применение которого позволяет замещать функции .NET на функции, также разрабатываемые в среде .NET. Для понимания приводимого алгоритма нам придется углубиться в реализацию CLR (общеязыковой исполняющей среды) .NET. При описании реализации CLR некоторые подробности мы будем упрощать во избежание усложнения понимания общей сути.

1. Способ вызова методов в CLR


В CLR каждая функция (метод) представляет собой набор IL-команд и вся информация о ней хранится в метаданных модуля. При загрузке модуля для каждого его класса система CLR создает таблицу MethodTable, содержащую информацию о методах класса. Каждый метод класса описывается структурой MethodDesc, одно из полей которой содержит адрес скомпилированного метода в памяти (при выполненной JIT-компиляции метода), а другое содержит индекс в таблице MethodTable, по которому указан адрес переходника (thunk), содержимое которого изменяется в процессе выполнения в зависимости от того, скомпилирован метод или нет.



Первоначально (до выполнения JIT-компиляции) в качестве переходника выступает один из четырех т.н. precode переходников CLR: StubPrecode, FixupPrecode, RemotingPrecode или NDirectImportPrecode. Поскольку последний переходник используется только для вызова API-функций Windows, которые можно перехватить и напрямую, то его мы рассматривать не будем.

Основной задачей каждого из precode-переходников является передача адреса структуры MethodDesc,
определяющей используемый метод, внутренней функции ThePreStub (ThePreStubAMD64 для платформы x64, на рисунке отмечена как Stub), которая выполняет следующие задачи:
  1. JIT-компиляция метода, идентифицируемого структурой MethodDesc;
  2. установка указателя в структуре MethodDesc на сгенерированный native-код;
  3. перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
  4. выполнение сгенерированного native-кода.

Таким образом, в результате первоначального вызова целевого метода не только сгенерируется и выполнится код метода, но и изменится содержимое переходника, что приведет к прямому вызову сгенерированного native-кода при последующих вызовах метода.

Любой метод .NET, вызываемый из среды CLR, проходит через адрес в таблице MethodTable методов класса. Однако среда CLR предоставляет возможность вызова метода из неуправляемой среды С/С++. Для этого служат следующие функции: GetFunctionPointer класса RuntimeMethodHandle и GetFunctionPointerForDelegate класса Marshal. Адреса, возвращаемые указанными функциями, также являются адресами переходников, среди которых могут быть уже упомянутые StubPrecode, FixupPrecode и RemotingPrecode. В результате первоначального вызова метода происходит его компиляция и выполнение, при последующем вызове – прямой переход на сгенерированный код. При этом важным для нас является то, что для некомпилированного метода при вызове его как через таблицу методов, так и через возвращаемые упомянутыми функциями указатели, происходит вызов внутренней функции ThePreStub.

2. Precode-переходники CLR


Рассмотрим сейчас по отдельности precode-переходники CLR и укажем как, зная только бинарный код самого переходника, в процессе выполнения можно определить адрес структуры MethodDesc, связанной с данным переходником, а также адрес внутренней функции ThePreStub (в дальнейшем нам это пригодится). Кроме того, укажем как в указанном переходнике определить адрес сгенерированного кода после выполнения JIT-компиляции.
  1. StubPrecode. В указанный переходник в момент его создания значение адреса структуры MethodDesc встраивается системой CLR напрямую (в качестве непосредственного значения в ассемблерной команде). Код переходника зависит только от аппаратной платформы и не зависит от версии CLR. Для различных аппаратных платформ он имеет следующий вид:
    x86:    
            mov eax, pMethodDesc 
            mov ebp, ebp         
            jmp ThePreStub       
    x64: 
            mov r10, pMethodDesc 
            jmp ThePreStub       
            

    Таким образом, адрес структуры MethodDesc передается функции ThePreStub в регистре eax (для x86) или r10 (для x64). В процессе выполнения при анализе памяти указанный адрес можно явно прочитать по смещению 1 (для x86) или 2 (для x64) переходника с учетом разрядности процессора. Адрес же функции ThePreStub можно вычислить путем сложения относительного смещения, встроенного в последнюю команду jmp, с адресом завершения указанной команды.

    После выполнения JIT-компиляции адрес перехода заменяется с адреса функции ThePreStub на адрес сгенерированного кода и содержимое переходника становится следующим:
    x86: 
            mov eax, pMethodDesc 
            mov ebp, ebp         
            jmp NativeCode       
    x64: 
            mov r10, pMethodDesc 
            jmp NativeCode       
            

    Способ определения адреса сгенерированного кода после выполнения JIT-компиляции совпадает со способом определения адреса функции ThePreStub до выполнения JIT-компиляции.

  2. FixupPrecode. Указанный переходник был разработан с целью оптимизации использования памяти. Он на всех аппаратных платформах занимает 8 байт, что меньше, чем размер переходника StubPrecode (12 байт для x86 и 16 байт для x64). Код переходника для всех аппаратных платформ и версий CLR имеет следующий вид:

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

    При использовании FixupPrecode-переходников CLR соблюдает следующие два требования:

    1. предназначенные для переходников структуры MethodDesc объединяются в непрерывных блоках памяти MethodDescChunk:

    2. FixupPrecode-переходники также объединяются в непрерывный блок памяти, причем в указанном блоке после окончания переходников системой CLR встраивается базовый адрес pMethodDescChunkBase структур MethodDesc в блоке памяти 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)
                 


    При такой организации памяти адрес структуры MethodDesc для определенного переходника FixupPrecode задается по следующей формуле:

    aдрес MethodDesc = pMethodDescChunkBase + MethodDescChunkIndex * sizeof(void*),

    где базовое смещение (pMethodDescChunkBase) извлекается по следующему адресу:

    адрес pMethodDescChunkBase = адрес FixupPrecode + 8 + PrecodeChunkIndex * 8,

    а MethodDescChunkIndex и PrecodeChunkIndex — байтовые значения, встроенные в PrecodeFixupThunk.

    Значение адреса структуры MethodDesc средой CLR вычисляется внутри дополнительного переходника PrecodeFixupThunk, который существует в памяти в единственном числе и предназначен только для вычисления и передачи указанного адреса функции ThePreStub в регистре eax (для x86) или r10 (для x64). Приведем код переходника PrecodeFixupThunk для различных аппаратных платформ.
    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                                       
            

    Адрес внутренней функции ThePreStub с использованием FixupPrecode-переходника можно вычислить в два этапа:

    1. вычислить адрес переходника PrecodeFixupThunk путем сложения относительного смещения, встроенного в первую команду call FixupPrecode-переходника, с адресом завершения указанной команды;
    2. для всех платформ, кроме CLR 2.0 x86, вычислить адрес ThePreStub путем сложения относительного смещения, встроенного в последнюю команду jmp переходника PrecodeFixupThunk, с адресом завершения указанной команды;
    3. для платформы CLR 2.0 x86, извлечь адрес ThePreStub по адресу, который встроен в последнюю команду jmp (косвенная адресация через внутреннюю переменную g_dwPreStubAddr).


    После выполнения JIT-компиляции в переходнике FixupPrecode первая команда call заменяется командой jmp с заменой адреса перехода с адреса переходника PrecodeFixupThunk на адрес сгенерированного кода. Кроме того, если за первой командой следует байт 0x5E, то он заменяется байтом 0x5F (указанные байты являются индикатором присутствия или отсутствия JIT-компиляции, байт 0xCC означает отсутствие информации). Таким образом, после замены содержимое переходника представляет собой следующее:

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

    Адрес сгенерированного кода после выполнения JIT-компиляции вычисляется путем сложения относительного смещения, встроенного в первую команду jmp, с адресом завершения указанной команды.

  3. RemotingPrecode. Указанный переходник используется при вызове методов объектов, которые могут существовать в другом домене приложений. Код переходника имеет следующий вид:
    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
            

    Как и в случае с переходником StubPrecode, в RemotingPrecode в момент его создания значение адреса структуры MethodDesc встраивается системой CLR напрямую (в качестве непосредственного значения в ассемблерной команде). Указанное значение можно извлечь по смещению 1 (для x86) и 37 (для x64). Адрес же функции ThePreStub представляет собой результат сложения относительного смещения, встроенного в последнюю команду jmp, с адресом завершения указанной команды (для x86) или непосредственное значение по смещению 25 (для x64).

    Для объектов, не принадлежащих другим доменам, после выполнения JIT-компиляции адрес перехода заменяется с адреса функции ThePreStub на адрес сгенерированного кода, поэтому способ определения адреса сгенерированного кода после выполнения JIT-компиляции совпадает со способом определения адреса функции ThePreStub до выполнения JIT-компиляции. Для объектов, принадлежащих другим доменам, после выполнения JIT компиляции тело переходника RemotingPrecode не изменяется. Для упрощения далее не рассматриваем вариант использования RemotingPrecode для объектов, не принадлежащих домену приложения.

3. Функция ThePreStub


Как уже упоминалось, внутренняя функция ThePreStub выполняет следующие действия:
  1. JIT-компиляция метода, идентифицируемого структурой MethodDesc;
  2. установка указателя в структуре MethodDesc на сгенерированный native-код;
  3. перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
  4. выполнение сгенерированного native-кода.

Во всех версиях CLR и аппаратных платформах функция ThePreStub реализована в CLR на аппаратном уровне через вызов внутренней функции PreStubWorker с последующей передачей управления (через команду jmp) на адрес, возвращенный указанной функцией. Для полноты описания приведем код функции ThePreStub для различных платформ.

Код функции ThePreStub (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 (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    
    


Зная бинарную структуру precode-переходников, адрес функции ThePreStub можно определить следующим образом:
  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


Функция PreStubWorker выполняет следующие действия:
  1. JIT-компиляция метода, идентифицируемого структурой MethodDesc;
  2. установка указателя в структуре MethodDesc на сгенерированный native-код;
  3. перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
  4. возврат функции ThePreStub адреса измененного переходника.

Функция PreStubWorker имеет следующее объявление на языке C (согласно исходным текстам CLR):

для 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.

Зная адрес внутренней функции ThePreStub и на основе приведенных листингов ее кода, можно указать алгоритм вычисления адреса внутренней функции PreStubWorker, не используя фиксированные смещения внутри функции ThePreStub (которые, как видно, меняются с каждой новой версией CLR):
  1. для платформы x86 и x64 (кроме CLR 2.0) указанным адресом будет результат сложения относительного смещения, встроенного в единственную в функции ThePreStub команду call, с адресом завершения указанной команды;
  2. для CLR 2.0 платформы x64 указанным адресом будет результат сложения относительного смещения, встроенного в команду call, которой предшествует команда lea, с адресом завершения команды call.

Найти требуемые команды call в процессе выполнения можно при наличии встроенного дизассемблера, способного определять коды и размеры команд в режиме выполнения.

5. Алгоритм перехвата


Обобщая все вышесказанное, можно предложить следующий способ перехвата .NET-функций:
  1. получить адрес замещающего метода с использованием вызова RuntimeMethodHandle.GetFunctionPointer;
  2. если заменяемый метод уже JIT-скомпилирован, то найти адрес в памяти сгенерированного native-кода и перехватить указанный адрес для выполнения замещающего метода;
  3. если заменяемый метод еще не JIT-скомпилирован, то
    1. вычислить адрес его структуры MethodDesc;
    2. вычислить адрес и выполнить перехват функции PreStubWorker таким образом, чтобы в заменяющем PreStubWorker методе вызывалась исходная реализация;
    3. добавить в заменяющую PreStubWorker функцию дополнительную логику для случая использования функцией адреса MethodDesc, совпадающего с требуемым адресом. В этом случае после вызова исходной реализации получить адрес сгенерированного native-метода и выполнить перехват полученного адреса для выполнения замещающего метода.


После всего сказанного ранеее, приведенные пункты алгоритма не требует подробных объяснений, за исключением пунктов 2 и 3.1.

В пункте 2 говорится об определении адреса реального сгенерированного native-кода (безо всяких переходников). Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или возвращает NULL при отсутствии JIT-компиляции).
  1. Получить адрес .NET-метода через вызов RuntimeMethodHandle.GetFunctionPointer.
  2. Если команды по полученному адресу совпадают с образцом переходника StubPrecode или RemotingPrecode, то извлечь адрес скомпилированного кода, как описано в п.1 и п.3 раздела 2. Если указанный адрес совпадает с адресом функции ThePreStub, то JIT-компиляция метода не проводилась и следует вернуть NULL. В противном случае вернуть адрес скомпилированного кода.
  3. До тех пор пока текущий адрес не совпадает с адресом функции ThePreStub выполнять следующее:
    1. если текущий адрес указывает на команду jmp, то перейти на адрес назначения для команды jmp;
    2. иначе, если текущий адрес указывает на команду call, то проверить адрес назначения команды call. Если он равен переходнику PrecodeFixupThunk (случай FixupPrecode-переходника до проведения JIT компиляции), то вернуть NULL. В противном случае вернуть адрес, по которому расположена команда call (или адрес назначения для команды call);
    3. иначе, вернуть текущий адрес.

  4. Вернуть NULL, поскольку достигнут адрес функции ThePreStub.

В пункте 3.1 говорится об определении адреса структуры MethodDesc для некомпилированного метода. Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или NULL в некоторых случаях при наличии JIT-компиляции).
  1. Получить адрес .NET-метода через вызов RuntimeMethodHandle.GetFunctionPointer.
  2. Если команды по полученному адресу совпадают с образцом переходника StubPrecode или RemotingPrecode, то вычислить адрес структуры MethodDesc, как описано в п.1 и п.3 раздела 2.
  3. До тех пор пока текущий адрес не совпадает с адресом функции ThePreStub выполнять следующее:
    1. если текущий адрес указывает на команду jmp, то то проверить байт сразу после команды jmp. Если он равен 0x5F (случай FixupPrecode после проведения JIT-компиляции), то вычислить адрес структуры MethodDesc, как описано в п.2 раздела 2. В противном случае перейти на адрес для команды jmp;
    2. иначе, если текущий адрес указывает на команду call, то проверить адрес назначения команды call. Если он равен переходнику PrecodeFixupThunk (случай FixupPrecode-переходника до проведения JIT компиляции), то вычислить адрес структуры MethodDesc, как описано в п.2 раздела 2. В противном случае вернуть NULL;
    3. иначе, вернуть NULL.

  4. Вернуть NULL (указанный пункт должен быть недостижимым).

6. Заключение


Работоспособность приведенного алгоритма была неоднократно проверена на практике (в том числе, в промышленных разработках) на различных версиях .NET и аппаратных платформах. На основе его была разработана библиотека .NET, с использованием которой перехват .NET функций становится достаточно простым в применении. Приведем пример применения перехвата при помощи разработанной библиотеки.

Пусть требуется перехватить функцию Open класса SqlConnection. Тогда код перехвата при использовании разработанной библиотеки может выглядеть на языке C# следующим образом:

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); 
    } 
} 
    

Здесь переменная OpenHandle содержит описатель, с использованием которого можно вызвать реализацию заменяемой функции и который инициализируется в результате назначения перехвата:

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

где класс ConnectionEntry является т.н. “диспетчером перехвата”:

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
        ); 
    }
}
    

Тогда при выполнении функции Test

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(); 
}
    

в консоли будет отображено следующее сообщение:

Server=(localdb)\v11.0;AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;
    
Поделиться с друзьями
-->

Комментарии (20)


  1. Scratch
    04.08.2016 10:54

    Приватные методы таким образом тоже можно перехватывать?


    1. ForwardAA
      04.08.2016 16:06

      Да, любые методы, которые можно получить через reflection.


  1. tangro
    04.08.2016 11:43
    +11

    На основе его была разработана библиотека .NET, с использованием которой перехват .NET функций становится достаточно простым в применении.

    Была разработана и куда дальше делась? В опенсорсе лежит, за деньги продаётся?


    1. ForwardAA
      04.08.2016 16:09

      Пока, особо не афишируясь, используется в промышленных разработках, где необходимо перехватывать функции .NET.
      Например, при перехвате обращений к базе данных SqlServer для обеспечения безопасного соединения.


      1. ForwardAA
        04.08.2016 16:18

        В планах написать такую же библиотеку для Mono под Linux-ые платформы.


        1. Scratch
          04.08.2016 19:28
          +1

          Вы даже близко не представляете, сколько благодарности на вас свалится если вы её выложите на гитхаб


  1. Schrodingers_Cat
    04.08.2016 12:17

    Написание целого класса для перехвата вызова одного метода выглядит очень громоздко. Было бы удобнее ставить хук таким образом:


    using (Hook.On(type, methodName, bindingFlags))
    {
      // Some code
    }

    А для перехвата нескольких методов можно было бы создать какой-нибудь builder c fluent синтаксисом.


    1. ForwardAA
      04.08.2016 16:12

      Да, для простоты в примере приведен класс для одного метода.
      Но в этом же классе можно перехватить сколько угодно методов для произвольных классов.
      Для этого в функции GetTypes нужно указать все требуемые классы, а в функции OnLoad
      (в зависимости от принятого класса) перехватить сколько угодно его функций.


  1. akamajoris
    04.08.2016 13:51

    x86:
    mov eax, pMethodDesc
    mov ebp, ebp
    jmp ThePreStub


    Наверное, все-таки
    mov eax, pMethodDesc 
    mov ebp, esp
    jmp ThePreStub
    

    Пост интересный. В закладки.


    1. ForwardAA
      04.08.2016 16:15
      +1

      Нет, именно(!!!) mov ebp, ebp.
      Команда смысла не несет, а используется в качестве идентифицирующего признака переходника.


  1. den_golub
    06.12.2016 09:12

    А в QD-LED больший спектр достигается использованием нескольких цветов точек? Или как-то по другому? Что мешает использовать другие светофильтры в обычных светодиодах, ведь в подсветка имеет практически сплошной спектр?

    Насколько я знаю пока нет светофильтров, с помощью которых можно было бы сильно улучшить цветопередачу. А в QD-LED вообще не используются светофильтр, как таковой, по сути мы видим не белый прошедший через фильтр, что само по себе ограничивает диапазон, а саму волну определенной длины (читай определенного цвета) сразу.


  1. sidristij
    04.08.2016 20:22

    браво :)


  1. Ununtrium
    05.08.2016 12:27
    +1

    Что насчет .NET Core? Какова вероятность что все это отвалиться в следующих версиях?


    1. ForwardAA
      05.08.2016 17:01

      Способ вызова через переходники не меняется с самого начала CLR (указанный способ можно мониторить в исходниках CLR на github).
      Единственное, что приходится учитывать — не изменилась ли реализация ThePreStub, поскольку способ поиска PrestubWorker основывается на том, что ThePreStub не вызывает других функций, кроме PrestubWorker. Функции ThePreStub нет в исходниках (поскольку она реализована на ассемблере), приходится проверять на практике.


  1. PsyHaSTe
    05.08.2016 14:13

    В процессе выполнения при анализе памяти указанный адрес можно явно прочитать по смещению 1 (для x86) или 2 (для x64) переходника с учетом разрядности процессора.

    Может наоборот, 2 для х86 и 1 для х64?


    1. ForwardAA
      05.08.2016 17:08

      Команда mov eax, imm для x86 содержит 4-байтовый операнд imm по смещению 1 от начала команды (которая занимает 5 байт).
      Команда mov rax, imm для x64 содержит 8-байтовый операнд imm по смещению 2 от начала команды (которая занимает 10 байт).


      1. PsyHaSTe
        05.08.2016 17:14

        Ясно. Просто как раз на днях столкнулся с кодом, который особо не понял, как получает смещения. Можете кстати прокомментировать? Особенно дебаг версию :)
        http://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method/36415711#36415711


        1. ForwardAA
          05.08.2016 18:55
          +1

          1) Отличия следующих команд для различных платформ только в размерности адреса.

          int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
          long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;

          только в размерности адреса.

          2) Следующие строки (скорее всего) получают адреса слотов в таблице MethodTable (см. приведенную картинку)

          int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
          int* tar = (int*)methodToReplace.MethodHandle.Value.ToPointer() + 2;

          3) Следующие строки определяют адрес переходника FixupPrecode после компиляции для двух функций

          byte* injInst = (byte*)*inj;
          byte* tarInst = (byte*)*tar;

          4) Следующие строки

          int* injSrc = (int*)(injInst + 1);
          int* tarSrc = (int*)(tarInst + 1);

          в командах jmp NativeCode (см. описание FixupPrecode после компиляции) находят смещение сгенерированного кода относительно окончания самих команд (которые занимают 5 байт);

          5) Следующая строка вычисляет относительное смещение сгенерированного внедряемого кода относительно окончания команды jmp NativeCode для перехватываемого кода(!!!) и записывает его в команду jmp NativeCode для перехватываемого кода

          *tarSrc = (((int)injInst + 5) + *injSrc) — ((int)tarInst + 5);

          Таким образом, в переходнике команда jmp NativeCode(Source) заменяется на jmp NativeCode(Inject)

          6) В Release-версии адрес напрямую заменяется в слотах таблицы MethodTable;


        1. ForwardAA
          06.08.2016 09:16
          +1

          Замечу, правда, что приведенный по ссылке метод не является переносимым (т.е. может работать не всегда). И вот почему:

          1) Иногда поток управления проходит через переходник, минуя адрес в таблице слотов;
          2) Способ определения адреса слота в таблице MethodTable может изменяться с каждой версией CLR;
          3) Вызов

          RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);

          не всегда гарантирует выполнение JIT-компиляции;

          4) Переходники могут не быть FixupPrecode (особенно для NGen-модулей).


  1. Serginio1
    06.08.2016 10:04

    Для .Net Core можно получить ссылку на статический метод Кроссплатформенное использование классов .Net из неуправляемого кода. Или аналог IDispatch на Linux

    Пока не нашел как это сделать на большом .Net