Иногда при разработке программного обеспечения требуется встроить дополнительную функциональность в уже существующие приложения без модификации исходного текста приложений. Более того, зачастую сами приложения существуют только в скомпилированном бинарном виде без наличия исходного текста. Широко известным способом решения указанной задачи является т.н. “сплайсинг” – метод перехвата функций путем изменения кода целевой функции. Обычно при сплайсинге первые байты целевой функции перемещаются по другим адресам, а на их исходное место записывается команда безусловного перехода (jmp) на замещающую функцию. Поскольку сплайсинг требует низкоуровневых операций с памятью, то он осуществляется с использованием языка ассемблера и С/C++, что также накладывает определенные ограничения на реализацию замещающих функций – они обычно также реализованы на С/C++ (реже на ассемблере).
Метод сплайсинга для перехвата 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-метода и выполнить перехват полученного адреса для выполнения замещающего метода.
После всего сказанного ранеее, приведенные пункты алгоритма не требует подробных объяснений, за исключением пунктов 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);
- иначе, вернуть текущий адрес.
- Вернуть 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.
- Вернуть 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:MyFolderMyData.mdf;Integrated Security=true;";
connection.Open ();
connection.Close();
}
в консоли будет отображено следующее сообщение:
Server=(localdb)v11.0;AttachDbFileName=C:MyFolderMyData.mdf;Integrated Security=true;
Автор: ForwardAA