Статья Техники поиска скрытых процессов

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

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!ObHeaderCookie


3. Практическая часть

Ну и теперь соберём всё сказанное под один капот.
Чтобы продемонстрировать идею на практике, я создал форточку с двумя списками ListBox, куда буду постить сведения о найденных процессах. Сам поиск реализуется сначала посредством Process32First/Next() и выводом инфы в левое окно, после чего зовём NtQuerySystemInformation() и по идентификаторам процессов PID, сравниваем два поля в структурах по схеме ниже. Правда в первом ListBox PID будет храниться в виде строки, а в базе хэндлов у нас LONG, поэтому функцией _ltoa() из Ntdll.dll придётся причесать его в строку для сравнения, отправкой мессаги LB_FINDSTRING компоненту ListBox.

HndlEntry.webp

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>

Result.webp


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

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

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

pHacker.webp

5. Заключение

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

Вложения

Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab