- PVSM.RU - https://www.pvsm.ru -
Иногда при разработке программного обеспечения требуется встроить дополнительную функциональность в уже существующие приложения без модификации исходного текста приложений. Более того, зачастую сами приложения существуют только в скомпилированном бинарном виде без наличия исходного текста. Широко известным способом решения указанной задачи является т.н. “сплайсинг” – метод перехвата функций путем изменения кода целевой функции. Обычно при сплайсинге первые байты целевой функции перемещаются по другим адресам, а на их исходное место записывается команда безусловного перехода (jmp) на замещающую функцию. Поскольку сплайсинг требует низкоуровневых операций с памятью, то он осуществляется с использованием языка ассемблера и С/C++, что также накладывает определенные ограничения на реализацию замещающих функций – они обычно также реализованы на С/C++ (реже на ассемблере).
Метод сплайсинга для перехвата API-функций в Windows широко описан в Интернете и в различных литературных источниках. Простота указанного перехвата определяется следующими факторами:
Реализация замещающих функций на C/C++ при перехвате API-функций является оптимальным вариантом, поскольку Windows API реализовано, как известно, на языке C, и замещающие функции могут оперировать теми же понятиями, что и заменяемые.
С появлением технологии .NET ситуация коренным образом изменилась. Динамически подключаемые библиотеки, созданные для .NET, уже не содержат статических функций (функции генерируются динамически на основе команд промежуточного языка IL). Как следствие этого, сложно предсказать адрес в памяти, по которому будут размещаться функции после динамической компиляции (JIT-компиляции), а также отследить сам момент JIT-компиляции. Кроме того, без дополнительных усилий в качестве замещающей функции невозможно использовать .NET-функцию, поскольку та сама не является статической и не реализуется на языке C/C++.
В указанной статье будет описан алгоритм, применение которого позволяет замещать функции .NET на функции, также разрабатываемые в среде .NET. Для понимания приводимого алгоритма нам придется углубиться в реализацию CLR (общеязыковой исполняющей среды) .NET. При описании реализации CLR некоторые подробности мы будем упрощать во избежание усложнения понимания общей сути.
В CLR каждая функция (метод) представляет собой набор IL-команд и вся информация о ней хранится в метаданных модуля. При загрузке модуля для каждого его класса система CLR создает таблицу MethodTable, содержащую информацию о методах класса. Каждый метод класса описывается структурой MethodDesc, одно из полей которой содержит адрес скомпилированного метода в памяти (при выполненной JIT-компиляции метода), а другое содержит индекс в таблице MethodTable, по которому указан адрес переходника (thunk), содержимое которого изменяется в процессе выполнения в зависимости от того, скомпилирован метод или нет.

Первоначально (до выполнения JIT-компиляции) в качестве переходника выступает один из четырех т.н. precode переходников CLR: StubPrecode, FixupPrecode, RemotingPrecode или NDirectImportPrecode. Поскольку последний переходник используется только для вызова API-функций Windows, которые можно перехватить и напрямую, то его мы рассматривать не будем.
Основной задачей каждого из precode-переходников является передача адреса структуры MethodDesc,
определяющей используемый метод, внутренней функции ThePreStub (ThePreStubAMD64 для платформы x64, на рисунке отмечена как Stub), которая выполняет следующие задачи:
Таким образом, в результате первоначального вызова целевого метода не только сгенерируется и выполнится код метода, но и изменится содержимое переходника, что приведет к прямому вызову сгенерированного native-кода при последующих вызовах метода.
Любой метод .NET, вызываемый из среды CLR, проходит через адрес в таблице MethodTable методов класса. Однако среда CLR предоставляет возможность вызова метода из неуправляемой среды С/С++. Для этого служат следующие функции: GetFunctionPointer класса RuntimeMethodHandle и GetFunctionPointerForDelegate класса Marshal. Адреса, возвращаемые указанными функциями, также являются адресами переходников, среди которых могут быть уже упомянутые StubPrecode, FixupPrecode и RemotingPrecode. В результате первоначального вызова метода происходит его компиляция и выполнение, при последующем вызове – прямой переход на сгенерированный код. При этом важным для нас является то, что для некомпилированного метода при вызове его как через таблицу методов, так и через возвращаемые упомянутыми функциями указатели, происходит вызов внутренней функции ThePreStub.
Рассмотрим сейчас по отдельности precode-переходники CLR и укажем как, зная только бинарный код самого переходника, в процессе выполнения можно определить адрес структуры MethodDesc, связанной с данным переходником, а также адрес внутренней функции ThePreStub (в дальнейшем нам это пригодится). Кроме того, укажем как в указанном переходнике определить адрес сгенерированного кода после выполнения JIT-компиляции.
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-компиляции.
call PrecodeFixupThunk
db 0x5E
db MethodDescChunkIndex
db PrecodeChunkIndex
или
call PrecodeFixupThunk
db 0xСС
db MethodDescChunkIndex
db PrecodeChunkIndex
При использовании FixupPrecode-переходников CLR соблюдает следующие два требования:
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-переходника можно вычислить в два этапа:
После выполнения 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, с адресом завершения указанной команды.
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 для объектов, не принадлежащих домену приложения.
Как уже упоминалось, внутренняя функция ThePreStub выполняет следующие действия:
Во всех версиях 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 можно определить следующим образом:
public delegate void EmptyDelegate();
[MethodImplAttribute(
MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void Empty() {}
EmptyDelegate function = Empty;
GCHandle gc = GCHandle.Alloc(function);
IntPtr methodPtr = function.Method.MethodHandle.GetFunctionPointer();
gc.Free();
Функция PreStubWorker выполняет следующие действия:
Функция 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:
Зная адрес внутренней функции ThePreStub и на основе приведенных листингов ее кода, можно указать алгоритм вычисления адреса внутренней функции PreStubWorker, не используя фиксированные смещения внутри функции ThePreStub (которые, как видно, меняются с каждой новой версией CLR):
Найти требуемые команды call в процессе выполнения можно при наличии встроенного дизассемблера, способного определять коды и размеры команд в режиме выполнения.
Обобщая все вышесказанное, можно предложить следующий способ перехвата .NET-функций:
После всего сказанного ранеее, приведенные пункты алгоритма не требует подробных объяснений, за исключением пунктов 2 и 3.1.
В пункте 2 говорится об определении адреса реального сгенерированного native-кода (безо всяких переходников). Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или возвращает NULL при отсутствии JIT-компиляции).
В пункте 3.1 говорится об определении адреса структуры MethodDesc для некомпилированного метода. Приводимый ниже алгоритм основан на знании бинарной структуры переходников, генерируемых средой CLR, и вычисляет указанный адрес (или NULL в некоторых случаях при наличии JIT-компиляции).
Работоспособность приведенного алгоритма была неоднократно проверена на практике (в том числе, в промышленных разработках) на различных версиях .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
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/167055
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/307088/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.