![]() |
![]() |
Правила Форума редакция от 22.06.2020 |
|
|
Окажите посильную поддержку, мы очень надеемся на вас. Реквизиты для переводов ниже. |
|
![]() |
|
Опции темы | Опции просмотра |
![]() |
![]() |
![]() |
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() Всем привет
В этой теме постараюсь раскрыть способы обнаружения скрытых процессов, естественно что не все способы лично мне знакомы, так что буду узнавать вместе с вами. Ну, поехали....... Многие пользователи привыкли к тому, что в 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; Очевидно, что любой скрытый процесс при таком перечислении найден не будет, поэтому эта функция будет образцовой для отделения скрытых процессов от нескрытых. Последний раз редактировалось Deementor; 19.12.2006 в 18:04.. |
![]() |
![]() ![]() |
Эти 2 пользователя(ей) сказали cпасибо за это полезное сообщение: |
Реклама: | ледовое шок | подарочная корзина для мужчины на 23 февраля | смотреть фильм министерство джентльменских дел | раскладной стол трансформер | лупа ювелирная 10х |
![]() |
![]() |
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() Получение списка процессов через 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(SystemProcessesAndThreadsInf ormation, 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) им обнаруживаются. |
![]() |
![]() ![]() |
Эти 3 пользователя(ей) сказали cпасибо за это полезное сообщение: |
![]() |
![]() |
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() Получение списка процессов путем анализа связанных с ним хэндлов.
Также, можно применить еще один метод основанный на перечислении хэндлов. Его суть состоит в том, чтобы найти не хэндлы открытые искомым процессом, а хэндлы других процессов связанные с ним. Это могут быть хэндлы самого процесса либо его потоков. При получении хэндла процесса, можно определить его 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. P.S. К сожалению автор данного текста не известен и ресурс с которого был получен на данноый момент не доступен. |
![]() |
![]() ![]() |
Эти 2 пользователя(ей) сказали cпасибо за это полезное сообщение: |
![]() |
![]() |
|||||||||||||||||||
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() 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; } Eдем дальше: Получение списка процессов из двусвязного списка структур 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), то этот способ не поможет, так как при этом процесс удаляется из списка процессов.
|
|||||||||||||||||||
![]() |
![]() ![]() |
Сказали спасибо: |
![]() |
![]() |
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() Получение списка процессов перехватом системных вызовов.
Любой работающий процесс взаимодействует с системой через 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, но найти и пропатчить все эти места также не очень сложно, поэтому полностью полагаться на результаты этой проверки тоже нельзя. |
![]() |
![]() ![]() |
Сказали спасибо: |
![]() |
![]() |
ViP
![]() Пол: ![]() Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
![]() Получение списка процессов перехватом SwapContext.
Если в системе имеется процесс, то соотвестсвенно он будет иметь свои потоки. Если нам удастся поймать переключение потоков, то можно будет по полученным данным построить список процессов. Для начала кратко рассмотрим механизм переключения потоков. Через определенные промежутки времени (10-15 мс) системный таймер генерирует прерывание, которое вызывает планировщик и если квант времени связанный с текущим потоком истек, то происходит переключение потоков. Само переключение потоков выполняется неэкспортируемой функцией ядра SwapContext, которую тем не менее можно найти в отладочных символах на ntoskrnl.exe. Эта функция вызывается планировщиком при истечении кванта времени потока, либо при ожидании потоком какого-либо события. В первом случае эта функция вызывается из KiDispatchInterrupt, а во втором случае из неэкспортируемой функции находящейся глубоко в недрах ядра, которая в свою очередь вызывается из KeWaitForSingleObject, KeDelayExecutionThread и KeWaitForMultipleObjects. Параметры функции SwapContext передаются в регистрах и имеют следующее назначение: cl - определяет режим обработки APC, edi - указатель на поток отдающий управление, esi - указатель на поток получающий управление, ebx - указатель на PCR. Нас интересуют только указатели на переключающиеся потоки передаваемые в регистрах esi и edi. Для начала нам нужно найти адрес функции SwapContext. Для этого будем дизассемблировать KiDispatchInterrupt и искать код следующего вида: .text:00404E76 call sub_404C5A .text:00404E7B mov cl, 1 .text:00404E7D call SwapContext Из этого участка мы и будем извлекать адрес SwapContext. Этот метод поиска весьма надежен и универсален, так как позволяет найти SwapContext в любой Windows, начиная он 2000 и заканчивая 2003 server включая все их сервиспаки. А вот и код осуществляющий поиск SwapContext: void GetSwapContextAddress() { PUCHAR cPtr, pOpcode; ULONG Length; for (cPtr = (PUCHAR)KiDispatchInterrupt; cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE; cPtr += Length) { Length = SizeOfCode(cPtr, &pOpcode); if (!Length) break; if (*(PUSHORT)pOpcode == 0x01B1 && *(pOpcode + 2) == 0xE8) { pSwapContext = (PVOID)(*(PULONG)(pOpcode + 3) + (ULONG)cPtr + 7); break; } } return; } После нахождения адреса SwapContext нам нужно ее перехватить. Единственный возможный в этом случае способ - это сплайсинг ее кода. Для этого скопируем первые несколько инструкций с перехватываемой функции в буфер, в его конце поставим jmp на продолжение, а начало своей функции заменим на jmp на свой обработчик. При этом если в копируемом в буфер коде нам встретятся команды содержащие relative offset, то нужно их подкорректировать, иначе мы получим неизбежный BSOD. Для установки и снятие таких хуков воспользуемся следующим кодом: #define MemOpen() __asm cli; __asm mov eax, cr0; __asm mov oData, eax; \ __asm and eax, 0xFFFEFFFF; __asm mov cr0, eax; #define MemClose() __asm mov eax, oData; __asm mov cr0, eax; __asm sti; UCHAR SaveOldFunction(PUCHAR Proc, PUCHAR Old) { ULONG Size; PUCHAR pOpcode; ULONG Offset; PUCHAR oPtr; ULONG Result = 0; Offset = (ULONG)Proc - (ULONG)Old; oPtr = Old; while (Result < 5) { Size = SizeOfCode(Proc, &pOpcode); memcpy(oPtr, Proc, Size); if (IsRelativeCmd(pOpcode)) *(PULONG)((ULONG)pOpcode - (ULONG)Proc + (ULONG)oPtr + 1) += Offset; oPtr += Size; Proc += Size; Result += Size; } *(PUCHAR)((ULONG)Old + Result) = 0xE9; *(PULONG)((ULONG)Old + Result + 1) = Offset - 5; return (UCHAR)Result; } PVOID HookCode(PVOID TargetProc, PVOID NewProc) { ULONG Address; PVOID OldFunction; PVOID Proc = TargetProc; ULONG oData; Address = (ULONG)NewProc - (ULONG)Proc - 5; MemOpen(); OldFunction = ExAllocatePool(NonPagedPool, 20); *(PULONG)OldFunction = (ULONG)Proc; *(PUCHAR)((ULONG)OldFunction + 4) = SaveOldFunction((PUCHAR)Proc, (PUCHAR)((ULONG)OldFunction + 5)); *(PUCHAR)Proc = 0xE9; *(PULONG)((ULONG)Proc + 1) = Address; MemClose(); return (PVOID)((ULONG)OldFunction + 5); } void UnhookCode(PVOID OldProc) { PUCHAR Proc, pMem; PUCHAR pOpcode; ULONG Size, ThisSize; ULONG SaveSize, Offset; ULONG oData; Proc = (PUCHAR)(*(PULONG)((ULONG)OldProc - 5)); pMem = Proc; SaveSize = *(PUCHAR)((ULONG)OldProc - 1); Offset = (ULONG)Proc - (ULONG)OldProc; MemOpen(); memcpy(Proc, OldProc, SaveSize); ThisSize = 0; while (ThisSize < SaveSize) { Size = SizeOfCode(Proc, &pOpcode); if (IsRelativeCmd(pOpcode)) *(PULONG)((ULONG)pOpcode + 1) -= Offset; Proc += Size; ThisSize += Size; } MemClose(); ExFreePool((PVOID)((ULONG)OldProc - 5)); return; } Сам обработчик SwapContext будет выглядеть так: void __declspec(naked) NewSwapContext() { __asm { pushad pushfd push edi call ThreadCollect push esi call ThreadCollect popfd popad jmp OldSwapContext } } Метод сплайсинга несомненно легок и удобен, но при неправильном применении он может создать весьма немало проблем. Опасными являются моменты установки и снятия хуков, так как на многопроцессорной системе (либо на системе с Hiperthreading процессором) другой поток может вызвать перехватываемую функцию в когда патч ее кода еще не завершен. Давайте сделаем приблизительную оценку величины этой вероятности. Средняя частота вызова этой функции на двухпроцессорном Pentium 4 2400 была равна 785 вызовов в секунду. Допустим, что патч начала функции занимает 0.01 мкс (на самом деле гораздо меньше), при этом мы получаем вероятность радения системы равную 0,00000785, тоесть на 127380 запусков программы будет одно падение системы. Для такой программы как детектор руткитов (который к тому же редко запускают), это вполне приемлемая величина, но если подобный метод будет использоваться в постоянно применяемой программе (например антивирусе), то использовать сплайсинг следует исключительно однократно (установка хуков без их последующего снятия), и только в момент загрузки системы (в boot драйвере). В этом случае вероятность падения системы настолько приближена к нулю, что ее можно вообще не принимать в расчет. Пока что я не наблюдал ни одного случая падения системы из за этой причины. Но существует еще одна, гораздо более реальная причина нестабильности сплайсинга (и любых другим методов хуков тоже). Если драйвер допускает выгрузку и при выгрузке снимает хуки, то выгрузка может произойти в тот момент, когда какой-либо поток находится внутри обработчика перехвата. Вероятность такого расклада зависит от содержимого обработчика перехвата и от частоты вызова перехватываемой функции. Практика показала, что вероятность падения системы при выгрузке драйвера достаточно велика, и смириться с ней никак нельзя, но этого можно избежать, если синхронизировать выгрузку драйвера с функцией обработчиком. А еще лучше - сделать драйвер невыгружаемым, что избавит нас от тормозов при синхронизации. Как вы видите, при правильном применении сплайсинг может быть абсолютно безопасным, хотя непонимание его работы может привести к весьма неслабым глюкам, так что используйте, но с осторожностью. wasm.ru Последний раз редактировалось Deementor; 13.02.2007 в 01:38.. |
![]() |
![]() ![]() |
Сказали спасибо: |
![]() |
![]() |
![]() На wasm.ru есть утилитка Ms-Rem для обнаружения и килла процессов.
|
|
![]() |
![]() ![]() |
![]() |
![]() |
Новичок
Пол: ![]() Регистрация: 12.04.2008
Сообщений: 3
Репутация: 0
|
![]() _ttp://www.wasm.ru/print.php?article=hiddndt
|
![]() |
![]() ![]() |
![]() |
![]() |
Неактивный пользователь
Пол: ![]() Регистрация: 13.10.2007
Адрес: Москва
Сообщений: 149
Репутация: 49
|
![]() |
![]() |
![]() ![]() |
![]() |
![]() |
Новичок
![]() ![]() Пол: ![]() Регистрация: 23.07.2009
Сообщений: 20
Репутация: 2
|
![]() К сожалению, некоторые из вышеприведенных методов позволяют определить только ProcessId, но не имя процесса. Следовательно, нам нужно уметь получить имя процесса по pid. ToolHelp API для этого использовать естественно не стоит, так как процесс можкт быть скрытым, поэтому мы будем открывать память процесса на чтение и читать имя из его PEB. Адрес PEB в процессе можно определить с помощью функции ZwQueryInformationProcess. А вот и код осуществляющий все это:
function GetNameByPid(Pid: dword): string; |
![]() |
![]() ![]() |
![]() |
![]() |
||||
Тема | Автор | Раздел | Ответов | Последнее сообщение |
Маскировка процессов | ribakaba | Антихакинг | 3 | 14.02.2010 10:52 |
Маскировка процессов | ribakaba | Скорая помощь | 9 | 08.01.2009 00:57 |
Зависание процессов в WindowsXP | KEQ | Microsoft Windows | 6 | 17.02.2008 12:00 |
|
|