Руткиты и малварь в целом активно пытаются скрыть своё присутствие в системе, для чего используют всё новые способы маскировки. Как правило стратегия начинается с того, что вредоносный код пытается скрыть свой процесс от аверов, и прочих посторонних глаз типа "Диспетчер задач". В данной статье мы рассмотрим несколько таких методов, которые являются базовыми для всего класса вирусов и червей.
1. Основная идея
Для создания списка активных процессов из под юзера, система предлагает нам несколько своих WinAPI - в классическом варианте это такие функции как
Такой расклад ограничивает наши возможности и приходится искать иные способы вылавливания блох, которые руткиты могли не учесть. Ситуацию усугубляет и то, что мы находимся в сессии(1) пространства юзера, в то время как все сервисы системы с привилегией "System" закрыты на замок в сессии(0). Как результат мы не сможем открыть некоторые процессы системы даже имея права админа, пока не напишем свою службу (о драйверах ядра можно забыть, т.к. на х64 они требуют подписи майков).
Поэтому всё-что нам остаётся, это создать список активных процессов двумя разными способами, после чего сравнить их на соответствие. То-есть прикинувшись байтом создаём первый лист штатными средствами типа
В силу перечисленных особенностей мы пойдём другим путём, и для создания второго/эталонного списка процессов соберём ..все дескрипторы в системе. У каждого процесса своя таблица дескрипторов, где будут хаотично разбросаны не только дескрипторы процессов, но и буквально всех ядерных объектов, например файлов, потоков, таймеров, портов, драйверов и многое другое (на Win7 это аж 42 типа объектов, а на Win10 все 50). Единственная проблема здесь в том, чтобы среди этой кучи отфильтровать только принадлежащие процессам дескрипторы (на инглише Handle), для чего необходимо будет узнать системную константу "ObjectType".
Список всех активных на данный момент дескрипторов возвращает та-же
Вот аргументы
Так в буфере получим массив структур "SYSTEM_HANDLE_INFO_ENTRY", а кол-во этих структур в массиве будет прописано в первом поле "NumberOfHandles". На своём узле я получил порядка 18.000 действительных хэндлов, и учитывая размер одной структуры =24 байт, мне нужен буф объёмом 432 КБ.
2. Объект "Process" и его дескрипторы
Теперь нам нужно найти константу "ObjectType", которая олицетворяет дескриптор процесса в системе. Это даст возможность искать хэндлы по полю "ObjectTypeNumber" структуры выше SYSTEM_HANDLE_INFO_ENTRY. Для начала запросим у отладчика WinDbg все типы ядерных объектов, чтобы получить адреса их структур OBJECT_TYPE. Как видим объект типа "Process" описывает структура по адресу
Для надёжности проверим содержимое структуры по этому адресу, и точно - в поле "Name" указано "Process".
А вот в соседнем поле "Index" прописывается уже нужная нам константа "ObjectType", и как видим для объектов типа "Process" она равна(7). Чтобы разобраться, почему её назвали именно "Index", а не более приземлённо например "Type", нужно копнуть глубже. Дело в том, что в ядре имеется спец.таблица "типа объектов", на которую указывает переменная
Кстати этот-же индекс хранится и в структуре заголовка объекта OBJECT_HEADER, которая имеет размер 0x30 байт, и всегда предваряет сам объект. Например файловый объект описывает структура FILE_OBJECT, объект устройства DEVICE_OBJECT, а процессы - нашумевшая EPROCESS. Так вот если запросить адрес EPROCESS любого экзешника, то отняв от него 0x30 получим адрес заголовка, где будет маячить поле
Таким образом мы узнали, что для перечисления всех процессов по глобальной базе хэндлов, нам нужно искать их по флагу(7) в структурах HANDLE_INFO_ENTRY64 функции
Константы для всех остальных объектов ядра найдёте в спойлере ниже:
2.1. Поиск константы "ObjectType" на Win10
Начиная с Win10 индекс адреса в таблице
Первая и основная составляющая - это системная переменная размером в байт
Таким образом, значение поля "TypeIndex" будет напрямую зависеть от адреса структуры OBJECT_HEADER в памяти (из-за ASLR меняется при каждом ребуте системы). В общем случае формула для Win10 такая, и что примечательно, на выходе всегда получим всё ту-же константу для процессов "Index=7":
3. Практическая часть
Ну и теперь соберём всё сказанное под один капот.
Чтобы продемонстрировать идею на практике, я создал форточку с двумя списками ListBox, куда буду постить сведения о найденных процессах. Сам поиск реализуется сначала посредством
4. Альтернативные методы
Конечно-же это не единственный способ поиска скрытых процессов в системе, хотя всё сводится к сравнению двух (полученных разными способами) списков. Здесь главное придумать вариант, который с вероятностью хотя-бы 70% может упустить из виду малварь. Хорошие результаты даёт так-же создание полного списка потоков Thread в системе функцией
Более того, системные процессы System и CSRSS.EXE хранят в себе дескрипторы всех запущенных процессов, а потому совсем необязательно собирать глобальную базу по рассмотренной выше схеме - достаточно пропарсить только хэндлы в System или Csrss.exe на выбор. На скрине ниже видно, что у System аж 541 открытых дескрипторов, а у csrss.ехе вообще 626, среди которых непременно будут и дескрипторы процессов.
5. Заключение
Здесь мы рассмотрели только базовые методы обнаружения процессов, и если у кого есть идеи на этот счёт, просьба поделиться ими в комментах. В скрепку кладу исходник для сборки ассемблером FASM, и готовый EXE для тестов. Код сырой и мне не удалось его протестировать на разных машинах. Поэтому если софт у кого-нибудь найдёт скрытые процессы, то плиз дайте об этом мне знать. Всем удачи, и спокойной жизни без руткитов!
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 <------------//
03 fffffa800c6f8950 Type DebugObject
04 fffffa800c760b40 Type TpWorkerFactory
05 fffffa800c7609f0 Type Adapter
fffffa800c6f7b70 Type Token
08 fffffa800c75c200 Type EventPair
09 fffffa800d63c920 Type PcwObject
fffffa800c793350 Type WmiGuid
11 fffffa800c794350 Type EtwRegistration
12 fffffa800c762690 Type Session
fffffa800c75f640 Type Timer
13 fffffa800c75ff30 Type Mutant
16 fffffa800c7604b0 Type IoCompletion
17 fffffa800c760de0 Type WindowStation
fffffa800c760080 Type Profile
18 fffffa800c760360 Type File
21 fffffa800c75f790 Type Semaphore
23 fffffa800c795350 Type EtwConsumer
25 fffffa800c762de0 Type TmTx
fffffa800c6f7de0 Type SymbolicLink
26 fffffa800c720390 Type Key
fffffa800c760f30 Type KeyedEvent
fffffa800c75fde0 Type Callback
fffffa800d1f12d0 Type FilterConnectionPort
28 fffffa800c6f8c90 Type UserApcReserve
fffffa800c6f7950 Type Job
29 fffffa800c7608a0 Type Controller
fffffa800c6f8b40 Type IoCompletionReserve
30 fffffa800c760750 Type Device
fffffa800c6f7f30 Type Directory
31 fffffa800c7627e0 Type Section
fffffa800c762b40 Type TmEn
fffffa800c6f8de0 Type Thread
32 fffffa800c6fef30 Type Type
33 fffffa800d1f4f30 Type FilterCommunicationPort
fffffa800c72ef00 Type PowerRequest
35 fffffa800c762c90 Type TmRm
fffffa800c75c350 Type Event
36 fffffa800c729bf0 Type ALPC Port
fffffa800c760600 Type Driver
0: kd>
Для надёжности проверим содержимое структуры по этому адресу, и точно - в поле "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 '' <---------// Наш клиент!
+0x02c TotalNumberOfObjects : 0x3e
+0x030 TotalNumberOfHandles : 0x211
+0x034 HighWaterNumberOfObjects : 0x48
+0x038 HighWaterNumberOfHandles : 0x2af
+0x040 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x0b0 TypeLock : _EX_PUSH_LOCK
+0x0b8 Key : 0x636f7250
+0x0c0 CallbackList : _LIST_ENTRY [ 0xfffff8a0`0014a9e0 - 0xfffff8a0`0014a9e0 ]
А вот в соседнем поле "Index" прописывается уже нужная нам константа "ObjectType", и как видим для объектов типа "Process" она равна(7). Чтобы разобраться, почему её назвали именно "Index", а не более приземлённо например "Type", нужно копнуть глубже. Дело в том, что в ядре имеется спец.таблица "типа объектов", на которую указывает переменная
nt!ObTypeIndexTable. В этой таблице собраны адреса всех структур OBJECT_TYPE, а доступ к этим адресам осуществляется как-раз по индексу. Вот дамп этой таблицы где видно, что по индексу(7) лежит уже знакомый нам из предыдущего дампа адрес 0xfffffa80`0c6f8f30.
Код:
0: kd> x nt!ObTypeIndexTable
fffff800`0247c100 nt!ObTypeIndexTable = <no type information>
0: kd> dps fffff800`0247c100
fffff800`0247c100 00000000`00000000 <---+--- Индексы(0:1) резерв.
fffff800`0247c108 00000000`bad0b0b0 <---+
fffff800`0247c110 fffffa80`0c6fef30
fffff800`0247c118 fffffa80`0c6f7f30
fffff800`0247c120 fffffa80`0c6f7de0
fffff800`0247c128 fffffa80`0c6f7b70
fffff800`0247c130 fffffa80`0c6f7950
fffff800`0247c138 fffffa80`0c6f8f30 <------- Index(7)
fffff800`0247c140 fffffa80`0c6f8de0
fffff800`0247c148 fffffa80`0c6f8c90
fffff800`0247c150 fffffa80`0c6f8b40
0: kd>
Кстати этот-же индекс хранится и в структуре заголовка объекта 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 <------------------------//
+0x019 TraceFlags : 0 ''
+0x01a InfoMask : 0x8 ''
+0x01b Flags : 0 ''
+0x020 ObjectCreateInfo : 0xfffffa80`106a54c0 _OBJECT_CREATE_INFORMATION
+0x020 QuotaBlockCharged : 0xfffffa80`106a54c0 Void
+0x028 SecurityDescriptor : 0xfffff8a0`0252f5ce Void
+0x030 Body : _QUAD
0: kd>
Таким образом мы узнали, что для перечисления всех процессов по глобальной базе хэндлов, нам нужно искать их по флагу(7) в структурах HANDLE_INFO_ENTRY64 функции
NtQuerySystemInformation():
C-подобный:
struct SYSTEM_HANDLE_INFO_ENTRY64
ProcessId dd 0
ObjectTypeNumber db 0 <----// Процесс = 7
Flags db 0
Handle dw 0
Object dq 0
GrantedAccess dq 0
ends
Константы для всех остальных объектов ядра найдёте в спойлере ниже:
C-подобный:
;// ObjectTypeNumber для Win7+
;//---------------------------
Type = 2
Directory = 3
SymbolicLink = 4
Token = 5
Job = 6
Process = 7
Thread = 8
UserApcReserve = 9
IoCompletionRsv = 10
DebugObject = 11
Event = 12
EventPair = 13
Mutant = 14
Callback = 15
Semaphore = 16
Timer = 17
Profile = 18
KeyedEvent = 19
WindowStation = 20
Desktop = 21
TpWorkerFactory = 22
Adapter = 23
Controller = 24
Device = 25
Driver = 26
IoCompletion = 27
File = 28
TmTm = 29
TmTx = 30
TmRm = 31
TmEn = 32
Section = 33
Session = 34
Key = 35
ALPC Port = 36
PowerRequest = 37
WmiGuid = 38
EtwRegistration = 39
EtwConsumer = 40
FilterConnectionPort = 41
FilterCommunicationPort = 42
PcwObject = 43
2.1. Поиск константы "ObjectType" на Win10
Начиная с Win10 индекс адреса в таблице
nt!ObTypeIndexTable вычисляется теперь иначе. Здесь используются целых три составляющих, над которыми производится операция XOR. Как результат, поле "TypeIndex" в заголовке объектов одного типа (в данном случае процессов) будут иметь разные значения, что предотвратит некоторые атаки на ядро.Первая и основная составляющая - это системная переменная размером в байт
nt!ObHeaderCookie. Далее берётся второй байт из адреса структуры OBJECT_HEADER, после чего два эти байта ксорятся между собой. На заключительном этапе читается поле "TypeIndex" из заголовка объекта (в котором хранится уже не 7, а произвольное значение), и так-же XOR с результатом этапа(1). Таким образом, значение поля "TypeIndex" будет напрямую зависеть от адреса структуры OBJECT_HEADER в памяти (из-за ASLR меняется при каждом ребуте системы). В общем случае формула для Win10 такая, и что примечательно, на выходе всегда получим всё ту-же константу для процессов "Index=7":
Index = TypeIndex ^ 2-й младший байт адреса OBJECT_HEADER ^ nt!ObHeaderCookie3. Практическая часть
Ну и теперь соберём всё сказанное под один капот.
Чтобы продемонстрировать идею на практике, я создал форточку с двумя списками ListBox, куда буду постить сведения о найденных процессах. Сам поиск реализуется сначала посредством
Process32First/Next() и выводом инфы в левое окно, после чего зовём NtQuerySystemInformation() и по идентификаторам процессов PID, сравниваем два поля в структурах по схеме ниже. Правда в первом ListBox PID будет храниться в виде строки, а в базе хэндлов у нас LONG, поэтому функцией _ltoa() из Ntdll.dll придётся причесать его в строку для сравнения, отправкой мессаги LB_FINDSTRING компоненту ListBox.
C-подобный:
;//************************************************************
;//****************** "WM_INITDIALOG" *************************
;//************************************************************
@init: invoke SetWindowText,[hwnddlg],<'*** Hidden Process List v1.0 ***',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,<'%-4d %4d %s',0>,\
[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,<' ',0>
invoke SendDlgItemMessage,[hwnddlg],ID_LISTBOX2,LB_ADDSTRING,0,\
<' Чисто! Скрытые процессы не обнаружены!',0>
4. Альтернативные методы
Конечно-же это не единственный способ поиска скрытых процессов в системе, хотя всё сводится к сравнению двух (полученных разными способами) списков. Здесь главное придумать вариант, который с вероятностью хотя-бы 70% может упустить из виду малварь. Хорошие результаты даёт так-же создание полного списка потоков Thread в системе функцией
Thread32First/Next(), чтобы получить PID процесса-родителя.Более того, системные процессы System и CSRSS.EXE хранят в себе дескрипторы всех запущенных процессов, а потому совсем необязательно собирать глобальную базу по рассмотренной выше схеме - достаточно пропарсить только хэндлы в System или Csrss.exe на выбор. На скрине ниже видно, что у System аж 541 открытых дескрипторов, а у csrss.ехе вообще 626, среди которых непременно будут и дескрипторы процессов.
5. Заключение
Здесь мы рассмотрели только базовые методы обнаружения процессов, и если у кого есть идеи на этот счёт, просьба поделиться ими в комментах. В скрепку кладу исходник для сборки ассемблером FASM, и готовый EXE для тестов. Код сырой и мне не удалось его протестировать на разных машинах. Поэтому если софт у кого-нибудь найдёт скрытые процессы, то плиз дайте об этом мне знать. Всем удачи, и спокойной жизни без руткитов!