Многие пользователи привыкли к
тому, что в 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 Threads then 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
перехвата будет обнаружено вышеприведенным
способом. Но если процесс скрыт с помощью метода
DKOM (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, поэтому для
перехвата системных вызовов нам нужно изменить
дескриптор соответствующего прерывания в idt. Для
этого нам нужно сначала определить положение 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,
Idt.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,
Idt.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:
Как вы видите, этот код
безопасным образом копирует переданные указатели,
проверяя присутствие их в границах
пользовательских адресов, проверяет права доступа
и наличие привилегии "SeDebugPrivilege", после
чего извлекает ProcessId из структуры CLIENT_ID и
передает его функции PsLookupProcessByProcessId,
задача которой - получить по ProcessId указатель
на EPROCESS. Дальнейшее продолжение функции
приводить не имеет смысла, поэтому заглянем теперь
в PsLookupProcessByProcessId:
PAGE:0049D725
public PsLookupProcessByProcessId PAGE:0049D725
PsLookupProcessByProcessId proc near
PAGE:0049D725
PAGE:0049D725 PAGE:0049D725 ProcessId =
dword ptr 8 PAGE:0049D725 Process = dword ptr
0Ch PAGE:0049D725 PAGE:0049D725 mov edi,
edi PAGE:0049D727 push ebp PAGE:0049D728 mov
ebp, esp PAGE:0049D72A push
ebx PAGE:0049D72B push esi PAGE:0049D72C mov
eax, large fs:124h PAGE:0049D732 push
[ebp+ProcessId] PAGE:0049D735 mov esi,
eax PAGE:0049D737 dec dword ptr
[esi+0D4h] PAGE:0049D73D push
PspCidTable PAGE:0049D743 call
ExMapHandleToPointer PAGE:0049D748 mov ebx,
eax PAGE:0049D74A test ebx,
ebx PAGE:0049D74C mov [ebp+ProcessId],
STATUS_INVALID_PARAMETER PAGE:0049D753 jz short
loc_49D787 PAGE:0049D755 push
edi PAGE:0049D756 mov edi,
[ebx] PAGE:0049D758 cmp byte ptr [edi],
3 PAGE:0049D75B jnz short
loc_49D77A PAGE:0049D75D cmp dword ptr
[edi+1A4h], 0 PAGE:0049D764 jz short
loc_49D77A PAGE:0049D766 mov ecx,
edi PAGE:0049D768 call
sub_4134A9 PAGE:0049D76D test al,
al PAGE:0049D76F jz short
loc_49D77A PAGE:0049D771 mov eax,
[ebp+Process] PAGE:0049D774 and
[ebp+ProcessId], 0 PAGE:0049D778 mov [eax],
edi PAGE:0049D77A loc_49D77A: PAGE:0049D77A
push ebx PAGE:0049D77B push
PspCidTable PAGE:0049D781 call
ExUnlockHandleTableEntry PAGE:0049D786 pop
edi PAGE:0049D787 loc_49D787: PAGE:0049D787
inc dword ptr [esi+0D4h] PAGE:0049D78D jnz
short loc_49D79A PAGE:0049D78F lea eax,
[esi+34h] PAGE:0049D792 cmp [eax],
eax PAGE:0049D794 jnz
loc_52388A PAGE:0049D79A loc_49D79A:
PAGE:0049D79A mov eax,
[ebp+ProcessId] PAGE:0049D79D pop
esi PAGE:0049D79E pop ebx PAGE:0049D79F pop
ebp PAGE:0049D7A0 retn 8
То что мы видим
здесь, подтверждает наличие второй таблицы
процессов, организованной как HANDLE_TABLE. Сама
таблица называется PspCidTable и хранит в себе
списки процессов и потоков, и используется еще в
функциях PsLookupProcessThreadByCid и
PsLookupThreadByThreadId. Как мы видим, хэндл и
указатель на таблицу хэндлов передаются функции
ExMapHandleToPointer, которая (при валидности
хэндла) возвращает указатель на элемент таблицы
описывающий данный хэндл - HANDLE_TABLE_ENTRY.
Скормив файл ntoskrnl.pdb программе PDBdump и
порывшись в полученном логе, можно откопать
следующее:
struct _HANDLE_TABLE_ENTRY
{ // static data
------------------------------------ //
non-static data
-------------------------------- /**/ /*|0x4|*/
void* Object; /**/ /*|0x4|*/ unsigned long
ObAttributes; /**/ /*|0x4|*/ struct
_HANDLE_TABLE_ENTRY_INFO* InfoTable; /**/
/*|0x4|*/ unsigned long Value; /**/ /*|0x4|*/
unsigned long GrantedAccess; /**/ /*|0x2|*/
unsigned short GrantedAccessIndex; /**/
/*|0x2|*/ unsigned short
CreatorBackTraceIndex; /**/ /*|0x4|*/ long
NextFreeTableEntry; };// Из этого можно
составить такую структуру
HANDLE_TABLE_ENTRY:
typedef struct
_HANDLE_TABLE_ENTRY { union { PVOID
Object; ULONG
ObAttributes; PHANDLE_TABLE_ENTRY_INFO
InfoTable; ULONG Value; };
union
{ union { ACCESS_MASK
GrantedAccess; struct { USHORT
GrantedAccessIndex; USHORT
CreatorBackTraceIndex; }; };
LONG
NextFreeTableEntry; }; } HANDLE_TABLE_ENTRY,
*PHANDLE_TABLE_ENTRY; Что полезного мы можем из
этого извлечь? Первым делом нас интересует
содержимое поля Object, которое является суммой
указателя на описываемый хэндлом объект и флага
указывающего на занятость данного элемента таблицы
(подробнее этот момент мы рассмотрим немного
позже). Весьма интересным является поле
GrantedAccess, которое указывает допустимые права
доступа к объекту по этому хэндлу. Нпример, можно
открыть файл на чтение, поправить поле
GrantedAccess и писать в этот файл. Подобный метод
можно использовать для чтения/записи файлов,
которых не удается открыть с требуемыми правами
доступа (например занятых другим процессом). Но
вернемся к нашей задаче - получить список
процессов путем просмотра PspCidTable. Для этого
нам нужно разобраться с форматом таблицы хендлов,
для того чтобы суметь их перечислить. С этого
момента начинается серьезная разница между Windows
2000 и Windows XP. Форматы их таблиц хэндлов
сильно отличаются, и нам придется разобраться с их
форматом для каждой ОС отдельно.
Для начала
рассмотрим формат таблицы хэндлов в Windows 2000,
так как там она гораздо проще для понимания. Для
начала заглянем в код функции
ExMapHandleToPointer:
PAGE:00493285
ExMapHandleToPointer proc near PAGE:00493285
PAGE:00493285 PAGE:00493285 HandleTable =
dword ptr 8 PAGE:00493285 Handle = dword ptr
0Ch PAGE:00493285 PAGE:00493285 push
esi PAGE:00493286 push
[esp+Handle] PAGE:0049328A push
[esp+4+HandleTable] PAGE:0049328E call
ExpLookupHandleTableEntry PAGE:00493293 mov
esi, eax PAGE:00493295 test esi,
esi PAGE:00493297 jz short
loc_4932A9 PAGE:00493299 push
esi PAGE:0049329A push
[esp+4+HandleTable] PAGE:0049329E call
ExLockHandleTableEntry PAGE:004932A3 neg
al PAGE:004932A5 sbb eax, eax PAGE:004932A7
and eax, esi PAGE:004932A9 loc_4932A9:
PAGE:004932A9 pop esi PAGE:004932AA retn
8 PAGE:004932AA ExMapHandleToPointer
endp
Здесь происходит вызов функции
ExMapHandleToPointer которая производит сам поиск
по HANDLE_TABLE, и вызов ExLockHandleTableEntry
которая устанавливает Lock Bit. Для понимания
работы таблицы хэндлов нам придется разобрать обе
эти функции. Начнем с
ExpLookupHandleTableEntry:
PAGE:00493545
ExpLookupHandleTableEntry proc near
PAGE:00493545
PAGE:00493545 PAGE:00493545 HandleTable =
dword ptr 0Ch PAGE:00493545 Handle = dword ptr
10h PAGE:00493545 PAGE:00493545 push
esi PAGE:00493546 push edi PAGE:00493547 mov
edi, [esp+Handle] PAGE:0049354B mov eax,
0FFh PAGE:00493550 mov ecx,
edi PAGE:00493552 mov edx, edi PAGE:00493554
mov esi, edi PAGE:00493556 shr ecx,
12h PAGE:00493559 shr edx, 0Ah PAGE:0049355C
shr esi, 2 PAGE:0049355F and ecx,
eax PAGE:00493561 and edx, eax PAGE:00493563
and esi, eax PAGE:00493565 test edi,
0FC000000h PAGE:0049356B jnz short
loc_49358A PAGE:0049356D mov eax,
[esp+HandleTable] PAGE:00493571 mov eax,
[eax+8] PAGE:00493574 mov ecx,
[eax+ecx*4] PAGE:00493577 test ecx,
ecx PAGE:00493579 jz short
loc_49358A PAGE:0049357B mov ecx,
[ecx+edx*4] PAGE:0049357E test ecx,
ecx PAGE:00493580 jz short
loc_49358A PAGE:00493582 lea eax,
[ecx+esi*8] PAGE:00493585 loc_493585:
PAGE:00493585 pop edi PAGE:00493586 pop
esi PAGE:00493587 retn 8 PAGE:0049358A
loc_49358A: PAGE:0049358A xor eax,
eax PAGE:0049358C jmp short
loc_493585 PAGE:0049358C
ExpLookupHandleTableEntry endp
В дополнение
к этому приведу структуру HANDLE_TABLE полученную
из дампа ntoskrnl.pdb:
struct _HANDLE_TABLE
{ // static data
------------------------------------ //
non-static data --------------------------------
/**/ /*|0x4|*/ unsigned long Flags; /**/
/*|0x4|*/ long HandleCount; /**/ /*|0x4|*/
struct _HANDLE_TABLE_ENTRY*** Table; /**/
/*|0x4|*/ struct _EPROCESS* QuotaProcess; /**/
/*|0x4|*/ void* UniqueProcessId; /**/
/*|0x4|*/ long FirstFreeTableEntry; /**/
/*|0x4|*/ long NextIndexNeedingPool; /**/
/*|0x38|*/ struct _ERESOURCE HandleTableLock;
/**/ /*|0x8|*/ struct _LIST_ENTRY
HandleTableList; /**/ /*|0x10|*/ struct _KEVENT
HandleContentionEvent; }; // По этим данным
восстановим структуру таблицы
хэндлов:
typedef struct _WIN2K_HANDLE_TABLE
{ ULONG Flags; LONG
HandleCount; PHANDLE_TABLE_ENTRY
**Table; PEPROCESS QuotaProcess; HANDLE
UniqueProcessId; LONG
FirstFreeTableEntry; LONG
NextIndexNeedingPool; ERESOURCE
HandleTableLock; LIST_ENTRY
HandleTableList; KEVENT
HandleContentionEvent; } WIN2K_HANDLE_TABLE ,
*PWIN2K_HANDLE_TABLE ; Из всего этого очевидно,
что значение хэндла раскладывается на три части,
которые являются индексами в трехуровневой таблице
объектов. Теперь посмотрим в функцию
ExLockHandleTableEntry:
PAGE:00492E2B
ExLockHandleTableEntry proc near PAGE:00492E2B
PAGE:00492E2B PAGE:00492E2B var_8 = dword
ptr -8 PAGE:00492E2B var_4 = dword ptr
-4 PAGE:00492E2B HandleTable = dword ptr
8 PAGE:00492E2B Entry = dword ptr
0Ch PAGE:00492E2B PAGE:00492E2B push
ebp PAGE:00492E2C mov ebp, esp PAGE:00492E2E
push ecx PAGE:00492E2F push
ecx PAGE:00492E30 push ebx PAGE:00492E31
push esi PAGE:00492E32 xor ebx,
ebx PAGE:00492E34 loc_492E34: PAGE:00492E34
mov eax, [ebp+Entry] PAGE:00492E37 mov esi,
[eax] PAGE:00492E39 test esi,
esi PAGE:00492E3B mov [ebp+var_8],
esi PAGE:00492E3E jz short
loc_492E89 PAGE:00492E40 jle short
loc_492E64 PAGE:00492E42 mov eax,
esi PAGE:00492E44 or eax, 80000000h // set
WIN2K_TABLE_ENTRY_LOCK_BIT PAGE:00492E49 mov
[ebp+var_4], eax PAGE:00492E4C mov eax,
[ebp+var_8] PAGE:00492E4F mov ecx,
[ebp+Entry] PAGE:00492E52 mov edx,
[ebp+var_4] PAGE:00492E55 cmpxchg [ecx],
edx PAGE:00492E58 cmp eax, esi PAGE:00492E5A
jnz short loc_492E64 PAGE:00492E5C mov al,
1 PAGE:00492E5E loc_492E5E: PAGE:00492E5E
pop esi PAGE:00492E5F pop ebx PAGE:00492E60
leave PAGE:00492E61 retn 8 PAGE:00492E64
loc_492E64: PAGE:00492E64 mov eax,
ebx PAGE:00492E66 inc ebx PAGE:00492E67 cmp
eax, 1 PAGE:00492E6A jb
loc_4BC234 PAGE:00492E70 mov eax,
[ebp+HandleTable] PAGE:00492E73 push offset
unk_46D240 ; Timeout PAGE:00492E78 push 0 ;
Alertable PAGE:00492E7A push 0 ;
WaitMode PAGE:00492E7C add eax,
5Ch PAGE:00492E7F push 0 ;
WaitReason PAGE:00492E81 push eax ;
Object PAGE:00492E82 call
KeWaitForSingleObject PAGE:00492E87 jmp short
loc_492E34 PAGE:00492E89 loc_492E89:
PAGE:00492E89 xor al, al PAGE:00492E8B jmp
short loc_492E5E PAGE:00492E8B
ExLockHandleTableEntry endp
Смысл данного
кода состоит в том, что он проверяет 31 бит в
элементе Object структуры HANDLE_TABLE_ENTRY,
устанавливает его, а в случае, если он установлен
- ждет HandleContentionEvent в HANDLE_TABLE. Для
нас важен лишь факт установки
TABLE_ENTRY_LOCK_BIT, так как он являтся частью
адреса объекта, и при сброшенном бите мы получим
невалидный адрес. С форматом таблицы хэндлов мы
вроде разобрались, теперь можно написать код
перебора объектов в таблице:
void
ScanWin2KHandleTable(PWIN2K_HANDLE_TABLE
HandleTable) { int i, j,
k; PHANDLE_TABLE_ENTRY Entry;
for (i =
0; i Table[i]) { for (j = 0; j
Table[i][j]) { for (k = 0; k
Table[i][j][k];
if (Entry->Object)
ProcessObject((PVOID)((ULONG)Entry->Object
| WIN2K_TABLE_ENTRY_LOCK_BIT)); }
CopKiller |