Pull to refresh
25
0
Send message
Для языка C++, кстати, C#-регулярка поиска начала функции (без ref-квалификаторов) выглядит так:
@"((?<!\b(?:switch|for|while|if|catch)\s*)\((?:[^'""()]|'(?:[^']|\\.)*'|""(?:[^""]|\\.)*""|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)\s*(?:const\s*)?(?:override\s*)?\{)"
А кто мешает просто использовать регулярные выражения на этапе компиляции?
Делается таким образом:
1) Написать класс, скажем FunctionEntry, который в конструкторе через Reflection сохраняет все параметры функции, в которой он используется, а также ссылку на внешний класс FunctionEntry (из внешней вызывающей функции);
2) Список классов FunctionEntry сохранять в локальной памяти потока (для того, чтобы можно было узнать внешний класс FunctionEntry без передачи его в качестве параметра функции);
3) Написать утилиту времени компиляции (вызов которой будет происходить каждый раз при сборке проекта), которая с использованием регулярных выражений
a) определяет начало каждой используемой функции и вставляет в нее создание класса FunctionEntry; а также начало блока try;
b) определяет конец каждой используемой функции и вставляет в нее блок catch, который ловит исключение и при помощи списка классов FunctionEntry из локальной памяти потока находит значения всех параметров
4) После завершения компиляции обратная утилита убирает все вставленные блоки.
Используется при защите соединений с базами данных (сфера информационных технологий). В двух словах: при перехвате функции создания соединения с MSSQL-сервером выполняется функциональность дополнительной двухфакторной аутентификации для последующего зашифрования/расшифрования передаваемых данных «на-лету».
Да, только оно датируется 2006 годом. И многие вещи там устарели.
Спасибо! При изучении внутреннего строения CLR я в основном пользовался анализом ассемблерного кода. Все переходники были изучены таким образом, но обобщить их (особенно понять логику FixupPrecode) мне помогла следующая ссылка:
https://github.com/dotnet/coreclr/blob/775003a4c72f0acc37eab84628fcef541533ba4e/Documentation/botr/method-descriptor.md
Замечу, правда, что приведенный по ссылке метод не является переносимым (т.е. может работать не всегда). И вот почему:

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

RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);

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

4) Переходники могут не быть FixupPrecode (особенно для NGen-модулей).
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;
Команда mov eax, imm для x86 содержит 4-байтовый операнд imm по смещению 1 от начала команды (которая занимает 5 байт).
Команда mov rax, imm для x64 содержит 8-байтовый операнд imm по смещению 2 от начала команды (которая занимает 10 байт).
Способ вызова через переходники не меняется с самого начала CLR (указанный способ можно мониторить в исходниках CLR на github).
Единственное, что приходится учитывать — не изменилась ли реализация ThePreStub, поскольку способ поиска PrestubWorker основывается на том, что ThePreStub не вызывает других функций, кроме PrestubWorker. Функции ThePreStub нет в исходниках (поскольку она реализована на ассемблере), приходится проверять на практике.
В планах написать такую же библиотеку для Mono под Linux-ые платформы.
Нет, именно(!!!) mov ebp, ebp.
Команда смысла не несет, а используется в качестве идентифицирующего признака переходника.
Да, для простоты в примере приведен класс для одного метода.
Но в этом же классе можно перехватить сколько угодно методов для произвольных классов.
Для этого в функции GetTypes нужно указать все требуемые классы, а в функции OnLoad
(в зависимости от принятого класса) перехватить сколько угодно его функций.
Пока, особо не афишируясь, используется в промышленных разработках, где необходимо перехватывать функции .NET.
Например, при перехвате обращений к базе данных SqlServer для обеспечения безопасного соединения.
Да, любые методы, которые можно получить через reflection.

Information

Rating
Does not participate
Registered
Activity