procedure HookFunction(const ATargetFunction, AHandler: Pointer); const MaxPossibleSize = 8; var OldProtectionCode: Cardinal; begin VirtualProtect(ATargetFunction, MaxPossibleSize, PAGE_READWRITE, @OldProtectionCode); try ... // другие действия, не имеющие значения для этой задачки finally VirtualProtect(ATargetFunction, MaxPossibleSize, OldProtectionCode, @OldProtectionCode); end; end;Данный код предназначен для установки ловушки (хука) на функцию. Сначала мы изменяем защиту сегмента кода на чтение-запись, чтобы можно было его изменить (ведь обычно запись в сегмент кода запрещена). Одновременно мы сохраняем предыдущий режим доступа в
OldProtectionCode
. После чего (действия в "...") мы перезаписываем начало функции на инструкцию JMP
, которая передаст управление на наш обработчик AHandler
. И, наконец, мы возвращаем атрибуты доступа (типа, ничего и не произошло).Но даже в таком коротком кусочке кода есть одна проблема. При некоторых условиях этот код будет вылетать с Access Violation. Не могли бы вы указать эти условия и как следует исправить этот код, чтобы он работал правильно?
Примечание: разумеется, задачка дана в предположении, что аргументы функции верны (контракт вызова соблюдён). Т.е. оба указателя указывают на начала некоторых функций в сегментах кода.
Ответ.
Будет проблема, если DEP включён, а между try и finally функция ATargetFunction каким-то образом (прямо или косвенно) вызывается. Даже нет, не она сама (зачем такой изврат), а другая нужная функция в пределах страниц, на которые попадают байтики из диапазона ATargetFunction - ATargetFunction+MaxPossibleSize.
ОтветитьУдалитьВместо PAGE_READWRITE нужно PAGE_EXECUTE_READWRITE. Скорее всего, это.
Если нет, то навскидку можно придумать ещё пару проблем:
1) VirtualProtect пишет в OldProtectionCode прежде чем читает новое значение. Исправление очевидно: завести ещё одну переменную, например, NewProtectionCode, и передавать в последнем вызове указатель на неё.
2) Если мы под отладчиком, и на начало функции ATargetFunction поставлена точка останова, то там код уже изменён, дебажий хук стоит. Что тут может сломаться, правда, непонятно. И VirtualProtect тут вроде как ни при чём.
Дополнительное примечание: ATargetFunction не вызывается, размер ATargetFunction больше 8-ми байт.
ОтветитьУдалитьЭто связано с атрибутом PAGE_READWRITE. VirtualProtect изменяет атрибуты защиты всей станицы, в которой расположены наши 8 байт. Если перед этим страница имела атрибут PAGE_EXECUTE_READWRITE, то теперь код, расположенный в ней, станет неисполнимым, что может повлечь за собой AV при его вызове.
ОтветитьУдалитьДополнительно было бы неплохо проверять результат, возвращаемый VirtualProtect'ом, а также вызвать FlushInstructionCache для сброса кэша инструкций процессора после изменения кода.
Примерное решение выглядит так:
procedure HookFunction(const ATargetFunction, AHandler: Pointer);
const
MaxPossibleSize = 8;
var
OldProtectionCode: Cardinal;
begin
if VirtualProtect(ATargetFunction, MaxPossibleSize, PAGE_EXECUTE_READWRITE, @OldProtectionCode) then
try
... // другие действия, не имеющие значения для этой задачки
finally
VirtualProtect(ATargetFunction, MaxPossibleSize, OldProtectionCode, @OldProtectionCode);
FlushInstructionCache(GetCurrentProcess, ATargetFunction, 8);
end;
end;
Как уточнение: 8 байт могут затронуть 2 страницы.
ОтветитьУдалить1) мне весьма не нравится, что не проверяется return value VirtualProtect
ОтветитьУдалить2) мне очень любопытно, что будет если вашу функцию натравить на нея самою. Не выдернет она у себя из под ног право на исполнение себя самой?
3) что будет, если скармливается функция размером меньше 8 байт, стоящая на самом конце стpаницы, и следующей стрaницы просто-таки не существует ( ну или guard page, etc) ? Кажется при загрузке DLL создавались заглушки Jump Tables по 5 байт на рыло ($E9 JMP FAR && address:DWORD) ?
ОтветитьУдалить3.1) видимо использование Jump Tables подпадает под комментарий выше о размере функции >= 8. Но может и нет.
ОтветитьУдалить(пока писал ответы - комменты не читал. Во избежание подглядывания)
2.1) такой пример выглядит как намеренный выстрел в ногу. Пример попроще - что если ATargetFunction не является самой HookFunction, но расположена в той же странице ? ну а фикс уже был выше - давать права и на выполнение в том числе. Или копировать себя в отдельную страницу. Или отказывать, если адреса перекрываются.
Ну и в любом случае - проверять что возвращает VirtualProtect, а то элементарно вторым вызовом мусор запишем в права.
и до кучи, в многопоточной программе этот код может отработать прекрасно, а вот другой код в это время вылететь и с AV и с invalid opcode и с чем угодно. Надо все другие потоки усыплять перед этим :-)
ОтветитьУдалитьFlushInstructionCache(GetCurrentProcess, ATargetFunction, MaxPossibleSize)
ОтветитьУдалитьсказал MSDN :)
ну тут еще много чего стоит подправить, но для коня в вакууме нехватает именно сброса кэша