Marylin
22.04.2026, 01:56
Шпионское ПО активно использует различные методы внедрения своих библиотек, чтобы скрыться в легитимных процессах системы. После внедрения удалить такую DLL довольно проблематично, если только не завершить процесс целиком. Однако имеется нюанс, при помощи которого всё-таки можно попытаться "вырезать" вредоносную либу из памяти инфицированного процесса, о чём и пойдёт речь в данной статье. Всё сводится к тому, чтобы найти и сбросить счётчик загрузок DLL в памяти пациента. Первую часть, где обсуждался наоборот инжект, можно найти здесь (https://forum.antichat.xyz/threads/592732/).
Оглавление:
1. Вводная часть
2. Поиск счётчика LoadCount в структурах РЕВ
3. Сбор информации об активных процессах
4. Несколько слов об интерфейсе программы
5. Заключение
1. Вводная часть - назначение счётчиков
Ресурсы системы не безграничны и если не вести над ними контроль, ситуация может уйти в разнос. Катастрофу предотвращают всего несколько счётчиков "Counter", которые можно разделить на 3 основных типа: HandleCount (ведёт учёт открытых дескрипторов), PointerCount (счётчик ссылок на объект), и LoadCount (счётчик загрузок DLL). Эта троица позволяет отслеживать, кто использует объект ядра, сколько ресурсов занято, и когда их можно безопасно освободить. При этом первые два счётчика лежат в компетенции системного менеджера объектов, а третьим LoadCount управляет уже загрузчик образов исполняемых файлов.
Тема настолько мутная, что проще объяснить её простым языком - вот байка...
1. HandleCount - это количество гостей в отеле (дескрипторов), которые держат ключ от номера (объекта). Пока хотя бы один гость не сдаст ключ на ресепшн, горничная (система) не может зайти убрать номер. Даже если гость уже выехал, но забыл ключ в кармане (не вызвал CloseHandle) - номер считается занятым. Типичная утечка памяти - это когда гость ушёл через окно, а ключ так и висит на доске ресепшна.
2. PointerCount - этот счётчик считает не только гостей с ключами, но и всех, кто просто посмотрел в сторону номера: горничную, сантехника, полицейского, случайно заглянувшую кошку. Когда счётчик обнуляется - объект действительно мёртв. Другими словами, если HandleCount - это количество рук держащих объект, то PointerCount - количество глаз, которые смотрят на него.
3. LoadCount - работает по принципу "тёщи в гостях", и напрямую зависит от баланса вызовов
LoadLibrary() = FreeLibrary()
. Первый раз тёща приехала на день +1, потом решает остаться ещё на неделю, уже +2. Но каждый раз, когда вы уходите на работу, она незаметно вызывает
LoadLibrary()
за вашей спиной ещё N-раз (динамическая загрузка DLL). Таким образом, чтобы отправить тёщю обратно домой, вы должны точно знать, до какого значения она накрутила LoadCount, иначе рискуете прожить с ней вечно, т.к. не сможете сбросить счётчик в нуль вызовом
FreeLibrary()
в цикле N-раз.
Таким образом:
HandleCount - сколько людей держат дверь,
PointerCount - сколько камер следят за дверью,
LoadCount - сколько раз вы позвали гостя, но забыли выпроводить.
1.1. Менеджер объектов
Процессы и потоки относятся к объектам ядра, и соответственно создаются в его недрах. Cчётчиками Handle и Pointer оперирует системный Object-Manager, который реализован как набор функций из Ntoskrnl.exe с префиксами Ob_xx. Например
ObpIncrementHandleCount()
увеличивает счётчик Handle на 1, а сопутствующая ей
ObpDecrementHandleCount()
уменьшает. В свою очередь счётчиком Pointer заправляет
ObReferenceObjectEx()
+1, а в обратную сторону
ObDeferenceObject()
-1.
К примеру если драйвер ядра хочет создать список всех активных процессов системы, он в цикле обходит все структуры EPROCESS, внутри которой имеется ссылка LIST_ENTRY на сл.структуру в цепочке. Но может получиться так, что пока драйвер приступит к чтению, ничего не подозревающий юзер закроет приложение снаружи и тогда неминуем BSOD. Поэтому перед чтением драйвер увеличивает счётчик PointerCount на 1 вызовом
ObReferenceObject()
, в результате чего система уже не сможет выгрузить из памяти нужную EPROCESS, пока дров не освободит заблокированный объект через
ObDeferenceObject()
.
Чтобы было немного наглядней, можно запросить инфу о любом процессе в отладчике WinDbg - пусть это будет калькулятор.
Код:
0: kd> !process 0 0 calc.exe
PROCESS fffffa800ca648b0
Session: 1 Cid: 13fc Peb: 7fffffd6000 ParentCid: 0598
DirBase: 91a8b000 ObjectTable: fffff8a00a815ad0 HandleCount: 97 dt _object_header fffffa800ca648b0-30
nt!_OBJECT_HEADER
+0x000 PointerCount : 54
+0x008 HandleCount : 3
......
+0x030 Body : _QUAD !process 0 0 calc.exe
PROCESS fffffa800fd28b00
Session: 1 Cid: 0eb4 Peb: 7fffffdc000 ParentCid : 04c4
DirBase: 31ee53000 ObjectTable: fffff8a00d154590 HandleCount: 97
Image : calc.exe
;//------- Переключаемся на контекст процесса ---------------//
0: kd> .process /r fffffa800fd28b00
Implicit process is now fffffa80`0fd28b00
;//------- Находим указатель на PEB_LDR_DATA ----------------//
0: kd> dt _peb 7fffffdc000 Ldr
nt!_PEB
+0x018 Ldr : 0x00000000`77df2e40 _PEB_LDR_DATA
;//------- Получаем адрес LDR_DATA_TABLE_ENTRY --------------//
0: kd> dt _peb_ldr_data 0x00000000`77df2e40
nt!_PEB_LDR_DATA
+0x000 Length : 0x58
+0x004 Initialized : 0x1
+0x008 SsHandle : (null)
+0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x00292880 - 0x2d2140 ] dt _ldr_data_table_entry 0x2d2140
nt!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x77df2e50 - 0x2d2050 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x77df2e60 - 0x2d2060 ]
+0x020 InInitOrderLinks : _LIST_ENTRY [ 0x77df2e70 - 0x2d2070 ]
+0x030 DllBase : 0x000007fe`e04b0000 Void
+0x038 EntryPoint : 0x000007fe`e04b104c Void
+0x040 SizeOfImage : 0x54000
+0x048 FullDllName : _UNICODE_STRING "C:\Windows\system32\oleacc.dll"
+0x058 BaseDllName : _UNICODE_STRING "oleacc.dll"
+0x068 Flags : 0xc4004
+0x06c LoadCount : 12
Как видно из этого лога, структура описывает одну из загруженных в процесс калькулятора либу Oleacc.dll - есть полный путь до неё, база в памяти, точка-входа, и наконец счётчик. Но почему он имеет такое большое значение 12 ? Дело в том, что эта библиотека может иметь зависимости, т.е. в ней нуждается ещё какая-то DLL в этом-же процессе, и такая цепочка может быть очень длинной. Каждая из библиотек не подозревает о существовании соседа, в результате чего счётчик может иметь вообще заоблачные значения, например 1000, в чём мы убедимся позже.
Кстати начиная с Win8 счётчик LoadCount сменил своё место жительства - в хвост структуры LDR_DATA_TABLE_ENTRY были добавлены новые поля, и счётчик теперь можно найти в доп.структуре LDR_DDAG_NODE. Однако хоть он и переехал, но до сих пор действительная старая прописка осталась, только на ней стоит штамп "Obsolete" (устарело).
Код:
struct LDR_DATA_TABLE_ENTRY_W10 ;//<--- sizeof 0x120 bytes
InLoadOrderLinks LIST_ENTRY ;// 0x00
InMemoryOrderLinks LIST_ENTRY ;// 0x10
InInitOrderLinks LIST_ENTRY ;// 0x20
DllBase dq 0 ;// 0x30
EntryPoint dq 0 ;// 0x38
SizeOfImage dq 0 ;// 0x40
FullDllName dq 0,0 ;// 0x48 = UNICODE_STRING
BaseDllName dq 0,0 ;// 0x58 = UNICODE_STRING
Flags dd 0 ;// 0x68
Obsolete_LoadCount dw 0 ;// 0x6c <--- Счётчик на Win7
........
DdagNode dq 0 ;// 0x98 = LDR_DDAG_NODE <-- Счётчик теперь здесь
........ ;// |
ends ;// |
;// |
struct LDR_DDAG_NODE ;// |
Modules LIST_ENTRY ;// 0x00 |
ServiceTagList dq 0 ;// 0x10 |
LoadCount dd 0 ;// 0x18 <----+
........
ends
2.1. Доступ к структуре РЕВ удалённого процесса
Таким образом, если мы хотим выгрузить вредоносную DLL из памяти произвольного процесса, нужно:
1. Найти и прочитать его структуру РЕВ,
2. Используя РЕВ_LDR_DATA, cоздать список загруженных в процесс DLL,
3. Прочитать значение счётчика LoadCount каждой из найденных библиотек,
4. Определить, вредоносная она или нет (можно проверить путь),
5. Если да, используя счётчик в качестве цикла, вызывать
FreeLibrary()
,
6. Как только цикл закончится, загрузчик образов сам отправит либу в топку.
На словах всё кажется страшно, а на деле выходит элементарно. Главное здесь найти адрес структуры РЕВ в удалённом процессе. Если в своём достаточно прочитать регистр
FS/GS
, то с целевым процессом такой фокус уже не пройдёт, т.к. у него свои регистры. Однако в резерве у нас имеется багаж нативных функций из Ntdll.dll, при помощи которых можно пролезть в самые закрома системы.
В частности
NtQueryInformationProcess()
с классом ProcessBasicInfo=0, вернёт нам требуемый линк на РЕВ в структуру сл.характера:
C-подобный:
WINAPI
NtQueryInformationProcess
(
In HANDLE ProcessHandle
,
;
// берём у OpenProcess()
In INFOCLASS ProcessInformationClass
,
;
// класс = 0
Out PVOID ProcessInformation
,
;
// адрес приёмного буфа
In ULONG ProcessInformationLength
,
;
// sizeof.PROCESS_BASIC_INFO_64
Out PULONG ReturnLength
;
// реальное кол-во данных
struct PROCESS_BASIC_INFO_64
ExitStatus dd
0
,
0
;
// код выхода
PebBaseAddress dq
0
;
// линк на РЕВ <-------
AffinityMask dq
0
;
// привязка к ядрам ЦП
BasePriority dd
0
,
0
;
// приоритет
UniqueProcessId dq
0
;
// PID процесса
InheritedFromUniquePid dq
0
;
// PID родителя
ends
3. Сбор информации об активных процессах
Для перечисления всех активных процессов, Win предлагает несколько своих API, например:
Process32First/Next()
из kernel32.dll, или
EnumProcesses()
из psapi.dll. Но в своём коде я использовал
NtQuerySystemInformation()
с классом ProcessAndThreadInfo=5, поскольку за один выстрел она возвращает не только инфу обо всех процессах, но и потоках каждого из них, в то время как первые две функции нужно вызывать в цикле. Выхлоп нативной впечатляет, и получим массив таких структур, в которой первое поле NextEntryOffset будет хранить указатель на аналогичную структуру сл.процесса. Терминальный нуль в этом поле определяет конец массива:
C-подобный:
;
// Info class = 5
;
//--------------------------------
struct SYSTEM_PROCESS_INFORMATION64
;
//<---- Размер = 264 байта
NextEntryOffset dd
0
;
// линк на структуру сл.процесса
NumberOfThreads dd
0
;
// всего тредов в текущем
WorkingSetPrivateSize dq
0
HardFaultCount dd
0
NumberOfThreadsWatermark dd
0
CycleTime dq
0
CreateTime dq
0
UserTime dq
0
KernelTime dq
0
ImageName UNICODE_STRING
BasePriority dq
0
UniqueProcessId dq
0
;
// PID
InheritedFromUniquePId dq
0
HandleCount dd
0
;
// счётчик дескрипторов
SessionId dd
0
;
// сессия
UniqueProcessKey dq
0
PeakVirtualSize dq
0
VirtualSize dq
0
PageFaultCount dq
0
PeakWorkingSetSize dq
0
WorkingSetSize dq
0
QuotaPeakPagedPoolUsage dq
0
QuotaPagedPoolUsage dq
0
QuotaPeakNonPagedPool dq
0
QuotaNonPagedPoolUsage dq
0
PagefileUsage dq
0
PeakPagefileUsage dq
0
PrivatePageCount dq
0
ReadOperationCount dq
0
WriteOperationCount dq
0
OtherOperationCount dq
0
ReadTransferCount dq
0
WriteTransferCount dq
0
OtherTransferCount dq
0
ends
Теперь берём идентификатор PID из этой структуры, и через
OpenProcess()
пытаемся открыть его. Если ОК, то получим дескриптор процесса, который откроет для нас все двери, например для чтения структуры РЕВ через
ReadProcessMemory()
, со всеми вытекающими. Главное требование здесь - это права админа, и привилегия SeDebugPrivilege, иначе не сможем открыть системные процессы Csrss/Lsass/Svchost.exe и прочие, а так-же процессы в закрытой сессии нуль.
4. Несколько слов об интерфейсе
Всё это дело я запихал в форточку, под катом которой работает движок. Разбитый на 5 столбцов элемент "ListView" как нельзя лучше подходит для сброса инфы, но некоторые нюансы хотелось-бы прояснить.
Значит список всех активных процессов лежит в раскрывающемся "ComboBox", а при выборе строки в нём, заполняется таблица "ListView" с именами модулей. В самой таблице можно выбрать уже библиотеку, и если в столбце "Счётчик" найдём значение не
0xFFFF
(т.е. либа загружается динамически), значит её можно выгрузить и активируется одноимённый буттон в подвале окна.
Внимание! Поскольку операция выгрузки DLL из памяти активного процесса без чёткого понимания происходящего может привести не только к краху самого процесса, но и потери данных на диске (например если процесс системный и критически важен для неё), то в данной версии утилиты я вырезал функционал кнопки "Выгрузить" - при нажатии на неё просто получим мессагу "Демо-версия". Найти левую библиотеку из всего списка можно проверив полный её путь - если она грузиться не из папки system32, это должно настораживать.
Процессы могут быть разные, а у легальных всегда должна присутствовать информация об авторских правах. Она хранится в ресурсе исполняемого файла под ником VERSIONINFO. Для чтения этих данных предусмотрены специальные API из либы version.dll - первая
GetFileVersionInfo()
сбрасывает весь блок в буфер, а вторая
VerQueryValue()
читает нужный раздел. Как результат под каждым выбранным процессом получим его описание.
Чтобы было понятно о чём речь, вот несколько разных скринов.
Как видим, с правами админа и привилегией SeDebug, можно собрать сведения и с процессов в закрытой сессии нуль.
https://forum.antichat.xyz/attachments/4951712/img_2b676ced8b.png
Проводник системы - явный чемпион по кол-ву модулей в своей тушке
https://forum.antichat.xyz/attachments/4951712/img_0a40a1bd05.png
Счётчик LoadCount может иметь исполинские значения
https://forum.antichat.xyz/attachments/4951712/img_7d0318774d.png
5. Заключение
Биологические вирусы появились в результате жёсткой борьбы за существование. Чтобы не добывать пропитание самим, они научились паразитировать на других существах, но со временем медленно притирались друг к другу, образуя различные формы симбиоза. К примеру каждый из нас носит в себе кишечную палочку, можно даже сказать кишит ею, и ничего.. живём припеваючи.
Внедрение DLL внешне похоже на паразитирование, но это не так. Когда народ обменивался файлами, таская под мышками дискеты схема работала. Однако с появлением Инета ситуация в корень изменилась - теперь все перешли на централизованную раздачу с официальных серверов по типу общепита, и необходимость паразитировать на других программах отпала сама-собой. Сейчас инжект носит лишь характер маскировки для сокрытия факта своего присутствия в системе - пробравшись на чужую территорию он просто ждёт своего часа, а о захвате ресурсов речь вообще не идёт. С другой стороны кибернетический мир меняется быстро, и мы не можем предугадать, что будет завтра.
В скрепку кладу инклуд "ntdll.inc" с описанием структур, исходник программы для сборки ассемблером FASM, а так-же готовый исполняемый файл для тестов. Всем удачи, пока!
Оглавление:
1. Вводная часть
2. Поиск счётчика LoadCount в структурах РЕВ
3. Сбор информации об активных процессах
4. Несколько слов об интерфейсе программы
5. Заключение
1. Вводная часть - назначение счётчиков
Ресурсы системы не безграничны и если не вести над ними контроль, ситуация может уйти в разнос. Катастрофу предотвращают всего несколько счётчиков "Counter", которые можно разделить на 3 основных типа: HandleCount (ведёт учёт открытых дескрипторов), PointerCount (счётчик ссылок на объект), и LoadCount (счётчик загрузок DLL). Эта троица позволяет отслеживать, кто использует объект ядра, сколько ресурсов занято, и когда их можно безопасно освободить. При этом первые два счётчика лежат в компетенции системного менеджера объектов, а третьим LoadCount управляет уже загрузчик образов исполняемых файлов.
Тема настолько мутная, что проще объяснить её простым языком - вот байка...
1. HandleCount - это количество гостей в отеле (дескрипторов), которые держат ключ от номера (объекта). Пока хотя бы один гость не сдаст ключ на ресепшн, горничная (система) не может зайти убрать номер. Даже если гость уже выехал, но забыл ключ в кармане (не вызвал CloseHandle) - номер считается занятым. Типичная утечка памяти - это когда гость ушёл через окно, а ключ так и висит на доске ресепшна.
2. PointerCount - этот счётчик считает не только гостей с ключами, но и всех, кто просто посмотрел в сторону номера: горничную, сантехника, полицейского, случайно заглянувшую кошку. Когда счётчик обнуляется - объект действительно мёртв. Другими словами, если HandleCount - это количество рук держащих объект, то PointerCount - количество глаз, которые смотрят на него.
3. LoadCount - работает по принципу "тёщи в гостях", и напрямую зависит от баланса вызовов
LoadLibrary() = FreeLibrary()
. Первый раз тёща приехала на день +1, потом решает остаться ещё на неделю, уже +2. Но каждый раз, когда вы уходите на работу, она незаметно вызывает
LoadLibrary()
за вашей спиной ещё N-раз (динамическая загрузка DLL). Таким образом, чтобы отправить тёщю обратно домой, вы должны точно знать, до какого значения она накрутила LoadCount, иначе рискуете прожить с ней вечно, т.к. не сможете сбросить счётчик в нуль вызовом
FreeLibrary()
в цикле N-раз.
Таким образом:
HandleCount - сколько людей держат дверь,
PointerCount - сколько камер следят за дверью,
LoadCount - сколько раз вы позвали гостя, но забыли выпроводить.
1.1. Менеджер объектов
Процессы и потоки относятся к объектам ядра, и соответственно создаются в его недрах. Cчётчиками Handle и Pointer оперирует системный Object-Manager, который реализован как набор функций из Ntoskrnl.exe с префиксами Ob_xx. Например
ObpIncrementHandleCount()
увеличивает счётчик Handle на 1, а сопутствующая ей
ObpDecrementHandleCount()
уменьшает. В свою очередь счётчиком Pointer заправляет
ObReferenceObjectEx()
+1, а в обратную сторону
ObDeferenceObject()
-1.
К примеру если драйвер ядра хочет создать список всех активных процессов системы, он в цикле обходит все структуры EPROCESS, внутри которой имеется ссылка LIST_ENTRY на сл.структуру в цепочке. Но может получиться так, что пока драйвер приступит к чтению, ничего не подозревающий юзер закроет приложение снаружи и тогда неминуем BSOD. Поэтому перед чтением драйвер увеличивает счётчик PointerCount на 1 вызовом
ObReferenceObject()
, в результате чего система уже не сможет выгрузить из памяти нужную EPROCESS, пока дров не освободит заблокированный объект через
ObDeferenceObject()
.
Чтобы было немного наглядней, можно запросить инфу о любом процессе в отладчике WinDbg - пусть это будет калькулятор.
Код:
0: kd> !process 0 0 calc.exe
PROCESS fffffa800ca648b0
Session: 1 Cid: 13fc Peb: 7fffffd6000 ParentCid: 0598
DirBase: 91a8b000 ObjectTable: fffff8a00a815ad0 HandleCount: 97 dt _object_header fffffa800ca648b0-30
nt!_OBJECT_HEADER
+0x000 PointerCount : 54
+0x008 HandleCount : 3
......
+0x030 Body : _QUAD !process 0 0 calc.exe
PROCESS fffffa800fd28b00
Session: 1 Cid: 0eb4 Peb: 7fffffdc000 ParentCid : 04c4
DirBase: 31ee53000 ObjectTable: fffff8a00d154590 HandleCount: 97
Image : calc.exe
;//------- Переключаемся на контекст процесса ---------------//
0: kd> .process /r fffffa800fd28b00
Implicit process is now fffffa80`0fd28b00
;//------- Находим указатель на PEB_LDR_DATA ----------------//
0: kd> dt _peb 7fffffdc000 Ldr
nt!_PEB
+0x018 Ldr : 0x00000000`77df2e40 _PEB_LDR_DATA
;//------- Получаем адрес LDR_DATA_TABLE_ENTRY --------------//
0: kd> dt _peb_ldr_data 0x00000000`77df2e40
nt!_PEB_LDR_DATA
+0x000 Length : 0x58
+0x004 Initialized : 0x1
+0x008 SsHandle : (null)
+0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x00292880 - 0x2d2140 ] dt _ldr_data_table_entry 0x2d2140
nt!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x77df2e50 - 0x2d2050 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x77df2e60 - 0x2d2060 ]
+0x020 InInitOrderLinks : _LIST_ENTRY [ 0x77df2e70 - 0x2d2070 ]
+0x030 DllBase : 0x000007fe`e04b0000 Void
+0x038 EntryPoint : 0x000007fe`e04b104c Void
+0x040 SizeOfImage : 0x54000
+0x048 FullDllName : _UNICODE_STRING "C:\Windows\system32\oleacc.dll"
+0x058 BaseDllName : _UNICODE_STRING "oleacc.dll"
+0x068 Flags : 0xc4004
+0x06c LoadCount : 12
Как видно из этого лога, структура описывает одну из загруженных в процесс калькулятора либу Oleacc.dll - есть полный путь до неё, база в памяти, точка-входа, и наконец счётчик. Но почему он имеет такое большое значение 12 ? Дело в том, что эта библиотека может иметь зависимости, т.е. в ней нуждается ещё какая-то DLL в этом-же процессе, и такая цепочка может быть очень длинной. Каждая из библиотек не подозревает о существовании соседа, в результате чего счётчик может иметь вообще заоблачные значения, например 1000, в чём мы убедимся позже.
Кстати начиная с Win8 счётчик LoadCount сменил своё место жительства - в хвост структуры LDR_DATA_TABLE_ENTRY были добавлены новые поля, и счётчик теперь можно найти в доп.структуре LDR_DDAG_NODE. Однако хоть он и переехал, но до сих пор действительная старая прописка осталась, только на ней стоит штамп "Obsolete" (устарело).
Код:
struct LDR_DATA_TABLE_ENTRY_W10 ;//<--- sizeof 0x120 bytes
InLoadOrderLinks LIST_ENTRY ;// 0x00
InMemoryOrderLinks LIST_ENTRY ;// 0x10
InInitOrderLinks LIST_ENTRY ;// 0x20
DllBase dq 0 ;// 0x30
EntryPoint dq 0 ;// 0x38
SizeOfImage dq 0 ;// 0x40
FullDllName dq 0,0 ;// 0x48 = UNICODE_STRING
BaseDllName dq 0,0 ;// 0x58 = UNICODE_STRING
Flags dd 0 ;// 0x68
Obsolete_LoadCount dw 0 ;// 0x6c <--- Счётчик на Win7
........
DdagNode dq 0 ;// 0x98 = LDR_DDAG_NODE <-- Счётчик теперь здесь
........ ;// |
ends ;// |
;// |
struct LDR_DDAG_NODE ;// |
Modules LIST_ENTRY ;// 0x00 |
ServiceTagList dq 0 ;// 0x10 |
LoadCount dd 0 ;// 0x18 <----+
........
ends
2.1. Доступ к структуре РЕВ удалённого процесса
Таким образом, если мы хотим выгрузить вредоносную DLL из памяти произвольного процесса, нужно:
1. Найти и прочитать его структуру РЕВ,
2. Используя РЕВ_LDR_DATA, cоздать список загруженных в процесс DLL,
3. Прочитать значение счётчика LoadCount каждой из найденных библиотек,
4. Определить, вредоносная она или нет (можно проверить путь),
5. Если да, используя счётчик в качестве цикла, вызывать
FreeLibrary()
,
6. Как только цикл закончится, загрузчик образов сам отправит либу в топку.
На словах всё кажется страшно, а на деле выходит элементарно. Главное здесь найти адрес структуры РЕВ в удалённом процессе. Если в своём достаточно прочитать регистр
FS/GS
, то с целевым процессом такой фокус уже не пройдёт, т.к. у него свои регистры. Однако в резерве у нас имеется багаж нативных функций из Ntdll.dll, при помощи которых можно пролезть в самые закрома системы.
В частности
NtQueryInformationProcess()
с классом ProcessBasicInfo=0, вернёт нам требуемый линк на РЕВ в структуру сл.характера:
C-подобный:
WINAPI
NtQueryInformationProcess
(
In HANDLE ProcessHandle
,
;
// берём у OpenProcess()
In INFOCLASS ProcessInformationClass
,
;
// класс = 0
Out PVOID ProcessInformation
,
;
// адрес приёмного буфа
In ULONG ProcessInformationLength
,
;
// sizeof.PROCESS_BASIC_INFO_64
Out PULONG ReturnLength
;
// реальное кол-во данных
struct PROCESS_BASIC_INFO_64
ExitStatus dd
0
,
0
;
// код выхода
PebBaseAddress dq
0
;
// линк на РЕВ <-------
AffinityMask dq
0
;
// привязка к ядрам ЦП
BasePriority dd
0
,
0
;
// приоритет
UniqueProcessId dq
0
;
// PID процесса
InheritedFromUniquePid dq
0
;
// PID родителя
ends
3. Сбор информации об активных процессах
Для перечисления всех активных процессов, Win предлагает несколько своих API, например:
Process32First/Next()
из kernel32.dll, или
EnumProcesses()
из psapi.dll. Но в своём коде я использовал
NtQuerySystemInformation()
с классом ProcessAndThreadInfo=5, поскольку за один выстрел она возвращает не только инфу обо всех процессах, но и потоках каждого из них, в то время как первые две функции нужно вызывать в цикле. Выхлоп нативной впечатляет, и получим массив таких структур, в которой первое поле NextEntryOffset будет хранить указатель на аналогичную структуру сл.процесса. Терминальный нуль в этом поле определяет конец массива:
C-подобный:
;
// Info class = 5
;
//--------------------------------
struct SYSTEM_PROCESS_INFORMATION64
;
//<---- Размер = 264 байта
NextEntryOffset dd
0
;
// линк на структуру сл.процесса
NumberOfThreads dd
0
;
// всего тредов в текущем
WorkingSetPrivateSize dq
0
HardFaultCount dd
0
NumberOfThreadsWatermark dd
0
CycleTime dq
0
CreateTime dq
0
UserTime dq
0
KernelTime dq
0
ImageName UNICODE_STRING
BasePriority dq
0
UniqueProcessId dq
0
;
// PID
InheritedFromUniquePId dq
0
HandleCount dd
0
;
// счётчик дескрипторов
SessionId dd
0
;
// сессия
UniqueProcessKey dq
0
PeakVirtualSize dq
0
VirtualSize dq
0
PageFaultCount dq
0
PeakWorkingSetSize dq
0
WorkingSetSize dq
0
QuotaPeakPagedPoolUsage dq
0
QuotaPagedPoolUsage dq
0
QuotaPeakNonPagedPool dq
0
QuotaNonPagedPoolUsage dq
0
PagefileUsage dq
0
PeakPagefileUsage dq
0
PrivatePageCount dq
0
ReadOperationCount dq
0
WriteOperationCount dq
0
OtherOperationCount dq
0
ReadTransferCount dq
0
WriteTransferCount dq
0
OtherTransferCount dq
0
ends
Теперь берём идентификатор PID из этой структуры, и через
OpenProcess()
пытаемся открыть его. Если ОК, то получим дескриптор процесса, который откроет для нас все двери, например для чтения структуры РЕВ через
ReadProcessMemory()
, со всеми вытекающими. Главное требование здесь - это права админа, и привилегия SeDebugPrivilege, иначе не сможем открыть системные процессы Csrss/Lsass/Svchost.exe и прочие, а так-же процессы в закрытой сессии нуль.
4. Несколько слов об интерфейсе
Всё это дело я запихал в форточку, под катом которой работает движок. Разбитый на 5 столбцов элемент "ListView" как нельзя лучше подходит для сброса инфы, но некоторые нюансы хотелось-бы прояснить.
Значит список всех активных процессов лежит в раскрывающемся "ComboBox", а при выборе строки в нём, заполняется таблица "ListView" с именами модулей. В самой таблице можно выбрать уже библиотеку, и если в столбце "Счётчик" найдём значение не
0xFFFF
(т.е. либа загружается динамически), значит её можно выгрузить и активируется одноимённый буттон в подвале окна.
Внимание! Поскольку операция выгрузки DLL из памяти активного процесса без чёткого понимания происходящего может привести не только к краху самого процесса, но и потери данных на диске (например если процесс системный и критически важен для неё), то в данной версии утилиты я вырезал функционал кнопки "Выгрузить" - при нажатии на неё просто получим мессагу "Демо-версия". Найти левую библиотеку из всего списка можно проверив полный её путь - если она грузиться не из папки system32, это должно настораживать.
Процессы могут быть разные, а у легальных всегда должна присутствовать информация об авторских правах. Она хранится в ресурсе исполняемого файла под ником VERSIONINFO. Для чтения этих данных предусмотрены специальные API из либы version.dll - первая
GetFileVersionInfo()
сбрасывает весь блок в буфер, а вторая
VerQueryValue()
читает нужный раздел. Как результат под каждым выбранным процессом получим его описание.
Чтобы было понятно о чём речь, вот несколько разных скринов.
Как видим, с правами админа и привилегией SeDebug, можно собрать сведения и с процессов в закрытой сессии нуль.
https://forum.antichat.xyz/attachments/4951712/img_2b676ced8b.png
Проводник системы - явный чемпион по кол-ву модулей в своей тушке
https://forum.antichat.xyz/attachments/4951712/img_0a40a1bd05.png
Счётчик LoadCount может иметь исполинские значения
https://forum.antichat.xyz/attachments/4951712/img_7d0318774d.png
5. Заключение
Биологические вирусы появились в результате жёсткой борьбы за существование. Чтобы не добывать пропитание самим, они научились паразитировать на других существах, но со временем медленно притирались друг к другу, образуя различные формы симбиоза. К примеру каждый из нас носит в себе кишечную палочку, можно даже сказать кишит ею, и ничего.. живём припеваючи.
Внедрение DLL внешне похоже на паразитирование, но это не так. Когда народ обменивался файлами, таская под мышками дискеты схема работала. Однако с появлением Инета ситуация в корень изменилась - теперь все перешли на централизованную раздачу с официальных серверов по типу общепита, и необходимость паразитировать на других программах отпала сама-собой. Сейчас инжект носит лишь характер маскировки для сокрытия факта своего присутствия в системе - пробравшись на чужую территорию он просто ждёт своего часа, а о захвате ресурсов речь вообще не идёт. С другой стороны кибернетический мир меняется быстро, и мы не можем предугадать, что будет завтра.
В скрепку кладу инклуд "ntdll.inc" с описанием структур, исходник программы для сборки ассемблером FASM, а так-же готовый исполняемый файл для тестов. Всем удачи, пока!