» Перехват API функций. Основы Borland Delphi. Win Api. windows. Блог программистов


Блог программистов






200920 Янв

Перехват API функций. Основы

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

   В этой статье я расскажу наиболее действенную методику перехвата API функций — это сплайсинг. Сплайсинг это подмена кода функции. Конечно, есть другой метод перехвата это редактирование таблицы импорта приложения, редактирование таблицы экспорта. Рассказывать буду по порядку.
   Когда вы пишете в своём приложении так

Function Func1(param:type):restype;stdcall;external ‘libname.dll’;

Вы импортируете функцию статически. Адрес функции прописывается в таблице импорта вашего приложения (допустим, что адрес нашей функции $7BC56010).

адрес	    значение
……
$00405F56  7BC56010
……

А при вызове функции происходит так

Push …
push …
call dword ptr [$00405F56]

Следовательно, для перехвата функции нам надо только подменить значение по адресу $00405F56 на своё, а для вызова оригинальной функции получать адрес функции через GetProcAddress. Но приложение может также получить адрес функции через GetProcAddress и вызывать перехватываемую функцию минуя, перехватчик. Данный метод бесперспективен.

   Идём дальше. Что такое сплайсинг? Наша функция находится по адресу $7C80B529 и допустим, что там такой код

7C80B529      8BFF             mov     edi, edi                     
7C80B52B      55               push    ebp
7C80B52C      8BEC             mov     ebp, esp
7C80B52E      837D 08 00       cmp     dword ptr ss:[ebp+8], 0

Для перехвата функции от нас требуется только переписать начальный код функции, так чтобы он передавал управление нашему обработчику. Для передачи управления нашему обработчику достаточно всего лишь одной инструкции jmp на абсолютный адрес, на адрес нашего обработчика . Эта инструкция займёт всего лишь 5 байт – сам опкод этой инструкции ($E9) и значение для прыжка. Это значение вычисляется так

 v=0-(s-d)
s - Смещение следующей команды
d - Требуемый адрес для jmp, т.е. адрес обработчика

Если немного переделать эту формулу, то она будет выглядеть так

v=d-FunctionAddress-5

Теперь при каждом вызове целевой функции, всегда будет передаваться управление нашему обработчику. А как теперь вызвать оригинальную функцию? При установке перехвата нам надо сохранять первые 5 байт функции. Для вызова оригинала надо восстанавливать начало функции и вызывать ее, потом снова устанавливать перехват. Объявим структуру в которой будем сохранять первые 5 байт функции:

PFunctionRestoreData = ^ TFunctionRestoreData;
TFunctionRestoreData = packed record
  Address:Pointer;
  val1:Byte;
  val2:DWORD;
 end;

Поле Address фактически в этой структуре не нужен (он просто не к чему), поле нужно только для того чтобы было удобнее снимать перехват. Назовём эту структуру «мост» к старой функции.

   Теперь напишем функцию, которая будет устанавливать перехват:

function SetCodeHook(ProcAddress, NewProcAddress: pointer; RestoreDATA:PFunctionRestoreData):boolean;
var
  OldProtect, JMPValue:DWORD;
begin
  Result:=False;
  if not VirtualProtect(ProcAddress,5,PAGE_EXECUTE_READWRITE,OldProtect) then exit;
  JMPValue := DWORD (NewProcAddress) - DWORD (ProcAddress) - 5;
  RestoreDATA^.val1:= Byte(ProcAddress^);
  RestoreDATA^.val2:= DWORD(Pointer(DWORD(ProcAddress)+1)^);
  RestoreDATA^.Address:=ProcAddress;
  byte(ProcAddress^):=$E9;
  DWORD(Pointer(DWORD(ProcAddress)+1)^):=JMPValue;
  Result:=VirtualProtect(ProcAddress,5,OldProtect,OldProtect);
end;

Мы сначала устанавливает атрибуты доступа к коду функции, так чтобы можно было его переписывать. Потом вычисляем значение для прыжка. Сначала сохраняем начало функции в запись, потом переписываем начало функции. В конце устанавливаем старые атрибуты доступа.
   Теперь напишем функцию, которая будет снимать перехват:

function UnHookCodeHook(RestoreDATA:PFunctionRestoreData):Boolean;
var
  ProcAddress:Pointer;
  OldProtect,JMPValue:DWORD;
begin
  Result:=False;
  ProcAddress:=RestoreDATA^.Address;
  if not VirtualProtect(ProcAddress,5,PAGE_EXECUTE_READWRITE,OldProtect) then exit;
  Byte(ProcAddress^):=RestoreDATA^.val1;
  DWORD(Pointer(DWORD(ProcAddress)+1)^):=RestoreDATA^.val2;
  Result:=VirtualProtect(ProcAddress,5,OldProtect,OldProtect);
end;

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

   Теперь напишем функцию, которая будет устанавливать перехват по имени функции.

function SetProcedureHook(ModuleHandle: HMODULE; ProcedureName : PChar; NewProcedureAddress :Pointer;   RestoreDATA : PFunctionRestoreData) :Boolean;
var
  ProcAddress:Pointer;
begin
  ProcAddress:=GetProcAddress(ModuleHandle,ProcedureName);
  Result:=SetCodeHook(ProcAddress,NewProcedureAddress,RestoreDATA);
end;

   Едем далее. Описанные выше функции могут перехватывать функции только в текущем процессе. А как нам перехватывать функции в других процессах? Наиболее простой метод это засунуть перехватчик функции в DLL и в коде библиотечной функции устанавливать перехват, если DLL загружается в процесс и снимать перехват, если она выгружается. Тут ещё одна проблема: как заставить другой процесс загрузить нашу DLL. Наиболее простое решение это создание удалённых потоков. Теперь всё по порядку.
   Удалённый поток создаётся функцией CreateRemoteThread.

HANDLE CreateRemoteThread(
    HANDLE hProcess,	// хендл процесса в котором создаётся поток  
    LPSECURITY_ATTRIBUTES lpThreadAttributes,//атрибуты безопасности
    DWORD dwStackSize,	//размер стека
    LPTHREAD_START_ROUTINE lpStartAddress,//адрес функции потока 
    LPVOID lpParameter,	// параметр для функции  
    DWORD dwCreationFlags,	// флаги создания 
    LPDWORD lpThreadId 	// указатель на переменную, в которой будет сохранён ID потока
   );

Функция потока должна иметь следующие атрибуты

DWORD WINAPI ThreadFunc(PVOID pvPararn)

У функции только один параметр –это обычный указатель. Теперь проанализируем ситуацию… Думаем…Шевелим мозгами…Функция LoadLibraryA имеет такие же атрибуты. Она принимает указатель на первый символ имени файла DLL (строка должна кончаться символом #0). Следовательно, функция LoadLibraryA полностью подходит для того, что бы она могла выступать в качестве функции потока. Так как она принимает указатель на строку в своём процессе, нам надо будет записать в память чужого процесса нашу строку и именем файла DLL. Это делается функцией WriteProcessMemory. Вот её описание

BOOL WriteProcessMemory(
    HANDLE hProcess, // хендл процесса   
    LPVOID lpBaseAddress,// адрес по которому надо писать  
    LPVOID lpBuffer,	// указатель на буфер
    DWORD nSize,	// количество байт для записи
    LPDWORD lpNumberOfBytesWritten //количество реально записанных байт 
   );

Адрес функции LoadLibraryA мы получаем с помощью функции GetProcAddress так библиотеки kernel32.dll и ntdll.dll грузятся во все процессы всегда по одним и тем же адресам, следовательно, адрес, полученный в адресном пространстве нашего процесса, будет действителен и адресном пространстве любого другого процесса. Теперь напишем функцию, которая загружает вашу DLL в адресное пространство чужого процесса.

function LoadLibrary_Ex(ProcessID:DWORD;LibName:PChar):boolean;
var
  pLL,pDLLPath:Pointer;
  hProcess,hThr:THandle;
  LibPathLen,_WR,ThrID:DWORD;
begin
  Result:=False;
  LibPathLen:=Length(string(LibName));
  hProcess:=OpenProcess(PROCESS_ALL_ACCESS,false,ProcessID);
  if hProcess=0 then exit;
  pDLLPath:=VirtualAllocEx(hProcess,0,LibPathLen+1,MEM_COMMIT,PAGE_READWRITE);
  if DWORD(pDLLPath)=0 then exit;
  pLL:=GetProcAddress(GetModuleHandle(kernel32),'LoadLibraryA');
  WriteProcessMemory(hProcess,pDLLPath,LibName,LibPathLen+1,_WR);
  hThr:=CreateRemoteThread(hProcess,0,0,pLL,pDLLPath,0,ThrID);
  if hThr=0 then exit;
  Result:=CloseHandle(hProcess);
end;

Таким образом, мы загрузили свою DLL в чужой процесс. Вообще, внедрение своего кода в чужие процессы это совсем другая история и требует написания отдельной статьи. Вышеприведённый пример это самый простой способ внедриться в чужой процесс. Обычному процессу не разрешается изменение памяти системных процессов, таких как winlogon.exe, lsass.exe, smss.exe, csrss.exe и др. для этого нужна привилегия SeDebugPrivilege. В приложенных к статье исходниках есть функция EnableDebugPrivilege, которая включает эту привилегию для текущего процесса.

   Идём дальше. Теперь у нас мы научились загружать свою DLL в чужой процесс. Но для должного эффекта нам надо перехватывать DLL во всех процессах системы. Но как это сделать. Можно просто перечислять процессы через ToolHelp32 и загружать свою DLL в каждый найденный процесс. Но не приемлемо, так как во вновь созданных процессах функции не будут перехвачены. Но можно каждую секунду перечислять процессы, короче это тоже неприемлемо и очень долгая история. Самый простой метод это воспользоваться тем что предоставляет нам механизм хуков. Когда мы ставим какой-либо глобальный хук с помощью функции SetWindowsHookEx то DLL, в которой находится функция обработчик хука, загружается во все процессы, которые получают сообщения от системы через функции GetMessage и PeekMessage. Каркас DLL с перехватом функций будет выглядеть так примерно так

library HideDLL;

uses
  Windows,
  ExtendedAPIFunctions,
  apihooktools;

{$R *.res}
const
  MutexName='__API_HOOK';


function MsgProc(code:DWORD;wParam,lparam:DWORD):DWORD;stdcall;
begin
  Result:=CallNextHookEx(SH,code,wParam,lparam);
end;

procedure SetWindowsHook(e:Boolean); stdcall;
var
  M:THandle;
begin
  if e then
   begin
    M:=CreateMutex(0,false,MutexName);
    if GetLastErrorERROR_ALREADY_EXISTS then
     begin
     SH:=SetWindowsHookEx(WH_GETMESSAGE,@MsgProc,HInstance,0);
     MutexHandle:=M;
    end                                   else
    CloseHandle(M);
   end
       else
   begin
    UnhookWindowsHookEx(SH);
    CloseHandle(MutexHandle);
   end;
end;
      
procedure DLLEntryPoint(dwReason:DWord);
begin
  case dwReason of
    DLL_PROCESS_ATTACH:
     begin
     // StopProcess(GetCurrentProcessId);
      SetWindowsHook(true);
      SetProcedureHook(GetModuleHandle('ntdll.dll'),'ZwQuerySystemInformation',@NewSystemFunction,@SystemFunctionBridge);
     // ResumeProcess(GetCurrentProcessId);
     end;
    DLL_PROCESS_DETACH:
     begin
     // StopProcess(GetCurrentProcessId);
      UnHookCodeHook(@SystemFunctionBridge);
      SetWindowsHook(false);
      //ResumeProcess(GetCurrentProcessId);
     end;
   end;
end;

begin
   DllProc:= @DLLEntryPoint; 
   DLLEntryPoint(DLL_PROCESS_ATTACH);
end.

Чтобы установить перехват на API функции во всех процессах (во всех GUI процессах) достаточно просто загрузить нашу DLL. Достаточно написать вот такой код:

LoadLibrary(pchar(ExtractFileDir(Application.ExeName)+'\'+'DLL.dll'));

   Также следует помнить одну меру предосторожности при установке и снятии перехвата на функции: надо остановить все остальные потоки текущего процесса, так как во время установки перехвата другой поток может вызывать искомую функцию и это приведёт к непредсказуемым последствиям.
   В архиве с исходниками есть заголовочный файл для Delphi (apihooktools.pas), в котором описаны все функции, которые я сегодня использовал и описывал.
   Вот, пожалуй, и всё на сегодня. В архиве так же есть пример скрытия процесса lsass.exe.

Скачать исходник

Комментарии

  1. Anton
    20 марта, 2009 | 10:55

    Пример отличный, но только ExtendedAPIFunctions, apihooktools — их нет в исходниках, если можно их выложить, то было бы очень хорошо!

  2. rpy3uH
    23 марта, 2009 | 12:07

    Архив обновлён. Теперь всё есть

  3. 7ser7ega7
    27 марта, 2009 | 19:29

    обалденно… благодарствую. И расписанно всё попонятнее чем на делфисосрс, а то там в основном голый листинг 🙁

  4. reqyz
    28 февраля, 2010 | 12:23

    с помощью этих функций можно перехватывать все функции процесса для просмотра или изменения, и если да, то как?

  5. rpy3uH
    28 февраля, 2010 | 17:24

    надо наложить перехват на все функции в целевом процессе

  6. Олег
    10 мая, 2010 | 19:12

    скажите, можно отправить вопрос на личный контакт? есть вопрос по перехвату API одной программы (не Windows).

  7. supermaxus
    26 декабря, 2011 | 00:10

    Статья хорошая и познавательная в большей части, но есть один минус. И обойти его, ИМХО, указанными методами будет сложновато. Дело в том, что после того, как восстановлена оригинальная функция для оригинального вызова и совершен оригинальный вызов, до его окончания код-хук остается нерабочим. В то же время, ОС вполне способна переключить поток, и другой поток дернет эту же функцию, код-хука на которой уже не будет(такое сплошь и рядом при вызове COM функций, Sleep и всех синхронизирующих механизмов, например). Конечно, впоследствии код-хук восстановится, но если применить данный метод для защиты чего-то, то защита будет дырявой.

  8. Виталий
    19 марта, 2012 | 18:47

    Спасибо за статью, подскажите чайнику, если скомпилировать компилятором win64, будет работать для 64 разрядных процессов? Если нет, то подскажите, что нужно подправить?

  9. rpy3uH
    20 апреля, 2012 | 14:52

    не будет работать. менять там надо слишком много, там проще всё заново переписать

  10. Виталий
    2 октября, 2012 | 14:44

    Здравствуйте!
    Пытаюсь портировать Ваш код под FreePascal — с обычными хуками все вроде норм, но с Вашей задачкой не могу справиться, при запуске DLL крашится вся система Windows XP SP3, возможно ли все это сделать используя только Api функции без манипуляций с памятью на свой страх и риск?

  11. molotovartem fucking-dog gmail com?
    7 июля, 2014 | 04:56

    Только что запустил этот inject dll код под х64 bit с 64 bit dll. Все прекрасно работает. Rad studio XE5

  12. molotovartem fucking-dog gmail com?
    7 июля, 2014 | 05:00

    Ошибочка. Тестил не этот код. Если получиться портировать этот под x64 — отпишусь. По поводу другого кода писать на почту (код работает, но может тупить, не ручаюсь)