Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Использование Debug API. Пример перехвата вызовов функций Win32 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 сказал, что я все придумал очень правильно (ай да Кэтмар! ай да сукин сын! простите, классика :-) - так люди и делают. ну он большой, ему виднее :-) }

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. Если кто-то поможет в деле перевода статьи на английский - буду очень благодарен.

Проект Delphi World © Выпуск 2002 - 2004
Автор проекта: ___Nikolay