DELPHISOURCE
| Домой | Статьи | Книги | FAQ | Компоненты | Программы |
| Архив сайта | Реклама на сайте | Ссылки | Связь |
Использование Debug API: пример перехвата вызовов функций Win32 API
Благодарности:
Вынесены сюда, ибо в конце их никто не читает. :-)
Прежде всего -
Sleepyhead''у (AKA Alexander V. Yeryomin). Без его деятельной помощи я бы
до сих пор писал и разбирался. Огромное спасибо, век воли не видать! :-)
[Енота: да, братишка, юмор у тебя всегда был сомнительный...]
Jorban'у
Russel'у - за исходники примитивного компилятора паскаля. Ничего
откровненного и сокровенного там нет, но есть простейший отладчик, который
напомнил мне некоторые напрочь забытые вещи. А еще у него есть
великолепный фришный инсталлятор. Даже исходники дают. Если вам нужен
инсталлятор - забудьте про толстые коммерческие продукты! InnoSetup -
круче всех! :-) Идите на http://www.jrsoftware.org/прямо
сейчас.
Еноте - за поддержку в трудное для меня время [Енота: я его
пинаю постоянно, а он благодарит! нет, мужики все же старнные
создания...]
Создателям сайта "МАСТЕРА DELPHI" - за великолепный
сайт. Почему-то из всех виденных он мне больше всего пришелся по душе.
Сделан стартовой страницей [Енота: странно. а когда я за машину сажусь
после него, стартовая - обязательно какое-нибудь порно...]
Фирме
"В.А.Т. Прилуки" - за помощь в погублении моего здоровья
Bruce
Dickinson и Владимиру Высоцкому - их я слушал попеременно, пока писал и
разбирался
...и прочим, кого я забыл...
С чего все начиналось:
С начала. Мне нужно было написать перехватчик вызовов WinSock. Дабы любая программа могла работать через SOCKS5-проксик. Я посчитал, что перехват вызовов DLL'ки проще, чем судорожные попытки написать драйвер (да и сейчас так считаю). Енота, правда, ехидно улыбалась и говорила "ну-ну", но я-таки справился. SOCKS сниффер еще пишу, но в принципах перехвата уже разобрался :-) [Енота: разобраться-то он действительно разобрался, а соксифиера нет до сих пор...]
Как все будет:
Я предпочитаю не писать сухие статьи с кучей теории. Поскольку я люблю читать работающий исходный код, то и здесь будет только исходный код. Все пояснения я буду вставлять прямо в исходник - в виде комментариев. Впрочем, не надейтесь, что вам будет достаточно выдрать отсюда исходник, и он скомпилится. :-) Это не потому, что я специально что-то скрыл, а потому, что я вырезал кучу вспомогательных процедур, которые каждый может написать сам. Если вы, все же, паталогически ленивы - скачайте архив с полными рабочими исходниками. Оттуда точно заработает.
Исходники:
Наконец-то... начнем.
procedure DoDebugLoop;
{ собственно, это главная
процедура перехватчика. большую часть времени он крутится именно в ней
}
var
Event: TDebugEvent;
{ стандартная Win32
стректура. для интересующихся:
ЕDebugEvent =
record
dwDebugEventCode: DWORD;
// тип пришедшего
события
dwProcessId: DWORD;
// Id прерванного
процесса
dwThreadId: DWORD;
// Id прерванного
потока
case Integer of
0: (Exception: TExceptionDebugInfo);
1:
(CreateThread: TCreateThreadDebugInfo);
2: (CreateProcessInfo:
TCreateProcessDebugInfo);
3: (ExitThread: TExitThreadDebugInfo);
4:
(ExitProcess: TExitThreadDebugInfo);
5: (LoadDll:
TLoadDLLDebugInfo);
6: (UnloadDll: TUnloadDLLDebugInfo);
7:
(DebugString: TOutputDebugStringInfo);
8: (RipInfo:
TRIPInfo);
// эти части смотрите сами - не могу же я все
разжевывать! :-)
end;
следует добавить, что Microsoft -
ребята странные. Функция GetThreadContext, при помощи которой
реализуется пошаговая отладка и просмотр регистров, требует на входе хэндл
процесса. а нам дают только его Id. после безуспешных поисков функции
типа ConvertThreadIdToHandle [Енота: мечтатель, однако...] я
решил, что придется заводить список запущенных потоков. в событии
CREATE_THREAD_DEBUG_EVENT нам дают-таки хэндл. придется запоминать все
созданные потоки (не забывая их забывать ( сорри :-) в
EXIT_THREAD_DEBUG_EVENT). позже Sleepyhead сказал, что я все придумал
очень правильно (ай да Кэтмар! ай да сукин сын! простите, классика :-) -
так люди и делают. ну он большой, ему виднее :-) }
dwContinueStatus:
DWORD;
{ как системе обрабатывать событие в ContinueDebugEvent.
обнаружилось, что если это событие - не исключение
(EXCEPTION_DEBUG_EVENT), то этот флажок системе "по сараю". а если
исключение, то есть два варианта: DBG_CONTINUE - наш "отладчик" успешно
обработал все сам, и DBG_EXCEPTION_NOT_HANDLED, что значиит - передать
исключение системе на обработку }
CurThread:
DWORD;
{ хэндл потока, найденный в нашем списке потоков (см.
замечание чуть повыше) }
HProc: DWORD;
{ хэндл процесса,
который мы отлаживаем }
Context: TContext;
{ контекст
потока. проще говоря - содержание его регистров }
ThreadList:
array[0..99] of record Id, Handle: DWORD; end;
{ тот самый
пресловутый список потоков, который мы своими ручками будем создавать и
поддерживать. в принципе, это должен быть список или динамический массив,
ибо количество потоков, которые может создать программа, заранее не
известно, но не будем заморачиваться. код-то демонстрационный!
}
RetAddr: DWORD;
{ здесь будет храниться адрес
возврата из перехваченной API-функции (так, на всякий случай. чтобы вы
видели, как и откуда его можно добыть) }
BPAddr:
DWORD;
{ в учебных целях мы будем перехватывать только одну
функцию. поэтому вместо списка обойдемся просто переменной. здесь будет
храниться адрес первого байтика перехваченной функции
}
OrigByte: Byte;
{ а здесь будет храниться сам
первый байтик }
RestoreBreak: Boolean;
{ флажок,
который указывает обработчику события EXCEPTION_SINGLE_STEP надо ли
восстанавливать точку останова. весь перехват выглядит так:
нашли
стартовый адрес процедуры (это можно сделать просмотром таблицы экспорта у
соответствующей DLL-ки. как именно - здесь не пишу. или разбирайтесь сами,
или качайте мои исходники - там все есть. не то чтобы мне жалко, но к
Debug API это имеет отношение весьма косвенное. опять же, если народ будет
очень интересоваться, сделаю статью с quick overview формата
PE);
запомнили ее первый байт;
записали вместо первого байта код $CC
(это Int3 - DEBUG_EXCEPTION);
по приходу DEBUG_EXCEPTION:
проверили, точно ли мы прервались на
адресе нашей точки останова. если нет - не делаем ничего.
иначе:
восстановили первый байт;
установили флажок
SINGLE_STEP;
установили флажок ResoteBreak;
ожидаем прихода события
EXCEPTION_SINGLE_STEP;
по приходу EXCEPTION_SINGLE_STEP:
если установлен флажок
RestoreBreak:
вернули на место $CC;
сбросили флажок ResoteBreak;
}
ProcessFinished: Boolean;
{ флажок, указывающий,
завершился ли отлаживаемый процесс. Sleepyhead говорит, что иногда процесс
не завершается корректно (к примеру, отладчик, который отлаживает
отладчик, который отлаживает отладчик... [Енота: GNU's not Unix :-)]),
поэтому если процесс не завершится сам, мы прибьем его руками }
begin
FillChar(ThreadList, SizeOf(ThreadList), 0);
HProc := 0;
{ хэндл процесса, который будем отлаживать.
пока процесс не запущенным считается, соответственно - хэндла нету
}
ProcessFinished := True;
{ поскольку процесс не
запустился, то он считается завершенным :-) }
BPAddr :=
0;
{ точку останова уточним, когда загрузится нужная DLL
}
RestoreBreak := False;
repeat
if not WaitForDebugEvent(Event, INFINITE) then
break;
{ ожидаем прихода отладочного события. в реальном
отладчике здеесь вместо INFINITE лучше задать маленькую константу, ожидать
в цикле, там же в цикле организовывать взаимодействие с юзверем. или
вообще для интерфейса отдельный поток создать }
dwContinueStatus
:= DBG_EXCEPTION_NOT_HANDLED;
{ поскольку большинство
исключений мы не обрабатываем, то по умолчанию так и говорим системе
}
CurThread := GetThreadHandleFromList(ThreadList,
Event.dwThreadId);
{ просто поиск в массиве ThreadList. Id нам
известен, ищем хэндл }
case Event.dwDebugEventCode
of
{ проверим - а что, собственно случилось?
}
CREATE_PROCESS_DEBUG_EVENT:
{ запустился новый процесс.
запомним его хэндл, и сбросим флажок ProcessFinished
}
begin
HProc :=
Event.CreateProcessInfo.HProcess;
ProcessFinished :=
False;
AddThreadToList(ThreadList, Event.dwThreadId,
Event.CreateProcessInfo.hThread);
end;
EXIT_PROCESS_DEBUG_EVENT:
{
процесс завершился - значит, можно смело закрывать наш перехватчик. заодно
установим флажок ProcessFinished }
begin
ProcessFinished :=
True;
ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId,
DBG_CONTINUE);
{ это на всякий случай - чтобы ось точно прибила
и процесс, и отладчик. в принципе, оно не надо, но смотри выше комментарий
к ProcessFinished }
break; { все, из цикла отладки можно
смело выходить }
end;
CREATE_THREAD_DEBUG_EVENT:
{ процесс запустил новый
поток. здесь у нас есть единственная возможность запомнить его хэндл. так
и делаем }
AddThreadToList(ThreadList, Event.dwThreadId,
Event.CreateThread.hThread);
EXIT_THREAD_DEBUG_EVENT:
{
процесс завершил исполнение потока. забудем его хэндл
}
DeleteThreadFromList(ThreadList,
Event.dwThreadId);
LOAD_DLL_DEBUG_EVENT:
{ процесс загрузил какую-то DLL'ку.
проверим, не та ли это, которая нам нужна. если та, установим точку
останова. текст процедуры смотрите ниже
}
ProcessDLLExport(HProc,
DWORD(Event.LoadDll.lpBaseOfDll));
UNLOAD_DLL_DEBUG_EVENT:
{
процесс выгрузил какую-то DLL'ку. по-правилам, это надо бы обработать, но
поскольку я перехватываю вызовы kernel32.dll, который всегда (за
очень-очень редким исключением :-) линкуется статически, то это событие я
просто игнорирую. а вообще-то надо запомнить адрес загрузки нужной нам DLL
в LOAD_DLL_DEBUG_EVENT (ибо это единственный способ идентифицировать
DLL'ку), а здесь проверять - не наша ли это. если наша - обнулить BPAddr.
можете дописать сами - как любят говорить авторы книг: "в качестве
упражнения" :-) [Енота: ага. а сам, когда видит в книге эту фразу,
разражается потоком нецензурной лексики :-)] }
WriteLn('unloading DLL:
', IntToHex(DWORD(Event.UnloadDll.lpBaseOfDll), 8));
EXCEPTION_DEBUG_EVENT:
{ какое-то исключение. проверим
поточнее... }
case Event.Exception.ExceptionRecord.ExceptionCode
of
EXCEPTION_BREAKPOINT:
{ это - точка останова. здесь мы
уточним: наша или нет. дело в том, что система сама генерирует это
событие, когда процесс загрузился, но перед тем, как он запущен (полсе
того, как системный загрузчик загрузил процесс и все его DLL'ки. как раз
перед тем, как исполнить первую инструкцию процесса). плюс - мало ли,
какой код внутри исследуемого процесса может быть? так что...
}
begin
dwContinueStatus := DBG_CONTINUE;
{ скажем
системе, что это исключение мы обработали сами, пусть не напрягается
}
Context.ContextFlags := CONTEXT_CONTROL or CONTEXT_INTEGER or
CONTEXT_SEGMENTS;
GetThreadContext(CurThread, Context);
{
получили контекст прерванного потока. больше всего нас интересуют IP и
Flags. остальные регистры запросили просто для полноты картины
}
if (BPAddr <> 0) and (Context.EIP = BPAddr + 1)
then
begin
{ если мы уже установили нашу точку
останова и прервались именно на ней... }
RetAddr :=
ReadProcessLong(HProc, Context.ESP);
{ то получим адрес
возврата из перехваченной нами функции. он нам не нужен, на самом-то деле,
это просто пример - откуда его брать. если вам нужны параметры -
ReadProcessLong(HProc, Context.ESP + 4) будет первым, ...+ 8) -
вторым, и так далее... кстати, ReadProcessLong - просто обертка для
системной функции ReadProcessMemory. читает 4 байтика. для
удобства. думаю, что у вас не будет проблем сделать себе такую же :-)
}
WriteLn('Return address: 0x', IntToHex(RetAddr, 8));
{
дальше - уменьшим IP на еденичку (чтобы исполнить ту инструкцию, которую
мы заменили на нашу точку останова)... реально, EIP-1 хранится в BPAddr.
так и запишем... }
Context.EIP := BPAddr;
{ ...и
восстановим оригинальный первый байтик этой инструкции
}
WriteProcessByte(HProc, BPAddr, OrigByte);
{
установим флажок для того, чтобы система генерировала событие
EXCEPTION_SINGLE_STEP. в этом событии надо будет вернуть точку останова на
место, иначе перехват состоится ровно один раз :-) [Енота: а то бы
читатель сам не догадался...] }
RestoreBreak :=
True;
Context.EFlags := Context.EFlags or EFLAGS_TRACE;
{
вышеприведенной инструкцией мы сообщаем системе, что хотим получать по
событию (EXCEPTION_SINGLE_STEP) после каждой исполненной в отлаживаемом
процессе машинной команды. кстати, значение константы EFLAGS_TRACE = $100
}
Context.ContextFlags :=
CONTEXT_CONTROL;
SetThreadContext(CurThread, Context);
{
установим новое значение регистров потока
}
end;
end;
EXCEPTION_SINGLE_STEP:
{ выполнена
одна машинная команда. скорее всего, возниконовение этого события -
результат выполнения нашей точки останова, но кто знает? проверим флажки.
если надо - восстановим точку останова
}
begin
dwContinueStatus := DBG_CONTINUE;
{ скажем
системе, что это исключение мы обработали сами, пусть не напрягается
}
Context.ContextFlags :=
CONTEXT_CONTROL;
GetThreadContext(CurThread, Context);
if
RestoreBreak and (Context.EIP >= BPAddr) and (Context.EIP <= BPAddr
+ 32) then
begin
{ это действительно "наше" событие.
восстановим точку останова, чтобы перехватчик работал и дальше
}
OrigByte := WriteInt3(HProc, BPAddr);
RestoreBreak :=
False;
Context.EFlags := Context.EFlags and not EFLAGS_TRACE;
{
сбросим флажок трассировки, ибо больше это событие нам не надо
}
end
else
if RestoreBreak then
Context.EFlags :=
Context.EFlags or EFLAGS_TRACE;
{ вернем флажок трассировки,
если событие не наше - нам ведь надо нашего дождаться. у меня система сама
скидывает сей флаг, так что на всякий случай... }
Context.ContextFlags :=
CONTEXT_CONTROL;
SetThreadContext(CurThread,
Context);
end;
end;
end;
if not ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId,
dwContinueStatus) then break;
{ все. смело позволяем
отлаживаемому процессу исполняться дальше }
until
False;
{ сюда мы попадем только при каком-нибудь сбое или
завершении процесса. на всякий случай (по совету SleepyHead'а) проверим: а
точно наш отлаживаемый процесс завершился? если нет - прибьем руками
}
if not ProcessFinished
then
begin
repeat
TerminateProcess(HProc, RetAddr);
if not
WaitForDebugEvent(Event, INFINITE) then break;
if
(Event.dwDebugEventCode = EXIT_PROCESS_DEBUG_EVENT) then break;
if not
ContinueDebugEvent(Event.dwProcessId, Event.dwThreadId, DBG_CONTINUE) then
break;
until False;
ContinueDebugEvent(Event.dwProcessId,
Event.dwThreadId, DBG_CONTINUE);
end;
{ все. закончили :-)
}
end;
{ а вот процедурка, которая устанавливает точку останова
}
procedure ProcessDLLExport(PrcH, Base:
DWORD);
var
DLLName: string;
ExpTbl: TExportHeader;
N:
DWORD;
begin
if (BPAddr <> 0) then exit;
{ если уже
установлена - не делать ничего }
if not FindExportTable(PrcH, Base,
ExpTbl) then exit;
{ если не смогли найти в DLL'ке таблицу экспорта
(мало ли...) - тоже ничего не делать }
DLLName :=
ANSILowerCase(GetASCIIZString(PrcH, ExpTbl.NameRVA + Base));
{
получили имя DLL'ки }
if (DLLName <> 'kernel32.dll') then
exit;
{ не наша? если да - снова не делаем ничего }
N :=
FindExportIndexByName(PrcH, Base, 'AllocConsole', ExpTbl);
N :=
FindExportByIndex(PrcH, Base, N, ExpTbl);
{ нашли по таблице
экспорта точку входа (если не нашли - опять же ничего делать не надо
}
if (N = 0) then exit;
{ а если нашли - запомним необходимую
информацию и установим останов }
BPAddr := N;
OrigByte :=
WriteInt3(PrcH, N);
{ WriteInt3 просто возвращает в качестве
результата старый байтик, и на его место записывает код $CC - инструкция
Int3. когда система встречает эту инструкцию, она генерирует исключение
EXCEPTION_BREAKPOINT }
end;
Все. Не так страшен черт, как его малюют [Енота: или: не так страшен
Гейтс... :-)]. Остались мелочи.
Если вы запускаете процесс сами, не
забудьте указать в CreateProcess флажок
DEBUG_ONLY_THIS_PROCESS, чтобы отладчик мог работать, и чтобы
процессы, которые может запустить отлаживаемая программа не отлаживались
нами (а зачем нам дочерние процессы? если хотим перехватывать вызовы и в
них, проще будет ловить непосредственно CreateProcess, и для
каждого "новорожденного" запускать свою копию отладчика. Тем более, что
если мы присоединяемся к уже запущенному процессу, то система по умолчанию
ставит флажок DEBUG_ONLY_THIS_PROCESS. Так что перехватывать
CreateProcess надежнее).
Если же вы хотите присоединиться к
уже запущенному процессу, то узнайте его Id (с помощью TaskManager в
NT или программно), и смело пишите
DebugActiveProcess(ProcessId). В дальнейшем никаких различий между
работой с процессом, запущенным нами и процессом, к которому мы
присоединились "на лету" уже нет.
И еще: учтите, что если наш отладчик
завершится, то система автоматически прибьет и процесс, который мы имели
счастье отлаживать. Способа "отсоединиться" от процесса нет: взялся за
гуж, не говори, что не дюж. :-)
Также замечу, что полезно обрабатывать
возможные ошибки при вызове системных функций. Здесь я их - в основном -
смело игнорирую, но вам бы лучше так не поступать.
Полные рабочие исходники можно взять с нашего сайта: http://www.piranha-home.org./Если кто-то поможет в деле перевода статьи на английский - буду очень благодарен.
Со всеми замечаниями смело обращайтесь на keith@piranha-home.org. Также буду не прочь узнать - помог ли я вам. Просто напишите письмецо, если вам понравилась (и даже если не понравилась) статейка. Если вы скажете пару добрых слов Еноте, будет совсем здорово. :-) [Енота: как трогательно... :-)]
зыж (P.S. по-аглицки :-)
Если вдруг нечаянно вкрались ошибки -
простите. Пишу в WordPad'е, так что проверки правописания нет. :-) А
ставить Word специально для проверок... Да ну его в колодец!
| Delphisource (2006г.) |