10 августа 2012 г.

Задачка №15

В чём проблема в этом коде?

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. Не могли бы вы указать эти условия и как следует исправить этот код, чтобы он работал правильно?

Примечание: разумеется, задачка дана в предположении, что аргументы функции верны (контракт вызова соблюдён). Т.е. оба указателя указывают на начала некоторых функций в сегментах кода.

Ответ.

9 комментариев :

  1. Будет проблема, если DEP включён, а между try и finally функция ATargetFunction каким-то образом (прямо или косвенно) вызывается. Даже нет, не она сама (зачем такой изврат), а другая нужная функция в пределах страниц, на которые попадают байтики из диапазона ATargetFunction - ATargetFunction+MaxPossibleSize.
    Вместо PAGE_READWRITE нужно PAGE_EXECUTE_READWRITE. Скорее всего, это.

    Если нет, то навскидку можно придумать ещё пару проблем:
    1) VirtualProtect пишет в OldProtectionCode прежде чем читает новое значение. Исправление очевидно: завести ещё одну переменную, например, NewProtectionCode, и передавать в последнем вызове указатель на неё.
    2) Если мы под отладчиком, и на начало функции ATargetFunction поставлена точка останова, то там код уже изменён, дебажий хук стоит. Что тут может сломаться, правда, непонятно. И VirtualProtect тут вроде как ни при чём.

    ОтветитьУдалить
  2. Дополнительное примечание: ATargetFunction не вызывается, размер ATargetFunction больше 8-ми байт.

    ОтветитьУдалить
  3. Это связано с атрибутом 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;

    ОтветитьУдалить
  4. Как уточнение: 8 байт могут затронуть 2 страницы.

    ОтветитьУдалить
  5. 1) мне весьма не нравится, что не проверяется return value VirtualProtect

    2) мне очень любопытно, что будет если вашу функцию натравить на нея самою. Не выдернет она у себя из под ног право на исполнение себя самой?

    ОтветитьУдалить
  6. 3) что будет, если скармливается функция размером меньше 8 байт, стоящая на самом конце стpаницы, и следующей стрaницы просто-таки не существует ( ну или guard page, etc) ? Кажется при загрузке DLL создавались заглушки Jump Tables по 5 байт на рыло ($E9 JMP FAR && address:DWORD) ?

    ОтветитьУдалить
  7. 3.1) видимо использование Jump Tables подпадает под комментарий выше о размере функции >= 8. Но может и нет.

    (пока писал ответы - комменты не читал. Во избежание подглядывания)

    2.1) такой пример выглядит как намеренный выстрел в ногу. Пример попроще - что если ATargetFunction не является самой HookFunction, но расположена в той же странице ? ну а фикс уже был выше - давать права и на выполнение в том числе. Или копировать себя в отдельную страницу. Или отказывать, если адреса перекрываются.

    Ну и в любом случае - проверять что возвращает VirtualProtect, а то элементарно вторым вызовом мусор запишем в права.

    ОтветитьУдалить
  8. и до кучи, в многопоточной программе этот код может отработать прекрасно, а вот другой код в это время вылететь и с AV и с invalid opcode и с чем угодно. Надо все другие потоки усыплять перед этим :-)

    ОтветитьУдалить
  9. FlushInstructionCache(GetCurrentProcess, ATargetFunction, MaxPossibleSize)

    сказал MSDN :)
    ну тут еще много чего стоит подправить, но для коня в вакууме нехватает именно сброса кэша

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.