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