понедельник, 8 августа 2011 г.

Хак №15: замещение динамических и message-методов в run-time

Это перевод Hack#15: Overriding message and dynamic methods at run-time. Автор: Hallvard Vassbotn.

В прошлый раз мы посмотрели на способ полной замены класса. Как мы говорили, у этой техники есть несколько проблем. Но есть много способов снять шкуру с кошки (извините, любители кошек!), и в нашем случае (исправление мерцания TProgressBar без изменения интерфейса этого класса) есть как минимум три других решения.

Одно из решений заключается в исправлении VMT TProgressBar в run-time. Мы знаем из предыдущих статей, что VMT содержит много информации о классе - включая массив указателей на реализацию виртуальных методов (это и есть собственно VMT - таблица виртуальных методов), а также разрежённый массив замещённых и новых динамических и message-методов (известный как DMT или таблица динамических методов). В этой статье мы посмотрим на то, как мы можем исправить VMT существующего класса и подменить записи в таблицах методов на те, которые нам нужны.

Заметьте, что компилятор хранит таблицы VMT классов в защищённых от записи страницах памяти (даже хотя нельзя строго сказать, что они содержат исполняемый код). Это означает, что если мы наивно попытаемся записать что-то в область VMT, то немедленно схлопочем Access Violation, сгенерированный аппаратной защитой страниц. Правильный метод написания само-модифицирующегося кода заключается в использовании функции VirtualProtect для изменения атрибутов страницы памяти перед выполнением записи и последующего восстановления атрибутов после записи. Вот функция, которая умеет изменять один DWORD в сегменте кода:
procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);
var
  RestoreProtection, Ignore: DWORD;
begin
  if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE, RestoreProtection) then
  try
    Code^ := Value;
  finally
    VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore);
    FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^));
  end
  else
    RaiseLastOSError;
end;
Чтобы перестраховаться - мы следуем рекомендациям Microsoft и сбрасываем кэш инструкций процессора после изменения страниц кода - вызовом FlushInstructionCache. Это необходимо для избежания мерзких проблем, когда только что пропатченный код игнорируется процессором, потому что его старый вариант уже загружен в его кэш.

Теперь нам надо определить, что же нам патчить и на что патчить. Мы уже в деталях разобрали работу динамических и виртуальных методов в предыдущих постах. Изучая исходный код TProgressBar и подтверждая это в отладчике, мы видим, что у него нет замещения динамических и message-методов. Хорошие новости - указатель DynamicTable в VMT будет равен nil, что делает наш патч проще. Так что мы знаем, где надо патчить - будем патчить слот DMT в VMT класса TProgressBar. Чтобы было проще, мы используем константу vmtDynamicTable из модуля System:
{ Virtual method table entries }
const
  // ...
  vmtDynamicTable      = -48;
Мы можем вычислить адрес DMT слота в VMT по данной ссылке на класс TProgressBar, используя этот код:
var
  OriginalDmt: PDWORD;
begin
  OriginalDmt := PDWORD(TProgressBar);
  Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD));
  if OriginalDmt^ = 0 then // Проверка на отсутствие Dmt
Теперь надо определиться с тем, на что заменять nil DMT. Мы можем попытаться построить DMT таблицу сами, вручную - используя информацию по реверсированию таблицы, но это будет пустой тратой сил. Компилятор справится с задачей генерации DMT значительно лучше. Мы можем просто использовать класс TProgressBarVistaFix из нашего предыдущего поста:
type
  TProgressBarVistaFix = class(TProgressBar)
  private
    procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
  end;

procedure TProgressBarVistaFix.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
  DefaultHandler(Message);
end;
Мы можем использовать готовый DMT от этого класса для внедрения его в DMT слот класса TProgressBar. Вот первый вариант моего патча:
procedure PatchTProgressBarDMT;
var
  OriginalDmt, NewDmt: PDWORD;
begin
  OriginalDmt := PDWORD(TProgressBar);
  Inc(OriginalDmt, vmtDynamicTable div SizeOf(DWORD));
  if OriginalDmt^ = 0 then
  begin
    NewDmt := PDWORD(TProgressBarVistaFix);
    Inc(NewDmt, vmtDynamicTable div SizeOf(DWORD));
    PatchCodeDWORD(OriginalDmt, NewDmt^);
  end;
end;
Тестирование этого кода показывает, что он работает, вызывается обработчик WM_ERASEBKFND и мерцание в Vista пропадает. Но я не удовлетворён этим - так что я переписал его в отдельный модуль. На этот раз - используя модуль HVVMT и указатель PVmt:
unit HVPatching;

interface

uses
  Windows;

procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);

procedure ReplaceClassDmt(TargetClass, SourceClass: TClass);

implementation

uses
  HVVMT;

procedure PatchCodeDWORD(Code: PDWORD; Value: DWORD);
var
  RestoreProtection, Ignore: DWORD;
begin
  if VirtualProtect(Code, SizeOf(Code^), PAGE_EXECUTE_READWRITE,
    RestoreProtection) then
  begin
    Code^ := Value;
    VirtualProtect(Code, SizeOf(Code^), RestoreProtection, Ignore);
    FlushInstructionCache(GetCurrentProcess, Code, SizeOf(Code^));
  end;
end;

procedure ReplaceClassDmt(TargetClass, SourceClass: TClass);
begin
  Assert(Assigned(TargetClass) and Assigned(SourceClass));
  PatchCodeDWORD(@GetVmt(TargetClass).DynamicTable, 
    DWORD(GetVmt(SourceClass).DynamicTable));
end;

end.
Теперь мы можем использовать это так:
initialization
  ReplaceClassDmt(TProgressBar, TProgressBarVistaFix);
end.
В этом случае нам повезло. В других случаях класс для патча уже может иметь один или несколько динамических или message-методов - так что класс будет иметь не пустой DMT слот в VMT. В этом случае мы должны сыграть в компилятор и создать DMT с дополнительной записью, скопировать туда исходную DMT (вместе с индексами и указателями методов) и, наконец, вписать новую DMT в класс. Это существенно сложнее описанного здесь алгоритма, но если будет интерес в дальнейшей раскопке этого хака, мы можем вернуться к нему в будущих постах.

2 комментария:

  1. Не могли бы вы поделиться модулем HVRTTIUtils а то доступа к сайту производителя нету :(

    MegaVoltik@gmail.com

    Заранее благодарен :)

    ОтветитьУдалить
  2. Да вроде работает ссылка.

    Нет EDN учётки? Завести можно здесь: https://members.embarcadero.com/newuser.aspx

    Бесплатно. Наличие Embarcadero-ских лицензий не требуется. Это обычная учётка на форуме. Можно входить по OpenID.

    Правда, при создании профиля они просят вводить телефон и адрес, но эти поля - для тех, кто регистрирует на этот аккаунт лицензии. А по-простому туда можно и нули забить.

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

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

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

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

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

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