Win – это работающая по определённым правилам замкнутая система и если не придерживаться этих правил, её можно пустить по ложному следу. Тут главное не идти на поводу Microsoft, которая вправляет нам мозги своей/увесистой документацией. Сабж рассматривает несколько способов скрытия программ от системного "Диспетчера задач" как на пользовательском уровне, так и на уровне ядра.
4.5.0. Ядерный уровень
Всякий процесс в системе описывается своей структурой EPROCESS – сколько запущенных процессов, столько и структур. Чтобы держать все процессы под контролем, система связывает их в цепочку через механизм связных-списков LIST_ENTRY. Сам список состоит всего из двух элементов – это указатель FLink на следующую структуру EPROCESS в цепочке (Forward), и указатель BLink на предыдущую структуру в цепочке (Backward). На рисунке ниже, PsActiveProcessHead – это системный связной список, с него мастдай начинает сканировать активные процессы (в примере их всего три):
При такой схеме, чтобы скрыть процесс находящийся по-середине, нужно подправить FLink левого процесса так, чтобы он указывал на структуру EPROCESS сразу правого процесса, минуя средний. В свою очередь BLink правого процесса требует правки на структуру сразу левого, в обход среднего. В результате – средний процесс станет невидимкой. Разберёмся с деталями подробней..
По смещению 0x88 в структурах EPROCESS каждого из процессов есть поле ActiveProcessLinks, которое и представляет собой связной-список. Ознакомиться с ним можно в WinDbg, но сначала командой !process 0 0 получим список всех процессов в памяти. В первой строчке лога будет указан адрес структуры EPROCESS для представленного процесса (выделен цветом), который нужно вскормить следующей команде dt (display-type).
Для наглядности, на скрине ниже я запустил подряд три процесса вставив свой по середине - его и буду скрывать от ‘Диспетчера задач’ и прочих утилит подобного рода. Последний процесс в моей цепочке это ‘AkelPad’, соответственно его Flink будет указывать на системную PsActiveProcessHead, т.е. круг замкнётся. Точка в конце имени поля, раскрывает список:
Разберём эту схему на атомы..
Как видно, Flink моего/красного процесса имеет значение 0х80561358 (в кв.скобках), и это как-раз адрес Flink'а, который принадлежит следущему процессу в цепочке "AkelPad.exe" (т.е. двигаемся вперёд). В тоже время, значение моего Blink равно 0х89ad0ad8, и это в аккурат указатель на Blink предыдущего процесса "FSViewer.exe".
Теперь, если изменить данную схему как на рисунке ниже, мой красный процесс исчезнет из цепочки, хотя по-прежнему будет считаться активным, имея EPROCESS в системном пуле. То-есть мы копируем свой Flink в структуру предыдущего процесса, а Blink отправляем в следующий процесс:
Нужно сказать, что такой способ позволяет скрыть процесс от всех, включая саму систему. ОС не ведёт учёт конкретных значений в полях 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 (хотя могут быть и другие):
В своём младенчестве, эта недокументированная функция могла возвращать 150 типов информации о системе (т.н. класс), однако позже выяснилось, что большинство из них оказывают медвежью услугу, предоставляя хакерам широкий ассортимент. Так в последующих виндах классы урезали и на данный момент их осталось всего 54-штуки, хотя на сайте мелкософт указано итого меньше – 5 классов с пометкой, что остальные ушли в резерв для производственных нужд системы.
В этом списке под номером 5 числится класс ProcessesAndThreadsInformation, который и возвращает информацию о процессах (см.аргумент на рисунке выше).
Когда мы вызываем эту функцию с аргументом 5, то в приёмный буфер (обязательно должен быть выровнен на 4-байтную границу) сбрасывается инфа не только о процессах, но и потоках каждого из них. Поэтому размер буфера у ZwQuerySystemInformation() не фиксирован и зависит от числа активных на данный момент процессов. Если при вызове умышленно задать размер буфера в байт-так 16 (чего явно не хватит), то функция вернёт в регистр EAX ошибку 0xC0000004 - STATUS_INFO_LENGTH_MISMATCH и ..реально требуемый размер в байтах. Теперь можно выделить нужное кол-во памяти через VirtualAlloc(), и повторно запросить ZwQuerySystemInformation(). Прототипы у этих функций такие:
Если VIrtualAlloc() отработает удачно, то в регистре EAX получим указатель на выделенный ею блок памяти, иначе – нуль Отправив этот указатель как аргумент в ZwQuerySystemInformation(), в приёмном буфере получим инфу о всех процессах. Описатель каждого из процессов в буфере состоит из одной структуры PROCESS_INFO с общей информацией о данном процессе, и одной или нескольких (если процесс многопоточный) структур THREAD_INFO с информацией о его потоках. Описание этих структур приводится ниже:
Для скрытия процесса, информация о его тредах нам вообще ни к чему. Значимой фигурой является только поле NextEntryOffset – RVA-указатель на начало структуры следующего процесса в цепочке. В описателе последнего процесса, NextEntryOffset будет равен терминальному нулю.
На рисунке ниже, я привёл фрагмент дампа буфера ZwQuerySystemInformation(), где поле NextEntryOffset выделено овалом (это инфа только двух процессов из общего пула). В нём видно, что описатель моего процесса начинается по-адресу 0x00186Е40, а NextEntryOffset имеет значение 0x00000110. Если сложить эти два значения, то получим адрес описателя следующего процесса 0x00186Е40 + 0x0110 = 0x00186F50, который является последним в цепочке, т.к. его NextEntryOffset равен нулю. Указатель на Unicode-имя моего процесса лежит по адресу 0x00186Е7С и равен 0x00186F38.
Таким образом, чтобы скрыть свой процесс, мне нужно по имени найти его описатель и скорректировать поле NextEntryOffset предыдущего от меня процесса так, чтобы оно указывало на следующую запись – т.е. берём предыдущий NextEntryOffset и прибавляем к нему своё значение. После этих манипуляций, структура моего процесса будет считаться продолжением структуры предыдущего, что позволит скрыть её от посторонних глаз.
4.5.2. Практическая часть
Покончив с теорией – перейдём к практике..
Здесь есть несколько моментов, на которые следует обратить внимание:
1. Прежде всего, нужно перехватить функцию ZwQuerySystemInformation() из библиотеки Ntdll.dll, причём не в своём пространстве, а в пространстве “Диспетчера задач”. Дело в том, что перечислять процессы в своём окне будет диспетчер, а не я. Ему по-барабану, что я сброшу карту всех процессов и скорректирую её в своём буфере – он сам вызывает эту функцию и получает свою карту. Значит я должен опередить его вызов, возвратив ему фиктивные данные.
2. Когда после перехвата я получу управление, в стеке будет лежать адрес его буфера (диспетчер положит его в качестве аргумента функции) – мне остаётся лишь скрыть себя в его буфере. Любой, кто вызывает эту функцию с аргументом(5) хочет получить список процессов, поэтому буду проверять (опять таки в стеке) вызов именно на пятёрку, а остальные – игнорировать.
3. Возможно, что юзер вообще не планирует запускать “Диспетчера задач”, поэтому мне нужно отловить факт его запуска, и только потом инжектить свой шелл в его процесс. Для этого, можно с некоторым интервалом просто искать окно диспетчера по его названию функцией FindWindow(), и лучше всего создать для этого отдельный поток Thread. Тогда основной поток не отвлекаясь будет заниматься своими/тёмными делишками, а дополнительный поток будет охранять его тылы. Период поиска окна функцией Sleep() можно выставить на 1-сек, поскольку сам диспетчер в дефолте сканирует процессы с интервалом в 2-сек:
Думаю нет смысла выкладывать здесь готовый код скрытия процесса – программист должен дойти до этого сам. Кому интересно, задавайте вопросы и мы сделаем это вместе. А пока я просто приведу пример техники разбора и модификации карты-процессов, которую предоставит нам функция ZwQuerySystemInformation(). Код сбоксит в мессагу: имя, pid, число потоков, кол-во расходуемой памяти всех процессов, кроме своего. То-есть я сначала скрою свой процесс (от себя-же), а потом в цикле обойду всю карту точно так, как это делает системный “Диспетчер задач”:
Системный диспетчер обходит карту процессов точно таким-же способом, как в примере выше. Если-бы я оформил свой код в виде шелла и заинжектил-бы его в процесс диспетчера, то мой процесс пропал-бы и в его окне, а так только в моём. Это один из бородатых способов скрытия процессов, который уходит корнями в Win-98.
В сети можно встретить и другие варианты, которые и назвать-то скрытием можно с натяжкой – например сворачивание окон, или оформление программ в виде сервисов. Однако финты с картой ZwQuerySestemInformation() – это самый нижний уровень, который доступен прикладной задаче. С другой стороны, право на существование имеют любые варианты, лишь-бы они удовлетворяли нашим потребностям.
4.5.0. Ядерный уровень
Всякий процесс в системе описывается своей структурой EPROCESS – сколько запущенных процессов, столько и структур. Чтобы держать все процессы под контролем, система связывает их в цепочку через механизм связных-списков LIST_ENTRY. Сам список состоит всего из двух элементов – это указатель FLink на следующую структуру EPROCESS в цепочке (Forward), и указатель BLink на предыдущую структуру в цепочке (Backward). На рисунке ниже, PsActiveProcessHead – это системный связной список, с него мастдай начинает сканировать активные процессы (в примере их всего три):
При такой схеме, чтобы скрыть процесс находящийся по-середине, нужно подправить FLink левого процесса так, чтобы он указывал на структуру EPROCESS сразу правого процесса, минуя средний. В свою очередь BLink правого процесса требует правки на структуру сразу левого, в обход среднего. В результате – средний процесс станет невидимкой. Разберёмся с деталями подробней..
По смещению 0x88 в структурах EPROCESS каждого из процессов есть поле ActiveProcessLinks, которое и представляет собой связной-список. Ознакомиться с ним можно в WinDbg, но сначала командой !process 0 0 получим список всех процессов в памяти. В первой строчке лога будет указан адрес структуры EPROCESS для представленного процесса (выделен цветом), который нужно вскормить следующей команде dt (display-type).
Для наглядности, на скрине ниже я запустил подряд три процесса вставив свой по середине - его и буду скрывать от ‘Диспетчера задач’ и прочих утилит подобного рода. Последний процесс в моей цепочке это ‘AkelPad’, соответственно его Flink будет указывать на системную PsActiveProcessHead, т.е. круг замкнётся. Точка в конце имени поля, раскрывает список:
Разберём эту схему на атомы..
Как видно, Flink моего/красного процесса имеет значение 0х80561358 (в кв.скобках), и это как-раз адрес Flink'а, который принадлежит следущему процессу в цепочке "AkelPad.exe" (т.е. двигаемся вперёд). В тоже время, значение моего Blink равно 0х89ad0ad8, и это в аккурат указатель на Blink предыдущего процесса "FSViewer.exe".
Теперь, если изменить данную схему как на рисунке ниже, мой красный процесс исчезнет из цепочки, хотя по-прежнему будет считаться активным, имея EPROCESS в системном пуле. То-есть мы копируем свой Flink в структуру предыдущего процесса, а Blink отправляем в следующий процесс:
Нужно сказать, что такой способ позволяет скрыть процесс от всех, включая саму систему. ОС не ведёт учёт конкретных значений в полях 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 (хотя могут быть и другие):
- ZwQuerySystemInformation() из библиотеки Ntdll.dll;
- CreateToolhelp32Snapshot() из Kernel32.dll;
- EnumProcesses() из либы Psapi.dll.
В своём младенчестве, эта недокументированная функция могла возвращать 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
На рисунке ниже, я привёл фрагмент дампа буфера ZwQuerySystemInformation(), где поле NextEntryOffset выделено овалом (это инфа только двух процессов из общего пула). В нём видно, что описатель моего процесса начинается по-адресу 0x00186Е40, а NextEntryOffset имеет значение 0x00000110. Если сложить эти два значения, то получим адрес описателя следующего процесса 0x00186Е40 + 0x0110 = 0x00186F50, который является последним в цепочке, т.к. его NextEntryOffset равен нулю. Указатель на Unicode-имя моего процесса лежит по адресу 0x00186Е7С и равен 0x00186F38.
Таким образом, чтобы скрыть свой процесс, мне нужно по имени найти его описатель и скорректировать поле NextEntryOffset предыдущего от меня процесса так, чтобы оно указывало на следующую запись – т.е. берём предыдущий NextEntryOffset и прибавляем к нему своё значение. После этих манипуляций, структура моего процесса будет считаться продолжением структуры предыдущего, что позволит скрыть её от посторонних глаз.
4.5.2. Практическая часть
Покончив с теорией – перейдём к практике..
Здесь есть несколько моментов, на которые следует обратить внимание:
1. Прежде всего, нужно перехватить функцию ZwQuerySystemInformation() из библиотеки Ntdll.dll, причём не в своём пространстве, а в пространстве “Диспетчера задач”. Дело в том, что перечислять процессы в своём окне будет диспетчер, а не я. Ему по-барабану, что я сброшу карту всех процессов и скорректирую её в своём буфере – он сам вызывает эту функцию и получает свою карту. Значит я должен опередить его вызов, возвратив ему фиктивные данные.
2. Когда после перехвата я получу управление, в стеке будет лежать адрес его буфера (диспетчер положит его в качестве аргумента функции) – мне остаётся лишь скрыть себя в его буфере. Любой, кто вызывает эту функцию с аргументом(5) хочет получить список процессов, поэтому буду проверять (опять таки в стеке) вызов именно на пятёрку, а остальные – игнорировать.
3. Возможно, что юзер вообще не планирует запускать “Диспетчера задач”, поэтому мне нужно отловить факт его запуска, и только потом инжектить свой шелл в его процесс. Для этого, можно с некоторым интервалом просто искать окно диспетчера по его названию функцией FindWindow(), и лучше всего создать для этого отдельный поток Thread. Тогда основной поток не отвлекаясь будет заниматься своими/тёмными делишками, а дополнительный поток будет охранять его тылы. Период поиска окна функцией Sleep() можно выставить на 1-сек, поскольку сам диспетчер в дефолте сканирует процессы с интервалом в 2-сек:
Думаю нет смысла выкладывать здесь готовый код скрытия процесса – программист должен дойти до этого сам. Кому интересно, задавайте вопросы и мы сделаем это вместе. А пока я просто приведу пример техники разбора и модификации карты-процессов, которую предоставит нам функция 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
Системный диспетчер обходит карту процессов точно таким-же способом, как в примере выше. Если-бы я оформил свой код в виде шелла и заинжектил-бы его в процесс диспетчера, то мой процесс пропал-бы и в его окне, а так только в моём. Это один из бородатых способов скрытия процессов, который уходит корнями в Win-98.
В сети можно встретить и другие варианты, которые и назвать-то скрытием можно с натяжкой – например сворачивание окон, или оформление программ в виде сервисов. Однако финты с картой ZwQuerySestemInformation() – это самый нижний уровень, который доступен прикладной задаче. С другой стороны, право на существование имеют любые варианты, лишь-бы они удовлетворяли нашим потребностям.