Моляков А. С., аспирант Российского государственного гуманитарного университета
ОБНАРУЖЕНИЕ СКРЫТЫХ ПРОЦЕССОВ В 3 КОЛЬЦЕ ЗАЩИТЫ ОС WINDOWS
Я рассмотрю простые методы обнаружения, которые могут быть применены в 3 кольце, без использования драйверов. Они основаны на том, что каждый запущенный процесс порождает побочные проявления своей деятельности, по которым его и можно обнаружить. Этими проявлениями могут быть открытые им хэндлы, окна, созданные системные объекты. От подобных методик обнаружения несложно скрыться, но для этого нужно учесть ВСЕ побочные проявления работы процесса. Ни в одном из публичных руткитов это пока еще не сделано (приватные версии к сожалению ко мне не попали). Юзермодные методы просты в реализации, безопасны в применении, и могут дать положительный эффект, поэтому их использованием не стоит пренебрегать. Для начала определимся с форматом данных возвращаемых функциями поиска, пусть это будут связанные списки: type
PProcList = ATProcList; TProcList = packed record Nextltem: 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)); NewItemA.ProcessId := Process.th32ProcessID; NewItemA.ParrentPID := Process.th32ParentProcessID; lstrcpy(@NewItemA.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(@NewItemA.ProcessName,
PChar(WideCharToString(InfoA.ProcessName.Buffer))); NewItemA.ProcessId := InfoA.ProcessId; NewItemA.ParrentPID := InfoA.InheritedFromProcessId; AddItem(List, NewItem);
Info := pointer(dword(info) + infoA.NextEntryDelta); until InfoA.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 InfoA.NumberOfHandles do if InfoA.Information[r].ProcessId <> OldPid then begin
OldPid := InfoA.Information[r].ProcessId; GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItemA.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(PListA, ProcId) then begin
GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItemA.ProcessId := ProcId; AddItem(PListA, 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(@NewItemA.ProcessName,
PChar(WideCharToString(InfoA.ProcessName.Buffer))); NewItemA.ProcessId := InfoA.ProcessId; NewItemA.ParrentPID := InfoA.InheritedFromProcessId; Info := pointer(dword(info) + infoA.NextEntryDelta); AddItem(List, NewItem); until InfoA.NextEntryDelta = 0; end;
FreeMem(mPtr); end;
Получение списка процессов путем анализа связанных с ним хэндлов Также, можно применить еще один метод основанный на перечислении хэндлов. Его суть состоит в том, чтобы найти не хэндлы открытые искомым процессом, а хэндлы других процессов связанные с ним. Это могут быть хэндлы самого процесса либо его потоков. При получении хэндла процесса, можно определить его PID с ZwQueryInformationProcess. Для потока можно вызвать ZwQueryInformationThread и получить Id его процесса. Все процессы существующие в системе были кем-то запущены, следовательно родительские процессы будут иметь их хэндлы (если только не успели их закрыть), также хэндлы всех работающих процессов имеются в сервере подсистемы Win32 (csrss.exe). Также в Windows NT активно используются Job объекты, которые позволяют обьединять процессы (например все процессы определенного прользователя, или какие-либо службы), следовательно при нахождении хэндла Job объекта, не стоит принебрегать возможностью получить Id всех обьединенных им процессов. Делается это с помощью функции QueryInformationJobObject с классом информации — JobObjectBasicProcessIdList. Код производящий поиск процесов путем анализа открытых другими процессами хэндлов будет выглядеть так: {
Получение списка процессов через проверку хэнжлов в других процессах.
}
procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean); var
EcmecmeeHHbie u mexHunecKue HayKu, № 6, 2006
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 HandlesInfoA.NumberOfHandles do if HandlesInfoA.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then begin
hProcess := OpenProcess(PROCESS_DUP_HANDLE, false, HandlesInfoA.Information[r].ProcessId);
if DuplicateHandle(hProcess, HandlesInfoA.Information[r].Handle, INVALID_HANDLE_VALUE, @tHandle, 0, false, DUPLICATE_SAME_ACCESS) then
begin
case HandlesInfoA.Information[r].ObjectTypeNumber of OB_TYPE_PROCESS : begin
if Processes and (HandlesInfoA.Information[r].ProcessId = CsrPid) then if ZwQueryInformationProces
Для дальнейшего прочтения статьи необходимо приобрести полный текст. Статьи высылаются в формате PDF на указанную при оплате почту. Время доставки составляет менее 10 минут. Стоимость одной статьи — 150 рублей.