PDA

Просмотр полной версии : Техники поиска скрытых процессов


Marylin
29.01.2026, 11:49
Руткиты и малварь в целом активно пытаются скрыть своё присутствие в системе, для чего используют всё новые способы маскировки. Как правило стратегия начинается с того, что вредоносный код пытается скрыть свой процесс от аверов, и прочих посторонних глаз типа "Диспетчер задач". В данной статье мы рассмотрим несколько таких методов, которые являются базовыми для всего класса вирусов и червей.

1. Основная идея
2. Объект "Process" и его дескрипторы "Handle"
3. Практическая часть - пишем софт
4. Альтернативные методы
5. Заключение

1. Основная идея

Для создания списка активных процессов из под юзера, система предлагает нам несколько своих WinAPI - в классическом варианте это такие функции как

EnumProcess()

,

Process32First/Next()

с предварительно созданным снапшотом

CreateToolhelp32Snapshot()

, а так-же бронебойная

NtQuerySystemInformation()

с флагом ProcessAndThreads=5. Как видим выбор не велик, и чтобы замаскировать своё присутствие, малварь просто хукает эти функции, возвращая нам в ответ фейковый результат.

Такой расклад ограничивает наши возможности и приходится искать иные способы вылавливания блох, которые руткиты могли не учесть. Ситуацию усугубляет и то, что мы находимся в сессии(1) пространства юзера, в то время как все сервисы системы с привилегией "System" закрыты на замок в сессии(0). Как результат мы не сможем открыть некоторые процессы системы даже имея права админа, пока не напишем свою службу (о драйверах ядра можно забыть, т.к. на х64 они требуют подписи майков).

Поэтому всё-что нам остаётся, это создать список активных процессов двумя разными способами, после чего сравнить их на соответствие. То-есть прикинувшись байтом создаём первый лист штатными средствами типа

Process32Next()

, а для второго списка нужно будет придумать нестандартный ход конём, который малварь пропустила между ног. Если поразмыслить, можно в обход вызовов API использовать прямой вызов ядерных сервисов посредством инструкции

syscall

, тогда мы опустимся ниже перехваченной малварью функции, и сможем обойти её дворами. Вариант хороший, но для кроссплатформенности требует много телодвижений, т.к. номера сервисов в ядре отличаются даже внутри одной линейки Win с разными версиями, не говоря уже о Win7/8/10/11.

В силу перечисленных особенностей мы пойдём другим путём, и для создания второго/эталонного списка процессов соберём ..все дескрипторы в системе. У каждого процесса своя таблица дескрипторов, где будут хаотично разбросаны не только дескрипторы процессов, но и буквально всех ядерных объектов, например файлов, потоков, таймеров, портов, драйверов и многое другое (на Win7 это аж 42 типа объектов, а на Win10 все 50). Единственная проблема здесь в том, чтобы среди этой кучи отфильтровать только принадлежащие процессам дескрипторы (на инглише Handle), для чего необходимо будет узнать системную константу "ObjectType".

Список всех активных на данный момент дескрипторов возвращает та-же

NtQuerySystemInformation()

, только с аргументом "SystemHandleInfo=16". Учитывая, что кроме 5 и 16 эта API может принимать запросы на возврат аж 53 различных типов информации, малварь обычно перехватывает лишь запрос под номером(5) "ProcessesAndThreadInfo", не обращая внимания на "HandleInfo=16". Это даём нам надежду, что таким способом мы сможем обхитрить гадкого вредоноса.

Вот аргументы

NtQuerySystemInfo()

, и возвращаемые ею данные. Поскольку в каждый момент времени в системе может быть различное кол-во дескрипторов, мы не можем определить точный размер буфера для приёма данных в аргументе

NtQuerySystemInfo()

. Поэтому лучше указать его сразу с запасом, например 1 МБ.

C-подобный:



invoke VirtualAlloc
,
0
,
1024
*
1024
,
MEM_RESERVE
+
MEM_COMMIT
,
PAGE_READWRITE
invoke NtQuerySystemInformation
,
SysHandleInfo
,
rax
,
1024
*
1024
,
0


Так в буфере получим массив структур "SYSTEM_HANDLE_INFO_ENTRY", а кол-во этих структур в массиве будет прописано в первом поле "NumberOfHandles". На своём узле я получил порядка 18.000 действительных хэндлов, и учитывая размер одной структуры =24 байт, мне нужен буф объёмом 432 КБ.

C-подобный:



;
// Info Class = 16
;
//-----------------------------
struct SYSTEM_HANDLE_INFORMATION
NumberOfHandles dd
0
;
// dq для х64
Entries SYSTEM_HANDLE_INFO_ENTRY32
;
// массив структур
ends

struct SYSTEM_HANDLE_INFO_ENTRY32
;
// размер = 14 байт
ProcessId dw
0
;
// PID процесса, кому принадлежит хэндл
ObjectTypeNumber db
0
;
// Тип объекта - нам нужен "Process"
Flags db
0
;
// Флаг наследования
Handle dw
0
;
// Номер дескриптора объекта
Object dd
0
;
// Его адрес в ядерной памяти
GrantedAccess dd
0
;
// Маска доступа
ends
struct SYSTEM_HANDLE_INFO_ENTRY64
;
// размер = 24 байта
ProcessId dd
0
ObjectTypeNumber db
0
Flags db
0
Handle dw
0
Object dq
0
GrantedAccess dq
0
ends


2. Объект "Process" и его дескрипторы

Теперь нам нужно найти константу "ObjectType", которая олицетворяет дескриптор процесса в системе. Это даст возможность искать хэндлы по полю "ObjectTypeNumber" структуры выше SYSTEM_HANDLE_INFO_ENTRY. Для начала запросим у отладчика WinDbg все типы ядерных объектов, чтобы получить адреса их структур OBJECT_TYPE. Как видим объект типа "Process" описывает структура по адресу

0xfffffa80`0c6f8f30

:

Код:



0: kd> !object \ObjectTypes
Object: fffff8a0000065b0 Type: (fffffa800c6f7f30) Directory
ObjectHeader: fffff8a000006580 (new version)
HandleCount: 0 PointerCount: 44
Directory Object: fffff8a0000045d0 Name: ObjectTypes

Hash Address Type Name
---- ---------------- ---- ----
00 fffffa800c762f30 Type TmTm
01 fffffa800c760c90 Type Desktop
fffffa800c6f8f30 Type Process


Для надёжности проверим содержимое структуры по этому адресу, и точно - в поле "Name" указано "Process".

Код:



0: kd> dt nt!_object_type fffffa800c6f8f30
+0x000 TypeList : _LIST_ENTRY [ 0xfffffa80`0c6f8f30 - 0xfffffa80`0c6f8f30 ]
+0x010 Name : _UNICODE_STRING "Process"
+0x020 DefaultObject : (null)
+0x028 Index : 0x7 '' x nt!ObTypeIndexTable
fffff800`0247c100 nt!ObTypeIndexTable =

0: kd> dps fffff800`0247c100
fffff800`0247c100 00000000`00000000


Кстати этот-же индекс хранится и в структуре заголовка объекта OBJECT_HEADER, которая имеет размер 0x30 байт, и всегда предваряет сам объект. Например файловый объект описывает структура FILE_OBJECT, объект устройства DEVICE_OBJECT, а процессы - нашумевшая EPROCESS. Так вот если запросить адрес EPROCESS любого экзешника, то отняв от него 0x30 получим адрес заголовка, где будет маячить поле

"TypeIndex=7"

:

Код:



0: kd> !process 0 0 fasmw.exe
PROCESS fffffa800cac6060
SessionId: 1 Cid: 0bc8 Peb: fffdf000 ParentCid: 1164
DirBase: 05e90000 ObjectTable: fffff8a00513d960 HandleCount: 92.
Image: FASMW.EXE

0: kd> dt _object_header fffffa800cac6060-0x30
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n49
+0x008 HandleCount : 0n4
+0x008 NextToFree : 0x00000000`00000004 Void
+0x010 Lock : _EX_PUSH_LOCK
+0x018 TypeIndex : 0x7


Таким образом мы узнали, что для перечисления всех процессов по глобальной базе хэндлов, нам нужно искать их по флагу(7) в структурах HANDLE_INFO_ENTRY64 функции

NtQuerySystemInformation()

:

C-подобный:



struct SYSTEM_HANDLE_INFO_ENTRY64
ProcessId dd
0
ObjectTypeNumber db
0

invoke CreateToolhelp32Snapshot
,
TH32CS_SNAPPROCESS
,
0
mov
[
snapHndl
]
,
rax
;
// Парсим все активные процессы, и дампим их в ListBox
invoke Process32First
,
[
snapHndl
]
,
ppe
@@
:
invoke Process32Next
,
[
snapHndl
]
,
ppe
or eax
,
eax
jz @f
;
// На выход, если ошибка
inc
[
counter
]
cinvoke wsprintf
,
strBuff
,

,
\
[
ppe
.
th32ProcessID
]
,
\
[
ppe
.
th32ParentProcessID
]
,
\
ppe
.
szExeFile

invoke SendDlgItemMessage
,
[
hwnddlg
]
,
ID_LISTBOX1
,
LB_ADDSTRING
,
0
,
strBuff
jmp @b

@@
:
invoke CloseHandle
,
[
snapHndl
]
invoke SetDlgItemInt
,
[
hwnddlg
]
,
ID_Count1
,
[
counter
]
,
0
;
// Сдампили все процессы в первый ListBox!
;
// Теперь выделяем 1 МБ памяти, и заполняем её дескрипторами
invoke VirtualAlloc
,
0
,
1024
*
1024
,
MEM_RESERVE
+
MEM_COMMIT
,
PAGE_READWRITE
mov
[
HandleBuff
]
,
rax
invoke NtQuerySystemInformation
,
SysHandleInfo
,
rax
,
1024
*
1024
,
0
mov
[
counter
]
,
0
;
// Счётчик найденных в ноль
mov rcx
,
[
HandleBuff
]
;
// Адрес буфера с хэндлами
mov rsi
,
rcx
;
//
mov rcx
,
[
rcx
]
;
// Кол-во структур в массиве
add rsi
,
8
;
// RSI = указатель на первую структуру
@CompareHandle
:
push rcx rsi
cmp byte
[
rsi
+
SYSTEM_HANDLE_INFO_ENTRY64
.
ObjectTypeNumber
]
,
7
jnz @f
;
// Пропустить, если это не дескриптор процесса
xor eax
,
eax
mov qword
[
buff
]
,
rax
movzx eax
,
word
[
rsi
+
SYSTEM_HANDLE_INFO_ENTRY64
.
ProcessId
]
mov
[
pid
]
,
rax
;
// Иначе возьмём его PID
invoke _ltoa
,
eax
,
buff
,
10
;
// Переведём PID в строку для поиска в ListBox(1)
invoke SendDlgItemMessage
,
[
hwnddlg
]
,
ID_LISTBOX1
,
LB_FINDSTRING
,
-
1
,
buff
cmp eax
,
LB_ERR
jnz @f
;
// Если нет совпадения, значит процесс скрытый!
;
// Открываем его, и запрашиваем путь до файла
invoke OpenProcess
,
PROCESS_QUERY_INFORMATION
,
0
,
[
pid
]
push rax
mov dword
[
strBuff
]
,
0
invoke QueryFullProcessImageName
,
eax
,
0
,
strBuff
,
retVal
pop rax
invoke CloseHandle
,
eax
;
// Закрыть процесс,
inc
[
counter
]
;
// ..и вывести его имя в ListBox(2)
invoke SendDlgItemMessage
,
[
hwnddlg
]
,
ID_LISTBOX2
,
LB_ADDSTRING
,
0
,
strBuff

@@
:
pop rsi rcx
;
// Восстановить данные цикла
add rsi
,
24
;
// сл.структура в массиве..
dec rcx
;
// Это конец массива?
jnz @CompareHandle
;
// Нет = на повтор
invoke VirtualFree
,
[
HandleBuff
]
,
0
,
MEM_DECOMMIT
cmp
[
counter
]
,
0
jnz @exit
invoke SendDlgItemMessage
,
[
hwnddlg
]
,
ID_LISTBOX2
,
LB_ADDSTRING
,
0
,

invoke SendDlgItemMessage
,
[
hwnddlg
]
,
ID_LISTBOX2
,
LB_ADDSTRING
,
0
,
\



https://forum.antichat.xyz/attachments/4950690/img_fe71737d60.png

4. Альтернативные методы

Конечно-же это не единственный способ поиска скрытых процессов в системе, хотя всё сводится к сравнению двух (полученных разными способами) списков. Здесь главное придумать вариант, который с вероятностью хотя-бы 70% может упустить из виду малварь. Хорошие результаты даёт так-же создание полного списка потоков Thread в системе функцией

Thread32First/Next()

, чтобы получить PID процесса-родителя.

Более того, системные процессы System и CSRSS.EXE хранят в себе дескрипторы всех запущенных процессов, а потому совсем необязательно собирать глобальную базу по рассмотренной выше схеме - достаточно пропарсить только хэндлы в System или Csrss.exe на выбор. На скрине ниже видно, что у System аж 541 открытых дескрипторов, а у csrss.ехе вообще 626, среди которых непременно будут и дескрипторы процессов.

https://forum.antichat.xyz/attachments/4950690/img_6d0022816f.png
5. Заключение

Здесь мы рассмотрели только базовые методы обнаружения процессов, и если у кого есть идеи на этот счёт, просьба поделиться ими в комментах. В скрепку кладу исходник для сборки ассемблером FASM, и готовый EXE для тестов. Код сырой и мне не удалось его протестировать на разных машинах. Поэтому если софт у кого-нибудь найдёт скрытые процессы, то плиз дайте об этом мне знать. Всем удачи, и спокойной жизни без руткитов!

Gemfory
04.02.2026, 00:34
Ого, Marylin пишет под x64, редкое явление однако

Marylin
04.02.2026, 05:51
Да, в дефолте я привык к х32, и собираю х64 только в случае крайней необходимости. Здесь для страховки выбрал х64, т.к. имеются известные проблемы с перенаправлением FS и реестра, когда обращения идут из wow. В общем если в прототипе какой-либо функции указано "Начиная с Vista", то компилирую в х64, чтобы где-нибудь не всплыл баг.

Ахимов
04.02.2026, 12:38
SYSTEM_OBJECT_INFORMATION.HandleCount для обьектов, созданных на этапе инит. процесса наверно покажет наличие скрытых процессов. Может быть трудность с указателями на обьекты aslr

Проще потоки скрывать.

Marylin
04.02.2026, 14:35
Ахимов сказал(а):

SYSTEM_OBJECT_INFORMATION.HandleCount


Спасибо, интересный вариант, нужно будет протестить.