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


Содержание

Перехват API функции

Доброго времени суток всем! Уважаемые форумчене помогите решить задачу, я взял функцию MessageBox и перехватил её. Объясните нубу как перехватить любую другую функцию API, с этой я разобрался, а другие не как не выходит реализовать, кому не сложно, подскажите как изменить участок когда внизу так, чтобы осуществлялся перехват функции EditBox или любой другой. Если не затруднит, выложите пожалуйста скрин работы программы перехвата функции EditBox.

27.05.2014, 12:09

Перехват API
Сразу код ) DWORD dwProtect = PAGE_READWRITE; BYTE old; BYTE * fPtr; #pragma.

Перехват API вызовов
Здравствуйте! Пытаюсь перехватить вызов функции CreateFileW и заменить на свою MyCreateFileW. Для.

Перехват API, Рихтер
Всем доброго времени суток! Собственно такой вопросик. я вот читаю сейчас Рихтера и мне интересен.

Перехват API функций
Разработать программу, которая запускает приложение, переданное программе на вход. После запуска.

Перехват API с возвратом значения
Я тут решил разобраться в перехвате API с помощью инжектированной dll. Спустя 2 дня написал.

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

Posted by admin under Исходники, Статьи

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

Когда вы пишете в своём приложении так

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

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

А при вызове функции происходит так
Push …
push …
call dword ptr

Следовательно, для перехвата функции нам надо только подменить значение по адресу $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 фактически в этой структуре не нужен (он просто не к чему), но, тем не менее, так удобнее снимать перехват, потом сами поймете, зачем это поле нужно. Назовём эту структуру “мост” к старой функции.
Теперь напишем функцию, которая будет устанавливать перехват:

<
ProcAddress, . ?? . .
NewProcAddress:pointer; . ?? . .
RestoreDATA:PFunctionRestoreData . ?? . ? . .

Result: boolean .
>

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;

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

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;

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

Теперь напишем функцию, которая будет устанавливать перехват по имени функции. Я думаю что она тоже не слишком сложная.
<
ModuleHandle:HMODULE; Хендл модуля, в котором находится целевая функция
ProcedureName:PChar; Имя процедуры
NewProcedureAddress:Pointer; Указатель на процедуру перехватчик
RestoreDATA:PFunctionRestoreData Указатель на мост к старой функции

Result: Boolean Успешность установки перехвата
>

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)

По ходу наверно WINAPI обозначает stdcall это не важно, что она обозначает, всё равно она должна быть объявлена как stdcall. У функции только один параметр -это обычный указатель. Теперь проанализируем ситуацию… Думаем…Шевелим мозгами…Функция 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 в исходник модуля для перехвата API функций есть функция EnableDebugPrivilege которая включает эту привилегию для текущего процесса.
Идём дальше. Теперь у нас мы научились загружать свою DLL в чужой процесс. Но для должного эффекта нам надо перехватывать DLL во всех процессах системы. Но как это сделать. Можно просто перечислять процессы через ToolHelp32 и загружать свою DLL в каждый найденный процесс. Но не приемлемо, так как во вновь созданных процессах функции не будут перехвачены. Но можно каждую секунду перечислять процессы, короче это тоже неприемлемо и очень долгая история. Самый простой метод это воспользоваться техникой так называемых хуков. Когда мы ставим глобальный хук на сообщения окон с помощью функции SetWindowsHookEx этой функции мы передаёт адрес функции обработчика и хендл DLL в которой находится функция обработчик. После того как какое-либо приложение хочет отправить какому-нибудь окну, то проверяется, установлена ли ловушка на сообщение, которое он хочет послать окну. Если да то загружается DLL, в которой находится обработчик сообщений, вызывается обработчик и т.д. Следовательно, после установки хука на сообщения наша DLL будет находиться во всех процессах, которые хотя бы один раз отправили сообщения окнам, т.е. почти во всех GUI процессах. Каркас DLL с перехватом функций будет выглядеть так примерно так

const
MutexName=’__API_HOOK’;
……………
//обработчик сообщений
function MsgProc(code:DWORD;wParam,lparam:DWORD):DWORD;stdcall;
begin
CallNextHookEx(SH,code,wParam,lparam);
end;

//внедрение нашей DLL во все GUI процессы
procedure SetWindowsHook(e:Boolean); stdcall;
var
M:THandle;
begin
if e then
begin
m:=CreateMutex(0,false,MutexName);
if m=0 then exit;
SH:=SetWindowsHookEx(WH_GETMESSAGE,@MsgProc,HInstance,0);
end
else
UnhookWindowsHookEx(sh);
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);
SetWindowsHook(false);
UnHookCodeHook(@SystemFunctionBridge);
//ResumeProcess(GetCurrentProcessId);
end;
end;
end;

begin
DllProc:= @DLLEntryPoint; //устанавливаеем библиотечный обработчик
DLLEntryPoint(DLL_PROCESS_ATTACH);
end.

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

Тема: Перехват функций, часть 1: основы

Опции темы
Поиск по теме

Перехват функций, часть 1: основы

Оглавление:

  • Часть 1: основы
  • Часть 2: практика — пишем античит на HP

Внимание! Эта тема закрыта от комментариев. Если у вас есть вопросы по поводу перехватов — внимательно прочтите вторую часть урока, и если что-то останется для вас непонятным, задавайте вопросы в той теме.

Сегодня я расскажу вам о перехвате функций в Pawn.

Для начала разберёмся, что такое перехваты и с чем их едят.
Перехваты в SA:MP обычно используется для того, чтобы изменить поведение каких-либо функций.
Классическим примером может послужить написание античита на HP.
Предположим, у нас есть глобальный массив, в котором мы будем записывать HP игроков после его выдачи с помощью SetPlayerHealth.
Без перехватов придётся выискивать в моде все вызовы SetPlayerHealth и под каждым из них вручную записывать выдаваемое количество HP в ячейку массива.

В то же время, используя технику перехвата, достаточно всего лишь написать 1 перехватчик, в котором происходит та же запись в массив, и поместить его в самое начало мода, перед подключением a_samp.inc. В дальнейшем перехваты вместе с остальными переменными и функциями античита можно вынести в отдельный инклуд (например, «ac_hp.inc»), который при необходимости можно будет отключить, всего лишь закомментировав строку #include «ac_hp.inc».
Таким образом, техника перехватов даёт возможность «прицепить» свой код к стандартным функциям и коллбэкам без вмешательства в сам мод, в разы упростив дальнейшую разработку.

В этом уроке будет описан только один метод, который был открыт благодаря ipsBruno и Y_Less.
Вся суть данного метода состоит в том, что макропроцессор обрабатывает выражения «#if defined X» в 2 прохода, поэтому если «X» — название ещё не известной компилятору функции/переменной/константы, во время 1-го прохода макропроцессор пропустит такой макрос и сначала соберёт информацию о функциях, переменных и константах в скрипте, завершив 1-й проход.
После этого на 2-м проходе компилятор уже будет знать обо всех переменных, функциях и константах, расположенных не только перед строкой с «#if defined», но и после неё. В результате перехватчик подменит собой перехватываемую функцию, а затем вызовет её, если она существует. Если же перехватываемой функции не существует, будет вызван только перехватчик.

Преимущества такого способа перед другими методами:

  1. Не требуется вызывать CallLocalFunction, благодаря чему не происходит увеличения нагрузки из-за использования нативных функций.
  2. Не используется модификация кода во время его выполнения (из-за таких модификаций могла возникнуть несовместимость кода с JIT).

Рассматриваемый нами метод перехвата работает примерно так:

В Pawn существуют 2 способа реализации этого метода: один для перехвата коллбэков (функций, автоматически вызываемых сервером; пример: OnPlayerConnect) и один для нативных функций (функций, реализованных в самом сервере; пример: printf, SendClientMessage).

Перехват коллбэков реализуется следующим образом:

// CallbackFunc — название перехватываемой функции
// param1, param2 — параметры перехватываемой функции
public CallbackFunc ( param1 , param2 )
<
// здесь место для Вашего кода, ради которого и осуществляется перехват
// .

// если перехватываемая функция существует — нужно вызвать и её,
// после чего вернуть значение, которая вернёт перехватываемая функция
// (LibName — название Вашей библиотеки (в случае с Pawn — инклуда),
// в котором перехватывается функция CallbackFunc)
#if defined LibName__CallbackFunc
return Libname__CallbackFunc ( param1 , param2 );
#endif
>

// если объявлен макрос с префиксом «_ALS_» и именем функции после него —
// это значит, что такая функция уже была где-то перехвачена

// если функция CallbackFunc уже была перехвачена — уберём макрос, сделанный
// в предыдущем перехвате (назначение этого макроса будет объяснено далее)
#if defined _ALS_CallbackFunc
#undef CallbackFunc

// если же функция ещё не была перехвачена,
// дадим знать о её перехвате, объявив макрос «_ALS_CallbackFunc»
#else
#define _ALS_CallbackFunc
#endif

// сделаем макрос «CallbackFunc», чтобы заменить название перехватываемой
// функции CallbackFunc на Libname__CallbackFunc во время её объявления в основном скрипте
#define CallbackFunc LibName__CallbackFunc

// отныне перехватываемая функция будет называться не «CallbackFunc», а «LibName__CallbackFunc»,
// а название «CallbackFunc» достанется функции-перехватчику;
// остаётся лишь сделать для перехваченной функции опережающее объявление
// с её новым названием, чтобы избежать лишних предупреждений от компилятора
#if defined LibName__CallbackFunc
forward LibName__CallbackFunc ( param1 , param2 );
#endif

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

// функция-перехватчик
stock LibName__NativeFunc ( param1 , param2 )
<
// место для кода перехвата
// .

// вызов перехватываемой функции, на этот раз без проверки существования функции
return NativeFunc ( param1 , param2 );
>
#if defined _ALS_NativeFunc
#undef NativeFunc
#else
#define _ALS_NativeFunc
#endif
#define NativeFunc LibName__NativeFunc

Теперь разберёмся, как всё это выглядит в деле.
Методы для коллбэков и нативных функций разные, поэтому рассмотрены они будут отдельно.

    Перехват коллбэков на примере OnDialogResponse:

// массив, в котором записаны ID диалогов ,показанных другим игрокам
static player_dialog_ids [ MAX_PLAYERS ] = <- 1 , .>;

// перехватчик
public OnDialogResponse ( playerid , dialogid , response , listitem , inputtext [])
<
// если игрок посылает сигнал об ответе на диалог,
// который ему не показывали — это признак использования читов
// (и да, я знаю, что сейчас этой проблемы в SA:MP нет, это всего лишь пример!)
if( player_dialog_ids [ playerid ] != dialogid )
Kiсk ( playerid );
player_dialog_ids [ playerid ] = — 1 ;
#if defined spd__OnDialogResponse
return spd__OnDialogResponse ( playerid , dialogid , response , listitem , inputtext );
#endif
>
#if defined _ALS_OnDialogResponse
#undef OnDialogResponse
#else
#define _ALS_OnDialogResponse
#endif
#define OnDialogResponse spd__OnDialogResponse
#if defined spd__OnDialogResponse
forward spd__OnDialogResponse ( playerid , dialogid , response , listitem , inputtext []);
#endif

static player_dialog_ids [ MAX_PLAYERS ] = <- 1 , .>;

// перехватчик
stock spd__ShowPlayerDialog ( playerid , dialogid , type , header [], text [], button1 [], button2 [])
<
player_dialog_ids [ playerid ] = dialogid ;
return ShowPlayerDialog ( playerid , dialogid , type , header , text , button1 , button2 );
>
#if defined _ALS_ShowPlayerDialog
#undef ShowPlayerDialog
#else
#define _ALS_ShowPlayerDialog
#endif
#define ShowPlayerDialog spd__ShowPlayerDialog

На сегодня всё, удачного скриптинга!

Для тех, кто любит стрелять себе в ногу:


    Если вы только начинаете практиковаться с перехватами, сверяйте свой код с примерами из этого урока.

Не выдумывайте своих префиксов вместо «_ALS_».
Директива «#if defined _ALS_ » используется специально, чтобы ваш перехват правильно работал с другими перехватами той же функции.
Если вместо «_ALS_» использовать другой префикс, потеряется совместимость с другими перехватчиками.
Пример того, как не нужно делать:

Так вот, этот метод в корне неправильный.

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

// 2-й перехватчик для AddStaticVehicle
stock my2_AddStaticVehicle ( modelid , Float : spawn_x , Float : spawn_y , Float : spawn_z , Float : angle , color1 , color2 )
<
print( ‘Функция AddStaticVehicle перехвачена ещё раз’ );
return AddStaticVehicle ( modelid , spawn_x , spawn_y , spawn_z , angle , color1 , color2 );
>
#define AddStaticVehicle my2_AddStaticVehicle

// 1-й перехватчик для AddStaticVehicle
// Здесь будет «warning 201: redefinition of constant/macro (symbol «AddStaticVehicle»)».
stock my1_AddStaticVehicle ( modelid , Float : spawn_x , Float : spawn_y , Float : spawn_z , Float : angle , color1 , color2 )
<
print( ‘Функция AddStaticVehicle перехвачена’ );
return AddStaticVehicle ( modelid , spawn_x , spawn_y , spawn_z , angle , color1 , color2 );
>
#define AddStaticVehicle my1_AddStaticVehicle

Получилось скомпилировать код без варнингов и ошибок? Я предупреждал.

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

P.S.: Скоро будет готова 2-я часть урока, в которой будет рассматриваться использование перехватов на практике.
Часть 2.

Копирование данной статьи на других ресурсах без разрешения автора запрещено!

JA Mero Awesome Metro Joomla! Template

  • Печать
  • E-mail

В данном посте представлены способы перехвата API для системы команд x86.

Краткий обзор

В последующих параграфах мы введем читателя в перехват API: то, что мы можем сделать с его помощью, то, почему он полезен, и в его основные формы. После представления простых методов перехвата API мы охватим некоторые менее известные и используемые (если вообще используемые) методы, которые однажды могут пригодиться, а также некоторые техники, которые стоит иметь ввиду при использовании любых методов перехвата.

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

Введение

В программировании термин перехват (hooking) охватывает ряд методов, используемых для изменения или дополнения поведения операционной системы, приложений или других программных компонентов путем перехвата вызовов функций, сообщений или событий, передаваемых между программными компонентами. Код, который обрабатывает подобные перехваченные вызовы функций, события или сообщения, называется перехватчиком (hook).

Итак, мы установили, что перехват позволяет нам изменять поведение существующих программ. Перед тем, как двинуться дальше, мы представим краткий список примеров использования перехвата.

  • Профилирование: Насколько быстро выполняются определенные вызовы функций?
  • Мониторинг: Послали ли мы корректные параметры функции X?
  • .

Более полный список примеров использования перехвата функций можно найти здесь [1] [2] .

Данный список должен свидетельствовать о полезности перехвата функций. Теперь настало время перейти к перехвату функций как таковому.

Просим читателя заметить, что данный пост не охватывает точки останова, перехват IAT и т. д., так как эти темы заслуживают отдельного поста.

Базовый перехват API

Итак, если у нас есть две функции – A и B, как мы перенаправим выполнение с функции A в функцию B? Очевидно, что мы используем инструкцию перехода, поэтому осталось лишь вычислить правильное относительное смещение.

Предположим, что функция A расположена по адресу 0-401000, а функция B – по адресу 0x401800. Далее мы определим требуемое относительное смещение. Разница между адресами данных функций составляет 0x800 байт, и мы хотим перейти из функции A в функцию B, так что нам пока не нужно беспокоиться об отрицательных смещениях.

Дальше следует хитрый момент. Предположим, что мы уже записали нашу инструкцию перехода по адресу 0x401000 (функции A), и что данная инструкция выполняется. CPU при этом сделает следующее: сначала он добавит длину инструкции к Указателю на Инструкцию [3] (или Программному Счетчику). Длина инструкции перехода равна пяти байтам, как мы установили ранее. После этого к Указателю на Инструкцию добавляется относительное смещение. Другими словами, CPU вычисляет новое значение Указателя на Инструкцию следующим образом:

instruction_pointer = instruction_pointer + 5 + relative_offset-

Поэтому для вычисления относительного смещения нам нужно переписать формулу в следующем виде:

relative_offset = function_B — function_A — 5-

Мы вычитаем 5, поскольку это длина инструкции перехода, которую CPU добавляет при запуске данной инструкции, а function_A is вычитается из function_B, так как это относительный переход. Разница между адресами функций равна, как мы помним, 0x800 байтам. (Например, если мы забудем вычесть function_A, то CPU перейдет по адресу 0x401800 + 0x401000 + 5, что, очевидно, нежелательно).

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

До внедрения перехватчика в начале функции можно видеть несколько исходных инструкций. После внедрения они перезаписываются инструкцией jmp. Первые три исходные инструкции занимают 6 байт вместе взятые (т. е. можно видеть, что инструкция push ebx расположена по адресу 0x401006). Наша инструкция перехода использует только пять байтов, что оставляет нам один дополнительный байт. Мы перезаписали этот байт инструкцией nop (инструкция, не делающая ничего).

Инструкции, которые мы перезаписали, мы называем украденными байтами, в следующем параграфе мы рассмотрим их более подробно.

Трамплины

Следующий фрагмент кода показывает простой пример использования трамплина для запуска исходной функции из перехваченной функции. В нем function_A_trampoline обозначает трамплин к функции A (функции, которая была перехвачена).

// this is the hooked function void function_A(int value, int value2)-
// this is the Trampoline with which we can call
// function_A without executing the hook void (*function_A_trampoline)(int value, int value2)-
// this is the hooking function which is executed
// when function_A is called void function_B(int value, int value2) <
// alter arguments and execute the original function function_A_trampoline(value + 1, value2 + 2)- >

В примере перехвата, который мы только что обсудили, мы перезаписали первые пять байт функции A. Чтобы запустить исходную функцию без перехватчика, нам понадобится запустить байты, которые мы перезаписали при установке перехватчика, а затем перейти по адресу, на несколько байт большему, чем адрес функции A (чтобы пропустить код перехватчика). Это в точности то, что происходит в вышеуказанном фрагменте кода, но в коде на языке C вы этого не видите из-за относительно высокой абстракции. В любом случае, картинки говорят больше, чем тысяча слов, так что… вот изображение, которое показывает устройство трамплина.

На изображении вы видите следующий поток выполнения: запускается функция A, выполняется перехватчик, передавая тем самым управление функции B. Функция B выполняет некие действия, но на адресе 0x401820 она хочет выполнить исходную версию функции A (без перехватчика), где и приходит на помощь трамплин. Про трамплин можно написать множество слов, но одно изображение объясняет его полностью. Трамплин состоит из двух частей: исходных инструкций и перехода на ту часть функции A, которая следует за перехватчиком. Если вы вернетесь к изображению из раздела Базовый перехват API, вы увидите, что перезаписанные перехватчиком инструкции теперь располагаются в трамплине. Переход в трамплине вычисляется по указанной ранее формуле, однако, в данном случае адреса и смещения немного отличаются, так что формула принимает следующий вид:

relative_offset = (function_A_trampoline + 6) — (function_A + 6) — 5-

Отметим, что мы переходим с адреса 0x402006 (function_A_trampoline + 6) на 0x401006 (function_A + 6). Данные адреса можно проверить по рассмотренному ранее изображению. Ничего особенного, кроме того факта, что у нас получилось отрицательное смещение. Но это не доставит нам проблем (CPU сделает всю грязную работу по корректному представлению отрицательного относительного смещения).

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

Продвинутые методы перехвата

Обнаружение перехвата для примера, рассмотренного в разделе «Базовый перехват API», можно осуществить так:

Суть в том, что 0xe9 – опкод инструкции безусловного перехода с 32-битным относительным смещением. ПО, которое мы перехватываем, может обнаружить или не обнаружить подобный перехватчик. В любом случае, далее мы обсудим разные методы, которые пытаются обойти подобные алгоритмы обнаружения. (Заметим, что программы вроде GMER [4] обнаруживают все типы методов перехвата, так как сверяют виртуальную память с физическим образом на диске).

Метод I: вставка Nop в начало

По сути, вместо записи по адресу функции A, например, инструкции перехода, мы сначала запишем инструкцию nop (не делающую ничего), за которой уже последует функция перехода. Применяя данную технику, имейте в виду, что инструкция перехода теперь будет располагаться по адресу 0x401001 (function_A + 1), и это изменит относительное смещение на единицу.

Вот изображение, иллюстрирующую данную технику.

Поскольку первая инструкция функции A теперь – nop (а не jmp), чтобы обнаружить перехватчик, нам нужно переписать метод обнаружения подобным образом:

unsigned char *addr = function_A- while (*addr == 0x90) addr++- if(*addr == 0xe9) <
printf(«Hook detected in function A.\n»)- >

По существу, он пропускает любое количество последовательных инструкций nop, имеющих опкод 0x90, и проверяет присутствие инструкции перехода после всей цепочки nop-ов.

Метод II: Push/Retn

Этот метод занимает 6 байт и выглядит следующим образом. Отметим, что инструкция push принимает абсолютный адрес, а не относительный.

Обнаружение данного метода может выглядеть так, как показано ниже. Однако, учтите, что помещение nop-инструкций в начало или между инструкциями push и retn не позволит данному коду обнаружить перехватчик.

Илон Маск рекомендует:  Атрибут marginwidth в HTML

unsigned char *addr = function_A- if(*addr == 0x68 && addr[5] == 0xc3) <
printf(«Hook detected in function A.\n»)- >

Как вы наверное уже догадались, 0x68 – опкод инструкции push, а 0xc3 – опкод инструкции retn.

Метод III: Числа с плавающей запятой

Данный пример похож на метод push/retn: мы помещаем в стек фиктивное значение, которое затем перезаписываем настоящим адресом, после чего выполняем retn.

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


Следующее изображение демонстрирует данную технику. Перехватчик имеет размер 11 байт, что немного больше, чем для предыдущих методов, но это не так страшно. Также заметим, что floating_point – указатель на 64-битное число с плавающей запятой, которое равно адресу нашей перехватывающей функции (функции B).

Получить нужное значение с плавающей точкой довольно просто:

double floating_point_value = (double) function_B-

Функция B здесь, как и в других примерах,– наша перехватывающая функция.

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

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

Метод IV: MMX/SSE

Обе техники используют, как и метод с плавающей запятой, инструкции push/retn. Первый метод использует MMX-инструкции, в частности инструкцию movd. Она, как и инструкция fistp, позволяет прочитать значение из памяти и сохранить значение в стеке. Второй метод с SSE-инструкциями также использует инструкцию movd. Единственное различие между этими двумя методами в том, что MMX-инструкции оперируют 64-битными регистрами, тогда как SSE оперируют 128-битными регистрами. (Хотя в нашем случае это неважно, поскольку инструкция movd позволяет читать и записывать 32-битные значения).

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

Метод V: косвенный переход

Косвенный переход имеет длину 6 байтов, и пример перехвата выглядит так:

Заметим, что hook_func_ptr обозначает адрес, по которому находится адрес нашей перехватывающей функции (т. е. B).

Метод VI: инструкция Call

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

Данная техника работает как для прямого, так и для непрямого варианта инструкции call. Далее следует рисунок, иллюстрирующий рассмотренный метод.

Другие методы

Другие методы I: Hotpatching

Если функция допускает так называемый Hotpatch, она уже определенным образом подготовлена для перехвата. Первой инструкцией функции в этом случае будет mov edi, edi (два байта длиной), а перед собственно функцией расположены 5 nop-инструкций. Это позволяет разместить инструкцию близкого перехода (ту, что имеет длину два байта и принимает 8-битное относительное смещение) по адресу собственно функции (перезаписав инструкцию mov edi, edi) и обычную инструкцию перехода с 32-битным относительным смещением на место nop-инструкций.

Вот и все, что касается данной техники. Отметим, что вместо хотпатчинга подобной функции возможно также выполнить перехват одним из ранее рассмотренных методов, поместив перехватчик по адресу function+2, где два означает размер инструкции, вставленной компилятором для хотпатчинга (В этом случае остается возможность применения Hotpatch, несмотря на установку перехвата одним из наших излюбленных методов).

Вот изображение, иллюстрирующее хотпатчинг. Перехватываемой функцией здесь является MessageBoxA, а перехватывающей – hook_MessageBoxA (т. е. MessageBoxA = функция A, hook_MessageBoxA = функция B).

Другие методы II: Методы для классов C++

Эта техника относится к перехвату методов классов C++. Методы классов C++ используют так называемое соглашение вызова __thiscall [5] (по крайней мере в Windows).

Соглашение вызова __thiscall хранит указатель на информацию об объекте (к которому можно обратиться в методах класса через переменную this) в регистре ecx перед вызовом метода класса. Другими словами, если мы хотим перехватить метод класса, необходимо особое внимание.

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

Так как мы хотим перехватывать функции C++ обычными функциями C, нам нужно, чтобы первым параметром перехватывающей функции был указатель this. Пример перехватывающей функции и трамплин (который мы обсудим позднее) выглядят следующим образом. Отметим, что вместо this используется имя переменной self из-за того, что this – зарезервированное в C++ ключевое слово.

void (*function_A_trampoline)(void *self, int value, int value2, int value3)-
void function_B(void *self, int value, int value2, int value3)

Чтобы иметь возможность перехватывать методы классов C++ из обычных C-функций, нам придется изменить стек, поскольку нам нужно вставить в него указатель this. Следующий пример представляет разметку стека при вызове функции A (слева) и разметку, которую мы хотим иметь при вызове функции B (перехватывающей функции). Отметим, что ecx содержит значение указателя this, а вершина стека – это адрес, по которому расположен return_address.

К счастью для нас, мы можем вставить указатель this всего двумя инструкциями – довольно лаконично. Первый шаг – обмен значений указателя this (регистра ecx) и вершины стека (return_address). После данного обмена на вершине стека окажется указатель this, а в регистре ecx – return_address. Теперь мы можем просто положить значение регистра ecx на стек, и стек станет выглядеть в точности так, как мы хотели (см. изображение).

Вот ассемблерное представление перехвата метода класса C++.

Мы рассмотрели часть, касающуюся перехвата. Однако, нам также нужно адаптировать наш трамплин, поскольку трамплин принимает указатель this в качестве первого аргумента. Стек, который мы имеем и стек, к которому мы хотим прийти, выглядят следующим образом. (Очевидно, что значение this после всех манипуляций должно снова оказаться в регистре ecx).

Мы выполняем действия, в точности обратные тем, которые делали в ходе перехвата функции. Сначала мы выталкиваем из стека return_address, так что стек теперь указывает на this, а регистр ecx принимает значение return_address. Теперь мы обмениваем значение на вершине стека с регистром ecx, после чего стек принимает желаемый вид, а в регистр ecx загружается значение указателя this. Следующее изображение иллюстрирует трамплин, хотя инструкции из функции A на нем не показаны (т. е. данное изображение лишь показывает что особенного в трамплине к методу класса C++, а не то, что мы ранее обсуждали в разделе Трамплины).

Построение трамплинов

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

Как вы можете видеть, третья инструкция использует шестой байт, из-за этого мы не можем просто скопировать первые пять байт, нам придется скопировать инструкцию целиком. Чтобы сделать это мы используем так называемый LDE (Length Disassembler Engine). LDE способен вычислять длину инструкции (обычно путем махинаций с предопределенной таблицей, содержащей информацию о каждом опкоде).

Поскольку LDE может вычислить для нас длину инструкции, мы можем просто продолжать получать длины инструкций, пока не найдем достаточно инструкций для покрытия первых пяти байт. На вышеуказанном рисунке видно, что нам достаточно трех инструкций, чтобы набрать длину в 6 байт, достаточную для трамплина.

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

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

Многослойные перехваты

Предотвращение рекурсии перехвата

В целом, чтобы предотвратить данную проблему, нам нужно поддерживать счетчик перехватов. Всякий раз при запуске перехватчик будет производить проверку счетчика (который, очевидно будет увеличиваться при входе в перехватчик). Таким образом, мы сможем контролировать две вещи. Первая – то, что перехватчик не будет создавать вложенные перехваты (например, если мы перехватили функцию fwrite(), и в перехватывающей функции мы также вызываем fwrite(), перехватчик не отработает повторно). Вторая – максимальная вложенность рекурсии, например, не более трех вложенных перехватов. Впрочем, подобная функциональность обычно не нужна.

Отметим также, что подобный счетчик перехватов должен быть специфичным для потока. Для решения этой задачи, код, приведенный в разделе «Демонстрация», хранит счетчик в сегменте, на который указывает регистр fs [6] (данный раздел обозначает Блок Информации Потока, т. е. специфичную для потока информацию).

Перехват вызовов API-функций на Delphi

Привет, читатель. Наверное ты уже делал какую-либо прогу-заподлянку или троянчик,
но хитрый пользователь всегда замечал в Task Manager’е странный процесс.
=) Так вот, сегодня мы научимся перехватывать вызовы API-функций ОС Windows.

Итак, приступим к делу. Перехватывать мы будет по такому плану:

1) Определяем адрес перехватываемой функции.
2) Находим адрес таблицы импорта в процессе, вызовы которого перехватываем.
3) Перечисляем все структуры IMAGE_THUNK_DATA всех дескрипторов импорта в таблице импорта.
4) Найдя адрес, совпадающий с искомым, заменяем его адресом своей функции.

Для этого мы должны объявить такие структуры и типы:

type
PImageImportByName = ^TImageImportByName; // Описание адреса функции
_IMAGE_IMPORT_BY_NAME=packed record
HInst: Word;
Name: Byte;
end;
TImageImportByName =_IMAGE_IMPORT_BY_NAME;

TThunk=packed record // Описание функции
case Integer of
0: (ForwarderString: PByte);
1: (thFunction: PDWORD);
2: (Ordinal: DWORD);
3: (AddressOfData: PImageImportByName);
end;

PImageThunkData=^TImageThunkData;
_IMAGE_THUNK_DATA=packed record
Thunk: TThunk;
end;
TImageThunkData=_IMAGE_THUNK_DATA;
IMAGE_THUNK_DATA=_IMAGE_THUNK_DATA;

TCharcteristics=record
case Integer of
0: (Characteristics: DWORD);
1: (OriginalFirstThunk: PImageThunkData);
end;

PImageImportDescriptor=^TImageImportDescriptor;
_IMAGE_IMPORT_DESCRIPTOR = packed record // Дескриптор импорта
t: TCharcteristics;
TimeDateStamp: DWord;
ForwarderChain: DWORD;
Name: DWORD;
FirstThunk: PImageThunkData;
end;
TImageImportDescriptor=_IMAGE_IMPORT_DESCRIPTOR;
IMAGE_IMPORT_DESCRIPTOR=_IMAGE_IMPORT_DESCRIPTOR;

Так как для определения адреса таблицы импорта нам необходимо чтобы процесс загрузил нашу библиотеку. Это можно реализовать так:

procedure AttachDllToProcess(pID: integer; LibName: string);
var
ThreadID:Cardinal;
ThreadHndl:THandle;
AllocBuffer:Pointer;
BytesWritten:Cardinal;
ProcAddr:Pointer;
ExitCode:Cardinal;
hProcess: integer;
begin
hProcess:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, false, pID);
if (hProcess=0) then
Exit;

AllocBuffer:=VirtualAllocEx(hProcess, nil, length(LibName)+1, MEM_COMMIT, PAGE_READWRITE);
if (AllocBuffer<>nil) then
WriteProcessMemory(hProcess, AllocBuffer, PChar(LibName), length(LibName)+1, BytesWritten)
else
Exit;

ProcAddr:=GetProcAddress(LoadLibrary(PChar(‘Kernel32.dll’)), PChar(‘LoadLibraryA’));
ThreadHndl:=CreateRemoteThread(hProcess, nil, 0, ProcAddr, AllocBuffer, 0, ThreadID);

WaitForSingleObject(ThreadHndl, INFINITE);
GetExitCodeThread(ThreadHndl, ExitCode);
CloseHandle(ThreadHndl);
VirtualFreeEx(hProcess, AllocBuffer, 0, MEM_RELEASE);
CloseHandle(hProcess);

Следующая процедура реализует загрузку DLL другим процессом. В качестве параметров передаётся уникальный идентификатор процесса, его можно определить с помощью ToolHelp32-функций, и прямое имя загружаемой DLL. Обращаю ваше внимание на то, что имя дллки должно быть прямым, а не относительным, то есть если она лежит в одной папке с exe, то всё равно надо писать не Lib.dll, а Путь\до\папки\с\EXE\Lib.dll.

Как всё это работает. Сначала получаем дескриптор процесса с помощью OpenProcess.

function OpenProcess(
dwDesiredAccess: DWORD; // параметры доступа открытия процесса
bInheritHandle: BOOL;
dwProcessId: DWORD): // pID
THandle; stdcall;

Далее мы выделяем память под параметры к функции LoadLibrary. Также необходимо заметить, что выделяем мы не length(LibName) байт, а length(LibName)+1, это так, потому что в Windows используется тип PChar, который требует #0 в конце строки, а параметр LibName передаётся без него.

function VirtualAllocEx(
hProcess: THandle; // дескриптор процесса
lpAddress: Pointer; // адрес по которому происходит выделение памяти
dwSize, // размер выделяемой памяти
flAllocationType: DWORD; // что именно мы будем делать
flProtect: DWORD): // тип защиты выделенного участка памяти
Pointer; stdcall;

После этого мы может записать параметр в процессорную память.

function WriteProcessMemory(
hProcess: THandle; // дескриптор процесса
const lpBaseAddress: Pointer; // начальный адрес для записи
lpBuffer: Pointer; // буфер, который надо записать
nSize: DWORD; // его размер
var lpNumberOfBytesWritten: DWORD): // сколько записалось
BOOL; stdcall;


Получаем адрес LoadLibraryA в kernel32.dll. Создаём удалённый поток с параметрами указателя на адрес LoadLibrary и адрес по которому располагаются параметры этой функции.

function CreateRemoteThread(
hProcess: THandle; // дескриптор процесса
lpThreadAttributes: Pointer; // атрибуты защиты потока
dwStackSize: DWORD; // какой размер используемого стека
lpStartAddress: Pointer; // указатель на адрес функции создаваемого потока
lpParameter: Pointer; // параметры передаваемые потоку
dwCreationFlags: DWORD; // дополнительные флаги создания потока
var lpThreadId: DWORD): // ID потока
THandle; stdcall;

После присоединения библиотеки закрываем все открытые дескрипторы, освобождаем память.

function VirtualFreeEx(
hProcess: THandle; // дескриптор процесса
lpAddress: Pointer; // начальный адрес освобождаемой памяти
dwSize, // размер освобождаемой памяти
dwFreeType: DWORD): // тип освобождения
Pointer; stdcall;

Теперь мы можем приступить к написанию самого кода перехвата.
Помещаем этот код в DLL.

function InterceptDLLCall(hLocalModule: HModule; c_szDllName, c_szApiName:PChar;
pApiNew, pApiToChange: Pointer; var p_pApiOrg: Pointer): boolean;
var
pDOSHeader: PImageDosHeader;
pNTHeader: PImageNtHeaders;
pImportDesc: PImageImportDescriptor;
dwProtect, dwNewProtect, dwAddressToIntercept: DWORD;
pThunk: PImageThunkData;
bSuccess: Boolean;
BytesWritten: DWORD;
pTemp: Pointer;
instr: WORD;
pto: DWORD;
begin
result:=false;
pDOSHeader:=Windows.PIMAGEDOSHEADER (hLocalModule);
bSuccess:=false;

if (pApiToChange<>nil) then
dwAddressToIntercept:= DWORD(pApiToChange)
else
begin
pTemp:=GetProcAddress(GetModuleHandle(c_szDllName), c_szApiName);
ReadProcessMemory(GetCurrentProcess, pTemp, @instr, 2, BytesWritten);
ReadProcessMemory(GetCurrentProcess, pointer(dword(pTemp)+2), @pto, 4, BytesWritten);
if instr=$25FF then
pTemp:=pointer(pto);
dwAddressToIntercept:= DWORD(pTemp);
end;

if (IsBadReadPtr(Pointer(hLocalModule), sizeof(PImageNtHeaders))) then
exit;
if (pDOSHeader.e_magic<> IMAGE_DOS_SIGNATURE) then
exit;

pNTHeader:=PIMAGENTHEADERS( MakePtr(DWORD(pDOSHeader), pDOSHeader._lfanew));
if (pNTHeader.Signature<> IMAGE_NT_SIGNATURE) then
exit;

pImportDesc:=PImageImportDescriptor( MakePtr(hLocalModule,
pNTHeader.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
if (pImportDesc=PImageImportDescriptor( pNTHeader)) then
exit;

while(pImportDesc.Name>0)do
begin
pThunk:=PImageThunkData(MakePtr( hLocalModule, Dword(pImportDesc.FirstThunk)));
while(pThunk.Thunk.thFunction<>nil) do
begin
if DWord(pThunk.Thunk.thFunction)=dwAddressToIntercept then
begin
if (not IsBadWritePtr(Pointer( @pThunk.Thunk.thFunction), sizeof(DWORD))) then
begin
p_pApiOrg:=Pointer(pThunk.Thunk.thFunction);
pThunk.Thunk.thFunction:=PDWORD(pApiNew);
bSuccess:=true;
end
else
if VirtualProtect(Pointer(@pThunk.Thunk.thFunction), sizeof(DWORD), PAGE_EXECUTE_READWRITE, @dwProtect) then
begin
p_pApiOrg:=Pointer(pThunk.Thunk.thFunction);
pThunk.Thunk.thFunction:=PDWORD(pApiNew);
bSuccess:=true;
dwNewProtect:=dwProtect;
VirtualProtect(Pointer(@pThunk.Thunk.thFunction), sizeof(DWORD), dwNewProtect, @dwProtect);
end;
end;
Inc(PThunk);
end;
Inc(pImportDesc);
end;
result:=bSuccess;
end;

hLocalModule – модуль в котором находиться Import Table
c_szDllName – имя DLL, в которой находиться перехватываемая функция
c_szApiName – имя перехватываемой функции
pApiNew – указатель на нашу функцию, которая будет вызываться вместо перехватываемой.
pApiToChange – указатель на перехватываемую функцию, если равно nil, то адрес функции определяется через c_szDllName и c_szApiName.
p_pApiOrg – указатель на старую перехватываемую функцию
Если pApiToChange не определён, то начинаем своё определение адреса.

Для начала просто через GetProcAddress(), далее читаем сначала первые 2 байта по этому адресу, а потом и следующие 4.

function ReadProcessMemory(
hProcess: THandle; // дескриптор процесса
const lpBaseAddress: Pointer; // адрес по которому начинаем чтение
lpBuffer: Pointer; // куда будем читать
nSize: DWORD; // размер нашего буфера
var lpNumberOfBytesRead: DWORD): // сколько прочитано
BOOL; stdcall;

Если первые 2 байта равны $25FF(код перехода на указатель), то в следующих 4ёх лежит адрес перехода, его мы и записываем вместо полученного GetProcAddress’ом.
Получаем указатель на нужную нам таблицу. Перебираем все структуры Thunk. И если находим адрес равный искомому, то проверяем можем ли мы записать в память по этому адресу, если нет то ставим соответствующий атрибут защиты.

function VirtualProtect(
lpAddress: Pointer; // адрес памяти
dwSize, // размер
flNewProtect: DWORD; // новый атрибут
var OldProtect: DWORD): // старый атрибут
BOOL; stdcall;

И заменяем нашим адресом. Есть ещё одна тонкость, для использования в WinNT-based системах нужно установить привилегии отладчика.

При написании статьи мне помогли следующие материалы:

1) Книга Джеффри Рихтера “Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии
Windows”.

2) Статья “Система перехвата функций API платформы Win32” HI-TECH’а.

Перехват API функций в Delphi с помощью сплайсинга

Written on 26 Января 2009 . Posted in Delphi

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

Когда вы пишете в своём приложении так

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

Идём дальше. Что такое сплайсинг? Наша функция находится по адресу $7C80B529 и допустим, что там такой код
Для перехвата функции от нас требуется только переписать начальный код функции, так чтобы он передавал управление нашему обработчику. Для передачи управления нашему обработчику достаточно всего лишь одной инструкции jmp на абсолютный адрес, на адрес нашего обработчика . Эта инструкция займёт всего лишь 5 байт – сам опкод этой инструкции ($E9) и значение для прыжка. Это значение вычисляется так
s — Смещение следующей команды
d — Требуемый адрес для jmp, т.е. адрес обработчика

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

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

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

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

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

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

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

Удалённый поток создаётся функцией CreateRemoteThread.

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

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

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

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

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

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

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

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

В данном цикле статей используется язык Дельфи ( ахтунг! , детям и слабонервным просьба соблюдать необходимые меры предосторожности ^_^), поэтому мы долго колебались относительно того, стоит ли размещать его на сайте, однако учитывая, что материал может представлять интерес для многих посетителей, мы всё же пошли на это. Тем не менее просьба потенциальным авторам избегать проявлений паскальности в своих будущих статьях ^_^.

Как известно, в процессорах серии X86 и совместимых с ними присутствует аппаратная поддержка многозадачности и защиты. Код может исполняться на одном из четырех уровней (колец) защиты. Наиболее привилегированным является нулевое кольцо, наименее привилегированным — третье. В нулевом кольце можно все: доступны привилегированные команды, порты ввода-вывода, и вся память. В других кольцах могут быть установлены другие правила: запрет некоторых команд, запрет ввода-вывода и.т.д. Между уровнями защиты можно переключаться только через специальные шлюзы, определенные в системных таблицах процессора (GDT, LDT, IDT). Доступ к памяти в защищенном режиме происходит только через селекторы находящиеся в этих таблицах, а у каждого селектора есть уровень привилегий необходимый для его использования. Подобная система позволяет изолировать код, выполняющийся на непривилегированных уровнях защиты, и полностью контролировать его исполнение с помощью кода нулевого кольца.
В Windows NT используються только два уровня привилегий: нулевое и третье кольцо. В нулевом кольце работает ядро системы и системные драйвера, а в третьем — все запущенные приложения. Привилегированные команды и ввод-ввод для третьего кольца запрещены, для взаимодействия с аппаратной частью компьютера вызываются системные сервисы ядра ОС, которые оформлены как шлюзы. При вызове такого шлюза процесс переходит в нулевое кольцо, и там ядро ОС и драйвера обрабатывают запрос и возвращают результаты приложению. После перехода в нулевое кольцо приложение не может как-либо контролировать свое исполнение, пока управление не будет возвращено коду третьего кольца. Это есть необходимое условие защиты, оно обеспечивает безопасность всей системы.

В прошлых статьях я достаточно подробно рассмотрел технику перехвата API в третьем кольце, но недостаток этих методов в том, что такие перехваты подчиняются правилам третьего кольца, тоесть могут быть обнаружены и устранены приложением. Также приложение может вызывать напрямую сервисы ядра системы, минуя API (как это сделано в моей программе ProcessMaster), в этом случае никакой перехват не поможет. Для того чтобы действительно «поработить» систему мы должны залезть на самый высокий уровень привилегий — в нулевое кольцо, и производить свои действия там. Только это гарантирует, что приложение работающее в третьем кольце не сможет обойти перехват. Попав в нулевое кольцо, мы получаем действительно полный контроль над системой. Как сделать это — войти в нулевое кольцо, и что можно делать потом, я подробно опишу в этой статье.

Память в Windows NT

Для понимания дальнейшего материала, необходимо четко представлять организацию памяти в Windows NT. В общем виде она выглядит так: Адреса от 0x00000000 до 0x0000FFFF не используются, любое обращение к этим адресам вызывает ошибку. Это нужно для выявления нулевых указателей, так как обращение по нулевому указателю это частая ошибка в программах. Адреса от 0x00010000 до 0x7FFFFFFF представляют пользовательское адресное пространство (User Space), эта область памяти различна у каждого процесса в системе. В ней находятся код третьего кольца и связанные с ним данные процесса. Адреса от 0x800000000 до 0xFFFFFFFF представляют собой область памяти ядра системы (Kernel Space), эта область одна на всю систему, и у всех процессов одинакова. В ней размещается ядро системы, драйвера, файловый кеш, разделяемая память, системные пулы, а также все структуры ядра. Доступ к этой области памяти можно получить только из нулевого кольца, любое обращение к ней пользовательского кода вызывает ошибку. При работе с этой областью памяти следует соблюдать большую осторожность, так как не все исключения, возникающие в ней могут быть обработаны, что может вызвать падение системы. При разрушении памяти пользовательского режима, будет завершен только тот процесс, целостность памяти которого нарушена, но нарушение целостности структур памяти ядра немедленно ведет к падению системы с синим экраном.

Илон Маск рекомендует:  Командная строка

Методы входа в Ring0

По документации Microsoft для выполнения своего кода в нулевом кольце защиты нам потребуется написать драйвер режима ядра. Но существует еще один способ: через физическую память. В системе есть объект «секция» с именем \Device\PhysicalMemory который представляет из себя отображение физической памяти компьютера. Его можно открыть с помощью ZwOpenSection, после чего можно изменить содержимое системных таблиц (GDT, LDT, IDT) и создать в них свой шлюз, через который можно будет выполнить свой код в нулевом кольце. Существуют также методы перехода в нулевое кольцо путем использования какой-либо уязвимости системы, но так как эти методы работают только с необновленными системами мы их рассматривать не будем.

Пишем драйвер

Начнем с вполне документированного способа: написания драйвера. Для этого нам понадобиться Visual C++ 6.0 (или позже) и комплект DDK (Driver Development Kit). DDK идет под конкретную версию Windows NT, и раньше был доступен для скачивания с сайта Microsoft, но теперь мелкомягкие стали требовать за него денег. Однако, при большом желании, DDK всё же можно найти. Подойдет практически любая версия. После установки DDK можно приступать к созданию шаблона простейшего драйвера.

Функция DriverEntry выполняется при загрузке драйвера в систему, в качестве параметров ей передается указатель на объект драйвера и путь в реестре, по которому этот драйвер приписан пи установке. Следует учесть, что процедура DriverEntry выполняется в контексте процесса System, это может быть немаловажно при написании некоторых специфических функций драйвера. Для компиляции драйвера мы создадим еще несколько файлов:

makefile: !INCLUDE $(NTMAKEENV)\makefile.def
sources: make.bat:

После этого можно компилировать драйвер запуском make.bat.

Что делать дальше?

Итак, код в драйвере выполняется в нулевом кольце защиты и может делать все что угодно, но что конкретно мы можем сделать? Самое простое — убить систему, для этого нужно просто создать необработанное исключение, что немедленно приведет к синему экрану. Например следующий код исполненный в драйвере немедленно валит систему:

Здесь происходит обращеное к нулевому адресу памяти, а обращение к первым 0xFFFF байтам памяти является ошибкой, так как эта область адресного пространства специально зарезервирована для обнаружения нулевых указателей. В результате возникает исключение Page Fault, а так как для него отсутствует обработчик исключения, то в результате возникнет синий экран. Поэтому при программировании кода исполняющегося в нулевом кольце следует быть очень внимательным, и в критических местах устанавливать обработчики ошибок. Например в следующем примере возникающая ошибка обрабатывается и синий экран не возникает:

Но существует класс исключений не обрабатываемых в нулевом кольце — это например деление на ноль, обращение к отсутствующему участку ядерной памяти, или неверный опкод команды. Также невозможна вложенная обработка исключений, при возникновении исключения внутри обработчика исключения в любом случае получается синий экран.

Отладка:
При компиляции этого простейшего драйвера нам конечно не понадобиться ничего отлаживать, но в будущем нам понадобиться писать более сложные программы, и несомненно нужно как-то контролировать их исполнение. Для начала весьма удобным средством является вывод отладочных сообщений функцией DbgPrint, в нашем примере для этого даже существует макрос DPRINT, который производит такой вывод только в случае компиляции драйвера в отладочном режиме. Функция DbgPrint полностью аналогична функции printf, тоесть позволяет выводить форматированные строки различными способами. Для просмотра отладочных сообщений удобно использовать утилиту DbgView Марка Руссиновича, которую можно скачать с www.sysinternals.com, также эти сообщения можно просматривать в отладчике SoftIce.
Для более серьезной отладки необходимо следить за исполнением кода и иметь возможность вмешиваться в этот процесс, для этого нам нужен отладчик режима ядра. Для этого может подойти WinDBG от Microsoft, он имеет удобный графический интерфейс и прост в использовании, но я рекомендую установить SoftIce, так как его возможности значительно превосходят WinDBG, он имеет мощную систему команд, и при некотором навыке становиться гораздо более удобен в использовании. Я не буду останавливаться на установке и использовании SoftIce, это хорошо описано на www.cracklab.ru.

Перехват:

Для осуществления перехвата API на самом низком уровне — в ядре системы нам необходимо четко представлять иерархию вызова API в системе. В упрощенном виде это выглядит так:

В прошлых статьях мы перехватывали Win32 API и NativeAPI уровня пользователя, но теперь будем перехватывать NativeAPI уровня ядра.

На самом деле функции NativeAPI и являются функциями ядра, а аналогичные им функции из ntdll.dll являются переходниками, которые через интерфейс системных вызовов обращаются к соответствующим функциям ядра. При программировании драйверов мы можем использовать те же NativeAPI функции, что и в приложениях третьего кольца, но не можем использовать API более высокого уровня. Также на этом уровне становятся доступными многие функции экспортируемые ядром и предназначенные для использования только в драйверах. В NativeAPI пользовательского уровня мы имеем пары аналогичных функций, отличающиеся только префиксами Zw и Nt, там они отличаются только названием, а имеют одну и ту же точку входа. На уровне ядра мы также имеем аналогичные пары функций, но между ними имеется одно различие, функции с префиксом Zw производят перед выполнением действия проверки системы безопасности (прав пользователя), а функции с префиксом Nt — нет.

Из пользовательского режима через интерфейс системных вызовов нам доступны Nt функции. Zw функции следует вызывать тогда, когда передающиеся им параметры были взяты из Usermode, тогда будут проведены все необходимые проверки.


Рассмотрим подробнее работу интерфейса системных вызовов:

Из этой схемы следует, что вызов функции ядра, прежде чем будет передан соответствующей NativeAPI ядра проходит предварительно довольно сложную обработку. Сначала, в третьем кольце вызывается соответствующая функция в Ntdll, где а регистр EAX помещается номер вызываемого системного сервиса, а в регистр EDX — указатель на передаваемые параметры. Затем вызывается прерывание 2Eh (в Windows XP — команда sysenter) и происходит переход процесса в нулевое кольцо, где управление передается согласно записанному в IDT шлюзу прерывания, в этом месте происходит переключение окружения третьего кольца на нулевое, выполняется смена стека на стек ядра, и происходит перезагрузка сегментного регистра FS, который в нулевом кольце указывает на совершенно другие структуры, чем в третьем кольце. Затем управление передается обработчику прерывания 2Eh — функции ядра KiSystemService. Эта функция копирует передаваемые системному сервису параметры в стек ядра, и производит вызов NativeAPI функции ядра согласно содержимому ServiceDescriptorTable. Эта таблица находится в памяти ядра, и представляет собой структуру содержащую 4 таблицы системных сервисов (SST). Первая из этих таблиц описывает сервисы экспортируемые ядром (ntoskrnl.exe), вторая — графической подсистемой (win32k.sys), а остальные две зарезервированы на будующее и сейчас не используются. Формат этих структур следующий:

Число системных сервисов описываемых каждой SST находится в поле ServiceLimit, поле ServiceTable — указатель на массив содержащий адреса ядерных функций соответствующих экспортируемым сервисам. ArgumentTable — указатель на массив содержащий число аргументов принимаемых каждой экспортируемой функцией (используется KiSystemService при копировании параметров), CounterTable — указатель на массив счетчиков использования каждой функции (этот массив присутствует только в отладочном билде Windows). Из этого следует, что для того, чтобы перехватить какую-либо функцию экспортируемую через этот механизм в третье кольцо мы должны заменить её адрес в соответствующей SST на адрес своего обработчика, но перед этим мы должны сохранить оригинальный адрес функции для её последующего вызова.
Сделать это очень легко, так как указатель на SDT экспортируется ядром по имени KeServiceDescriptorTable, поэтому чтобы его получить, мы должны просто объявить внешнюю переменную: extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;

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

Теперь для перехвата какой-либо ядерной функции нам нужно просто создать для нее свой обработчик и заменить адрес в SST следующим образом: NTCALL(fNum) = NewFunction; где fNum — номер перехватываемого системного вызова, а NewFunction — его новый обработчик. Таблицу номеров функций экспортируемых ядром и графической подсистемой вы найдете в приложении к статье.

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

Следует обратить внимание, что в данном коде перед проверкой значения поля UniqueProcess производится сначала проверка указателя ClientId а затем копирование поля UniqueProcess внутри обработчика ошибок. Проверка указателя связана с тем, что он передается нашему коду из пользовательского процесса третьего кольца, а нам нельзя позволять адресовать область памяти ядра пользовательскому коду. Обработчик ошибок при копировании необходим по причине того, что указатель может указывать на отсутствующую область памяти, а обращение по такому указателю без обработки ошибок приведет к падению системы. При написании драйверов очень важно обращать внимание на такие моменты, иначе это может быть причиной уязвимости в вашей программе. Также следует обратить внимание на то, что перед изменением содержимого SST производится запрет прерываний и очистка WP бита в регистре CR0. Запрет прерываний необходим по причине того, что наш поток может быть прерван в момент записи в SST, и в это время другой поток обратится к перехватываемой функции, что приведет к падению системы. Также в некоторых конфигурациях в системы имеется защита от модификации ядерных страниц памяти. Этой защитой управляет WP бит в регистре CR0, если его не очищать перед модификацией памяти ядра, то это может стать причиной нестабильной работы драйвера, на некоторых системах он может работать, а на некоторых вызывать синий экран. Для загрузки драйвера я рекомендую использовать программу KmdManager от Four-F которая входит в состав его KmdKit (пакета DDK для masm32) который можно скачать с www.wasm.ru (раздел «инструменты»). В состав этого пакета входит еще несколько полезных программ, поэтому всем рекомендую его скачать.

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

Вот полный код драйвера выполняющего аналогичную предыдущему функцию. Его отличие в том, что перехват производится непосредственно заменой кода начала перехватываемой функции. Адрес определяется путем импорта его из ядра.

Следует обратить внимание на то, что перед объявлением структур far_jmp и OldCode производится установка выравнивания структур по одному байту с помощью директивы #pragma pack (push, 1), иначе компилятор будет выравнивать структуры по 8 байт, что приведет к неправильному представлению кода в памяти, и драйвер будет ронять систему в синий экран.

Взаимодействие драйвера с приложением:

В драйверах приведенных выше идентификатор защищаемого процесса жестко прописан в коде, но при практическом применении такой подход недопустим. Очень часто требуется организовать обмен данными между драйвером и приложением. Для этого можно использовать систему ввода-вывода. Для этого драйвер должен создать объект «устройство», через которое ему будут направляться запросы ввода-вывода. Это устройство будет находиться в первичном пространстве имен диспетчера объектов, в каталоге \Device. Для доступа к устройству можно использовать функции NativeAPI, либо для упрощения создать символическую ссылку на объект в каталоге \DosDevices, после чего устройство можно будет открыть через CreateFile. После этого любой запрос ввода-вывода посланный через ReadFile или WriteFile будет послан нашему драйверу путем вызова зарегистрированной им Callback функции ассоциированной с соответствующим типом запроса. Приведем пример драйвера создающего устройство, символическую ссылку на него и выводящего получаемые им данные через DbgPrint в отладочную консоль.

Каждый запрос ввода-вывода преобразуется в IRP пакет и направляется на обработчик назначенный на данный тип пакета. Драйвер обязательно должен обрабатывать запрос IRP_MJ_CREATE который посылается при открытии устройства приложением. В данном случае я еще обрабатываю запрос IRP_MJ_WRITE, который посылается при записи данных в созданное устройства. Обработчиком обоих типов пакетов назначена процедура DriverDispatcher, где происходит прием данных и вывод их через функцию DbgPrint. Так как при создании устройства я выбрал прямой, небуферизованный метод ввода-вывода (METHOD_NEITHER). При применении этого метода мы получаем данные из пользовательского адресного пространства, поэтому при операциях с этими данными необходимо устанавливать обработчик ошибок. Следует учесть одну особенность: процедура DriverDispatcher вызывается в контексте процесса выполняющего запрос ввода-вывода, и это следует учесть при доступе к каким-либо объектам по хэндлу. Если необходимо обращаться к объектам по хэндлу из разных процессов, то следует при открытии объекта поместить хэндл в таблицу хэндлов ядра путем оказания в OBJECT_ATTRIBUTES флага OBJ_KERNEL_HANDLE. Такой хэндл будет доступен из любых процессов, но только в режиме ядра. Хочу также вас предупредить, что вышеприведенный драйвер написан только с целью показать принципы работы ввода-вывода, и для практического применения не подходит по причине того, что содержит две уязвимости. Первая уязвимость состоит в том, что буфер данных полученный от приложения напрямую передается в DbgPrint без проверки длины строки и наличия завершающего ноля. При передаче строки которая не завершается нолем, а оканчивается на невыделенной области памяти произойдет падение системы. Вторая уязвимость — в том, что данные передаются функции осуществляющей форматный вывод, что вызывает уязвимость форматной строки. Это более серьезно, так как может привести к повышению привилегий. Подробно про использование такого рода уязвимостей вы можете прочитать на различных хакерских сайтах. Поэтому, при программировании драйверов следует всегда обращать внимание на такие моменты, так как тут очень велика вероятность возникновения уязвимостей. Для проверки этого драйвера мы напишем программу на Delphi, которая будет отправлять в драйвер текстовые строки. Процедура передачи строки будет выглядеть так:

Драйвер — очень удобный способ выхода в нулевое кольцо, но возникает вопрос — как загрузить драйвер. Для этого можно использовать два способа: использовать вполне документированный API SCM (Service Control Manager) или прописывать драйвер в реестре вручную и загружать его с помощью недокументированной Native API функции ZwLoadDriver. Первый способ очень прост и хорошо документирован, он подходит для постоянной установки драйверов. Второй же способ позволяет создать в реестре минимум необходимых записей, запустить драйвер, и тут же удалить его раздел из реестра. Это позволяет запускать драйвер быстро и незаметно и подходит для маленьких программ не требующих установки, но требующих запуска своего драйвера. Сейчас мы рассмотрим оба способа запуска драйвера.
Запуск драйвера с помощью SCM будет выглядеть так:

Этот метод я считаю весьма тормозным и неудобным в применении, есть более удобный и быстрый способ:

В общем, с загрузкой драйвера проблем не возникнет. Но есть также возможность загружать драйвер при старте системы, для этого нужно установить драйвер через SCM и указать тип загрузки не SERVICE_DEMAND_START, а SERVICE_AUTO_START, SERVICE_SYSTEM_START или SERVICE_BOOT_START. При использовании загрузки SERVICE_AUTO_START драйвер будет загружен менеджером сервисов после инициализации системы, при применении параметра SERVICE_SYSTEM_START драйвер будет загружен менеджером ввода-вывода в момент инициализации ядра, при использовании параметра SERVICE_BOOT_START драйвер будет загружен загрузчиком ntldr еще до запуска ядра системы, и будет запущен в начальной фазе инициализации ядра. При использовании последнего типа загрузки в момент запуска драйвера ему не будет доступен ввод-вывод и многие API ядра связанные еще неинициализированными системами.

Возможно, у вас может возникнуть задача скрытия загруженного драйвера. Первое что приходит в голову — это конечно же перехват API, но в нулевом кольце можно воспользоваться более простыми и надежными методами. Дело в том, что каждый загруженный драйвер представляется в первичном пространстве имен диспетчера объектов как объект типа «Driver» в каталоге \Driver. Каталог первичного пространства имен в структурах ядра выглядит как массив указателей на входящие в эту директорию поддиректории и объекты. В структурах описывающих поддиректории также имеются двухсвязные списки указателей на предыдущую и следующую директории списка. Для скрытия объекта в первичном пространстве имен нам нужно получить указатель на объект директории, найти в нем структуру описывающую скрываемый объект и корректно удалить её. Все эти действия осуществляет нижеприведенный код:

Впервые этот код был приведен на http://www.rootkit.com/newsread.php?news >

Теперь для скрытия какого-либо объекта достаточно вызвать HideObject(L»\\Driver», L»MyDriver»); где \Driver — директория где находится объект, а MyDriver — имя объекта. Полный код драйвера осуществляющего такое скрытие я здесь приводить не буду, но вы можете скачать в приложении к статье драйвер подобный предыдущему (выводящий в отладочную консоль получаемую информацию), но удаляющий себя из списка загруженных драйверов. Единственный недостаток этого метода в том, что скрытый таким образом драйвер нельзя будет выгрузить из системы, а также, если скрытие драйвера вызывается в процедуре DriverEntry, то драйвер при запуске будет возвращать код ошибки, но в памяти останется и будет нормально функционировать.

Здесь я дал только основные принципы написания драйверов. Это достаточно для того, чтобы начать их писать, но если вы интересуетесь этой темой, то отсылаю вас к специальной литературе на эту тему.

А что можно без драйвера?

Иногда возникает необходимость производить действия доступные только в нулевом кольце, а использовать для этого драйвер нежелательно или невозможно. Поэтому я опишу еще один способ, который позволяет перейти в нулевое кольцо и работать в нем не используя драйвер вообще. Способ этот основывается на открытии секции \Device\PhysicalMemory и модификации глобальной таблицы дескрипторов (GDT). В GDT добавляется дескриптор шлюза вызова CallGate который указывает на наш код выполняющийся в нулевом кольце. После чего производится вызов шлюза командой длинного вызова CALL FAR, которая изменяет привилегии кода и передает управление нашему коду. Структура описывающая шлюз вызова выглядит следующим образом:

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

Открытие памяти:

Следует обратить внимание на то, что нужную нам секцию нельзя сразу открыть на запись, так как полный доступ к ней имеет только система, нужно сначала изменить атрибуты безопасности секции разрешив текущему пользователю открывать её на запись, после чего секцию можно открыть и установить старые атрибуты безопасности обратно (для большей незаметности). Естественно все эти действия можно проделать только имея права администратора.
Установка шлюза будет выглядеть так:

В этом коде Ring0CallProc — адрес кода который может быть вызван через установленный шлюз и будет выполнен в нулевом кольце защиты. Селектор установленного шлюза будет равен DWORD(CurrentGate) — DWORD(ptrGDT) — offset, смещение неважно. Теперь для вызова нашего кода достаточно выполнить длинный вызов FAR CALL на установленный шлюз:

Когда необходимость в шлюзе отпадет, то его необходимо убрать с помощью следующего кода:

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

Как работать в ring0 без драйвера:

Теперь мы можем не используя драйвер выполнять участки кода в нулевом кольце. Но что мы можем сделать? Работая в драйвере мы можем использовать функции экспортируемые ядром и другими драйверами, и с помощью них взаимодействовать с системой, а здесь наш код лишен такой возможности по той причине, что мы не знаем адреса необходимых нам функций. Также, при работе в драйвере мы имеем другое содержимое сегментных регистров, которое заполняется при выполнении системного вызова, а в данном случае мы имеем те же регистры, что и в режиме пользователя. Поэтому для выполнения каких-либо практических задач с применением этого метода нам нужно получить адреса необходимых нам функций ядра и изменять содержимое регистра FS, через который осуществляется доступ к системным структурам связанным с текущим процессом. В режиме пользователя и в режиме ядра этот регистр указывает на совершенно разные структуры. Для получения адресов API ядра можно использовать следующую методику: загрузить ядро (ntoskrnl.exe) с помощью LoadLibraryEx с установленным флагом DONT_RESOLVE_DLL_REFERENCES в наше адресное пространство, функция возвратит нам адрес MZ заголовка ядра подгруженного в User Space. Теперь с помощью GetProcAddress мы можем получить адрес интересующей нас функции в User Space. Разность адреса функции и адреса подгруженного ядра будет смещением начала функции в ядре. Теперь нам нужно узнать адрес загрузки ядра в Kernel Space и прибавить к нему полученное смещение, и у нас будет адрес нужной нам функции в Kernel Space. Определить адрес загрузки системного модуля в Kernel Space можно путем вызова Nativa API функции ZwQuerySystemInformation с классом SystemModuleInformation, функция вернет нам список загруженных модулей и информацию о них. Для получения адреса загрузки модуля в Kernel Space можно использовать следующую функцию:

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

где dKernelBase — адрес ядра загруженного в User Space, KernelBase — адрес ядра в Kernel Space. Теперь, нам нужно получить таким способом адреса всех необходимых функций и сохранить их в глобальных переменных, после чего их можно будет легко вызывать из Ring0 кода. Также не следует забывать а необходимости перезагрузки регистра FS, что делается следующим кодом:

Попробуем теперь найти применение описанной методике. Для начала напишем функцию Ring0CopyMemory, которая будет копировать участки памяти в нулевом кольце, что позволит нам работать с памятью ядра. Но неприятная особенность копирования памяти в нулевом кольце состоит в том, что попытка доступа к невыделенной памяти неизбежно ведет к синему экрану, поэтому перед копированием желательно проверить переданные указатели на валидность. Для этого в ядре существует функция MmIsAddressValid, которая принимает проверяемый указатель, и возвращает в случае валидности указателя EAX > 0, или 0, если память невыделенна. Получим адрес этой функции по вышеописанной методике и занесем его в переменную AdrMmIsValid. После этого функция копирования памяти будет иметь следующий вид:

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

Теперь нам доступна память ядра, попробуем поработать с некоторыми ядерными структурами. Для начала, попробуем получить список всех процессов в системе. Казалось бы, зачем для этого лазить в нулевое кольцо, ведь есть ToolHelp API, которые весьма просты в использовании. Но недостаток перечисления процессов через API в том, что они могут быть перехвачены и процесс может быть скрыт. В первой части статьи я описал, как получить список процессов обращаясь к ядру через интерфейс системных вызовов, но этот метод не гарантирует, что будут обнаружены все процессы, так как API могут быть перехвачены в ядре. Поэтому сейчас мы попробуем получить список процессов на самом низком уровне — из структур ядра.

Каждый процесс в памяти ядра представлен структурой EPROCESS, которая хранит в себе информацию о параметрах этого процесса, а также содержит в себе ссылки на структуры предыдущего и следующего процессов в списке. Эта структура имеет различный формат для разных версий Windows NT, поэтому я не буду приводить её целиком, а рассмотрю только важные её части. Допустим, мы уже получили указатель на структуру EPROCESS интересующего нас процесса, теперь нам нужно извлеч из нее имя процесса, его Process Id и id родительского процесса. Для Windows XP имя процесса находится в структуре EPROCESS по смещению 174h и занимает 16 байт в ANSI кодировке, ProcessId — имеет смещение 84h и размер dword, Parrent Process ID имеет смещение 14Ch. В Windows 2000 эти данные имеют соответственно смещения 1FCh, 9Сh и 1С8h. В структуре EPROCESS каждого процесса содержатся указатели на структуры следующего и предыдущего процессов. Список структур EPROCESS всех процессов в системе начинается с переменной ядра PsActiveProcessesLink, которая ядром не экспортируется, но её можно получить с помощью анализа двухсвязного списка структур EPROCESS. В общем виде этот двухсвязный список будет выглядеть так:

Илон Маск рекомендует:  Что такое код pdf_add_weblink

ActiveProcessesLink — это элемент двухсвязного списка содержащий указатели FLink (на следующий элемент списка) и BLink (на предыдущий). Эти указатели в Windows XP имеют смещения 88h и 8Ch соответственно (в Windows 2000 — A0h и A4h). Заметьте, что указатели ActiveProcessesLink указывают не на начало структуры EPROCESS, а на слудующий элемент двухсвязного списка, поэтому для получения указателя на EPROCESS нам нужно отнять смешение ActiveProcessesLink в структуре EPROCESS. Для получения списка всех процессов в системе нам нужно получить указатель на EPROCESS любого процесса, после чего двигаться по спискам ActiveProcessesLink до тех пор, пока не окажемся на той структуре, с которой начали. В качестве эталона на который будет опираться весь дальнейший код я возьму указатель на EPROCESS процесса System, так как это единственный обязательный процесс в системе который существует все время её работы (после загрузки). Для получения этого указателя сначала получим с помощью функции ядра IoGetCurrentProcess указатель на EPROCESS текущего процесса, после чего будем двигаться по связанным спискам до тех пор, пока не будет обнаружен процесс с Parrent P >

Заполняется эта структура следующим кодом:

Также здесь определяются адреса ядерных функций которые нам в дальнейшем понадобятся. Теперь код поиска EPROCESS процесса System будет выглядеть так:

Теперь, получив указатель на EPROCESS мы можем получить список всех процессов в системе используя следующий код:

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

Итак, список процессов мы получили, теперь попробуем изменять структуры EPROCESS. Например, можно сменить PID процесса. Для удобства, все дальнейшие функции будут работать не с Id процесса, а с указателем на его EPROCESS, поэтому введем функцию для получения указателя на EPROCESS по Process Id:

Описывать как работает этот код тоже не стоит, так как это все та же работа со связанными списками. Смена же Process Id по указателю на EPROCESS выглядит и того проще:

Что еще можно сделать интересного с процессом? Например, можно скрыть процесс в системе без использования каких-либо перехватов API. Для этого нужно получить указатель на EPROCESS скрываемого процесса и изменить FLink следующего процесса и BLink предыдущего так, чтобы они указывали друг на друга. Тогда перечисление процессов будет идти в обход скрываемого. Этот метод работает потому, что планировщик Windows ничего не знает о процессах, он просто распределяет процессорное время между всеми потоками в системе независимо от принадлежности их какому-либо процессу. Поэтому в системе могут существовать «свободные» потоки, не принадлежащие никакому процессу. Итак, скрытие процесса осуществляет следующий код:

Иногда, желательно иметь возможность сделать скрытый процесс снова видимым. Для этого нужно перед скрытием сохранить указатель на EPROCESS, а потом вставить скрытый процесс в любое место связанного списка, соответствующим образом исправив указатели. Для простоты мы будем вставлять процессы сразу же после EPROCESS процесса System, и полный код осуществляющий это будет выглядеть так:

Возможность изменять имя процесса налету тоже может оказаться полезной. В Windows NT4 и более поздних версиях NT в структуре EPROCESS есть поле ImageFileName размером 16 байт. В нем храниться имя процесса возвращаемое при перечислении списка процессов. Начиная с Windows XP в структуре EPROCESS дополнительно появилось поле SE_AUDIT_PROCESS_CREATION_INFO которое содержит указатель на структуру UNICODE_STRING содержащую полный путь к исполняемому файлу(в NT формате) из которого был запущен процесс. Это нужно учесть при смене имени процесса в этих системах. Вот код осуществляющий смену имени процесса:

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

Но для прямого доступа к железу не обязательно выходить в нулевое кольцо, можно просто открыть коду третьего кольца доступ к портам ввода-вывода. Для этого следует изменить IOPM в TSS так, чтобы разрешенным для третьего кольца портам соответствовал 0 бит, а запрещенным — 1, после чего нужно разрешить использование IOPM для конкретных процессов. Для получения и установки карты ввода-вывода (IOPM) в ядре есть недокументированные функции Ke386GetIoAccessMap и Ke386IoSetAccessMap соответственно, а для разрешения или запрета использования IOPM процессом — Ke386IoSetAccessProcess. А вот и код, который все это осуществляет:

Для управления доступом к отдельным портам можно применить такой код:

Этот метод можно использовать для того, чтобы заставить работать под NT старые досовские игрушки, которые используют прямой доступ к железу. Для этого достаточно установить карту ввода-вывода, а её использование для DOS процессов разрешено по умолчанию.

Эту тему можно продолжать бесконечно, но так-как в одной статье все не описать, то эту статью можно считать законченной. В приложении к ней вы можете скачать все примеры драйверов приведенных в статье, а также мою библиотеку для работы с нулевым кольцом в Delphi и примеры её использования.

Перехват вызовов API-функций ОС Windows

Привет читатель. Сегодня мы научимся перехватывать вызовы API-функций ОС Windows.

Для начала немного теории:
Каждый exe-файл содержит PE(Portable Executable)-заголовок. Формат этого заголовка примерно таков:
1. DOS-заголовок
2. DOS-код
3. PE-заголовок
4. Таблица секций
5. Сами секции
DOS-заголовок – это рудимент, оставшийся со времён DOS.
DOS-код – это инструкции выполняемые при отсутствии PE-заголовка.
Далее идёт PE-заголовок, он представляет собой довольно сложную структуру. А после идёт самая важная часть – таблица секций и сами секции. В них содержится основной код и данные программы, секции можно создать директивой .name, где name – имя секции (для MASM).

Находим PE-заголовок типа TImageNtHeaders. В нём содержится структура TImageOptionalHeader, в которой есть массив структур IMAGE_DATA_DIRECTORY, нас интересует второй член массива — IMAGE_DIRECTORY_ENTRY_IMPORT.

Type
_IMAGE_DATA_DIRECTORY = record
VirtualAddress: DWORD;
Size: DWORD;
end;
VirtualAddress указывает на адрес нужной нам таблицы.

Итак, приступим к делу. Перехватывать мы будет по такому плану:
1) Определяем адрес перехватываемой функции.
2) Находим адрес таблицы импорта в процессе, вызовы которого перехватываем.
3) Перечисляем все структуры IMAGE_THUNK_DATA всех дескрипторов импорта в таблице импорта.
4) Найдя адрес, совпадающий с искомым, заменяем его адресом своей функции.
Для этого мы должны объявить такие структуры и типы:

type
PImageImportByName = ^TImageImportByName; //Описание адреса функции
_IMAGE_IMPORT_BY_NAME=packed record
HInt: Word;
Name: Byte;
end;
TImageImportByName =_IMAGE_IMPORT_BY_NAME;

TThunk=packed record //Описание функции
case Integer of
0: (ForwarderString: PByte);
1: (thFunction: PDWORD);
2: (Ordinal: DWORD);
3: (AddressOfData: PImageImportByName);
end;

PImageThunkData=^TImageThunkData;
_IMAGE_THUNK_DATA=packed record
Thunk: TThunk;
end;
TImageThunkData=_IMAGE_THUNK_DATA;
IMAGE_THUNK_DATA=_IMAGE_THUNK_DATA;

TCharcteristics=record
case Integer of
0: (Characteristics: DWORD);
1: (OriginalFirstThunk: PImageThunkData);
end;

PImageImportDescriptor=^TImageImportDescriptor;
_IMAGE_IMPORT_DESCRIPTOR = packed record //Дескриптор импорта
t: TCharcteristics;
TimeDateStamp: DWord;
ForwarderChain: DWORD;
Name: DWORD;
FirstThunk: PImageThunkData;
end;
TImageImportDescriptor=_IMAGE_IMPORT_DESCRIPTOR;
IMAGE_IMPORT_DESCRIPTOR=_IMAGE_IMPORT_DESCRIPTOR;
Так как для определения адреса таблицы импорта нам необходимо чтобы процесс загрузил нашу библиотеку. Это можно реализовать так:

procedure AttachDllToProcess(pID: integer; LibName: string);
var
ThreadID:Cardinal;
ThreadHndl:THandle;
AllocBuffer:Pointer;
BytesWritten:Cardinal;
ProcAddr:Pointer;
ExitCode:Cardinal;
hProcess: integer;
begin
hProcess:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_OPERATION or PROCESS_VM_WRITE, false, pID);
if (hProcess=0) then
Exit;


AllocBuffer:=VirtualAllocEx(hProcess, nil, length(LibName)+1, MEM_COMMIT, PAGE_READWRITE);
if (AllocBuffer<>nil) then
WriteProcessMemory(hProcess, AllocBuffer, PChar(LibName), length(LibName)+1, BytesWritten)
else
Exit;

ProcAddr:=GetProcAddress(LoadLibrary(PChar(‘Kernel32.dll’)), PChar(‘LoadLibraryA’));
ThreadHndl:=CreateRemoteThread(hProcess, nil, 0, ProcAddr, AllocBuffer, 0, ThreadID);

WaitForSingleObject(ThreadHndl, INFINITE);
GetExitCodeThread(ThreadHndl, ExitCode);
CloseHandle(ThreadHndl);
VirtualFreeEx(hProcess, AllocBuffer, 0, MEM_RELEASE);
CloseHandle(hProcess);

Следующая процедура реализует загрузку DLL другим процессом. В качестве параметров передаётся уникальный идентификатор процесса, его можно определить с помощью ToolHelp32-функций, и прямое имя загружаемой DLL. Обращаю ваше внимание на то, что имя дллки должно быть прямым, а не относительным, то есть если она лежит в одной папке с exe, то всё равно надо писать не Lib.dll, а Путь\до\папки\с\EXE\Lib.dll.
Как всё это работает. Сначала получаем дескриптор процесса с помощью OpenProcess.
function OpenProcess(
dwDesiredAccess: DWORD; //параметры доступа открытия процесса
bInheritHandle: BOOL;
dwProcessId: DWORD): // pID
THandle; stdcall;
Далее мы выделяем память под параметры к функции LoadLibrary. Также необходимо заметить, что выделяем мы не length(LibName) байт, а length(LibName)+1, это так, потому что в Windows используется тип PChar, который требует #0 в конце строки, а параметр LibName передаётся без него.
function VirtualAllocEx(
hProcess: THandle; //дескриптор процесса
lpAddress: Pointer; //адрес по которому происходит выделение памяти
dwSize, // размер выделяемой памяти
flAllocationType: DWORD; //что именно мы будем делать
flProtect: DWORD): //тип защиты выделенного участка памяти
Pointer; stdcall;
После этого мы может записать параметр в процессорную память.
function WriteProcessMemory(
hProcess: THandle; //дескриптор процесса
const lpBaseAddress: Pointer; //начальный адрес для записи
lpBuffer: Pointer; //буфер, который надо записать
nSize: DWORD; //его размер
var lpNumberOfBytesWritten: DWORD): //сколько записалось
BOOL; stdcall;
Получаем адрес LoadLibraryA в kernel32.dll. Создаём удалённый поток с параметрами указателя на адрес LoadLibrary и адрес по которому располагаются параметры этой функции.
function CreateRemoteThread(
hProcess: THandle; //дескриптор процесса
lpThreadAttributes: Pointer; //атрибуты защиты потока
dwStackSize: DWORD; //какой размер используемого стека
lpStartAddress: Pointer; //указатель на адрес функции создаваемого потока
lpParameter: Pointer; //параметры передаваемые потоку
dwCreationFlags: DWORD; //дополнительные флаги создания потока
var lpThreadId: DWORD): //ID потока
THandle; stdcall;
После присоединения библиотеки закрываем все открытые дескрипторы, освобождаем память.
function VirtualFreeEx(
hProcess: THandle; //дескриптор процесса
lpAddress: Pointer; //начальный адрес освобождаемой памяти
dwSize, //размер освобождаемой памяти
dwFreeType: DWORD): //тип освобождения
Pointer; stdcall;
Теперь мы можем приступить к написанию самого кода перехвата.
Помещаем этот код в DLL.

function InterceptDLLCall(hLocalModule: HModule; c_szDllName, c_szApiName:PChar;
pApiNew, pApiToChange: Pointer; var p_pApiOrg: Pointer): boolean;
var
pDOSHeader: PImageDosHeader;
pNTHeader: PImageNtHeaders;
pImportDesc: PImageImportDescriptor;
dwProtect, dwNewProtect, dwAddressToIntercept: DWORD;
pThunk: PImageThunkData;
bSuccess: Boolean;
BytesWritten: DWORD;
pTemp: Pointer;
instr: WORD;
pto: DWORD;
begin
result:=false;
pDOSHeader:=Windows.PIMAGEDOSHEADER(hLocalModule);
bSuccess:=false;

if (pApiToChange<>nil) then
dwAddressToIntercept:=DWORD(pApiToChange)
else
begin
pTemp:=GetProcAddress(GetModuleHandle(c_szDllName), c_szApiName);
ReadProcessMemory(GetCurrentProcess, pTemp, @instr, 2, BytesWritten);
ReadProcessMemory(GetCurrentProcess, pointer(dword(pTemp)+2), @pto, 4, BytesWritten);
if instr=$25FF then
pTemp:=pointer(pto);
dwAddressToIntercept:=DWORD(pTemp);
end;

if (IsBadReadPtr(Pointer(hLocalModule), sizeof(PImageNtHeaders))) then
exit;
if (pDOSHeader.e_magic<>IMAGE_DOS_SIGNATURE) then
exit;

pNTHeader:=PIMAGENTHEADERS(MakePtr(DWORD(pDOSHeader), pDOSHeader._lfanew));
if (pNTHeader.Signature<>IMAGE_NT_SIGNATURE) then
exit;

pImportDesc:=PImageImportDescriptor(MakePtr(hLocalModule,
pNTHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
if (pImportDesc=PImageImportDescriptor(pNTHeader)) then
exit;

while(pImportDesc.Name>0)do
begin
pThunk:=PImageThunkData(MakePtr(hLocalModule, Dword(pImportDesc.FirstThunk)));
while(pThunk.Thunk.thFunction<>nil) do
begin
if DWord(pThunk.Thunk.thFunction)=dwAddressToIntercept then
begin
if (not IsBadWritePtr(Pointer(@pThunk.Thunk.thFunction), sizeof(DWORD))) then
begin
p_pApiOrg:=Pointer(pThunk.Thunk.thFunction);
pThunk.Thunk.thFunction:=PDWORD(pApiNew);
bSuccess:=true;
end
else
if VirtualProtect(Pointer(@pThunk.Thunk.thFunction), sizeof(DWORD), PAGE_EXECUTE_READWRITE, @dwProtect) then
begin
p_pApiOrg:=Pointer(pThunk.Thunk.thFunction);
pThunk.Thunk.thFunction:=PDWORD(pApiNew);
bSuccess:=true;
dwNewProtect:=dwProtect;
VirtualProtect(Pointer(@pThunk.Thunk.thFunction), sizeof(DWORD), dwNewProtect, @dwProtect);
end;
end;
Inc(PThunk);
end;
Inc(pImportDesc);
end;
result:=bSuccess;
end;

Описание параметров:
hLocalModule – модуль в котором находиться Import Table
c_szDllName – имя DLL, в которой находиться перехватываемая функция
c_szApiName – имя перехватываемой функции
pApiNew – указатель на нашу функцию, которая будет вызываться вместо перехватываемой.
pApiToChange – указатель на перехватываемую функцию, если равно nil, то адрес функции определяется через c_szDllName и c_szApiName.
p_pApiOrg – указатель на старую перехватываемую функцию
Если pApiToChange не определён, то начинаем своё определение адреса.
Для начала просто через GetProcAddress(), далее читаем сначала первые 2 байта по этому адресу, а потом и следующие 4.
function ReadProcessMemory(
hProcess: THandle; //дескриптор процесса
const lpBaseAddress: Pointer; //адрес по которому начинаем чтение
lpBuffer: Pointer; //куда будем читать
nSize: DWORD; //размер нашего буфера
var lpNumberOfBytesRead: DWORD): //сколько прочитано
BOOL; stdcall;
Если первые 2 байта равны $25FF(код перехода на указатель), то в следующих 4ёх лежит адрес перехода, его мы и записываем вместо полученного GetProcAddress’ом.
Получаем указатель на нужную нам таблицу.
Перебираем все структуры Thunk. И если находим адрес равный искомому, то проверяем можем ли мы записать в память по этому адресу, если нет то ставим соответствующий атрибут защиты.
function VirtualProtect(
lpAddress: Pointer; //адрес памяти
dwSize, //размер
flNewProtect: DWORD; //новый атрибут
var OldProtect: DWORD): //старый атрибут
BOOL; stdcall;
И заменяем нашим адресом.
Есть ещё одна тонкость, для использования в WinNT-based системах, нужно установить привилегии отладчика. Смотри исходники.

При написании статьи мне помогли следующие материалы:
1) Книга Джеффри Рихтера “Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows”.
2) Статья “Система перехвата функций API платформы Win32” HI-TECH’а.

Перехват WinAPI и других библиотечных функций

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

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

В первом случае приходится перечислять все модули, загруженные в адресное пространство процесса, анализировать их таблицы импорта, корректировать, следить за загрузкой новых модулей… И все это не спасет, если адрес функции получается через GetProcAddress, а не через таблицу.

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

Как же его реализовать? Самый хардкорный способ — записать весь наш код в адресное пространство интересующего процесса (далее «жертва»), остановить все потоки, перенаправить выполнение одного из них на наш код и ждать результатов. Но это не слишком красиво: придется писать весь внедряемый код на ассемблере, а то и в машинных кодах, к тому же он должен быть базонезависимым. Куда проще оформить весь код перехвата в отдельную библиотеку, которую потом останется только загрузить.

Что же будет делать эта библиотека? При загрузке она найдет адрес перехватываемой функции, считает из ее начала N байт, и на их место запишет переход на нашу функцию. Позже, при выгрузке библиотеки или когда нам понадобится вызывать оригинал функции, необходимо будет вернуть на место эти N байт, тем самым сняв перехват. Ниже приведет код библиотеки, которая при загрузке подменяет функцию GetSystemTime на функцию dummy, реализованную в ней же.

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

Кроме того, прототип нашей функции dummy должен в точности соответствовать прототипу перехватываемой функции (GetSystemTime), в противном случае необходима корректировка стека.

    Инициализируемся
    Сохраняем первые байты перехватываемой функции и формируем код безусловного перехода

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

    1. Останавливаем потоки
    2. Снимаем перехват
    3. Вызываем оригинальную функцию
    4. Устанавливаем перехват
    5. Запускаем потоки
    6. Корректируем результат работы оригинальной функции
    7. Возвращаем управление

    Как же теперь заставить жертву загрузить эту библиотеку. Можно подправить реестр так, чтобы она загружала во все создаваемые процессы. Но я предпочитаю действовать более адресно. В Windows есть такая замечательная функция CreateRemoteThread, которая позволяет запустить поток в чужом процессе. Так что нам осталось только запустить код этого процесса в адресное пространство жертвы.

    Вот, что для этого надо:

    1. Узнаем, по какому адресу расположены в адресном пространстве жертвы функции LoadLibrary и ExitThread.
      Благодаря стараниям Microsoft по защите от вирусов и червей, мы не можем узнать адреса этих функций в своем процессе и вызвать по этим же адресам в соседнем (можем, конечно, но результат — непредсказуем). Поэтому узнаем базу kernel32 в соседнем процессе, смещение нужных функций от начала модуля, складываем и… вуаля, можем вызывать.
    2. Формируем набор команд, которые загрузят библиотеку
      А здесь мы как раз и вызываем. То есть делаем

    Это для win32. Для 64 бит аналогично, только вместо eax — rax и вместо push — mov rcx.

  • Пишем эти команды в жертву
    Выделяем память VirtualAllocEx’ом и пишем WriteProcessMemory
  • Создаем в жертве поток.Вот код, который это делает:
  • В конце не забываем подчистить за собой.

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

    Методы перехвата API-вызовов в Win32 (стр. 1 из 4)

    Игорь В. Филимонов

    Данная статья написана в результате анализа известных методов перехвата API-вызовов в Windows. В некоторых широко известных примерах реализации перехвата системных функций есть небольшие ошибки, которые в некоторых случаях приводят к тому, что перехват не работает. Один из таких примеров был описан в RSDN Magazine #1, другой – в известной книге Джеффри Рихтера «Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows», 4-е издание.

    Перехват системных функций операционной системы – приём, известный давно. Обычно перехватывается некоторая системная функция с целью мониторинга или изменения её поведения. Во времена DOS программисты перехватывали программные прерывания (int 21h, int 16h, int 10h). С приходом Win16 понадобились средства для перехвата API-функций. И, наконец, с появлением Win32 средства перехвата ещё раз эволюционировали, подстроившись под новую систему. Операционные системы семейства Windows никогда не содержали встроенных средств, специально предназначенных для перехвата системных функций. И понятно почему – всё-таки это немного хакерский приём. Поэтому перехват обычно осуществляется «подручными средствами», и для его реализации нужно чётко представлять многие глубинные аспекты устройства и функционирования операционной системы.

    В данной статье рассматриваются методы реализации перехвата системных API-функций в 32-разрядных операционных системах Windows. Рассматриваются особенности реализации перехвата в Win9X (Windows 95/98/98SE/ME) и WinNT (Windows NT/2000/XP/2003).

    Особенности организации памяти в Windows

    Так как перехват практически всегда связан с модификацией памяти (либо кода перехватываемой функции, либо таблиц импорта/экспорта), то для его осуществления необходимо учитывать особенности архитектуры памяти WinNT и Win9X.

    Каждому процессу (начиная с Windows 95) выделяется собственное виртуальное адресное пространство. Для 32-разрядных процессов его размер составляет 4 Гб. Это адресное пространство разбивается на разделы, функциональное назначение и свойства которых довольно сильно отличаются у семейств ОС WinNT и Win9Х.

    Адресное пространство любого процесса в Win9Х можно разделить на три раздела:

    Младшие два гигабайта (00400000-7FFFFFFF) – код и данные пользовательского режима (в диапазоне 00000000-003FFFFF расположены разделы для выявления нулевых указателей и для совместимости с программами DOS и Win16);

    Третий гигабайт – для общих файлов, проецируемых в память (MMF), и системных DLL.

    Четвёртый гигабайт – для кода и данных режима ядра (здесь располагается ядро операционной системы и драйверы).

    Старшие два гигабайта являются общими для всех процессов. Основные системные DLL – kernel32.dll, advAPI32.dll, user32.dll и GDI32.dll загружаются в третий гигабайт. По этой причине эти четыре библиотеки доступны всем процессам в системе. Поскольку этот гигабайт общий, они существуют во всех процессах по одним и тем же адресам. Из соображений безопасности Microsoft запретила запись в область, куда они загружаются. Если же запись туда всё же произвести (а это возможно из режима ядра или недокументированными методами), то изменения произойдут во всех процессах одновременно.

    В WinNT общих разделов у процессов нет, хотя системные библиотеки по-прежнему во всех процессах загружаются по одинаковым адресам (но теперь уже в область кода и данных пользовательского режима). Запись в эту область разрешена, но у образов системных библиотек в памяти стоит атрибут «копирование при записи» (copy-on-write). По этой причине попытка записи, например, в образ kernel32.dll приведёт к появлению у процесса своей копии изменённой страницы kernel32.dll, а на остальных процессах это никак не отразится.

    Все эти различия существенно влияют на способы реализации перехвата функций, расположенных в системных DLL.

    Перехваты можно разделить на два типа: локальные (перехват в пределах одного процесса) и глобальные (в масштабах всей системы).

    Локальный перехват с использованием раздела импорта

    Локальный перехват может быть реализован и в Win9X, и в WinNT посредством подмены адреса перехватываемой функции в таблице импорта. Для понимания механизма работы этого метода нужно иметь представление о том, как осуществляется динамическое связывание. В частности, необходимо разбираться в структуре раздела импорта модуля.

    В разделе импорта каждого exe- или DLL-модуля содержится список всех используемых DLL. Кроме того, в нем перечислены все импортируемые функции. Вызывая импортируемую функцию, поток получает ее адрес фактически из раздела импорта. Поэтому, чтобы перехватить определенную функцию, надо лишь изменить её адрес в разделе импорта. Для того чтобы перехватить произвольную функцию в некотором процессе, необходимо поправить её адрес импорта во всех модулях процесса (так как процесс может вызывать эту функцию не только из exe-модуля, но и из DLL-модулей). Кроме того, процесс может воспользоваться для загрузки DLL функциями LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW или, если она уже загружена, определить её адрес при помощи функции GetProcAddress. Поэтому для перехвата любой API-функции необходимо перехватывать и все эти функции.

    Существует несколько широко известных примеров реализации этого метода, в частности один из них описан в книге Джеффри Рихтера «Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows» (Jeffrey Richter «Programming Applications for Microsoft Windows»), 4-е издание. Другой пример – библиотека APIHijack, написанная Wade Brainerd на основе DelayLoadProfileDLL.CPP (Matt Pietrek, MSJ, февраль 2000). Для описания этого метода я взял за основу пример Джеффри Рихтера (с небольшими изменениями).

    Для реализации перехвата был создан класс CAPIHook, конструктор которого перехватывает заданную функцию в текущем процессе. Для этого он вызывает метод ReplaceIATEntryInAllMods, который, перечисляя все модули текущего процесса, вызывает для каждого метод ReplaceIATEntryInOneMod, в котором и реализуется поиск и замена адреса в таблице импорта для заданного модуля.

    vo >Name; pImportDesc++) < PSTR pszModName = (PSTR)((PBYTE) hmodCaller + pImportDesc->Name); if (lstrcmpiA(pszModName, pszCalleeModName) == 0) < //Нашли if (pImportDesc->Name == 0) return; //Ни одна функция не импортируется //Получим адрес таблицы импорта PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE) hmodCaller + pImportDesc->FirstThunk); //Переберёмвсеимпортируемыефункции for (; pThunk->u1.Function; pThunk++) < PROC* ppfn = (PROC*) &pThunk->u1.Function; //Получимадресфункции BOOL fFound = (*ppfn == pfnCurrent); //Егоищем? if (!fFound && (*ppfn > sm_pvMaxAppAddr)) < // Если не нашли, то поищем поглубже. // Если мы в Win98 под отладчиком, то // здесь может быть push с адресом нашей функции PBYTE pbInFunc = (PBYTE) *ppfn; if (pbInFunc[0] == cPushOpCode) < //Да, здесь PUSH ppfn = (PROC*) &pbInFunc[1]; //Нашадрес? fFound = (*ppfn == pfnCurrent); >> if (fFound) < //Нашли. DWORD dwDummy; //Разрешимзаписьвэтустраницу VirtualProtect(ppfn, sizeof(ppfn), PAGE_EXECUTE_READWRITE, &dwDummy); //Сменимадреснасвой WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL); //Восстановиматрибуты VirtualProtect(ppfn, sizeof(ppfn), dwDummy , &dwDummy); //Готово. return; >> > > //Здесь этой функции не нашлось >

    При помощи функции ImageDirectoryEntryToData определяется дескриптор таблицы импорта и, если он есть, перебираются все DLL, из которых импортируются функции. Если DLL находится, то среди функций, импортируемых из неё, ищется нужная, а затем при помощи WriteProcessMemory её адрес меняется на адрес своего обработчика. Теперь он будет вызываться каждый раз, когда из данного модуля будет происходить обращение к перехваченной функции.

    ПРИМЕЧАНИЕ Если вы читали уже упоминаемую выше книгу Джеффри Рихтера, то могли заметить, что в функции ReplaceIATEntryInOneMod я сделал одно изменение. У него она работала так: в таблице импорта находился список функций того модуля, функция из которого импортировалась, и если в этом списке эта функция не находилась, то ReplaceIATEntryInOneMod больше ничего не делала (т. е. перехват не происходил). Я столкнулся с таким поведением, когда написал тестовую программу на Delphi для примера DriveType2 (этот пример описан ниже, в разделе «Глобальный перехват методом тотального локального перехвата», он перехватывает функцию GetDriveTypeA во всех приложениях с использованием описываемого метода). Тест, написанный на Visual C++, работал прекрасно – функция GetDriveTypeA перехватывалась. А вот программа на Delphi всё равно для всех перехватываемых мной дисков возвращала реальные значения. Я посмотрел таблицу импорта тестовой программы при помощи утилиты DUMPBIN и обнаружил, что компилятор Delphi не поместил все импортируемые функции из kernel32.dll в один список, а разбил их на 3 части, причём GetDriveTypeA оказалась в третьей. Поэтому функция ReplaceIATEntryInOneMod Джеффри Рихтера, просмотрев все функции из первого списка Kernel32.dll, не нашла функции GetDriveTypeA, хотя она и импортировалась модулем DriveTypeTest.exe. Я исправил эту функцию таким образом, чтобы она проверяла всю таблицу импорта и перебирала все списки с функциями из kernel32.dll (как оказалось, их может быть несколько). В описании формата РЕ-файла нигде не оговаривается, что каждый модуль, из которого импортируются функции, должен встречаться в секции импорта только один раз, и, видимо, некоторые компиляторы этим пользуются.

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

    Основным достоинством данного метода является то, что он одинаково реализуется как в Win9X, так и в WinNT.

    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL