• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

    На последнюю неделю приходится экзамен, где нужно будет показать свои навыки, взломав ряд уязвимых учебных сайтов, и добыть флаги. Успешно сдавшие экзамен получат сертификат.

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья ASM для х86 (4.5.) Техники скрытия процессов

Win – это работающая по определённым правилам замкнутая система и если не придерживаться этих правил, её можно пустить по ложному следу. Тут главное не идти на поводу Microsoft, которая вправляет нам мозги своей/увесистой документацией. Сабж рассматривает несколько способов скрытия программ от системного "Диспетчера задач" как на пользовательском уровне, так и на уровне ядра.

4.5.0. Ядерный уровень

Всякий процесс в системе описывается своей структурой EPROCESS – сколько запущенных процессов, столько и структур. Чтобы держать все процессы под контролем, система связывает их в цепочку через механизм связных-списков LIST_ENTRY. Сам список состоит всего из двух элементов – это указатель FLink на следующую структуру EPROCESS в цепочке (Forward), и указатель BLink на предыдущую структуру в цепочке (Backward). На рисунке ниже, PsActiveProcessHead – это системный связной список, с него мастдай начинает сканировать активные процессы (в примере их всего три):

flink_0.png


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

По смещению 0x88 в структурах EPROCESS каждого из процессов есть поле ActiveProcessLinks, которое и представляет собой связной-список. Ознакомиться с ним можно в WinDbg, но сначала командой !process 0 0 получим список всех процессов в памяти. В первой строчке лога будет указан адрес структуры EPROCESS для представленного процесса (выделен цветом), который нужно вскормить следующей команде dt (display-type).

Для наглядности, на скрине ниже я запустил подряд три процесса вставив свой по середине - его и буду скрывать от ‘Диспетчера задач’ и прочих утилит подобного рода. Последний процесс в моей цепочке это ‘AkelPad’, соответственно его Flink будет указывать на системную PsActiveProcessHead, т.е. круг замкнётся. Точка в конце имени поля, раскрывает список:

flink_1.png


Разберём эту схему на атомы..
Как видно, Flink моего/красного процесса имеет значение 0х80561358 (в кв.скобках), и это как-раз адрес Flink'а, который принадлежит следущему процессу в цепочке "AkelPad.exe" (т.е. двигаемся вперёд). В тоже время, значение моего Blink равно 0х89ad0ad8, и это в аккурат указатель на Blink предыдущего процесса "FSViewer.exe".

Теперь, если изменить данную схему как на рисунке ниже, мой красный процесс исчезнет из цепочки, хотя по-прежнему будет считаться активным, имея EPROCESS в системном пуле. То-есть мы копируем свой Flink в структуру предыдущего процесса, а Blink отправляем в следующий процесс:

flink_2.png


Нужно сказать, что такой способ позволяет скрыть процесс от всех, включая саму систему. ОС не ведёт учёт конкретных значений в полях ActiveProcessLinks, а значит физически не сможет отследить их изменение. Процесс уйдёт из поля зрения не только виндового ‘Диспетчера задач’, но и буквально всех других утилит, типа ‘Process Explorer’ Марка Руссиновича. Но у данного алгоритма есть огромный недостаток – нужно проникнуть в ядро, т.к. это внутренние структуры оси и она держит их в своём сейфе.


4.5.1. Скрытие процессов на прикладном уровне

Ясно, что писать драйвер для скрытия обычного процесса никто не будет (хотя как знать), поэтому рассмотрим альтернативные решения для юзер-моды. Маскировка на местности осуществляется здесь по такому-же алгоритму – правка указателей FLink, только не в структуре EPROCESS, а в структурах прикладных API (они тоже имеют свои списки-связей). В порядке приоритетов от младшего к старшему, системные библиотеки расположены так:

Kernel32.dll (user) --> Ntdll.dll (user) --> Ntoskrnl.exe (kernel).

Это означает, что фактически, любую системную функцию выполняет ядро Ntoskrnl.exe, а остальные библиотеки просто передают запрос как эстафетную палочку вышестоящим инстанциям. Соответственно если мы скроем процесс на уровне Kernel32.dll, то для утилит использующих функции из Ntdll.dll процесс всё-равно будет виден. Как правило, для перечисления процессов под юзером утилиты прибегают к услугам следующих API (хотя могут быть и другие):
  1. ZwQuerySystemInformation() из библиотеки Ntdll.dll;
  2. CreateToolhelp32Snapshot() из Kernel32.dll;
  3. EnumProcesses() из либы Psapi.dll.
Учитывая, что функции 2-3 в конечном итоге всё-равно вызывают ZwQuerySystemInformation(), имеет смысл перехватить именно её. Эта функция отличается от остальных тем, что за один вызов возвращает информацию сразу обо всех активных процессах в системе, в то время как следующие две API нужно вызывать в цикле, для каждого процесса в отдельности. Ради интереса, можно вскрыть тушку «Диспетчера задач» (taskmgr.exe) в любом дизассемблере, и посмотреть на его таблицу-импорта – для перечисления процессов он использует именно ZwQuerySystemInformation():

hDasm.png


В своём младенчестве, эта недокументированная функция могла возвращать 150 типов информации о системе (т.н. класс), однако позже выяснилось, что большинство из них оказывают медвежью услугу, предоставляя хакерам широкий ассортимент. Так в последующих виндах классы урезали и на данный момент их осталось всего 54-штуки, хотя на сайте мелкософт указано итого меньше – 5 классов с пометкой, что остальные ушли в резерв для производственных нужд системы.

В этом списке под номером 5 числится класс ProcessesAndThreadsInformation, который и возвращает информацию о процессах (см.аргумент на рисунке выше). поддерживаемых классов выглядит так:

C-подобный:
enum _SYSTEM_INFORMATION_CLASS {
   BasicInformation,                  = 0
   ProcessorInformation,              = 1
   PerformanceInformation,            = 2
   TimeOfDayInformation,              = 3
   NotImplemented1,                   = 4
   ProcessesAndThreadsInformation,    = 5     ;// <---- наш клиент!
   CallCounts,                        = 6
   ConfigurationInformation,          = 7
   ProcessorTimes,                    = 8
   GlobalFlag,                        = 9
   NotImplemented2,                   = 10
   ModuleInformation,                 = 11
   LockInformation,                   = 12
   NotImplemented3,                   = 13
   NotImplemented4,                   = 14
   NotImplemented5,                   = 15
   HandleInformation,                 = 16
   ObjectInformation,                 = 17
   PagefileInformation,               = 18
   InstructionEmulationCounts,        = 19
   InvalidInfoClass1,                 = 20
   CacheInformation,                  = 21
   PoolTagInformation,                = 22
   ProcessorStatistics,               = 23
   DpcInformation,                    = 24
   NotImplemented6,                   = 25
   LoadImage,                         = 26
   UnloadImage,                       = 27
   TimeAdjustment,                    = 28
   NotImplemented7,                   = 29
   NotImplemented8,                   = 30
   NotImplemented9,                   = 31
   CrashDumpInformation,              = 32
   ExceptionInformation,              = 33
   CrashDumpStateInformation,         = 34
   KernelDebuggerInformation,         = 35
   ContextSwitchInformation,          = 36
   RegistryQuotaInformation,          = 37
   LoadAndCallImage,                  = 38
   PrioritySeparation,                = 39
   NotImplemented10,                  = 40
   NotImplemented11,                  = 41
   InvalidInfoClass2,                 = 42
   InvalidInfoClass3,                 = 43
   TimeZoneInformation,               = 44
   LookasideInformation,              = 45
   SetTimeSlipEvent,                  = 46
   CreateSession,                     = 47
   DeleteSession,                     = 48
   InvalidInfoClass4,                 = 49
   RangeStartInformation,             = 50
   VerifierInformation,               = 51
   AddVerifier,                       = 52
   SessionProcessesInformation        = 53
}SYSTEM_INFORMATION_CLASS;

Когда мы вызываем эту функцию с аргументом 5, то в приёмный буфер (обязательно должен быть выровнен на 4-байтную границу) сбрасывается инфа не только о процессах, но и потоках каждого из них. Поэтому размер буфера у ZwQuerySystemInformation() не фиксирован и зависит от числа активных на данный момент процессов. Если при вызове умышленно задать размер буфера в байт-так 16 (чего явно не хватит), то функция вернёт в регистр EAX ошибку 0xC0000004 - STATUS_INFO_LENGTH_MISMATCH и ..реально требуемый размер в байтах. Теперь можно выделить нужное кол-во памяти через VirtualAlloc(), и повторно запросить ZwQuerySystemInformation(). Прототипы у этих функций такие:
C-подобный:
LPVOID VirtualAlloc (               // EAX =0 ошибка!!!
   IN  lpvAddress,                  // база нового блока памяти (ставим нуль, и система сама найдёт)
   IN  dwSize,                      // размер нового блока
   IN  fdwAllocationType,           // MEM_COMMIT (выделить, а не зарезервировать память)
   IN  fdwProtect );                // атрибуты ставим PAGE_READWRITE

STATUS ZwQuerySystemInformation (   // EAX =0 успех!!!
   IN  SystemInformationClass,      // класс информации (в нашем случае =5)
   IN  SystemInformation,           // указатель на приёмный буфер (берём у VirtualAlloc)
   IN  SystemInformationLength,     // размер этого буфера (получим при первом вызове)
  OUT  ReturnLength );              // реально требуемый размер буфера

Если VIrtualAlloc() отработает удачно, то в регистре EAX получим указатель на выделенный ею блок памяти, иначе – нуль Отправив этот указатель как аргумент в ZwQuerySystemInformation(), в приёмном буфере получим инфу о всех процессах. Описатель каждого из процессов в буфере состоит из одной структуры PROCESS_INFO с общей информацией о данном процессе, и одной или нескольких (если процесс многопоточный) структур THREAD_INFO с информацией о его потоках. Описание этих структур приводится ниже:
C-подобный:
struct  SYSTEM_PROCESS_INFORMATION      ;// Размер = 0x00b8 (184 байта)
    NextEntryOffset      dd  0          ; RVA-указатель на структуру сл.процесса
    ThreadCount          dd  0          ; число потоков
    Reserved_01          dd  13 dup(?)  ;
    ProcessName          dd  0          ; VA-указатель на имя процесса
    BasePriority         dd  0          ; приоритет процесса
    UniqueProcessId      dd  0          ; pid-процесса
    ParentProcessID      dd  0          ; pid-родителя
    HandleCount          dd  0          ; число открытых дескрипторов
    Reserved_02          dd  05 dup(?)  ;
    PeakVirtualSize      dd  0          ; пик расхода виртуальной памяти
    VirtualSize          dd  0          ; текущий размер занимаемой памяти
    Reserved_03          dd  19 dup(?)  ;
ends

;//***************************

struct  SYSTEM_THREAD_INFORMATION       ;// Размер = 0x0040 (64 байта)
    KernelTime           dq  0
    UserTime             dq  0
    CreateTime           dq  0
    WaitTime             dd  0
    StartAddress         dd  0
    ClientId             dq  0
    Priority             dd  0
    BasePriority         dd  0
    ContextSwitches      dd  0
    ThreadState          dd  0
    WaitReason           dd  0
ends
Для скрытия процесса, информация о его тредах нам вообще ни к чему. Значимой фигурой является только поле NextEntryOffset – RVA-указатель на начало структуры следующего процесса в цепочке. В описателе последнего процесса, NextEntryOffset будет равен терминальному нулю.

На рисунке ниже, я привёл фрагмент дампа буфера ZwQuerySystemInformation(), где поле NextEntryOffset выделено овалом (это инфа только двух процессов из общего пула). В нём видно, что описатель моего процесса начинается по-адресу 0x00186Е40, а NextEntryOffset имеет значение 0x00000110. Если сложить эти два значения, то получим адрес описателя следующего процесса 0x00186Е40 + 0x0110 = 0x00186F50, который является последним в цепочке, т.к. его NextEntryOffset равен нулю. Указатель на Unicode-имя моего процесса лежит по адресу 0x00186Е7С и равен 0x00186F38.

zwQuery_01.png


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


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

Покончив с теорией – перейдём к практике..
Здесь есть несколько моментов, на которые следует обратить внимание:

1. Прежде всего, нужно перехватить функцию ZwQuerySystemInformation() из библиотеки Ntdll.dll, причём не в своём пространстве, а в пространстве “Диспетчера задач”. Дело в том, что перечислять процессы в своём окне будет диспетчер, а не я. Ему по-барабану, что я сброшу карту всех процессов и скорректирую её в своём буфере – он сам вызывает эту функцию и получает свою карту. Значит я должен опередить его вызов, возвратив ему фиктивные данные.

2. Когда после перехвата я получу управление, в стеке будет лежать адрес его буфера (диспетчер положит его в качестве аргумента функции) – мне остаётся лишь скрыть себя в его буфере. Любой, кто вызывает эту функцию с аргументом(5) хочет получить список процессов, поэтому буду проверять (опять таки в стеке) вызов именно на пятёрку, а остальные – игнорировать.

3. Возможно, что юзер вообще не планирует запускать “Диспетчера задач”, поэтому мне нужно отловить факт его запуска, и только потом инжектить свой шелл в его процесс. Для этого, можно с некоторым интервалом просто искать окно диспетчера по его названию функцией FindWindow(), и лучше всего создать для этого отдельный поток Thread. Тогда основной поток не отвлекаясь будет заниматься своими/тёмными делишками, а дополнительный поток будет охранять его тылы. Период поиска окна функцией Sleep() можно выставить на 1-сек, поскольку сам диспетчер в дефолте сканирует процессы с интервалом в 2-сек:

task_clock.png


Думаю нет смысла выкладывать здесь готовый код скрытия процесса – программист должен дойти до этого сам. Кому интересно, задавайте вопросы и мы сделаем это вместе. А пока я просто приведу пример техники разбора и модификации карты-процессов, которую предоставит нам функция ZwQuerySystemInformation(). Код сбоксит в мессагу: имя, pid, число потоков, кол-во расходуемой памяти всех процессов, кроме своего. То-есть я сначала скрою свой процесс (от себя-же), а потом в цикле обойду всю карту точно так, как это делает системный “Диспетчер задач”:

C-подобный:
include 'win32ax.inc'
.data
capt       db  'Marylin',0
frmt       db  'Потоков: %02d,  Pid: %04d,  Память: %06d К,  Handle: %04d,  Имя: %s',13,10,0
text       db   2048 dup(0)
textAddr   dd   text
procName   db   32 dup(0)
myName     db  'm',0,'a',0,'r',0,'y',0,'l',0,'i',0,'n',0    ; Unicode имя моего процесса
len        =    $ - myName
libName    db  'ntdll.dll',0
funcName   db  'ZwQuerySystemInformation',0
zwQuery    dd   0                          ; будет адресом ZwQuery в Ntdll.dll
buffSize   dd   0                          ; будет реальный размер буфера
align           4                          ; обязательно выровнить фиктивный буфер на 4-байтную границу
buff       dd   0                          ; буфер для первого вызова, а потом - его адрес
;//------------------
.code
start:
;// Получаю адрес fn. ZwQuerySystemInformation()
        invoke  GetModuleHandle,libName
        invoke  GetProcAddress,eax,funcName
        mov     [zwQuery],eax

;// Первый её вызов с заведомо кривым размером буфера,
;// чтобы получить валидный его размер в переменную "buffSize"
        push    buffSize 8 buff 5
        call    [zwQuery]

;// Выделяю память под буфер/карту процессов
        invoke  VirtualAlloc,0,[buffSize], MEM_COMMIT, PAGE_READWRITE
        mov     [buff],eax

;// Второй вызов fn. ZwQuery с уже валидным размером буфера.
;// VirtualAlloc() в регистр EAX вернула адрес выделенного блока памяти
        push    0 [buffSize] eax 5
        call    [zwQuery]

;// Теперь карта всех процессов лежит в буфере!
;// Скрываю в нём описатель своего процесса "marylin.exe"
        mov     ebp,[buff]             ; адрес начала карты процессов
@hide:  mov     esi,ebp                ; ^^^
        cmp     dword[esi+15*4],0      ; проверить на нуль 15-ый DWORD от начала
        je      @fuck                  ; если равно..
        mov     esi,[esi+15*4]         ; иначе: в нём лежит указатель на имя процесса
        mov     edi,myName             ; EDI = указатель на моё UNICODE-имя
        mov     ecx,len                ; ECX = его длина
        repe    cmpsb                  ; сравнить с тем, что в карте процессов
        or      ecx,ecx                ; по всей длине совпало?
        jnz     @fuck                  ; если нет..

        mov     ebx,[ebp]              ; иначе: берём поле "NextEntryOffset"
        cmp     ebx,0                  ; проверяем его на маркер окончания нуль
        jne     @okey                  ; если нет..
        mov     [eax],ebx              ; иначе: пихаем этот нуль в предыдущий "NextEntryOffset"
@okey:  add     [eax],ebx              ; если "NextEntryOffset" был не нуль,
        jmp     @stop                  ;  ..то добавляем свой адрес к предыдущему, и на выход!

@fuck:  mov     eax,ebp                ; имя не совпало.. EAX указывает на предыдущий блок
        add     ebp,[eax]              ; сделать EBP указателем на следующий!
        jmp     @hide                  ; продолжить поиск своего имени..

@stop:
;// Процесс "marylin.exe" найден и скрыт из карты процессов!
;// Теперь эмулирую работу "Диспетчера задач",
;// который тупо обходит все записи, и выводит из них информацию.
;// Маркером последней записи является нуль в поле "NextEntryOffset"
        mov     ebp,[buff]           ; EBP = начало карты процессов,
@nextProcess:                        ;   ..читаю в цикле из неё все данные.
        mov     eax,[ebp+01*4]       ; число потоков (см.структуру "PROCESS_INFO" выше)
        mov     ebx,[ebp+17*4]       ; pid
        mov     ecx,[ebp+26*4]       ; расход памяти
        shr     ecx,10               ;  ..в килобайтах (деление сдвигом - ecx/1024)
        mov     edx,[ebp+19*4]       ; дескрипторов
        mov     esi,[ebp+15*4]       ; имя
        push    eax                  ;
        cmp     esi,0                ; бывают процессы безымянные, проверим на нуль
        je      @done                ; если нарвались на такой..
        mov     edi,procName         ; иначе: EDI = приёмник
@unicode2ansi:                       ; делаем из UNICODE, ANSI-имя
        lodsw                        ;  ..т.е. читаем по 2-байта,
        or      ax,ax                ;    ..(пока не встретим пару нулей)
        je      @done                ;      ^^^
        stosb                        ;      ..а сохраняем в EDI по одному.
        jmp     @unicode2ansi        ;

@done:  pop     eax                  ; сохраняем инфу об очередном процессе для MessageBox
       cinvoke  wsprintf,[textAddr],frmt,eax,ebx,ecx,edx,procName

        mov     edi,procName         ; очищаем 32-байтное поле от предыдущего имени
        xor     eax,eax              ; забивать будем нулями
        mov     ecx,8                ; 8 * 4 = 32 (число повторов)
        rep     stosd                ; записываем в EDI сразу по 4-байта нулей

        mov     edi,text             ; чтобы не затереть уже записанную инфу wsprintf,
        mov     ecx,-1               ; ..пропустим её в текстовом буфере
        repne   scasb                ;
        dec     edi                  ;   ..не считая терминального нуля
        mov     [textAddr],edi       ; запомнить позицию для сброса инфы о сл.процессе..

        add     ebp,[ebp]            ; EBP = указатель на описатель сл.процесса в карте
        cmp     dword[ebp],0         ; это последний описатель в карте процессов?
        jne     @nextProcess         ; повторить, если нет..

;// Значит всё прочитали из карты процессов,
;// остался последний, в котором "NextEntryOffset" равен нуль.
;// прихватим его тоже..
        mov     eax,[ebp+01*4]       ; потоков
        mov     ebx,[ebp+17*4]       ; pid
        mov     ecx,[ebp+26*4]       ; память
        shr     ecx,10               ;  ..в килобайтах (ecx/1024)
        mov     edx,[ebp+19*4]       ; дескрипторов
        mov     esi,[ebp+15*4]       ; имя
        push    eax                  ;
        cmp     esi,0                ;
        je      @done                ;
        mov     edi,procName         ;
@unicode:                            ;
        lodsw                        ;
        or      ax,ax                ;
        je      @done1               ;
        stosb                        ;
        jmp     @unicode             ;
@done1: pop     eax                  ;
       cinvoke  wsprintf,[textAddr],frmt,eax,ebx,ecx,edx,procName

;// Теперь всю информацию считали с карты - освобождаем выделенную память,
;// и выводим нарытую инфу на экран!
;// При этом моего процесса "marylin.exe" уже в списке не будет.
        invoke  VirtualFree,[buff],[buffSize],MEM_DECOMMIT
        invoke  MessageBox,0,text,capt,0
        invoke  ExitProcess, 0
.end start
task.png


Системный диспетчер обходит карту процессов точно таким-же способом, как в примере выше. Если-бы я оформил свой код в виде шелла и заинжектил-бы его в процесс диспетчера, то мой процесс пропал-бы и в его окне, а так только в моём. Это один из бородатых способов скрытия процессов, который уходит корнями в Win-98.

В сети можно встретить и другие варианты, которые и назвать-то скрытием можно с натяжкой – например сворачивание окон, или оформление программ в виде сервисов. Однако финты с картой ZwQuerySestemInformation() – это самый нижний уровень, который доступен прикладной задаче. С другой стороны, право на существование имеют любые варианты, лишь-бы они удовлетворяли нашим потребностям.
 

Gg wp

One Level
05.02.2020
1
2
BIT
0
Здравствуй.Ты пишешь очень крутые статьи,продолжай,не останавливайся.Ты не просто 0day находишь,Ты учишь модели модели поиска 0day.Ты просто мегакрут.Твои статьи очень сильно повышают интеллект, а при определенной подготовке еще и базу навыков и знаний.
 
Последнее редактирование:
  • Нравится
Реакции: bin1101d и Marylin

bin1101d

One Level
09.03.2021
7
4
BIT
0
Win10x32, полёт нормальный, процессов стало больше, размера text = 2048 не хватает, 8192 хватило для отображения полного списка процессов системы.

Огромное спасибо Marylin
 
Последнее редактирование:
  • Нравится
Реакции: Marylin

userqwerty12

New member
30.12.2021
1
0
BIT
0
Это самая крутая статья на тему "как скрыть процесс". Автор профи, объясняет максимально доступно и понятно. По больше бы таких статей, а то в гугле не так уж и много информации на подобные темы.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!