Багато користувачів звикли до того, що в Windows NT диспетчер задач показує всі процеси, і багато хто вважає, що сховатися від нього взагалі неможливо. Насправді, приховати процес надзвичайно просто. Для цього існує безліч методів, і їх реалізації доступні в исходниках. Залишається тільки дивуватися, чому так рідкісні трояни використовують ці методики? Їх буквально 1 на 1000 не вміють ховатися. Я думаю, це пояснюється тим, що авторам троянів лінь, адже для цього необов’язково писати щось своє, завжди можна взяти готовий исходник і вставити у свою програму. Тому слід очікувати, що скоро приховування процесів буде застосовуватися у всіх широкораспостраненных рядових троянах.

Природно, від цього потрібно мати захист. Виробники антивірусів і фаерволлов відстали від життя, так як їх продукти не вміють виявляти приховані процеси. Для цього існує тільки кілька утиліт, з яких єдиною безкоштовною є Klister(працює тільки на Windows 2000), а за інші виробники вимагають чималих грошей. Причому всі ці утиліти досить легко обходяться.

Всі наявні зараз програми для виявлення прихованих процесів побудовані на якомусь одному принципі, тому для їх обходу можна придумати метод приховування від конкретного принципу виявлення, або прив’язуватися до однієї конкретної програми, що набагато простіше в реалізації. Користувач купив комерційну програму не може змінити її, а тому прив’язка до конкретної програмі буде працювати досить надійно, тому цей метод використовується в комерційних руткитах (наприклад hxdef Golden edition). Єдиним виходом буде створення безкоштовної Opensource програми для виявлення прихованих процесів в якій будуть застосовані декілька методів виявлення, що дозволить захиститися від фундаментальних принципів приховування, а від прив’язки до конкретних програм може захиститися кожен користувач, для цього потрібно всього лише взяти вихідні коди програми і переробити її під себе.

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

Виявлення User Mode
Для початку розглянемо прості методи виявлення, які можуть бути застосовані в 3 кільці, без використання драйверів. Вони засновані на тому, що кожен запущений процес породжує побічні прояви своєї діяльності, по яким його й можна виявити. Цими проявами можуть бути відкриті їм хэндлы, вікна, створені системні об’єкти. Від подібних методик виявлення нескладно сховатися, але для цього потрібно врахувати ВСІ побічні прояви процесу. Ні в одному з публічних руткітів це поки ще не зроблено (приватні версії на жаль до мене не потрапили). Юзермодные методи прості в реалізації, безпечні в застосуванні, і можуть дати позитивний ефект, тому їх використанням не варто нехтувати.

Для початку визначимося з форматом даних, що повертаються функціями пошуку, нехай це будуть зв’язані списки:

type
PProcList = ^TProcList;
TProcList = packed record
NextItem: pointer;
ProcName: array [0..MAX_PATH] of Char;
ProcId: dword;
ParrentId: dword;
end;

Отримання списку процесів через ToolHelp API
Для початку визначимо взірцеву функцію одержує список процесів, за її результатами ми будемо порівнювати результати отримані всіма іншими способами:

{
Отримання списку процесів через ToolHelp API.
}
procedure GetToolHelpProcessList(var List: PListStruct);
var
Snap: dword;
Process: TPROCESSENTRY32;
NewItem: PProcessRecord;
begin
Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if Snap INVALID_HANDLE_VALUE then
begin
Process.dwSize := SizeOf(TPROCESSENTRY32);
if Process32First(Snap, Process) then
repeat
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := Process.th32ProcessID;
NewItem^.ParrentPID := Process.th32ParentProcessID;
lstrcpy(@NewItem^.ProcessName, Process.szExeFile);
AddItem(List, NewItem);
until not Process32Next(Snap, Process);
CloseHandle(Snap);
end;
end;
Очевидно, що будь-прихований процес при такому перерахуванні знайдений не буде, тому ця функція буде зразковою для відділення прихованих процесів від нескрытых.

Отримання списку процесів через Native API
Наступним рівнем перевірки буде отримання списку процесів через ZwQuerySystemInformation (Native API). На цьому рівні також навряд чи що-небудь виявитися, але перевірити все-таки варто.

{
Отримання списку процесів через ZwQuerySystemInformation.
}
procedure GetNativeProcessList(var List: PListStruct);
var
Info: PSYSTEM_PROCESSES;
NewItem: PProcessRecord;
Mem: pointer;
begin
Info := GetInfoTable(SystemProcessesAndThreadsInformation);
Mem := Info;
if Info = nil then Exit;
repeat
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
lstrcpy(@NewItem^.ProcessName,
PChar(WideCharToString(Info^.ProcessName.Buffer)));
NewItem^.ProcessId := Info^.ProcessId;
NewItem^.ParrentPID := Info^.InheritedFromProcessId;
AddItem(List, NewItem);
Info := pointer(dword(info) + info^.NextEntryDelta);
until Info^.NextEntryDelta = 0;
VirtualFree(Mem, 0, MEM_RELEASE);
end;

Отримання списку процесів за списком відкритих хэндлов.
Багато програми приховують процес, не приховують відкриті їм хэндлы, отже перерахувавши відкриті хэндлы через ZwQuerySystemInformation ми можемо побудувати список процесів.

{
Отримання списку процесів за списком відкритих хэндлов.
Повертає тільки ProcessId.
}
procedure GetHandlesProcessList(var List: PListStruct);
var
Info: PSYSTEM_HANDLE_INFORMATION_EX;
NewItem: PProcessRecord;
r: dword;
OldPid: dword;
begin
OldPid := 0;
Info := GetInfoTable(SystemHandleInformation);
if Info = nil then Exit;
for r := 0 to Info^.NumberOfHandles do
if Info^.Information[r].ProcessId OldPid then
begin
OldPid := Info^.Information[r].ProcessId;
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := OldPid;
AddItem(List, NewItem);
end;
VirtualFree(Info, 0, MEM_RELEASE);
end;
На цьому етапі вже можна дещо знайти. Але покладатися на результат такої перевірки не варто, так як приховати відкриті процесом хэндлы нітрохи не складніше, ніж приховати сам процес, просто багато хто забуває це робити.

Отримання списку процесів за списком відкритих ними вікон.
Отримавши список вікон зареєстрованих в системі і викликавши для кожного GetWindowThreadProcessId можна побудувати список процесів мають вікна.

{
Отримання списку процесів за списком вікон.
Повертає тільки ProcessId.
}
procedure GetWindowsProcessList(var List: PListStruct);

function EnumWindowsProc(hwnd: dword; PList: PPListStruct): bool; stdcall;
var
ProcId: dword;
NewItem: PProcessRecord;
begin
GetWindowThreadProcessId(hwnd, ProcId);
if not IsPidAdded(PList^, ProcId) then
begin
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := ProcId;
AddItem(PList^, NewItem);
end;
Result := true;
end;

begin
EnumWindows(@EnumWindowsProc, dword(@List));
end;

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

Отримання списку процесів з допомогою прямого системного виклику.
Для приховування процесів в User Mode зазвичай використовується технологія впровадження свого коду в чужі процеси і перехоплення функції ZwQuerySystemInformation з ntdll.dll. Функції ntdll насправді є перехідниками до відповідних функцій ядра системи, і представляють з себе звернення до інтерфейсу системних викликів (Int 2Eh в Windows 2000 або sysenter в XP), тому найбільш простим і ефективним способом виявлення прихованих процесів Usermode API перехоплювачами буде пряме звернення до інтерфейсу системних викликів минаючи API.

Варіант функції замінює ZwQuerySystemInformation буде виглядати для Windows XP так:

{
Системний виклик ZwQuerySystemInformation для Windows XP.
}
Function XpZwQuerySystemInfoCall(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength: pdword): dword; stdcall;
asm
pop ebp
mov eax, $AD
call @SystemCall
ret $10
@SystemCall:
mov edx, esp
sysenter
end;
У зв’язку з іншим інтерфейсом системних викликів, Windows 2000 цей код буде виглядати інакше.

{
Системний виклик ZwQuerySystemInformation для Windows 2000.
}
Function Win2kZwQuerySystemInfoCall(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength: pdword): dword; stdcall;
asm
pop ebp
mov eax, $97
lea edx, [esp + $04]
int $2E
ret $10
end;
Тепер залишається перерахувати процеси не за допомогою функцій з ntdll.dll, а з допомогою певних функцій. Ось код, який це робить:

{
Отримання списку процесів через системний виклик
ZwQuerySystemInformation.
}
procedure GetSyscallProcessList(var List: PListStruct);
var
Info: PSYSTEM_PROCESSES;
NewItem: PProcessRecord;
mPtr: pointer;
mSize: dword;
St: NTStatus;
begin
mSize := $4000;
repeat
GetMem(mPtr, mSize);
St := ZwQuerySystemInfoCall(SystemProcessesAndThreadsInformation,
mPtr, mSize, nil);
if St = STATUS_INFO_LENGTH_MISMATCH then
begin
FreeMem(mPtr);
mSize := mSize * 2;
end;
until St STATUS_INFO_LENGTH_MISMATCH;
if St = STATUS_SUCCESS then
begin
Info := mPtr;
repeat
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
lstrcpy(@NewItem^.ProcessName,
PChar(WideCharToString(Info^.ProcessName.Buffer)));
NewItem^.ProcessId := Info^.ProcessId;
NewItem^.ParrentPID := Info^.InheritedFromProcessId;
Info := pointer(dword(info) + info^.NextEntryDelta);
AddItem(List, NewItem);
until Info^.NextEntryDelta = 0;
end;
FreeMem(mPtr);
end;
Цей метод практично 100% виявляє юзермодные руткіти, наприклад всі версії hxdef (в тому числі і Golden) їм виявляються.

Отримання списку процесів шляхом аналізу пов’язаних з ним хэндлов.
Також, можна застосувати ще один метод заснований на перерахування хэндлов. Його суть полягає в тому, щоб знайти хэндлы відкриті шуканим процесом, а хэндлы інших процесів пов’язані з ним. Це можуть бути хэндлы самого процесу або його потоків. При отриманні хэндла процесу, можна визначити його PID з ZwQueryInformationProcess. Для потоку можна викликати ZwQueryInformationThread і отримати Id його процесу. Всі процеси в системі були запущені, отже батьківські процеси будуть мати їх хэндлы (якщо тільки не встигли їх закрити), також хэндлы всіх працюючих процесів маються на сервері підсистеми Win32 (csrss.exe). У Windows NT активно використовуються Job об’єкти, які дозволяють об’єднувати процеси (наприклад, процеси певного прользователя, або які-небудь служби), отже при знаходженні хэндла Job об’єкта, не варто нехтувати можливістю отримати Id всіх об’єднаних їм процесів. Робиться це за допомогою функції QueryInformationJobObject з класом інформації — JobObjectBasicProcessIdList. Код виробляє пошук процесів шляхом аналізу відкритих іншими процесами хэндлов буде виглядати так:

{
Отримання списку процесів через перевірку хэнжлов в інших процесах.
}
procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean);
var
HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
ProcessInfo: PROCESS_BASIC_INFORMATION;
hProcess: dword;
tHandle: dword;
r, l: integer;
NewItem: PProcessRecord;
Info: PJOBOBJECT_BASIC_PROCESS_ID_LIST;
Size: dword;
THRInfo: THREAD_BASIC_INFORMATION;
begin
HandlesInfo := GetInfoTable(SystemHandleInformation);
if HandlesInfo nil then
for r := 0 to HandlesInfo^.NumberOfHandles do
if HandlesInfo^.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then
begin
hProcess := OpenProcess(PROCESS_DUP_HANDLE, false,
HandlesInfo^.Information[r].ProcessId);

if DuplicateHandle(hProcess, HandlesInfo^.Information[r].Handle,
INVALID_HANDLE_VALUE, @tHandle, 0, false,
DUPLICATE_SAME_ACCESS) then
begin
case HandlesInfo^.Information[r].ObjectTypeNumber of
OB_TYPE_PROCESS: begin
if Processes and (HandlesInfo^.Information[r].ProcessId = CsrPid) then
if ZwQueryInformationProcess(tHandle, ProcessBasicInformation,
@ProcessInfo,
SizeOf(PROCESS_BASIC_INFORMATION),
nil) = STATUS_SUCCESS then
if not IsPidAdded(List, ProcessInfo.UniqueProcessId) then
begin
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := ProcessInfo.UniqueProcessId;
NewItem^.ParrentPID := ProcessInfo.InheritedFromUniqueProcessId;
AddItem(List, NewItem);
end;
end;

OB_TYPE_JOB: begin
if Jobs then
begin
Size := SizeOf(JOBOBJECT_BASIC_PROCESS_ID_LIST) + 4 * 1000;
GetMem(Info, Size);
Info^.NumberOfAssignedProcesses := 1000;
if QueryInformationJobObject(tHandle, JobObjectBasicProcessIdList,
Info, Size, nil) then
for l := 0 to Info^.NumberOfProcessIdsInList — 1 do
if not IsPidAdded(List, Info^.ProcessIdList[l]) then
begin
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := Info^.ProcessIdList[l];
AddItem(List, NewItem);
end;
FreeMem(Info);
end;
end;

OB_TYPE_THREAD: begin
if then Threads
if ZwQueryInformationThread(tHandle, THREAD_BASIC_INFO,
@THRInfo,
SizeOf(THREAD_BASIC_INFORMATION),
nil) = STATUS_SUCCESS then
if not IsPidAdded(List, THRInfo.ClientId.UniqueProcess) then
begin
GetMem(NewItem, SizeOf(TProcessRecord));
ZeroMemory(NewItem, SizeOf(TProcessRecord));
NewItem^.ProcessId := THRInfo.ClientId.UniqueProcess;
AddItem(List, NewItem);
end;
end;

end;
CloseHandle(tHandle);
end;
CloseHandle(hProcess);
end;
VirtualFree(HandlesInfo, 0, MEM_RELEASE);
end;

На жаль, деякі з вищенаведених методів дозволяють визначити тільки ProcessId, але не ім’я процесу. Отже, нам потрібно вміти отримати ім’я процесу з pid. ToolHelp API для цього використовувати природно не варто, так як процес можкт бути прихованим, тому ми будемо відкривати пам’ять процесу на читання і читьть ім’я його PEB. Адреса PEB в процесі можна визначити за допомогою функції ZwQueryInformationProcess. А ось і код здійснює всі це:

function GetNameByPid(Pid: dword): string;
var
hProcess, Bytes: dword;
Info: PROCESS_BASIC_INFORMATION;
ProcessParametres: pointer;
ImagePath: TUnicodeString;
ImgPath: array[0..MAX_PATH] of WideChar;
begin
Result := “;
ZeroMemory(@ImgPath, MAX_PATH * SizeOf(WideChar));
hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, Pid);
if ZwQueryInformationProcess(hProcess, ProcessBasicInformation, @Info,
SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then
begin
if ReadProcessMemory(hProcess, pointer(dword(Info.PebBaseAddress) + $10),
@ProcessParametres, SizeOf(pointer), Bytes) and
ReadProcessMemory(hProcess, pointer(dword(ProcessParametres) + $38),
@ImagePath, SizeOf(TUnicodeString), Bytes) and
ReadProcessMemory(hProcess, ImagePath.Buffer, @ImgPath,
ImagePath.Length, Bytes) then
begin
Result := ExtractFileName(WideCharToString(ImgPath));
end;
end;
CloseHandle(hProcess);
end;
Природно, юзермодные методи виявлення на цьому не закінчуються. Якщо докласти трохи зусиль, то можна придумати ще кілька нових (наприклад завантаження своєї Dll у доступні процеси з допомогою SetWindowsHookEx з подальшим аналізом списку процесів, де наша Dll виявилася), але поки цих методів нам вистачить. Їх перевага в тому, що вони прості в програмуванні, але дозволяють виявити тільки процеси приховані API перехопленням у User Mode, або погано приховані Kernel Mode. Для дійсно надійного виявлення прихованих процесів нам доведеться писати драйвер і працювати з внутрішніми структурами ядра Windows.

Kernel Mode detection
Ось ми і дійшли до методів виявлення прихованих процесів в режимі ядра. Від юзермодных методів вони відрізняються в першу чергу тим, що списки процесів можна побудувати не використовуючи API, а працюючи безпосередньо з структурами планувальника. Сховатися від таких методів виявлення набагато важче, так як вони засновані на самих принципах роботи системи, і видалення всіх слідів процесу зі списків планувальника призведе до неможливості його роботи.

Що представляє з себе процес зсередини? Кожен процес має свій адресний простір, свої дескриптори, потоки, і. т. д. З цими речами пов’язані відповідні структури ядра. Кожен процес описується структурою EPROCESS, структури всіх процесів пов’язані кільцевої двухсвязный список. Один з методів приховування процесів полягає у зміні покажчиків так, щоб перерахування йшло в обхід приховуваного процесу. Для роботи процесу некритично, чи буде він брати участь у перерахуванні чи ні. Але структура EPROCESS завжди повинна бути, вона необхідна для роботи процесу. Більшість методів виявлення прихованих процесів в Kernel Mode так чи інакше пов’язані з виявленням цієї структури.

Спочатку визначимося з форматом зберігання отриманої інформації про процеси. Цей Формат повинен бути зручний для передачі драйвер в додаток. Нехай цим форматом буде наступна структура:

typedef struct _ProcessRecord
{
ULONG Visibles;
ULONG SignalState;
BOOLEAN Present;
ULONG ProcessId;
ULONG ParrentPID;
PEPROCESS pEPROCESS;
CHAR ProcessName[256];
} TProcessRecord, *PProcessRecord;
Нехай структури розташовуються в пам’яті по порядку, і в останній з них скинутий прапор Present.

Отримання списку процесів через ZwQuerySystemInformation в ядрі.
Почнемо як завжди з простого, з отримання зразкового списку процесів через ZwQuerySystemInformation:

PVOID GetNativeProcessList(ULONG *MemSize)
{
ULONG PsCount = 0;
PVOID Info = GetInfoTable(SystemProcessesAndThreadsInformation);
PSYSTEM_PROCESSES Proc;
PVOID Mem = NULL;
PProcessRecord Data;

if (!Info) return NULL; else Proc = Info;

do
{
Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
PsCount++;
} while (Proc->NextEntryDelta);

*MemSize = (PsCount + 1) * sizeof(TProcessRecord);

Mem = ExAllocatePool(PagedPool, *MemSize);

if (!Mem) return NULL; else Data = Mem;

Proc = Info;
do
{
Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
wcstombs(Data->ProcessName, Proc->ProcessName.Buffer, 255);
Data->Present = TRUE;
Data->ProcessId = Proc->ProcessId;
Data->ParrentPID = Proc->InheritedFromProcessId;
PsLookupProcessByProcessId((HANDLE)Proc->ProcessId, &Data->pEPROCESS);
ObDereferenceObject(Data->pEPROCESS);
Data++;
} while (Proc->NextEntryDelta);

Data->Present = FALSE;

ExFreePool(Info);

return Mem;
}
Нехай ця функція буде зразковою, так як будь-Kernel Mode метод приховування процесу не буде нею виявлено. Але юзермодные руткіти типу hxdef будуть тут виявлено.

У цьому коді застосовується функція GetInfoTable для простого отримання інформації. Для того щоб не виникало питань що це таке я приведу її тут повністю:

/*
Отримання буфера з результатом ZwQuerySystemInformation.
*/
PVOID GetInfoTable(ULONG ATableType)
{
ULONG mSize = 0x4000;
PVOID mPtr = NULL;
NTSTATUS St;
do
{
mPtr = ExAllocatePool(PagedPool, mSize);
memset(mPtr, 0, mSize);
if (mPtr)
{
St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);
} else return NULL;
if (St == STATUS_INFO_LENGTH_MISMATCH)
{
ExFreePool(mPtr);
mSize = mSize * 2;
}
} while (St == STATUS_INFO_LENGTH_MISMATCH);
if (St == STATUS_SUCCESS) return mPtr;
ExFreePool(mPtr);
return NULL;
}
Я думаю, що розуміння сенсу цієї функції ні в кого труднощів не викличе.

Отримання списку процесів з двусвязного списку структур EPROCESS.
Отже, йдемо далі. Наступним кроком буде отримання списку процесів проходом по двухсвязному списком структур EPROCESS. Список починається з голови — PsActiveProcessHead, тому для коректного перерахування процесів нам спочатку потрібно знайти цей неэкспортируемый символ. Для цього найпростіше скористатися тим властивістю, що процес System є першим у списку процесів. Нам потрібно перебуваючи в DriverEntry отримати покажчик на поточний процес з допомогою PsGetCurrentProcess (драйвера було завантажені з допомогою SC Manager API або ZwLoadDriver завжди вантажаться в контексті процесу System), і Blink по зсуву ActiveProcessLinks буде вказувати на PsActiveProcessHead. Виглядає це приблизно так:

PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4);
Тепер можна пройтися по двухсвязному списком та побудувати список процесів:

PVOID GetEprocessProcessList(ULONG *MemSize)
{
PLIST_ENTRY Process;
ULONG PsCount = 0;
PVOID Mem = NULL;
PProcessRecord Data;

if (!PsActiveProcessHead) return NULL;

Process = PsActiveProcessHead->Flink;

while (Process != PsActiveProcessHead)
{
PsCount++;
Process = Process->Flink;
}

PsCount++;

*MemSize = PsCount * sizeof(TProcessRecord);

Mem = ExAllocatePool(PagedPool, *MemSize);
memset(Mem, 0, *MemSize);

if (!Mem) return NULL; else Data = Mem;

Process = PsActiveProcessHead->Flink;

while (Process != PsActiveProcessHead)
{
Data->Present = TRUE;
Data->ProcessId = *(PULONG)((ULONG)Process — ActPsLink + pIdOffset);
Data->ParrentPID = *(PULONG)((ULONG)Process — ActPsLink + ppIdOffset);
Data->SignalState = *(PULONG)((ULONG)Process — ActPsLink + 4);
Data->pEPROCESS = (PEPROCESS)((ULONG)Process — ActPsLink);
strncpy(Data->ProcessName, (PVOID)((ULONG)Process — ActPsLink + NameOffset), 16);
Data++;
Process = Process->Flink;

}

return Mem;
}
Для отримання імені процесу, його Process Id і ParrentProcessId використовуються зміщення даних полів у структурі EPROCESS (pIdOffset, ppIdOffset, NameOffset, ActPsLink). Ці зміщення розрізняються в різних версіях Windows, тому їх отримання винесено в окрему функцію, яку ви можете побачити у вихідному коді програми Process Hunter (у додатку до статті).

Будь приховування процесу методом перехоплення API буде виявлено вищенаведеним способом. Але якщо процес схований за допомогою методу DCOM (Direct Kernel Object Manipulation), то цей спосіб не допоможе, так як при цьому процес видаляється зі списку процесів.

Отримання списку процесів за списками потоків планувальника.
Один з методів виявлення такого приховування полягає в получнии списку процесів за списком потоків в планувальнику. У Windows 2000 є три двусвязных списку потоків: KiWaitInListHead, KiWaitOutListHead, KiDispatcherReadyListHead. Перші два списки містять потоки очікують настання якого-небудь події, а третій містить потоки готові до виконання. Пройшовшись по списками і вычев зсув списку потоків в стуктуре ETHREAD ми отримаємо вказівник на ETHREAD потоку. Ця структура містить кілька покажчиків на процес пов’язаний з потоком, це struct _KPROCESS *Process (0x44, 0x150) і sruct _EPROCESS *ThreadsProcess (0x22C, зміщення вказані для Windows 2000). Перші два покажчика не мають жодного впливу на роботу потоку, тому легко можуть бути підмінені в цілях приховування. А третій покажчик використовується планувальником при перемиканні адресних просторів, тому підмінений бути не може. Його ми і будемо використовувати для визначення процесу, що володіє потоком.

Цей метод виявлення застосовується у програмі klister, головний недолік якої — робота тільки під Windows 2000 (і то не з усіма сервиспаками). Обумовлений цей недолік тим, що в Klister жорстко зашиті адреси списків потоків, які змінюються майже з кожним сервиспаком системи.

Зашивати адреси списків в програму — це дуже поганий метод, так як гарантує непрацездатність програми з наступними оновленнями ОС, так і допомагає сховатися від цього методу виявлення, тому адреси списків доведеться шукати динамічно, аналізом коду функцій, в яких вони використовуються.

Для початку спробуємо знайти KiWaitItListHead і KiWaitOutListHead в Windows 2000. Адреси цих списків використовуються у функції KeWaitForSingleObject в коді наступного виду:

.text:0042DE56 mov ecx, offset KiWaitInListHead
.text:0042DE5B test al, al
.text:0042DE5D jz short loc_42DE6E
.text:0042DE5F cmp byte ptr [esi+135h], 0
.text:0042DE66 jz short loc_42DE6E
.text:0042DE68 cmp byte ptr [esi+33h], 19h
.text:0042DE6C jl short loc_42DE73
.text:0042DE6E mov ecx, offset KiWaitOutListHead

Для отримання адрес цих списків треба пройтися дизассемблером довжин інструкцій (будемо використовувати мій LDasm) за KeWaitForSingleObject і коли вказівник (pOpcode) буде на команді mov ecx, KiWaitInListHead, то pOpcode + 5 буде вказувати на test al, al, а pOpcode + 24 на mov ecx, KiWaitOutListHead. Після цього адреси KiWaitItListHead і KiWaitOutListHead витягуються за вказівниками pOpcode + 1 і pOpcode + 25 відповідно. Код пошуку цих адрес буде виглядати так:

void Win2KGetKiWaitInOutListHeads()
{
PUCHAR cPtr, pOpcode;
ULONG Length;

for (cPtr = (PUCHAR)KeWaitForSingleObject;
cPtr Flink;

while (Item != ListHead)
{
CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
Item = Item->Flink;
}
}

return;
}
CollectProcess — це функція додає процес у список, якщо він ще не був туди доданий.

Отримання списку процесів перехоплення системних викликів.
Кожен працюючий процес взаємодіє з системою через API, і більшість цих запитів перетворюються в обігу до ядра системи через інтерфейс системних викликів. Звичайно, процес може працювати не викликаючи API, але тоді ніякої корисної (або шкідливої) роботи він виконувати не зможе. Загалом ідея полягає в тому, щоб перехопити звернення до інтерфейсу системних викликів, а в оброблювачі отримувати вказівник на EPROCESS поточного процесу. Список покажчиків доведеться збирати певний час, і в нього не увійдуть процеси жодного разу не виконували системні виклики за час збору цієї інформації (наприклад, процеси, потоки яких знаходяться в стані очікування).

У windows 2000 для системного виклику використовується переривання 2Eh, тому для перехоплення системних викликів нам потрібно змінити дескриптор відповідного переривання іdt. Для цього нам потрібно спочатку визначити положення idt в пам’яті за допомогою команди sidt. Ця команда повертає наступну структуру:

typedef struct _Idt
{
USHORT Size;
ULONG Base;
} TIdt;
Код змінює вектор переривання 2Eh буде виглядати так:

void Set2kSyscallHook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, NewSyscall
mov ebx, Іdt.Base
xchg [ebx + 0x170], si
rol esi, 0x10
xchg [ebx + 0x176], si
ror esi, 0x10
mov OldSyscall, esi
sti
popad
}
}
Природно, перед вивантаженням драйвера потрібно відновлювати:

void Win2kSyscallUnhook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, OldSyscall
mov ebx, Іdt.Base
mov [ebx + 0x170], si
rol esi, 0x10
mov [ebx + 0x176], si
sti
xor eax, eax
mov OldSyscall, eax
popad
}
}
У Windows XP використовується інтерфейс системних викликів побудований на основі команди sysenter/sysexit які з’явилися в процесорах Pentium 2. Роботою цих команд керують модельно-специфічні регістри (MSR). Адреса обробника системного виклику задається в MSR регістрі SYSENTER_EIP_MSR (номер 0x176). Читання MSR регістра виконується командою rdmsr, перед цим ЕСХ повинен бути поміщений номер читаного регістра, а результат поміщається в пару регістрів EDX:EAX. У нашому випадку регістр SYSENTER_EIP_MSR 32 бітний, тому в EDX буде 0, а в EAX адресу обробника системних викликів. Аналогічно, за допомогою wrmsr виконується запис у MSR регістр. Але тут є один підводний камінь: при записі в 32 бітний MSR регістр, EDX повинен бути обнулений, інакше це викличе винятків і призведе до негайного падіння системи.

З урахуванням вищесказаного, код замінює оброблювач системних викликів буде виглядати так:

void SetXpSyscallHook()
{
__asm
{
pushad
mov ecx, 0x176
rdmsr
mov OldSyscall, eax
mov eax, NewSyscall
xor edx, edx
wrmsr
popad
}
}
А відновлюючий старий обробник так:

void XpSyscallUnhook()
{
__asm
{
pushad
mov ecx, 0x176
mov eax, OldSyscall
xor edx, edx
wrmsr
xor eax, eax
mov OldSyscall, eax
popad
}
}
Особливість Windows XP в тому, що системний виклик може бути проведений як через sysenter, так і через int 2Eh, тому нам потрібно замінити обидва обробника своїми.

Новий обробник системного виклику повинен отримати покажчик на EPROCESS поточного процесу, і якщо це новий процес, додати цей процес у списки.

Відповідно, новий обробник системного виклику буде виглядати так:

void __declspec(naked) NewSyscall()
{
__asm
{
pushad
pushfd
push fs
mov di, 0x30
mov fs, di
mov eax, fs:[0x124]
mov eax, [eax + 0x44]
push eax
call CollectProcess
pop fs
popfd
popad
jmp OldSyscall
}
}
Для одержання повного списку процесів цей код повинен працювати деякий час, і в зв’язку з цим виникає наступна проблема: якщо процес знаходиться в списку буде видалений, то при подальшому перегляді списку ми отримаємо неправильний вказівник, в результаті ми або помилково знайдемо прихований процес, або взагалі отримаємо BSOD. Виходом з цієї ситуації є реєстрація з допомогою PsSetCreateProcessNotifyRoutine Callback функції, яка буде викликана при створенні або завершення процесу. При завершенні процесу його потрібно видаляти зі списку. Callback функція має наступний прототип:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);
Встановлення обробника проводиться так:

PsSetCreateProcessNotifyRoutine(NotifyRoutine, FALSE);
А видалити так:

PsSetCreateProcessNotifyRoutine(NotifyRoutine, TRUE);
Тут існує один неочевидний момент, Callback функція завжди викликається в контексті завершаемого процесу, отже не можна видаляти процес зі списків прямо в ній. Для цього ми скористаємося робочими потоками системи, спочатку виділимо пам’ять під робочий потік з допомогою IoAllocateWorkItem, а потім помістимо своє завдання в чергу робочого потоку з допомогою IoQueueWorkItem. В самому процесорі будемо не тільки видаляти зі списку завершилися процеси, але і додавати створюються. А ось і код самого обробника:

void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)
{
KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);

DelItem(&wLastItem, Data->pEPROCESS);

ObDereferenceObject(Data->pEPROCESS);

IoFreeWorkItem(Data->IoWorkItem);

ExFreePool(Data);

return;
}

void NotifyRoutine(IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create)
{
PEPROCESS process;
PWorkItemStruct Data;

if (Create)
{
PsLookupProcessByProcessId(ProcessId, &process);

if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);

ObDereferenceObject(process);

} else
{
process = PsGetCurrentProcess();

ObReferenceObject(process);

Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));

Data->IoWorkItem = IoAllocateWorkItem(deviceObject);

Data->pEPROCESS = process;

IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);
}

return;
}
Це досить надійний спосіб виявлення прихованих процесів, так як без системних викликів не може обійтися жоден процес, але деякі процеси можуть довго перебувати в стані очікування і не здійснювати системні виклики протягом тривалого часу, такі процеси не будуть виявлені.

Обійти цей метод виявлення при бажанні також нескладно, для цього потрібно змінити метод виконання системного виклику в приховуваних процесах (перебудувати на інше переривання або на каллгейт в GDT). Особливо легко це зробити для Windows XP, так як досить пропатчити KiFastSystemCall в ntdll.dll і створити відповідний шлюз для системного виклику. У Windows 2000 це зробити трохи складніше, так як там виклики int 2E розкидані по ntdll, але знайти і пропатчити всі ці місця також не дуже складно, тому повністю покладатися на результати цієї перевірки теж не можна.

Отримання списку процесів переглядом списку таблиць хэндлов.
Якщо ви коли-небудь приховували процес методом його видалення зі списку PsActiveProcesses, то напевно звернули увагу на те, що при перерахуванні хэндлов з допомогою ZwQuerySystemInformation хэндлы прихованого процесу беруть участь в перерахуванні, в тому числі визначається його ProcessId. Відбувається це тому, що для зручності перерахування хэндлов, всі таблиці хэндлов обьединены в двусвязный список HandleTableList. Зміщення цього списку в структурі HANDLE_TABLE для Windows 2000 одно 0x054, а для Windows XP — 0x01C, починається цей список з HandleTableListHead. Структура HANDLE_TABLE містить у собі вказівник на що володіє їй процес (QuotaProcess), зміщення вказівника Windows 2000 одно 0x00C, а в Windows XP — 0x004. Пройшовши за списком таблиць хэндлов ми можемо побудувати з ним список процесів. Для початку нам потрібно знайти HandleTableListHead. Дизасемблювання ядра показало, що посилання на нього знаходяться глибоко під вкладених функціях, тому метод пошуку шляхом дизассемблирования коду, який ми застосовували раніше, тут зовсім не підходить. Але для пошуку HandleTableListHead можна використовувати властивість, що HandleTableListHead — це глобальна змінна ядра, і отже вона знаходиться в одній із секцій його PE файлу, а інші елементи HandleTableList знаходяться в динамічно виділеної пам’яті, а отже завжди будуть за його межами. З цього випливає, що нам потрібно отримати покажчик на HandleTable будь-якого процесу, і рухатися по связаному списку до тих пір, поки його елемент не виявиться всередині PE файлу ядра. Цей елемент і буде HandleTableListHead. Для визначення бази та розміру файлу ядра в пам’яті використовуємо функцію ZwQuerySystemInformation з класом SystemModuleInformation. Вона поверне нам масив описателей завантажених модулів, в якому першим елементом завжди буде ядро. З урахуванням всього вышесказаного, код пошуку HandleTableListHead буде виглядати так:

void GetHandleTableListHead()
{
PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);
ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;
ULONG NtoskrnlSize = Info->Modules[0].Size;
PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);
PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);
PLIST_ENTRY CurrTable;

ExFreePool(Info);

for (CurrTable = HandleTableList->Flink;
CurrTable != HandleTableList;
CurrTable = CurrTable->Flink)
{
if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable Flink;
CurrTable != HandleTableListHead;
CurrTable = CurrTable->Flink)
{
QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable — HandleTableListOffset + QuotaProcessOffset);
if (QuotaProcess) CollectProcess(QuotaProcess);
}
}
Цей метод виявлення прихованих процесів застосовується у програмах F-Secure Black Light і в останній версії KProcCheck. Як його обійти, я думаю ви самі здогадаєтеся.

Отримання списку процесів шляхом сканування PspCidTable.
Ще одна особливість приховування процесу з допомогою видалення його з PsActiveProcesses полягає в тому, що це ніяк не заважає відкриттю процесу з допомогою OpenProcess. На цій особливості побудований метод виявлення процесів шляхом перебору їх pid зі спробою відкрити такий процес. Цей метод я наводити не став, так як на мою думку, він позбавлений яких-небудь переваг, в загальному, можна сказати — черезжопный метод. Але сам факт його існування свідчить про те, що в системі існує ще якийсь список процесів крім PsActiveProcesses, по якому і відбувається відкриття процесу. При переборі ProcessId виявляється ще одна особливість — один процес може бути відкритий за кількома різними pid, а це наводить на думку про те, що другий список процесів являє собою ні що інше, як HANDLE_TABLE. Для того, щоб упевнитися в цьому, заглянемо в функцію ZwOpenProces:

PAGE:0049D59E; NTSTATUS __stdcall NtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID ClientId)
PAGE:0049D59E public NtOpenProcess
PAGE:0049D59E NtOpenProcess proc near
PAGE:0049D59E
PAGE:0049D59E ProcessHandle = dword ptr 4
PAGE:0049D59E DesiredAccess = dword ptr 8
PAGE:0049D59E ObjectAttributes= dword ptr 0Ch
PAGE:0049D59E ClientId = dword ptr 10h
PAGE:0049D59E
PAGE:0049D59E push 0C4h
PAGE:0049D5A3 push offset dword_413560; int
PAGE:0049D5A8 call sub_40BA92
PAGE:0049D5AD xor esi, esi
PAGE:0049D5AF mov [ebp-2Ch], esi
PAGE:0049D5B2 xor eax, eax
PAGE:0049D5B4 lea edi, [ebp-28h]
PAGE:0049D5B7 stosd
PAGE:0049D5B8 mov eax, large fs:124h
PAGE:0049D5BE mov al, [eax+140h]
PAGE:0049D5C4 mov [ebp-34h], al
PAGE:0049D5C7 test al, al
PAGE:0049D5C9 jz loc_4BE034
PAGE:0049D5CF mov [ebp-4], esi
PAGE:0049D5D2 mov eax, MmUserProbeAddress
PAGE:0049D5D7 mov ecx, [ebp+8]
PAGE:0049D5DA cmp ecx, eax
PAGE:0049D5DC jnb loc_520CDE
PAGE:0049D5E2 loc_49D5E2:
PAGE:0049D5E2 mov eax, [ecx]
PAGE:0049D5E4 mov [ecx], eax
PAGE:0049D5E6 mov ebx, [ebp+10h]
PAGE:0049D5E9 test bl, 3
PAGE:0049D5EC jnz loc_520CE5
PAGE:0049D5F2 loc_49D5F2:
PAGE:0049D5F2 mov eax, MmUserProbeAddress
PAGE:0049D5F7 cmp ebx, eax
PAGE:0049D5F9 jnb loc_520CEF
PAGE:0049D5FF loc_49D5FF:
PAGE:0049D5FF cmp [ebx+8], esi
PAGE:0049D602 setnz byte ptr [ebp-1Ah]
PAGE:0049D606 mov ecx, [ebx+0Ch]
PAGE:0049D609 mov [ebp-38h], ecx
PAGE:0049D60C mov ecx, [ebp+14h]
PAGE:0049D60F cmp ecx, esi
PAGE:0049D611 jz loc_4CCB88
PAGE:0049D617 test cl, 3
PAGE:0049D61A jnz loc_520CFB
PAGE:0049D620 loc_49D620:
PAGE:0049D620 cmp ecx, eax
PAGE:0049D622 jnb loc_520D0D
PAGE:0049D628 loc_49D628:
PAGE:0049D628 mov eax, [ecx]
PAGE:0049D62A mov [ebp-2Ch], eax
PAGE:0049D62D mov eax, [ecx+4]
PAGE:0049D630 mov [ebp-28h], eax
PAGE:0049D633 mov byte ptr [ebp-19h], 1
PAGE:0049D637 loc_49D637:
PAGE:0049D637 or dword ptr [ebp-4], 0FFFFFFFFh
PAGE:0049D63B loc_49D63B:
PAGE:0049D63B
PAGE:0049D63B cmp byte ptr [ebp-1Ah], 0
PAGE:0049D63F jnz loc_520D34
PAGE:0049D645 loc_49D645:
PAGE:0049D645 mov eax, PsProcessType
PAGE:0049D64A add eax, 68h
PAGE:0049D64D push eax
PAGE:0049D64E push dword ptr [ebp+0Ch]
PAGE:0049D651 lea eax, [ebp-0D4h]
PAGE:0049D657 push eax
PAGE:0049D658 lea eax, [ebp-0B8h]
PAGE:0049D65E push eax
PAGE:0049D65F call SeCreateAccessState
PAGE:0049D664 cmp eax, esi
PAGE:0049D666 jl loc_49D718
PAGE:0049D66C push dword ptr [ebp-34h]; PreviousMode
PAGE:0049D66F push ds:stru_5B6978.HighPart
PAGE:0049D675 push ds:stru_5B6978.LowPart; PrivilegeValue
PAGE:0049D67B call SeSinglePrivilegeCheck
PAGE:0049D680 test al, al
PAGE:0049D682 jnz loc_4AA7DB
PAGE:0049D688 loc_49D688:
PAGE:0049D688 cmp byte ptr [ebp-1Ah], 0
PAGE:0049D68C jnz loc_520D52
PAGE:0049D692 cmp byte ptr [ebp-19h], 0
PAGE:0049D696 jz loc_4CCB9A
PAGE:0049D69C mov [ebp-30h], esi
PAGE:0049D69F cmp [ebp-28h], esi
PAGE:0049D6A2 jnz loc_4C1301
PAGE:0049D6A8 lea eax, [ebp-24h]
PAGE:0049D6AB push eax
PAGE:0049D6AC push dword ptr [ebp-2Ch]
PAGE:0049D6AF call PsLookupProcessByProcessId
PAGE:0049D6B4 loc_49D6B4:

Як ви бачите, цей код без?