Метод сплайсинга для перехвата API-функций в Windows широко описан в Интернете и в различных литературных источниках. Простота указанного перехвата определяется следующими факторами:
- целевая функция является статической – она сразу присутствует в памяти загруженного модуля;
- адрес целевой функции легко определить (через таблицу экспорта модуля или функцию 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), которая выполняет следующие задачи:
- JIT-компиляция метода, идентифицируемого структурой MethodDesc;
- установка указателя в структуре MethodDesc на сгенерированный native-код;
- перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
- выполнение сгенерированного native-кода.
Таким образом, в результате первоначального вызова целевого метода не только сгенерируется и выполнится код метода, но и изменится содержимое переходника, что приведет к прямому вызову сгенерированного native-кода при последующих вызовах метода.
Любой метод .NET, вызываемый из среды CLR, проходит через адрес в таблице MethodTable методов класса. Однако среда CLR предоставляет возможность вызова метода из неуправляемой среды С/С++. Для этого служат следующие функции: GetFunctionPointer класса RuntimeMethodHandle и GetFunctionPointerForDelegate класса Marshal. Адреса, возвращаемые указанными функциями, также являются адресами переходников, среди которых могут быть уже упомянутые StubPrecode, FixupPrecode и RemotingPrecode. В результате первоначального вызова метода происходит его компиляция и выполнение, при последующем вызове – прямой переход на сгенерированный код. При этом важным для нас является то, что для некомпилированного метода при вызове его как через таблицу методов, так и через возвращаемые упомянутыми функциями указатели, происходит вызов внутренней функции ThePreStub.
2. Precode-переходники CLR
Рассмотрим сейчас по отдельности precode-переходники CLR и укажем как, зная только бинарный код самого переходника, в процессе выполнения можно определить адрес структуры MethodDesc, связанной с данным переходником, а также адрес внутренней функции ThePreStub (в дальнейшем нам это пригодится). Кроме того, укажем как в указанном переходнике определить адрес сгенерированного кода после выполнения JIT-компиляции.
- 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-компиляции.
- 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 соблюдает следующие два требования:
- предназначенные для переходников структуры MethodDesc объединяются в непрерывных блоках памяти MethodDescChunk:
- 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-переходника можно вычислить в два этапа:
- вычислить адрес переходника PrecodeFixupThunk путем сложения относительного смещения, встроенного в первую команду call FixupPrecode-переходника, с адресом завершения указанной команды;
- для всех платформ, кроме CLR 2.0 x86, вычислить адрес ThePreStub путем сложения относительного смещения, встроенного в последнюю команду jmp переходника PrecodeFixupThunk, с адресом завершения указанной команды;
- для платформы 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, с адресом завершения указанной команды.
- предназначенные для переходников структуры MethodDesc объединяются в непрерывных блоках памяти MethodDescChunk:
- 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 выполняет следующие действия:
- JIT-компиляция метода, идентифицируемого структурой MethodDesc;
- установка указателя в структуре MethodDesc на сгенерированный native-код;
- перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
- выполнение сгенерированного native-кода.
Во всех версиях CLR и аппаратных платформах функция ThePreStub реализована в CLR на аппаратном уровне через вызов внутренней функции PreStubWorker с последующей передачей управления (через команду jmp) на адрес, возвращенный указанной функцией. Для полноты описания приведем код функции ThePreStub для различных платформ.
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
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 можно определить следующим образом:
- Определим произвольный статический метод CLR (можно даже его сделать пустым), запретив inline-встраивание и предварительную компиляцию:
public delegate void EmptyDelegate(); [MethodImplAttribute( MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static void Empty() {}
- Создадим и заблокируем в памяти делегат метода и определим адрес, возвращаемый функцией RuntimeMethodHandle.GetFunctionPointer:
EmptyDelegate function = Empty; GCHandle gc = GCHandle.Alloc(function); IntPtr methodPtr = function.Method.MethodHandle.GetFunctionPointer();
- Если команды по адресу methodPtr совпадают с образцом переходника StubPrecode, то следует воспользоваться способом вычисления адреса функции ThePreStub из пункта 1 раздела 2. Если же команды по полученному адресу совпадают с образцом переходника FixupPrecode, то следует воспользоваться способом вычисления адреса функции ThePreStub из пункта 2 раздела 2.
- Отменить блокировку памяти делегата метода:
gc.Free();
4. Функция PreStubWorker
Функция PreStubWorker выполняет следующие действия:
- JIT-компиляция метода, идентифицируемого структурой MethodDesc;
- установка указателя в структуре MethodDesc на сгенерированный native-код;
- перезапись переходника таким образом, чтобы он осуществлял безусловный переход (jmp) на сгенерированный native-код;
- возврат функции 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:
- для CLR 4.6 (и выше) указанное значение извлекается из второго переданного функции параметра;
- для CLR ниже 4.6 платформы x86 значение находится по смещению 8 структуры, адресуемой параметром pPFrame;
- для CLR ниже 4.6 платформы x64 значение находится адресу, на 16 байтов меньше значения адреса, расположенного по смещению 16 структуры, адресуемой параметром pPFrame.
Зная адрес внутренней функции ThePreStub и на основе приведенных листингов ее кода, можно указать алгоритм вычисления адреса внутренней функции PreStubWorker, не используя фиксированные смещения внутри функции ThePreStub (которые, как видно, меняются с каждой новой версией CLR):
- для платформы x86 и x64 (кроме CLR 2.0) указанным адресом будет результат сложения относительного смещения, встроенного в единственную в функции ThePreStub команду call, с адресом завершения указанной команды;
- для CLR 2.0 платформы x64 указанным адресом будет результат сложения относительного смещения, встроенного в команду call, которой предшествует команда lea, с адресом завершения команды call.
Найти требуемые команды call в процессе выполнения можно при наличии встроенного дизассемблера, способного определять коды и размеры команд в режиме выполнения.
5. Алгоритм перехвата
Обобщая все вышесказанное, можно предложить следующий способ перехвата .NET-функций:
- получить адрес замещающего метода с использованием вызова RuntimeMethodHandle.GetFunctionPointer;
- если заменяемый метод уже JIT-скомпилирован, то найти адрес в памяти сгенерированного native-кода и перехватить указанный адрес для выполнения замещающего метода;
- если заменяемый метод еще не JIT-скомпилирован, то
- вычислить адрес его структуры MethodDesc;
- вычислить адрес и выполнить перехват функции PreStubWorker таким образом, чтобы в заменяющем PreStubWorker методе вызывалась исходная реализация;
- добавить в заменяющую PreStubWorker функцию дополнительную логику для случая использования функцией адреса MethodDesc, совпадающего с требуемым адресом. В этом случае после вызова исходной реализации получить адрес сгенерированного native-метода и выполнить перехват полученного адреса для выполнения замещающего метода.
- вычислить адрес его структуры MethodDesc;
После всего сказанного ранеее, приведенные пункты алгоритма не требует подробных объяснений, за исключением пунктов 2 и 3.1.
В пункте 2 говорится об определении адреса реального сгенерированного native-кода (безо всяких переходников). Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или возвращает NULL при отсутствии JIT-компиляции).
- Получить адрес .NET-метода через вызов RuntimeMethodHandle.GetFunctionPointer.
- Если команды по полученному адресу совпадают с образцом переходника StubPrecode или RemotingPrecode, то извлечь адрес скомпилированного кода, как описано в п.1 и п.3 раздела 2. Если указанный адрес совпадает с адресом функции ThePreStub, то JIT-компиляция метода не проводилась и следует вернуть NULL. В противном случае вернуть адрес скомпилированного кода.
- До тех пор пока текущий адрес не совпадает с адресом функции ThePreStub выполнять следующее:
- если текущий адрес указывает на команду jmp, то перейти на адрес назначения для команды jmp;
- иначе, если текущий адрес указывает на команду call, то проверить адрес назначения команды call. Если он равен переходнику PrecodeFixupThunk (случай FixupPrecode-переходника до проведения JIT компиляции), то вернуть NULL. В противном случае вернуть адрес, по которому расположена команда call (или адрес назначения для команды call);
- иначе, вернуть текущий адрес.
- если текущий адрес указывает на команду jmp, то перейти на адрес назначения для команды jmp;
- Вернуть NULL, поскольку достигнут адрес функции ThePreStub.
В пункте 3.1 говорится об определении адреса структуры MethodDesc для некомпилированного метода. Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или NULL в некоторых случаях при наличии JIT-компиляции).
- Получить адрес .NET-метода через вызов RuntimeMethodHandle.GetFunctionPointer.
- Если команды по полученному адресу совпадают с образцом переходника StubPrecode или RemotingPrecode, то вычислить адрес структуры MethodDesc, как описано в п.1 и п.3 раздела 2.
- До тех пор пока текущий адрес не совпадает с адресом функции ThePreStub выполнять следующее:
- если текущий адрес указывает на команду jmp, то то проверить байт сразу после команды jmp. Если он равен 0x5F (случай FixupPrecode после проведения JIT-компиляции), то вычислить адрес структуры MethodDesc, как описано в п.2 раздела 2. В противном случае перейти на адрес для команды jmp;
- иначе, если текущий адрес указывает на команду call, то проверить адрес назначения команды call. Если он равен переходнику PrecodeFixupThunk (случай FixupPrecode-переходника до проведения JIT компиляции), то вычислить адрес структуры MethodDesc, как описано в п.2 раздела 2. В противном случае вернуть NULL;
- иначе, вернуть NULL.
- если текущий адрес указывает на команду jmp, то то проверить байт сразу после команды jmp. Если он равен 0x5F (случай FixupPrecode после проведения JIT-компиляции), то вычислить адрес структуры MethodDesc, как описано в п.2 раздела 2. В противном случае перейти на адрес для команды jmp;
- Вернуть 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)
tangro
04.08.2016 11:43+11На основе его была разработана библиотека .NET, с использованием которой перехват .NET функций становится достаточно простым в применении.
Была разработана и куда дальше делась? В опенсорсе лежит, за деньги продаётся?ForwardAA
04.08.2016 16:09Пока, особо не афишируясь, используется в промышленных разработках, где необходимо перехватывать функции .NET.
Например, при перехвате обращений к базе данных SqlServer для обеспечения безопасного соединения.
Schrodingers_Cat
04.08.2016 12:17Написание целого класса для перехвата вызова одного метода выглядит очень громоздко. Было бы удобнее ставить хук таким образом:
using (Hook.On(type, methodName, bindingFlags)) { // Some code }
А для перехвата нескольких методов можно было бы создать какой-нибудь builder c fluent синтаксисом.
ForwardAA
04.08.2016 16:12Да, для простоты в примере приведен класс для одного метода.
Но в этом же классе можно перехватить сколько угодно методов для произвольных классов.
Для этого в функции GetTypes нужно указать все требуемые классы, а в функции OnLoad
(в зависимости от принятого класса) перехватить сколько угодно его функций.
akamajoris
04.08.2016 13:51x86:
mov eax, pMethodDesc
mov ebp, ebp
jmp ThePreStub
Наверное, все-такиmov eax, pMethodDesc mov ebp, esp jmp ThePreStub
Пост интересный. В закладки.ForwardAA
04.08.2016 16:15+1Нет, именно(!!!) mov ebp, ebp.
Команда смысла не несет, а используется в качестве идентифицирующего признака переходника.
den_golub
06.12.2016 09:12А в QD-LED больший спектр достигается использованием нескольких цветов точек? Или как-то по другому? Что мешает использовать другие светофильтры в обычных светодиодах, ведь в подсветка имеет практически сплошной спектр?
Насколько я знаю пока нет светофильтров, с помощью которых можно было бы сильно улучшить цветопередачу. А в QD-LED вообще не используются светофильтр, как таковой, по сути мы видим не белый прошедший через фильтр, что само по себе ограничивает диапазон, а саму волну определенной длины (читай определенного цвета) сразу.
Ununtrium
05.08.2016 12:27+1Что насчет .NET Core? Какова вероятность что все это отвалиться в следующих версиях?
ForwardAA
05.08.2016 17:01Способ вызова через переходники не меняется с самого начала CLR (указанный способ можно мониторить в исходниках CLR на github).
Единственное, что приходится учитывать — не изменилась ли реализация ThePreStub, поскольку способ поиска PrestubWorker основывается на том, что ThePreStub не вызывает других функций, кроме PrestubWorker. Функции ThePreStub нет в исходниках (поскольку она реализована на ассемблере), приходится проверять на практике.
PsyHaSTe
05.08.2016 14:13В процессе выполнения при анализе памяти указанный адрес можно явно прочитать по смещению 1 (для x86) или 2 (для x64) переходника с учетом разрядности процессора.
Может наоборот, 2 для х86 и 1 для х64?ForwardAA
05.08.2016 17:08Команда mov eax, imm для x86 содержит 4-байтовый операнд imm по смещению 1 от начала команды (которая занимает 5 байт).
Команда mov rax, imm для x64 содержит 8-байтовый операнд imm по смещению 2 от начала команды (которая занимает 10 байт).PsyHaSTe
05.08.2016 17:14Ясно. Просто как раз на днях столкнулся с кодом, который особо не понял, как получает смещения. Можете кстати прокомментировать? Особенно дебаг версию :)
http://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method/36415711#36415711ForwardAA
05.08.2016 18:55+11) Отличия следующих команд для различных платформ только в размерности адреса.
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;
ForwardAA
06.08.2016 09:16+1Замечу, правда, что приведенный по ссылке метод не является переносимым (т.е. может работать не всегда). И вот почему:
1) Иногда поток управления проходит через переходник, минуя адрес в таблице слотов;
2) Способ определения адреса слота в таблице MethodTable может изменяться с каждой версией CLR;
3) Вызов
RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
не всегда гарантирует выполнение JIT-компиляции;
4) Переходники могут не быть FixupPrecode (особенно для NGen-модулей).
Serginio1
06.08.2016 10:04Для .Net Core можно получить ссылку на статический метод Кроссплатформенное использование классов .Net из неуправляемого кода. Или аналог IDispatch на Linux
Пока не нашел как это сделать на большом .Net
Scratch
Приватные методы таким образом тоже можно перехватывать?
ForwardAA
Да, любые методы, которые можно получить через reflection.