воскресенье, 26 мая 2013 г.

Правильное применение сплайсинга при перехвате функций подготовленных к HotPatch

В прошлой статье я рассмотрел пять вариантов перехвата функций включая их вариации.

Правда в ней я оставил не рассмотренными две неприятных ситуации:
1. Вызов перехваченной функции в тот момент, когда ловушка снята.
2. Одновременный вызов перехваченной функции из двух разных нитей.

В первом случае программист, установивший перехватчик не будет видеть всю картину в целом, т.к. часть данных пройдет мимо него.
Второй случай грозит более серьезными последствиями, вплоть до падения приложения, в котором установлен перехватчик.

Обе этих ситуации могут быть только в случае применения сплайсинга. При перехвате через таблицы импорта/экспорта и т.п. модификации тела перехватываемой функции не происходит, поэтому данные варианты перехвата не требуют излишних телодвижений.

В этой статье более подробно будет рассмотрен сплайсинг точки входа функции подготовленной к HopPatch, т.к. данные функции предоставляют нам способ ухода от вышеперечисленных ошибок.

Перехват сплайсингом через JMP NEAR OFFSET или PUSH ADDR + RET (наиболее уязвимый к данным ошибкам) рассмотрен не будет, т.к. по хорошему, без реализации дизассемблера длин, заставить данный вариант перехвата работать как нужно не получится.




1. Реализуем приложение перехватывающее вызов CreateWindowExW


Для начала подготовим приложение, которое наглядно покажет нам потерю данных при перехвате API из-за того, что вызов перехваченной функции может происходить в тот момент, когда перехват с нее снят.

Создайте новый проект и разместите на главной форме три элемента: TMemo, TOpenDialog и TButton.

Суть приложения: при нажатии кнопки будет устанавливаться перехват на функцию CreateWindowExW и отображаться диалог. После закрытия диалога в TMemo будет выводится информация о всех созданных диалогом окнах.

Для этого нам потребуется часть кода из предыдущей статьи, а именно:

1. Декларация типов и констант для перехвата:

const
  LOCK_JMP_OPKODE: Word = $F9EB;
  JMP_OPKODE: Word = $E9;

type
  // структура для обычного сплайса через JMP NEAR OFFSET
  TNearJmpSpliceRec = packed record
    JmpOpcode: Byte;
    Offset: DWORD;
  end;
  
  THotPachSpliceData = packed record
    FuncAddr: FARPROC;
    SpliceRec: TNearJmpSpliceRec;
    LockJmp: Word;
  end;

const
  NearJmpSpliceRecSize = SizeOf(TNearJmpSpliceRec);
  LockJmpOpcodeSize = SizeOf(Word);

2. Процедуры записи NEAR JMP и атомарной записи SHORT JMP

// процедура пищет новый блок данных по адресу функции
procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec);
var
  OldProtect: DWORD;
begin
  VirtualProtect(FuncAddr, NearJmpSpliceRecSize,
    PAGE_EXECUTE_READWRITE, OldProtect);
  try
    Move(NewData, FuncAddr^, NearJmpSpliceRecSize);
  finally
    VirtualProtect(FuncAddr, NearJmpSpliceRecSize,
      OldProtect, OldProtect);
  end;
end;

// процедура атомарно изменяет два байта по переданному адресу
procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word);
var
  OldProtect: DWORD;
begin
  VirtualProtect(FuncAddr, LockJmpOpcodeSize, PAGE_EXECUTE_READWRITE, OldProtect);
  try
    asm
      mov  ax, NewData
      mov  ecx, FuncAddr
      lock xchg word ptr [ecx], ax
    end;
  finally
    VirtualProtect(FuncAddr, LockJmpOpcodeSize, OldProtect, OldProtect);
  end;
end;

3. Нeмного модифицированная процедура инициализация структуры THotPachSpliceData

// процедура инициализирует структуру для установки перехвата
procedure InitHotPatchSpliceRec(const LibraryName, FunctionName: string;
  InterceptHandler: Pointer; out HotPathSpliceRec: THotPachSpliceData);
begin
  // запоминаем оригинальный адрес перехватываемой функции
  HotPathSpliceRec.FuncAddr :=
    GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName));
  // читаем два байта с ее начала, их мы будем перезатирать
  Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize);
  // инициализируем опкод JMP NEAR
  HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE;
  // рассчитываем адрес прыжка (поправка на NearJmpSpliceRecSize не нужна,
  // т.к. адрес находится уже со смещением)
  HotPathSpliceRec.SpliceRec.Offset :=
    PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr);
end;

Весь этот код разместим в отдельном модуле SpliceHelper, он нам потребуется в следующих главах.

Теперь перейдем к главной форме, нам потребуются две глобальных переменных:

var
  HotPathSpliceRec: THotPachSpliceData;
  WindowList: TStringList;

В переменной HotPathSpliceRec будет содержаться информация о перехватчике. Вторая будет содержать в себе список созданных окон.

В конструкторе формы произведем инициализацию структуры THotPachSpliceData.

procedure TForm1.FormCreate(Sender: TObject);
begin
  // инициализируем структуру для перехватчика
  InitHotPatchSpliceRec(user32, 'CreateWindowExW',
    @InterceptedCreateWindowExW, HotPathSpliceRec);
  // пишем прыжок в область NOP-ов
  SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize,
    HotPathSpliceRec.SpliceRec);
end;

Создадим функцию перехватчик, вызываемую вместо оригинальной функции.

function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar;
  lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
  hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
var
  S: string;
  Index: Integer;
begin
  // снимаем перехват
  SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
  try

    // запоминаем информацию о созданном окне
    Index := -1;
    if not IsBadReadPtr(lpClassName, 1) then
    begin
      S := 'ClassName: ' + string(lpClassName);
      S := IntToStr(WindowList.Count + 1) + ': ' + S;
      Index := WindowList.Add(S);
    end;

    // вызываем оригинальную функцию
    Result := CreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle,
      X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);

    // добавляем информацию о вызове в список
    if Index >= 0 then
    begin
      S := S + ', handle: ' + IntToStr(Result);
      WindowList[Index] := S;
    end;
    
  finally
    // восстанавливаем перехват
    SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
  end;
end;

И осталось в завершение реализовать обработчик кнопки.

procedure TForm1.Button1Click(Sender: TObject);
begin
  // перехватываем CreateWindowExW
  SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
  try
    // Создаем список в котором будет хранится информация о созданных окнах
    WindowList := TStringList.Create;
    try
      // открываем диалог
      OpenDialog1.Execute;
      // по завершении отображаем полученный список
      Memo1.Lines.Text := WindowList.Text;
    finally
      WindowList.Free;
    end;
  finally
    // снимаем перехват
    SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
  end;
end;

Все готово, можно запускать программу на исполнение.

Подробно рассказывать о реализованном в данной главе коде я не буду, он более чем подробно описан в предыдущей статье, второй раз расписывать не имеет смысла.

Запустите программу, нажмите кнопку и закройте диалог нажатием кнопки "Отмена", должно получиться так:


Таким образом мы выяснили, что при открытии обычного TOpenDialog создается 14 окон различных классов.

Теперь давайте выясним, на самом ли деле это так.

2. Создаем вспомогательную утилиту для просмотра дерева окон приложения.


Для проверки работы нашего перехватчика необходимо подстраховаться сторонней утилитой, которая может отобразить актуальный список окон у приложения, при помощи которой мы и выясним, всю ли информацию мы получили нашим перехватчиком или нет.

Можно конечно воспользоваться сторонними программами, наподобие Spy++ но мы же программисты, что нам стоит реализовать ее самостоятельно, тем более и время на реализацию копеечное.

Создайте новый проект и поместите на главной форме TTreeView после чего реализуйте следующий код:

type
  TdlgWindowTree = class(TForm)
    WindowTreeView: TTreeView;
    procedure FormCreate(Sender: TObject);
  private
    procedure Sys_Windows_Tree(Node: TTreeNode;
      AHandle: HWND; ALevel: Integer);
  end;

...

procedure TdlgWindowTree.FormCreate(Sender: TObject);
begin
  Sys_Windows_Tree(nil, GetDesktopWindow, 0);
end;

procedure TdlgWindowTree.Sys_Windows_Tree(Node: TTreeNode;
  AHandle: HWND; ALevel: Integer);
type
  TRootNodeData = record
    Node: TTreeNode;
    PID: Cardinal;
  end;
var
  szClassName, szCaption, szLayoutName: array[0..MAXCHAR - 1] of Char;
  szFileName : array[0..MAX_PATH - 1] of Char;
  Result: String;
  PID, TID: Cardinal;
  I: Integer;
  RootItems: array of TRootNodeData;
  IsNew: Boolean;
begin
  //Запускаем цикл пока не закончатся окна
  while AHandle <> 0 do
  begin
    //Получаем имя класса окна
    GetClassName(AHandle, szClassName, MAXCHAR);
    //Получаем текст (Его Caption) окна
    GetWindowText(AHandle, szCaption, MAXCHAR);
    // Получаем имя модуля
    if GetWindowModuleFilename(AHandle, szFileName, SizeOf(szFileName)) = 0 then
      FillChar(szFileName, 256, #0);
    TID := GetWindowThreadProcessId(AHandle, PID);

    // Раскладка процесса
    AttachThreadInput(GetCurrentThreadId, TID, True);
    VerLanguageName(GetKeyboardLayout(TID) and $FFFF, szLayoutName, MAXCHAR);
    AttachThreadInput(GetCurrentThreadId, TID, False);

    // Результат
    Result := Format('%s [%s] Caption = %s, Handle = %d, Layout = %s',
      [String(szClassName), String(szFileName), String(szCaption),
      AHandle, String(szLayoutName)]);

    // Смотрим в какое место добавлять окно
    if ALevel in [0..1] then
    begin
      IsNew := True;
      for I := 0 to Length(RootItems) - 1 do
        if RootItems[I].PID = PID then
        begin
          Node := RootItems[I].Node;
          IsNew := False;
          Break;
        end;
      if IsNew then
      begin
        SetLength(RootItems, Length(RootItems) + 1);
        RootItems[Length(RootItems) - 1].PID := PID;
        RootItems[Length(RootItems) - 1].Node :=  
          WindowTreeView.Items.AddChild(nil, 'PID: ' + IntToStr(PID));
        Node := RootItems[Length(RootItems) - 1].Node;
      end;
    end;

    // Пускаем рекурсию
    Sys_Windows_Tree(WindowTreeView.Items.AddChild(Node, Result),
      GetWindow(AHandle, GW_CHILD), ALevel + 1);

    //Получаем хэндл следующего (не дочернего) окна
    AHandle := GetNextWindow(AHandle, GW_HWNDNEXT);
  end;
end;

Собственно все, можно запускать на выполнение:



3. Анализируем результаты


Теперь сравним результаты работы обеих программ. Сделаем это следующим образом.
1. Запустите программу с перехватчиком и нажмите на кнопку, отображающую диалог.
2. Запустите утилиту из второй главы
3. Закройте диалог первой программы, для получения результата о перехваченных окнах.

Теперь сравниваем:


Красным выделено окно с классом Auto-Suggest DropDown, давайте посмотрим что оно из себя представляет:


А оно оказывается содержит в себе еще 4 окна, два скролбара, ListView, который к тому-же чайлдом держит SysHeader32. А вот это уже интересно. Хэнлы окна в обоих приложениях совпадают, но ни ListView, ни SysHeader32, даже двух скролов в первом приложении мы не видим.

Но, то что мы их не видим в первом списке еще ничего не означает. Создание этих окон происходило в тот момент, когда наш перехватчик был снят, а это могло произойти только по одной причине - по причине того, что вызов CreateWindowExW может привести к рекурсивному вызову самого себя.

Значит нужно реализовать код перехватчика таким образом, чтобы не требовалось снятие и восстановление перехвата.

4. Вызов перехваченной функции без снятия кода перехвата.


Давайте посмотрим на вот такую картинку из прошлой статьи.


Здесь показано начало функции MessageBoxW. Самой первой инструкцией идет ничего не делающая инструкция MOV EDI, EDI, предваряющаяся пятью инструкциями NOP.

Именно так в большинстве своем и выглядят функции, подготовленные к перехвату посредством HotPatch, в том числе и перехваченная нами CreateWindowExW.

В случае перехвата функции вместо выделенных семи байт, занятых ничем не делающими инструкциями будет расположен следующий код:


Собственно это и есть установленный нами перехватчик.
Вместо инструкции MOV EDI, EDI размещен код JMP -7, передающий управление на предыдущую инструкцию.
Вместо пяти инструкций NOP, расположен прыжок на начало функции перехватчика.

Если мы начнем выполнение не с адреса начала функции CreateWindowExW, а с адреса ее первой полезной инструкции PUSH EBP, то мы не затронем установленный нами перехватчик, а раз так, то и нет смысла его снимать.

В виде кода это выглядит таким образом:

type
  TCreateWindowExW = function(dwExStyle: DWORD; lpClassName: PWideChar;
    lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
    hWndParent: HWND; AMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;

function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar;
  lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
  hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
var
  S: string;
  Index: Integer;
  ACreateWindowExW: TCreateWindowExW;
begin

  // запоминаем информацию о созданном окне
  Index := -1;
  if not IsBadReadPtr(lpClassName, 1) then
  begin
    S := 'ClassName: ' + string(lpClassName);
    S := IntToStr(WindowList.Count + 1) + ': ' + S;
    Index := WindowList.Add(S);
  end;

  // вызываем оригинальную функцию
  @ACreateWindowExW := PAnsiChar(HotPathSpliceRec.FuncAddr) + LockJmpOpcodeSize;
  Result := ACreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle,
    X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);

  // добавляем информацию о вызове в список
  if Index >= 0 then
  begin
    S := S + ', handle: ' + IntToStr(Result);
    WindowList[Index] := S;
  end;

end;

Рассчитав адрес первой полезной инструкции, равный смещению от начала функции на два байта, мы запоминаем его во временной переменной ACreateWindowExW, после чего вызываем функцию привычным нам образом.

Давайте посмотрим что получится в этом случае, вот это мы ожидаем:


И именно это мы и находим в выдаваемом нам списке:


Ну вот мы и нашли наших "потеряшек", все таки 26 окон создается при вызове TOpenDialog, а не 14.

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


5. Ошибка при вызове перехватываемой функции из разных нитей.


С этой ошибкой то же все просто. Если постоянно снимать и восстанавливать перехватчик функции, то в какой-то момент нам будет выдана ошибка в функции SpliceLockJmp на инструкции "lock xchg word ptr [ecx], ax". Дело в том что в этот момент может завершиться операция возвращения атрибутов страницы по адресу перехватчика из другой нити и, не смотря на то, что мы в своей нити разрешили запись по данному адресу, реальные атрибуты страницы будут совершенно другими.

Именно с таким поведением столкнулся автор этой ветки: перехват recv.

Решать данную ошибку нужно таким-же способом как показано выше.
Правда при этом нужно не забывать и об обработчике перехвата, он тоже должен быть ThreadSafe, но реализация обработчика остается на ваше усмотрение.

6. Всегда ли можно пропустить первые два байта перехватываемой функции?


Интересный вопрос и ответ на него - нет, не всегда.
Когда функции подготавливаются к перехвату по методу HotPatch, Microsoft гарантирует только то, что перед ними всегда будет пять инструкций NOP и каждая такая функция будет начинаться с двухбайтовой инструкции. Больше нам ничего не гарантируется.

Если рассмотреть код MessageBoxW или CreateWindowExW, то можно увидеть что первая их полезная инструкция PUSH EBP занимает один байт. Таким образом, раз она не удовлетворяет условиям, тело данной функции предваряется пустым вызовом MOV EDI, EDI. Тоже будет верно и для функций начинающихся с инструкций длиной три и более байт. Однако, если функция начинается с двухбайтовый инструкции, не имеет смысла раздувать ее тело пустой заглушкой, ведь все условия для HotPatch соблюдены (пять NOP и 2 байта).

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

Пример такой функции - RtlCreateUnicodeString.
Она начинается с полезной инструкции PUSH $0C.


Самым простым решением было бы восстановление оригинальной инструкции перед вызовом оригинальной функции, но как я уже говорил с самого начала, это грозит ошибками.

Стало быть перед нами встала задача - обеспечить вызов затертой инструкции и обеспечить работоспособность функции даже с установленным кодом перехвата:


В принципе машинный код затертой инструкции у нас есть и хранится в структуре HotPathSpliceRec.LockJmp, но вызвать напрямую мы ее не можем по нескольким причинам.

Ну во первых данная структура расположена в куче (ну точнее не в куче, а в выделенной памяти, т.к. Delphi не работает с механизмом Heap напрямую) у которой нет атрибутов исполнения, т.е. если мы каким-то образом выполним CALL по адресу HotPathSpliceRec.LockJmp то получим ошибку.

Можно конечно выставить правильные атрибуты страницы, но это слишком топорно, все-же исполняемый код не должен перемешиваться с областью данных.

Во вторых даже если мы и передадим выполнение на эту инструкцию, мы должны после нее заставить выполнится инструкцию JMP на правильный адрес (в данном случае это будет $77B062FB, см. предыдущую картинку) с учетом оффсета вызываемой инструкции.

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

Попробуем решить все по порядку.

Чтобы не связываться с передачей параметров из асм вставки мы можем реализовать некую функцию-трамплин, возложив эту задачу на компилятор.

Т.е. грубо пишем перехватчик таким образом:

function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
  SourceString: PWideChar): Integer; stdcall;
begin
  asm
    db $90, $90, $90, $90, $90, $90, $90
  end;
end;

function InterceptedRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
  SourceString: PWideChar): Integer; stdcall;
begin
  Result := TrampolineRtlCreateUnicodeString(DestinationString, SourceString);
  ShowMessage(DestinationString^.Buffer);
end;

В данном случае перехватчик будет заниматься вызовом трамплина и логированием.

Внутри функции-трамплина зарезервировано 7 байт, что как раз хватит нам для записи двухбайтовой затертой инструкции и пятибайтовой NEAR JMP.
Сама функция расположена в области кода, и с ее вызовом затруднений возникнуть не должно.

А теперь важный нюанс.
Если писать эти 7 байт на место зарезервированного блока, то мы столкнемся с одной неприятной особенностью Delphi. Дело в том что компилятор Delphi практически всегда генерирует для функций пролог и эпилог.

К примеру допустим после патча код нашей функции стал выглядеть таким образом:

function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
  SourceString: PWideChar): Integer; stdcall;
begin
  asm
    push $0C        // выполняем затертый параметр
    jmp $77B062FB   // делаем прыжок на правильную инструкцию
  end;
end;

В действительности он превратится в следующее:


Т.е. на стеке, вместо двух параметров DestinationString и SourceString будут размещены значения регистров EBP и ECX, что приведет в результате к абсолютно не предсказуемым последствиям.

Этого нам абсолютно не нужно, поэтому мы поступим проще, а именно код трамплина будет писаться прямо с начала данной функции, перезатирая инструкции пролога функции.

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

Таким образом реализуем инициализацию перехватчика следующим способом:

// процедура инициализирует структуру для установки перехвата и подготавливает трамплин для вызова
procedure InitHotPatchSpliceRecEx(const LibraryName, FunctionName: string;
  InterceptHandler, Trampoline: Pointer; out HotPathSpliceRec: THotPachSpliceData);
var
  OldProtect: DWORD;
  TrampolineSplice: TNearJmpSpliceRec;
begin
  // запоминаем оригинальный адрес перехватываемой функции
  HotPathSpliceRec.FuncAddr :=
    GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName));
  // читаем два байта с ее начала, их мы будем перезатирать
  Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize);

  // Подготавливаем трамплин
  VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize,
    PAGE_EXECUTE_READWRITE, OldProtect);
  try
    Move(HotPathSpliceRec.LockJmp, Trampoline^, LockJmpOpcodeSize);
    TrampolineSplice.JmpOpcode := JMP_OPKODE;
    TrampolineSplice.Offset := PAnsiChar(HotPathSpliceRec.FuncAddr) -
      PAnsiChar(Trampoline) - NearJmpSpliceRecSize;
    Trampoline := PAnsiChar(Trampoline) + LockJmpOpcodeSize;
    Move(TrampolineSplice, Trampoline^, SizeOf(TNearJmpSpliceRec));
  finally
    VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize,
      OldProtect, OldProtect);
  end;

  // инициализируем опкод JMP NEAR
  HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE;
  // рассчитываем адрес прыжка (поправка на NearJmpSpliceRecSize не нужна,
  // т.к. адрес находится уже со смещением)
  HotPathSpliceRec.SpliceRec.Offset :=
    PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr);
end;

Сама инициализация и вызов перехваченной функции выглядит следующим образом:

type
  UNICODE_STRING = record
    Length: WORD;
    MaximumLength: WORD;
    Buffer: PWideChar;
  end;
  PUNICODE_STRING = ^UNICODE_STRING;

  function  RtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
    SourceString: PWideChar): BOOLEAN; stdcall; external 'ntdll.dll';

...

procedure TForm2.FormCreate(Sender: TObject);
begin
  // инициализируем структуру для перехватчика и трамплин
  InitHotPatchSpliceRecEx('ntdll.dll', 'RtlCreateUnicodeString',
    @InterceptedRtlCreateUnicodeString, @TrampolineRtlCreateUnicodeString,
    HotPathSpliceRec);
  // пишем прыжок в область NOP-ов
  SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize,
    HotPathSpliceRec.SpliceRec);
end;

procedure TForm2.Button1Click(Sender: TObject);
var
  US: UNICODE_STRING;
begin
  // перехватываем RtlCreateUnicodeString
  SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
  try
    RtlCreateUnicodeString(@US, 'Test UNICODE String');
  finally
    // снимаем перехват
    SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
  end;
end;

Теперь можно нажать на кнопку и увидеть результат перехвата в виде сообщения.

В качестве заключения


В итоге вариант реализации сплайсинга, показанный в шестой главе, является наиболее универсальным в случае перехвата функций, подготовленных к HotPatch-у. Он будет работать корректно и в случае заглушки MOV EDI, EDI и в случае наличия полезной инструкции в начале перехватываемой функции. Он не подвержен ошибкам, описанным в самом начале статьи, но правда перехватить обычные функции при помощи данного алгоритма не получится, впрочем об этом я уже писал ранее.

Извиняюсь что приходится дробить информацию на куски и выдавать не все сразу, но как мне посоветовали еще год назад, лучше давать материал малыми порциями, чтобы было время для его переваривания :)

С другой стороны если собрать весь материал в кучу, то во первых это займет достаточно продолжительное время, которого у меня нет в наличии, а во вторых приведет к его нечитабельности ввиду большого объема (прецеденты были).
Поэтому лучше так.

Исходный код к примерам можно забрать по данной ссылке.

---

© Александр (Rouse_) Багель
Май, 2013

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

  1. Все, что вы пишите, касается 32-х битных приложений, а каковы нюансы реализации перехвата в 64-х битных приложениях?

    ОтветитьУдалить
  2. сплайсинг уже довольно заезженная тема и статья хорошо подойдет для новичков.
    спасибо за труд

    ОтветитьУдалить
    Ответы
    1. Спасибо.
      Правда здесь не столько сплайсинг, сколько раскрытие вопроса о его правильном применении :)

      Удалить
  3. В коде "вспомогательной утилиты для просмотра дерева окон приложения" есть ошибка.
    У меня ко всем окнам отображается полный путь к текушему процессу. Я не проверял код процедуры GetWindowModuleFilename в Delphi 7 но в MSDN написано вот что:

    GetModuleFileName function
    Retrieves the fully qualified path for the file that contains the specified module. The module must have been loaded by the current process.

    To locate the file for a module that was loaded by another process, use the GetModuleFileNameEx function.

    Я изменил код так:
    var
    hProcess: THandle;
    ...
    // Получаем имя модуля
    // if GetWindowModuleFilename(AHandle, szFileName, SizeOf(szFileName)) = 0 then
    // FillChar(szFileName, 256, #0);

    TID := GetWindowThreadProcessId(AHandle, PID);

    hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, FALSE, PID);
    if hProcess <> 0 then begin
    try
    if GetModuleFileNameEx(hProcess, 0, @szFileName[0], SizeOf(szFileName)) = 0 then
    FillChar(szFileName, SizeOf(szFileName), #0)
    finally
    CloseHandle(hProcess);
    end
    end else begin
    FillChar(szFileName, MAX_PATH, #0)
    end;
    // Раскладка процесса

    ОтветитьУдалить
    Ответы
    1. Странно, видимо что-то поменялось, по крайней мере на моих скриншотах можно увидеть что пути возвращаются правильные :)

      Удалить
    2. Я вижу, что на картинке всё в порядке. Но скомпилировал проект Delphi 7 и проверил под Windows Server 2003. Пути все были одинаковые. Поэтому полез в API и прочёл, что для чужого процесса эта функция не подходит.

      PS.
      Вот же фигня гугловская. Из-за глюков даже подписаться не могу.

      Удалить
    3. Тут никаких вопросов, просто я писал этот проект очень давно и ориентировался на локальный MSDN (от шестой студии) там по всей видимости такого нюанса не было оговорено, поэтому и реализовал как есть, а сейчас видимо что-то поменялось. На старых версиях Windows достаточно обычного вызова :)

      Удалить