Сообщений 33    Оценка 221 [+1/-0]         Оценить  
Система Orphus

Перехват API-функций в Windows NT/2000/XP

Автор: Тихомиров В.А.
Источник: RSDN Magazine #1
Опубликовано: 11.11.2002
Исправлено: 13.03.2005
Версия текста: 1.0
Что такое «перехват API-функций»
Метод 1. Перехват API непосредственной записью в код системной функции.
Метод 2. Перехват API через таблицу импорта.
Внедрение кода в чужой процесс в Windows NT
Внедрение 1
Внедрение 2
Как отлаживать такие выкрутасы
Отладка кода загрузчика
Отладка функций, выполняющихся при старте DLL
Отладка функций – двойников, получающих управление при вызове перехваченных API функций
Тестирование
Заключение

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

С переходом на Windows использование системных ресурсов программистами в большом объеме стало осуществляться через функции API, и у многих «сиспрогов» стал возникать вопрос: «существуют ли в Windows технологии перехватов этих системных функций?» Особый интерес это вызывает применительно к высокозащищенным ОС, выполненным на ядре NT. Данная статья подробнейшим образом, с действующими примерами покажет практическую реализацию такой технологии (предполагается, что читатель знаком с принципами системного программирования в Windows и умеет применять в своей работе Visual C++).

Что такое «перехват API-функций»

Перехват системной функции API заключается в изменении некоторого адреса в памяти процесса или некоторого кода в теле функции таким образом, чтобы при вызове этой самой API-функции управление передавалось не ей, а вашей функции, подменяющей системную. Эта функция, работая вместо системной, выполняет какие-то запланированные вами действия, и затем, в зависимости от вашего желания, либо вызывает оригинальную функцию, либо не вызывает ее вообще. Перехват функций является очень полезным средством в том случае, если вы хотите отследить, изменить или заблокировать некоторые конкретные действия приложения.

Перехват функций чужого процесса удобнее всего осуществлять внедрением собственной DLL с функцией-двойником в адресное пространство того процесса, контроль над функциями API которого вы хотите установить. При написании двойников функций следует особое внимание обратить на соглашения о вызовах функций __cdecl и __stdcall. В __cdecl функциях подразумевается, что параметры кладутся в стек справа налево, и вызывающая функция очищает стек от аргументов. В __stdcall функциях подразумевается, что параметры кладутся в стек справа налево, но стек от аргументов очищает вызываемая функция. Кроме того, следует учитывать, что в Windows API многие функции встречается в 2-х экземплярах: ANSI и UNICODE. Первые обозначаются суффиксом A: например MessageBoxA, вторые – суффиксом W – например MessageBoxW.

Рассмотрим два метода перехвата API функций:

  1. Непосредственная запись в код функции.
  2. Подмена адреса функции в таблице импорта.

Метод 1. Перехват API непосредственной записью в код системной функции.

Прием заключается в том, чтобы в начало перехватываемой функции записать команду jmp ваша_функция_двойник или эквивалентную ей. Затираемые байты желательно где-нибудь сохранить. После вызова исправленной функции приложением управление будет передано вашей функции. Она должна корректно обработать стек, то есть извлечь переданные ей параметры и произвести необходимые вам действия. Затем, если вы собираетесь вызывать оригинальную функцию, необходимо восстановить затертые байты в начале оригинальной функции. Вызвать ее, передав ей все необходимые параметры. После возврата из оригинальной функции, необходимо снова в начало кода функции записать команду перехода на вашу функцию. Вернуть управление вызвавшей программе.

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

Недостаток: в многопоточных приложениях может возникнуть такая ситуация, когда один поток вызвал перехваченную вами функцию, управление было передано функции-двойнику, она восстановила оригинальное начало функции, но в этот момент параллельно выполняющийся поток произвел вызов той же функции. В результате управление будет передано сразу оригинальной функции, минуя вашу :(.

Разберем пример программы (в виде DLL-файла), перехватывающей функцию MessageBoxA методом 1.

Для работы нам потребуются следующие заголовочные файлы:

#include "stdafx.h"
#include "intercpt.h"

Далее подготовим структуру, содержащую код дальнего перехода на нашу функцию-двойник. Практика показала, что вместо обычного jmp лучше применять комбинацию

push xxxxxxxx 
ret 

где хххххххх – это адрес функции-двойника. В результате структура, которая будет хранить нужный код перехода, выглядит так:

struct jmp_far
{
  BYTE instr_push;  //здесь будет код инструкции push
  DWORD arg;         //аргумент push
  BYTE  instr_ret;    //здесь будет код инструкции ret
};

Зададим нужные переменные:

BYTE old[6]; //область для хранения 6-ти затираемых байт начала функции
DWORD adr_MessageBoxA //будущий адрес оригинальной функции
DWORD written; //вспомогательная переменная
jmp_far jump; //здесь будет машинный код инструкции перехода

Главная функция DLL будет выглядеть следующим образом:

BOOL APIENTRY DllMain( HANDLE hModule,  DWORD  ul_reason_for_call,
                       LPVOID lpReserved )
{
// Если система подключает DLL к какому-либо процессу, 
// она сначала вызовет главную функцию DLL с параметром 
// DLL_PROCESS_ATTACH, на что мы сразу вызовем нашу функцию 
// InterceptFunctions, которая произведет подмену стандартной API функции
// MessageBoxA нашей функцией Intercept_MessageBoxA (см. ниже)

  if(ul_reason_for_call = = DLL_PROCESS_ATTACH )
  {
    InterceptFunctions();
  }
  return TRUE;
}

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

void InterceptFunctions(void)
{
  DWORD op;
  //сначала получим абсолютный адрес функции для перехвата
  adr_MessageBoxA = (DWORD)GetProcAddress(GetModuleHandle("user32.dll"),
                    "MessageBoxA");
  if(adr_MessageBoxA == 0)
  {
    MessageBox(NULL, "Can`t get adr_MessageBoxA, "Error!", 0);
    return;
  }

  // Зададим машинный код инструкции перехода, который затем впишем 
  // в начало полученного адреса:
  jump.instr_push = 0x68;
  jump.arg = (DWORD)&Intercept_MessageBoxA;
  jump.instr_ret = 0xC3;

  //Прочитаем и сохраним первые оригинальные 6 байт стандартной API функции
  ReadProcessMemory(GetCurrentProcess(),(void*) adr_MessageBoxA, 
                    (void*)&old, 6, &written);

//Запишем команду перехода на нашу функцию поверх этих 6-ти байт
WriteProcessMemory(GetCurrentProcess(), (void*)adr_MessageBoxA, 
     (void*)&jump, sizeof(jmp_far), &written);
}

Теперь посмотрим, как выглядит сама функция-двойник. Она должна заменить стандартную MessageBoxA, поэтому её тип и состав параметров должны точно соответствовать оригиналу:

//данное определение аналогично __srtdcall
BOOL WINAPI Intercept_MessageBoxA(HWND hwnd, char *text, char *hdr, UINT utype)
{
  //Сначала восстанавливаем 6 первых байт функции. Это не обязательное 
  // действие, просто мы решили подшутить над пользователем, и все 
  // сообщения функции MessageBoxA переделать на свои, поэтому нам придется
  // вызвать оригинальную функцию, а для этого следует восстановить ее адрес:
  WriteProcessMemory(GetCurrentProcess(), (void*)adr_MessageBoxA, 
                     (void*)&old, 6, &written);

  //Здесь вы можете порезвиться от души и выполнить любые, пришедшие вам 
  // в голову действия. Мы просто заменили сообщение функции на свое:
  char *str = "Hi From MessageBOX!!!!";

  //Вызываем оригинальную функцию через указатель
  ((BOOL (__stdcall*)(HWND, char*, char*, UINT))adr_MessageBoxA)(hwnd, 
             str, hdr, utype);

  //Снова заменяем  6 байт функции на команду перехода на нашу функцию
  WriteProcessMemory(GetCurrentProcess(), (void*)adr_MessageBoxA, 
                     (void*)&jump, 6,&written);
  return TRUE;
}

Если откомпилировать этот код как DLL, то получим файл, который в дальнейшем (см.ниже) следует внедрить в процесс, в котором мы хотим перехватить API MessageBoxA.

Метод 2. Перехват API через таблицу импорта.

Прием заключается в замене адреса функции в таблице импорта на адрес функции-двойника. Для понимания данного метода потребуется знание формата PE исполняемых файлов Windows. Как известно, большинство приложений вызывает функции из dll через таблицу импорта, представляющую собой после загрузки exe файла в память списки адресов функций, импортируемых из различных Dll. Откомпилированный вызов функции через таблицу импорта выглядит следующим образом:

Call dword ptr[address_of_function] 

или что-то наподобие. Здесь address_of_function – адрес в таблице импорта, по которому находится адрес вызываемой функции. (Тем, кто не знаком со структурой PE заголовка EXE файла, рекомендуем заглянуть в Интернет за соответствующей информацией.)

При перехвате API через таблицу импорта надо:

Теперь при вызове подмененной функции вначале будет вызываться функция-двойник. После этого она может вызвать (или не вызывать) оригинальную функцию.

Достоинство данного метода в том, что он будет корректно работать в многопоточном приложении, когда несколько потоков одновременно вызывают подмененную функцию. Так же данный метод будет работать в ОС WINDOWS 9.x.

Недостаток – не все функции вызываются через таблицу импорта.

Ниже приведен пример программы, аналогичной приведенной выше, но использующей второй метод перехвата функции:

DWORD adr_MessageBoxA;

BOOL APIENTRY DllMain(HANDLE hModule, DWORD  ul_reason_for_call,
                      LPVOID lpReserved)
{
  if(ul_reason_for_call == DLL_PROCESS_ATTACH)
    InterceptFunctions();
  return TRUE;
}

// Эта функция ищет в таблице импорта - .idata нужный адрес и меняет на
// адрес процедуры-двойника 
void InterceptFunctions(void)
{
  // Начало отображения в памяти процесса
  BYTE *pimage = (BYTE*)GetModuleHandle(NULL); 
  BYTE *pidata;
  // Стандартные структуры описания PE заголовка
  IMAGE_DOS_HEADER *idh;
  IMAGE_OPTIONAL_HEADER *ioh;
  IMAGE_SECTION_HEADER *ish;
  IMAGE_IMPORT_DESCRIPTOR *iid;
  DWORD *isd;  //image_thunk_data dword

  // Получаем указатели на стандартные структуры данных PE заголовка
  idh = (IMAGE_DOS_HEADER*)pimage;
  ioh = (IMAGE_OPTIONAL_HEADER*)(pimage + idh->e_lfanew 
                                 + 4 + sizeof(IMAGE_FILE_HEADER));
  ish = (IMAGE_SECTION_HEADER*)((BYTE*)ioh + sizeof(IMAGE_OPTIONAL_HEADER));
  //если не обнаружен магический код, то у этой программы нет PE заголовка
  if (idh->e_magic != 0x5A4D) 
  {
    MessageBox(NULL, "Not exe hdr", "Error!", 0);
    return;
  }

  //ищем секцию .idata
  for(int i=0; i<16; i++)
    if(strcmp((char*)((ish+ i)->Name) , ".idata") == 0) break;
  if(i==16) 
  {
    MessageBox(NULL, "Unable to find .idata section", "Error!", 0);
    return;
  }

  // Получаем адрес секции .idata(первого элемента IMAGE_IMPORT_DESCRIPTOR)
  iid = (IMAGE_IMPORT_DESCRIPTOR*)(pimage + (ish +i)->VirtualAddress );
  
  // Получаем абсолютный адрес функции для перехвата
  adr_MessageBoxA = (DWORD)GetProcAddress(
                        GetModuleHandle("user32.dll"), "MessageBoxA");
  if(adr_MessageBoxA == 0)
  {
    MessageBox(NULL, "Can`t get addr_MessageBoxA", "Error!", 0);
    return;
  }

  // В таблице импорта ищем соответствующий элемент для 
  // библиотеки user32.dll
  while(iid->Name)  //до тех пор пока поле структуры не содержит 0
  {
    if(strcmp((char*)(pimage + iid->Name), "USER32.dll") ==0 ) break;
    iid++;
  }

  // Ищем в IMAGE_THUNK_DATA нужный адрес
  isd = (DWORD*)(pimage + iid->FirstThunk);
  while(*isd!=adr_MessageBoxA && *isd!=0)  isd++;
  if(*isd == 0)
  {
    MessageBox(NULL, "adr_MessageBoxA not found in .idata", "Error!", 0);
    return;
  }
  
  // Заменяем адрес на свою функцию
  
  DWORD buf =  (DWORD)&Intercept_MessageBoxA;
  DWORD op;
  
  // Обычно страницы в этой области недоступны для записи
  // поэтому принудительно разрешаем запись
  VirtualProtect((void*)(isd),4,PAGE_READWRITE, &op);
  
  // Пишем новый адрес
  WriteProcessMemory(GetCurrentProcess(), (void*)(isd),
                    (void*)&buf,4,&written);
  //восстанавливаем первоначальную защиту области по записи
  VirtualProtect((void*)(isd),4,op, &op);
  //если записать не удалось – увы, все пошло прахом…
  if(written!=4)
  {
    MessageBox(NULL, "Unable rewrite address", "Error!", 0);
    return;
  }
}

А вот так выглядит подстановочная функция:

BOOL WINAPI Intercept_MessageBoxA(HWND hwnd, char *text, 
                                  char *hdr, UINT utype)
{
  //здесь вы выполняете любые свои действия
  char *str = "Hi From MessageBOX!!!!";
  // вызываем оригинальную функцию через указатель
  ((BOOL (__stdcall*)(HWND, char*, char*, UINT))adr_MessageBoxA)(hwnd, 
            str, hdr, utype);
  return TRUE;
}

Внедрение кода в чужой процесс в Windows NT

Теперь осталось показать, как вышеописанные DLL можно внедрить в процесс, избранный в качестве жертвы эксперимента. (Нелишне напомнить, что для нашего примера процесс-жертва должен иметь окна со стандартными сообщениями MessageBox ).

Внедрить код – значит, записать некоторую программу в чужой процесс и исполнить ее от имени этого процесса. Таким образом, внедренный код становится частью процесса и получает доступ ко всем ресурсам, которыми обладает процесс. В отличие от DOS, семейство ОС Windows (на ядре NT) – операционные системы с разделяемой памятью, т.е каждое приложение выполняется в своем адресном пространстве, не пересекающемся с другими, и не имеет непосредственного доступа к памяти чужого приложения. Таким образом, внедрение кода является нетривиальной задачей. Существует несколько способов внедрить свой код:

1. «Вручную».

2. При помощи хуков.

Внедрение 1

Рассмотрим наиболее эффективный, на наш взгляд, способ внедрения – первый. Он заключается в записи короткого участка машинного кода в память процесса, который должен присоединить DLL к этому процессу, запустить ее код, после чего Dll сможет выполнять любые действия от имени данного процесса. В принципе можно и не присоединять DLL, а реализовать нужные действия во внедряемом машинном коде, но это будет слишком трудоемкой задачей, поскольку все смещения для данных потеряют смысл, и вы не сможете корректно обратиться к ним, не настроив соответствующим образом смещения (морока :( ).

При присоединении DLL загрузчик автоматически устанавливает все смещения в соответствии с адресом, по которому загружена DLL. Следует также отметить, что для записи кода в процесс и его исполнения необходимо открыть процесс с доступом как минимум:

PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION.

ПРЕДУПРЕЖДЕНИЕ

При реализации данного метода необходимо указать компилятору выравнивать структуры ПОБАЙТОВО. Иначе структура с машинным кодом будет содержать совершенно не тот код, что был запланирован.

Общая схема внедрения:

Внедряемый машинный код должен (по нашему сценарию) произвести такие действия:

В WinNT стартовый адрес отображения системных DLL (user32, kernel32 и т. д.) один и тот же для всех приложений. Это означает, что адрес некоторой функции из системной DLL в одном приложении будет актуален и в другом приложении. То есть точки входа функций системных DLL всегда одни и те же.

Ниже приведен пример процедуры, внедряющей dll с заданным именем в процесс с заданным PID (идентификатором процесса) (их можно наблюдать в закладке «процессы» диспетчера задач или получить с помощью стандартных API-функций).

//структура описывает поля, в которых содержится код внедрения
struct INJECTORCODE
{
  BYTE  instr_push_loadlibrary_arg; //инструкция push
  DWORD loadlibrary_arg;            //аргумент push  

  WORD  instr_call_loadlibrary;     //инструкция call []  
  DWORD adr_from_call_loadlibrary;

  BYTE  instr_push_exitthread_arg;
  DWORD exitthread_arg;

  WORD  instr_call_exitthread;
  DWORD adr_from_call_exitthread;

  DWORD addr_loadlibrary;
  DWORD addr_exitthread;     //адрес функции ExitTHread
  BYTE  libraryname[100];    //имя и путь к загружаемой библиотеке  
};

BOOL InjectDll(DWORD pid, char *lpszDllName)
{
  HANDLE hProcess;
  BYTE *p_code;
  INJECTORCODE cmds;
  DWORD wr, id;

  //открыть процесс с нужным доступом
  hProess=OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|
  PROCESS_VM_OPERATION, FALSE, pid);
  if(hProcess == NULL)
  {
    MessageBoxA(NULL, "You have not enough rights to attach dlls", 
               "Error!", 0);
    return FALSE;
  }
  
  //зарезервировать память в процессе
    p_code = (BYTE*)VirtualAllocEx(hProcess, 0, sizeof(INJECTORCODE),
                                   MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if(p_code==NULL)
    {
      MessageBox(NULL, "Unable to alloc memory in remote process",
                       "Error!", 0);
      return FALSE;
    }

  //инициализировать  машинный код
  cmds.instr_push_loadlibrary_arg = 0x68; //машинный код инструкции push
  cmds.loadlibrary_arg = (DWORD)((BYTE*)p_code 
           + offsetof(INJECTORCODE, libraryname));
  
  cmds.instr_call_loadlibrary = 0x15ff; //машинный код инструкции call
  cmds.adr_from_call_loadlibrary = 
  (DWORD)(p_code + offsetof(INJECTORCODE, addr_loadlibrary));
  
  cmds.instr_push_exitthread_arg  = 0x68;
  cmds.exitthread_arg = 0;
  
  cmds.instr_call_exitthread = 0x15ff; 
  cmds.adr_from_call_exitthread = 
  (DWORD)(p_code + offsetof(INJECTORCODE, addr_exitthread));
  
  cmds.addr_loadlibrary = 
  (DWORD)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
  
  cmds.addr_exitthread  = 
  (DWORD)GetProcAddress(GetModuleHandle("kernel32.dll"),"ExitThread");
  
  if(strlen(lpszDllName)>99)
  {
     MessageBox(NULL, "Dll Name too long", "Error!", 0);
     return FALSE;
  }
  strcpy((char*)cmds.libraryname, lpszDllName );
  
  /*После инициализации cmds в мнемонике ассемблера выглядит следующим
    образом:
      push  adr_library_name               ;аргумент ф-ции loadlibrary
      call dword ptr [loadlibrary_adr]     ; вызвать LoadLibrary 
      push exit_thread_arg                 ;аргумент для ExitThread
      call dword ptr [exit_thread_adr]     ;вызвать ExitThread     
  */
  
  //записать машинный код по зарезервированному адресу
  WriteProcessMemory(hProcess, p_code, &cmds, sizeof(cmds), &wr);
  
  //выполнить машинный код
  HANDLE z = CreateRemoteThread(hProcess, NULL, 0, 
               (unsigned long (__stdcall *)(void *))p_code, 0, 0, &id);

  //ожидать завершения удаленного потока
  WaitForSingleObject(z, INFINITE);
  //освободить память
  VirtualFreeEx(hProcess, (void*)p_code, sizeof(cmds), MEM_RELEASE);

  return TRUE;
}

Внедрение 2

Второй способ внедрения исполняемого кода (через хуки) наиболее прост в использовании. Он основан на технологии хуков, а именно: если установить хук на поток чужого процесса, то, как только поток получит сообщение, соответствующее заданному типу хука, система автоматически подключит DLL c хуком к данному процессу. Недостатком данного способа в том, что нельзя внедрить DLL в процесс, не имеющий очереди сообщений. Данная DLL будет присоединена к чужому процессу лишь до тех пор, пока запущена программа, установившая хук. Как только вы завершите эту программу, dll автоматически будет отключена. Первый способ лишен таких недостатков.

С другой стороны, первый способ будет работать лишь в WinNT, по причине использования функции VirtualAllocEx, которая резервирует память в заданном (отличном от того, в котором происходит вызов этой функции) процессе. Теоретически, данную проблему можно обойти, если писать код в некоторую часть отображения exe-файла чужого процесса, например в заголовок DOS, который после загрузки не используется. Но ОС не всегда позволяет писать в эту область памяти, даже если попытаться изменить разрешения при помощи VirtualProtextEx.

Есть еще и третий способ внедрения, но он наиболее опасен, так как может привести к краху системы. При помощи данного метода ОС сама внедряет указанную dll во все без исключения процессы операционной системы, даже защищенные. Для реализации необходимо прописать в реестре по пути Hkey_local_machine\software\microsoft\windowsnt\currentversion\windows в ключе AppInit_DLLs полный путь к своей dll.

Как отлаживать такие выкрутасы

Большинство программистов для отладки своих программ используют встроенные отладчики компиляторов. Они просты в использовании и удовлетворяют большинству требований, предъявляемых при отладке. Но если некоторый программный код будет внедрен и исполнен в рамках другого, постороннего процесса встроенный отладчик использовать очень тяжело. Для этих целей удобно применить системный отладчик SoftIce, который грузится раньше операционной системы, работает в нулевом кольце и поэтому имеет доступ к любым объектам ОС. Обсудим, как отлаживать внедренный код, выполняющий перехват API функций внутри постороннего процесса.

Все виды отладки условно можно разделить на 3 группы:

Отладка кода загрузчика

Итак, есть 2 процесса:

Задача заключается в том, чтобы поставить точку останова в П2 перед выполнением внедренного кода. Изначально неизвестно, по какому адресу будет внедрен код в П2. При этом предполагается, что П2 уже загружен и висит где-то в памяти. Для простоты запускаем П1 в каком-либо встроенном отладчике и трассируем, для того, чтобы узнать по какому адресу в П2 будет выделена память. Узнав этот адрес, включаем SoftIce (ctrl+d). Подключаемся к П2 (addr П2-name), при этом SoftIce установит контекст адресов, соответствующий процессу П2. Устанавливаем точку останова по узнанному адресу (bpx address). Закрываем SoftIce(ctrl+d). Выполняем П1. При этом он создает поток в П2. Когда этот поток начинает исполняться, на первой инструкции внедренного кода выскакивает окно SoftIce.

Отладка функций, выполняющихся при старте DLL

В примере это функция InterceptFunctions библиотеки intercpt.dll, которая вызывается из DllMain при присоединении библиотеки к процессу и выполняет перехват функций.

Для начала необходимо откомпилировать эту библиотеку с отладочной информацией, которую в дальнейшем SoftIce будет использовать для вывода инструкций на языке С. В MS Visual C это делается так: Project->Settings->C/C++ список Debug Info – там необходимо выбрать тип символьной информации – Program database for Edit and Continue, а так же Project->Settings->Link список Category -> debug, установить галочку в поле Debug info и выбрать формат отладочной информации, например Microsoft Format.

Альтернатива – можно просто установить тип конфигурации проекта, при этом все параметры для получения отладочной информации будут установлены автоматически. Это делается так : Build->Set Active Configuration -> Win32 Debug.

Теперь можно приступать к использованию SoftIce. Для начала нужно загрузить в отладчик символьную информацию из данной Dll, при этом сама dll еще загружена не будет. Символьная информация понадобится впоследствии, для установки точек останова и представления кодов на языке высокого уровня. Это делается при помощи утилиты Symbol Loader из комплекта SoftIce.

Вначале необходимо открыть модуль dll (пункт File->Open Module).

Затем необходимо загрузить его в отладчик (Module -> Load). При успешном выполнении всех этих операций на экране Symbol Loader должно быть что-то вроде этого:


Рисунок 1

Теперь приступим к главному. Необходимо поставить точку останова на функцию InterceptFunctions из dll, при этом сама Dll пока еще не присоединена к процессу! Запускаем SoftIce. Создаем точку останова по символьному имени:

bpx InterceptFunctions, (InterceptFunctions – символьное имя функции из таблицы символов. Чтобы просмотреть всю таблицу, можно воспользоваться командой sym). Теперь при помощи написанной ранее программы внедряем эту dll в указанный процесс. Должно произойти следующее: Dll присоединяется к процессу, выполняется DllMain, которая вызывает IntercptFunctions и в этот момент должен произойти останов и вылезти окно отладчика. При этом весь код из dll будет представлен на языке высокого уровня.

Отладка функций – двойников, получающих управление при вызове перехваченных API функций

Для начала необходимо загрузить символьную информацию о Dll перехвата.

В данном примере это intercpt.dll.

В утилите Symbol Loader выбираем File->Open Module, затем, в меню Module->Load, загружаем символьную информацию в отладчик. Dll пока еще не присоединена ни к какому процессу.

Далее, зная имена функций-двойников ставим точку останова на имя функции. Например, функция-двойник Intercept_MessageBoxA, которая будет вызываться всякий раз, когда произойдет вызов функции MessageBoxA из программы. Поставим точку останова на нее – в окне отладчика набираем: bpx Intercept_MessageBoxA.

Теперь можно присоединить intercpt.dll к какому-либо процессу.

Когда этот процесс вызовет перехваченную функцию MessageboxA, управление будет передано на функцию Intercpt_MessageBoxA и сработает точка останова.

Тестирование

Чтобы опробовать все вышесказанное в деле, сначала подыщите на своем компьютере какое-либо приложение, имеющее в своем составе окна сообщений типа MessageBox. Подопытное приложение написано нами самими, оно называется MESS.EXE и выводит друг за другом три окна сообщений, коллаж из которых показан на рисунке:


Рисунок 2

Затем, откомпилируйте примеры внедряемых DLL, описанных выше. Результат компиляции мы назвали у себя METOD1.DLL и METOD2.DLL.

Откомпилируйте пример процедуры внедрения этих DLL в код внешнего процесса. Для работоспособности этой процедуры к ней нужно добавить код главного модуля программы, нечто вроде:

int main(int argc, char* argv[])
{
  if(argc<3) 
  {
    printf("Parameters: PID , Dllname"); 
    getch();
    return 0;
  }
  InjectDll(atol(argv[1]), argv[2]);
  return 0;
}

При запуске этой программы (назовем ее ATTACH .EXE) в качестве параметров надо будет указать идентификатор процесса, в который мы внедряем свой код, и имя DLL, которую следует прицепить к внешнему процессу.

Скопируйте все три полученных модуля METOD1.DLL, METOD2.DLL, ATTACH .EXE в один каталог (например, C:\TEST\). Теперь можно приступать к тестированию.

Запустите программу-жертву (в нашем случае это MESS.EXE). Откройте Диспетчер задач, найдите в нем запущенный процесс (mess.exe):


Рисунок 3

и определите его PID (в нашем случае PID mess.exe равен 1076).

Теперь из командной строки запустите программу внедрения кода первой DLL:

АТТАСН.EXE 1076 C:\TEST\ METOD1.DLL

В результате при попытке вызвать окно MessageBox в программе MESS.EXE вы будете получать одно и то же изображение:


Рисунок 4

Перехват функции API произошел!

Заключение

“Не так страшен черт, как программы MicroSoft…” Тем не менее, если читатель вдумчиво пропустил через себя изложенный материал, то увидел, что, как обычно, все гениальное – просто. И даже такая вещь, как перехват API в Windows NT, не требует сверхсложного программного кода и может быть реализована по первому желанию.


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь
    Сообщений 33    Оценка 221 [+1/-0]         Оценить