Правильное применение сплайсинга при перехвате функций подготовленных к 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;
    


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

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

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

    image

    Таким образом мы выяснили, что при открытии обычного 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;
    


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

    image

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


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

    Смотрим:

    image

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

    image

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

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

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

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


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

    image

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

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

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

    image

    Собственно это и есть установленный нами перехватчик.
    Вместо инструкции 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, после чего вызываем функцию привычным нам образом.

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

    image

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

    image

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

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

    image

    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.

    image

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

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

    image

    В принципе машинный код затертой инструкции у нас есть и хранится в структуре 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;
    


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

    image

    Т.е. на стеке, вместо двух параметров 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
    Поделиться публикацией

    Комментарии 4

      0
      Почему никто не вспоминает о конфликтах перехвата? Тулза обязана определять, стоят ли на интересующих её функциях чужие хуки.

      Если стоят — то есть несколько вариантов действия. Наиболее разумное — после выполнения своего хука вызвать чужой хук. Обычно хуки ставятся с помощью jmp near long или jmp [imm]. Если такая инструкция установлена, переносим её в надёжное место (пересчитав адрес в первом случае), после выполнения своего хука прыгаем туда.

      Также можно убить чужие хуки. Для этого открываем образ нужной DLL (GetModuleFileName/CreateFile), читаем таблицу секций, рассчитываем адрес в файле чистой точки входа в функцию (не забывая, что библиотека может быть упакована, для этого нужно посчитать количество изменённых байт в окрестности точки входа в функцию и убедиться, что это разумная величина). Затем исправляем изменённые байты в ОЗУ. Правда, стоит помнить, что сняв несколько вражеских перехватов, можно вызвать нестабильную работу программы (например, сняли перехват с ZwClose, и теперь вражеский хук не сможет отслеживать закрываемые ресурсы, в результате получим как минимум утечку памяти через структуры данных чужой тулзы). Так что если снимать хуки — то как минимум со всей DLL-ки.

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

      Было бы куда лучше и безопаснее, если бы писаки хуков (вместо выдумывания очередных велосипедов) выработали общий стандарт, учитывающий, что хуков на функцию может ставиться несколько, чтобы не приходилось по всякому извращаться…
        0
        Я понмю, какой-то онлайн переводчик переводил threads как «нити». Не уверен, что этот термин корректен.
          0
          Я стараюсь придерживаться следующей терминологии:
          Thread — нить, Fiber — волокно, Stream — поток.
          0
          Если Вы сплайсите в точке вызова процедуры — Вы перехватите только ее вызовы из данной точки, если Вы сплайсите в точке входа в целевую процедуру — Вы перехватите все ее вызовы. Краткое содержание статьи.

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

          Самое читаемое