В прошлой статье я рассмотрел пять вариантов перехвата функций включая их вариации.
Правда в ней я оставил не рассмотренными две неприятных ситуации:
1. Вызов перехваченной функции в тот момент, когда ловушка снята.
2. Одновременный вызов перехваченной функции из двух разных нитей.
В первом случае программист, установивший перехватчик не будет видеть всю картину в целом, т.к. часть данных пройдет мимо него.
Второй случай грозит более серьезными последствиями, вплоть до падения приложения, в котором установлен перехватчик.
Обе этих ситуации могут быть только в случае применения сплайсинга. При перехвате через таблицы импорта/экспорта и т.п. модификации тела перехватываемой функции не происходит, поэтому данные варианты перехвата не требуют излишних телодвижений.
В этой статье более подробно будет рассмотрен сплайсинг точки входа функции подготовленной к HopPatch, т.к. данные функции предоставляют нам способ ухода от вышеперечисленных ошибок.
Перехват сплайсингом через JMP NEAR OFFSET или PUSH ADDR + RET (наиболее уязвимый к данным ошибкам) рассмотрен не будет, т.к. по хорошему, без реализации дизассемблера длин, заставить данный вариант перехвата работать как нужно не получится.
Для начала подготовим приложение, которое наглядно покажет нам потерю данных при перехвате API из-за того, что вызов перехваченной функции может происходить в тот момент, когда перехват с нее снят.
Создайте новый проект и разместите на главной форме три элемента: TMemo, TOpenDialog и TButton.
Суть приложения: при нажатии кнопки будет устанавливаться перехват на функцию CreateWindowExW и отображаться диалог. После закрытия диалога в TMemo будет выводится информация о всех созданных диалогом окнах.
Для этого нам потребуется часть кода из предыдущей статьи, а именно:
1. Декларация типов и констант для перехвата:
2. Процедуры записи NEAR JMP и атомарной записи SHORT JMP
3. Нeмного модифицированная процедура инициализация структуры THotPachSpliceData
Весь этот код разместим в отдельном модуле SpliceHelper, он нам потребуется в следующих главах.
Теперь перейдем к главной форме, нам потребуются две глобальных переменных:
В переменной HotPathSpliceRec будет содержаться информация о перехватчике. Вторая будет содержать в себе список созданных окон.
В конструкторе формы произведем инициализацию структуры THotPachSpliceData.
Создадим функцию перехватчик, вызываемую вместо оригинальной функции.
И осталось в завершение реализовать обработчик кнопки.
Все готово, можно запускать программу на исполнение.
Подробно рассказывать о реализованном в данной главе коде я не буду, он более чем подробно описан в предыдущей статье, второй раз расписывать не имеет смысла.
Запустите программу, нажмите кнопку и закройте диалог нажатием кнопки "Отмена", должно получиться так:
Таким образом мы выяснили, что при открытии обычного TOpenDialog создается 14 окон различных классов.
Теперь давайте выясним, на самом ли деле это так.
Для проверки работы нашего перехватчика необходимо подстраховаться сторонней утилитой, которая может отобразить актуальный список окон у приложения, при помощи которой мы и выясним, всю ли информацию мы получили нашим перехватчиком или нет.
Можно конечно воспользоваться сторонними программами, наподобие Spy++ но мы же программисты, что нам стоит реализовать ее самостоятельно, тем более и время на реализацию копеечное.
Создайте новый проект и поместите на главной форме TTreeView после чего реализуйте следующий код:
Собственно все, можно запускать на выполнение:
Теперь сравним результаты работы обеих программ. Сделаем это следующим образом.
1. Запустите программу с перехватчиком и нажмите на кнопку, отображающую диалог.
2. Запустите утилиту из второй главы
3. Закройте диалог первой программы, для получения результата о перехваченных окнах.
Теперь сравниваем:
Красным выделено окно с классом Auto-Suggest DropDown, давайте посмотрим что оно из себя представляет:
А оно оказывается содержит в себе еще 4 окна, два скролбара, ListView, который к тому-же чайлдом держит SysHeader32. А вот это уже интересно. Хэнлы окна в обоих приложениях совпадают, но ни ListView, ни SysHeader32, даже двух скролов в первом приложении мы не видим.
Но, то что мы их не видим в первом списке еще ничего не означает. Создание этих окон происходило в тот момент, когда наш перехватчик был снят, а это могло произойти только по одной причине - по причине того, что вызов CreateWindowExW может привести к рекурсивному вызову самого себя.
Значит нужно реализовать код перехватчика таким образом, чтобы не требовалось снятие и восстановление перехвата.
Давайте посмотрим на вот такую картинку из прошлой статьи.
Здесь показано начало функции MessageBoxW. Самой первой инструкцией идет ничего не делающая инструкция MOV EDI, EDI, предваряющаяся пятью инструкциями NOP.
Именно так в большинстве своем и выглядят функции, подготовленные к перехвату посредством HotPatch, в том числе и перехваченная нами CreateWindowExW.
В случае перехвата функции вместо выделенных семи байт, занятых ничем не делающими инструкциями будет расположен следующий код:
Собственно это и есть установленный нами перехватчик.
Вместо инструкции MOV EDI, EDI размещен код JMP -7, передающий управление на предыдущую инструкцию.
Вместо пяти инструкций NOP, расположен прыжок на начало функции перехватчика.
Если мы начнем выполнение не с адреса начала функции CreateWindowExW, а с адреса ее первой полезной инструкции PUSH EBP, то мы не затронем установленный нами перехватчик, а раз так, то и нет смысла его снимать.
В виде кода это выглядит таким образом:
Рассчитав адрес первой полезной инструкции, равный смещению от начала функции на два байта, мы запоминаем его во временной переменной ACreateWindowExW, после чего вызываем функцию привычным нам образом.
Давайте посмотрим что получится в этом случае, вот это мы ожидаем:
И именно это мы и находим в выдаваемом нам списке:
Ну вот мы и нашли наших "потеряшек", все таки 26 окон создается при вызове TOpenDialog, а не 14.
Все дело было в пресловутом рекурсивном вызове, который можно увидеть в стеке вызова процедур, если установить брякпойнт в начале функции InterceptedCreateWindowExW.
С этой ошибкой то же все просто. Если постоянно снимать и восстанавливать перехватчик функции, то в какой-то момент нам будет выдана ошибка в функции SpliceLockJmp на инструкции "lock xchg word ptr [ecx], ax". Дело в том что в этот момент может завершиться операция возвращения атрибутов страницы по адресу перехватчика из другой нити и, не смотря на то, что мы в своей нити разрешили запись по данному адресу, реальные атрибуты страницы будут совершенно другими.
Именно с таким поведением столкнулся автор этой ветки: перехват recv.
Решать данную ошибку нужно таким-же способом как показано выше.
Правда при этом нужно не забывать и об обработчике перехвата, он тоже должен быть ThreadSafe, но реализация обработчика остается на ваше усмотрение.
Интересный вопрос и ответ на него - нет, не всегда.
Когда функции подготавливаются к перехвату по методу 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, см. предыдущую картинку) с учетом оффсета вызываемой инструкции.
В третьих, помимо вызова, мы должны разместить на стеке в правильном порядке параметры, передаваемые вызываемой функции, что как минимум приведет нас к необходимости использования асм вставок.
Попробуем решить все по порядку.
Чтобы не связываться с передачей параметров из асм вставки мы можем реализовать некую функцию-трамплин, возложив эту задачу на компилятор.
Т.е. грубо пишем перехватчик таким образом:
В данном случае перехватчик будет заниматься вызовом трамплина и логированием.
Внутри функции-трамплина зарезервировано 7 байт, что как раз хватит нам для записи двухбайтовой затертой инструкции и пятибайтовой NEAR JMP.
Сама функция расположена в области кода, и с ее вызовом затруднений возникнуть не должно.
А теперь важный нюанс.
Если писать эти 7 байт на место зарезервированного блока, то мы столкнемся с одной неприятной особенностью Delphi. Дело в том что компилятор Delphi практически всегда генерирует для функций пролог и эпилог.
К примеру допустим после патча код нашей функции стал выглядеть таким образом:
В действительности он превратится в следующее:
Т.е. на стеке, вместо двух параметров DestinationString и SourceString будут размещены значения регистров EBP и ECX, что приведет в результате к абсолютно не предсказуемым последствиям.
Этого нам абсолютно не нужно, поэтому мы поступим проще, а именно код трамплина будет писаться прямо с начала данной функции, перезатирая инструкции пролога функции.
Ну а ведь в действительности, данные инструкции нам абсолютно не нужны, т.к. после прыжка в тело перехваченной функции и ее выполнения, управление вернется не в искореженную нашими действиями функцию-трамплин, а непосредственно в то место, откуда производился ее вызов, т.е. в функцию - обработчик перехвата.
Таким образом реализуем инициализацию перехватчика следующим способом:
Сама инициализация и вызов перехваченной функции выглядит следующим образом:
Теперь можно нажать на кнопку и увидеть результат перехвата в виде сообщения.
В итоге вариант реализации сплайсинга, показанный в шестой главе, является наиболее универсальным в случае перехвата функций, подготовленных к HotPatch-у. Он будет работать корректно и в случае заглушки MOV EDI, EDI и в случае наличия полезной инструкции в начале перехватываемой функции. Он не подвержен ошибкам, описанным в самом начале статьи, но правда перехватить обычные функции при помощи данного алгоритма не получится, впрочем об этом я уже писал ранее.
Извиняюсь что приходится дробить информацию на куски и выдавать не все сразу, но как мне посоветовали еще год назад, лучше давать материал малыми порциями, чтобы было время для его переваривания :)
С другой стороны если собрать весь материал в кучу, то во первых это займет достаточно продолжительное время, которого у меня нет в наличии, а во вторых приведет к его нечитабельности ввиду большого объема (прецеденты были).
Поэтому лучше так.
Исходный код к примерам можно забрать по данной ссылке.
Правда в ней я оставил не рассмотренными две неприятных ситуации:
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
Все, что вы пишите, касается 32-х битных приложений, а каковы нюансы реализации перехвата в 64-х битных приложениях?
ОтветитьУдалитьС 64 битами я не работал, пока что не было задач.
Удалитьсплайсинг уже довольно заезженная тема и статья хорошо подойдет для новичков.
ОтветитьУдалитьспасибо за труд
Спасибо.
УдалитьПравда здесь не столько сплайсинг, сколько раскрытие вопроса о его правильном применении :)
В коде "вспомогательной утилиты для просмотра дерева окон приложения" есть ошибка.
ОтветитьУдалитьУ меня ко всем окнам отображается полный путь к текушему процессу. Я не проверял код процедуры 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;
// Раскладка процесса
Странно, видимо что-то поменялось, по крайней мере на моих скриншотах можно увидеть что пути возвращаются правильные :)
УдалитьЯ вижу, что на картинке всё в порядке. Но скомпилировал проект Delphi 7 и проверил под Windows Server 2003. Пути все были одинаковые. Поэтому полез в API и прочёл, что для чужого процесса эта функция не подходит.
УдалитьPS.
Вот же фигня гугловская. Из-за глюков даже подписаться не могу.
Тут никаких вопросов, просто я писал этот проект очень давно и ориентировался на локальный MSDN (от шестой студии) там по всей видимости такого нюанса не было оговорено, поэтому и реализовал как есть, а сейчас видимо что-то поменялось. На старых версиях Windows достаточно обычного вызова :)
Удалить