Привет, читатель. Наверное ты уже делал какую-либо прогу-заподлянку или троянчик,
но хитрый пользователь всегда замечал в 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);

end;

Следующая процедура реализует загрузку 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’а.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии