Использование Debug API

=== Использование Debug API ===
С чего все начиналось: 
С начала. Мне нужно было написать перехватчик вызовов 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 сказал, что я все
 придумал очень правильно (ай да Кэтмар! ай да сукин сын! простите, 
 классика :-) - так люди и делают. ну он большой, ему виднее :-) } 
 { как системе обрабатывать событие в ContinueDebugEvent. обнаружилось, что если это событие - не исключение 
 (EXCEPTION_DEBUG_EVENT), то этот флажок системе "по сараю". а если исключение, то есть два варианта: DBG_CONTINUE - наш 
 "отладчик" успешно обработал все сам, и DBG_EXCEPTION_NOT_HANDLED, что значиит - передать исключение системе на обработку } 
 dwContinueStatus: DWORD; 
 { хэндл потока, найденный в нашем списке потоков (см. замечание чуть повыше) } 
 CurThread: DWORD; 
 { хэндл процесса, который мы отлаживаем } 
 HProc: DWORD; 
 { контекст потока. проще говоря - содержание его регистров } 
 Context: TContext; 
 { тот самый пресловутый список потоков, который мы своими ручками будем создавать и поддерживать. в принципе, это должен быть 
 список или динамический массив, ибо количество потоков, которые может создать программа, заранее не известно, но не будем 
 заморачиваться. код-то демонстрационный! } 
 ThreadList: array[0..99] of record Id, Handle: DWORD;
 
 { здесь будет храниться адрес возврата из перехваченной API-функции
 (так, на всякий случай. чтобы вы видели, как и откуда его можно добыть) } 
 RetAddr: DWORD; 
 { в учебных целях мы будем перехватывать только одну функцию.
 поэтому вместо списка обойдемся просто переменной.
 здесь будет храниться адрес первого байтика перехваченной функции } 
 BPAddr: DWORD; 
 { а здесь будет храниться сам первый байтик } 
 OrigByte: Byte; 
 { флажок, который указывает обработчику события 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; } 
 RestoreBreak: Boolean; 
 { флажок, указывающий, завершился ли отлаживаемый процесс.
 Sleepyhead говорит, что иногда процесс не завершается корректно
 (к примеру, отладчик, который отлаживает отладчик, который отлаживает отладчик... [Енота: GNU's not Unix :-)]), 
 поэтому если процесс не завершится сам, мы прибьем его руками } 
 ProcessFinished: Boolean; 
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. Если кто-то поможет в деле перевода статьи на английский - буду очень благодарен
Source: http://delphiworld.narod.ru/
ID: 02038