Здесь будут собраны мои мучения по WinDBG и всему такому, удачных бессонных ночей в ядре : )
SRV*C:\ms_symbols*https://msdl.microsoft.com/download/symbols;c:\symbols
*** кэшироваться будут в C:\ProgramData\dbg\sym\
*** если что-то не работает - !sym noisy будет выводиться больше инфы
.symfix C:\ms_symbols
.reload /f
.sympath+ C:\symbols
.reload
Мой странный, но рабочий (остальные постоянно ломаются) способ
Windbg на хосте ищет папку с дебаг символами, но обычно компилим мы на самой вартуалке, поэтому можем просто создать такую же папку на хосте, типо C:\Users\user\Source\Repos\driver1\x64\Release
И спокойно положить в неё наш driver1.pdb
При обновлении и перекомпиляции всё, что нам необходимо, так это просто закинуть новый файл с символами
профит
Каждый процесс представлен структурой EPROCESS (executive process block) в ядре
EPROCESS указывает на число связанных структур, например: у каждого процесса есть 1 или более потоков, которые представляются структурой ETHREAD
EPROCESS указывает на PEB (process environment block) в адресном пространстве процесса
ETHREAD указывает на TEB (thread environment block) в адресном пространстве процесса
!peb
dt nt!_PEB
kd> .process /p ffffe20fb340e080; !peb 10f90d5000
предварительно получив список процессов с адресами: !process 0 0
!teb
dt nt!_TEB
kd> !process 0 4 processname.exe
kd> dt nt!_KTHREAD ffffe20faeb39080
Общая идея примерно такая:
dt nt!_EPROCESS -l ActiveProcessLinks.Flink ffffca86`79a5b040
Английская статья, откуда брал почти весь материал и пара ссылок на структуры
Структура стека:
Что мы можем прочитать на msdn:
Включаем stack traces и page heap для процесса:
Меняем контекст процесса:
Summary about memory usage for your process.
If RegionUsageHeap or RegionUsagePageHeap is growing constantly, then you might have a memory leak on the heap. Proceed with the following steps.
!address --summary
Сделаем фиктивную утечку памяти и посмотрим на неё, можно что-то в этом духе:
Классная преза, с кучей полезного по windbg
Automatic pseudo-registers
User-defined pseudo-registers
Команда | Пояснение к ней |
---|---|
$ra | Return address currently on the stack. Useful in execution commands, i.e.: “g $ra” |
$ip | The instruction pointer x86 = EIP, Itanium = IIP, x64 = RIP |
$exentry | Entry point of the first executable of the current process |
$retreg | Primary return value register x86 = EAX, Itanium = ret0, x64 = rax |
$csp | Call stack pointer X86 = ESP, Itanium = BSP, x64 = RSP |
$peb | Address of the process environment block (PEB) |
$teb | Address of the thread environment block (TEB) of current thread |
$tpid | Process ID (PID) |
$tid | Thread ID (tID) |
$ptrsize | Size of a pointer |
$pagesize | Number of bytes in one page of memory |
MASM expressions
myfile.c:43
)C++ expressions
MASM operations are always byte based. C++ operations follow C++ type rules (including the scaling of
pointer arithmetic). In both cases numerals are treated internally as ULON64 values.
Note: Under the hood Application Verifier injects a number of DLLs (verifier.dll, vrfcore.dll, vfbasics.dll, vfcompat.dll, and more) into the target application. More precisely: It sets a registry key according to the selected tests for the image in question. The windows loader reads this registry key and loads the specified DLLs into the applications address space while starting it.
GFlags Application Verifier:
Application Verifier:
По псевдо регистрам офиц ман
Куча полезностей по командам
Microsoft изменили способ обработки прерываний в последних версиях Windows. Были опубликованы некоторые публичные ресёрчи по обработке прерываний на старых версиях Windows и на 32-битных системах, однако не так много информации можно найти о том, как это работает в современном мире. В этой статье я попытаюсь привести описание обработки исключений на 64-битной Windows 10, в особенности Windows 10 RS1 Anniversary Update Build 10.0.10586
Прерывания используются операционными системами, чтобы получать сообщения об ивентах, происходящих на оборудовании. Обработка исключений - это механизм, в котором процессор передаёт контроль исполнения программному обеспечению, чтобы обработать событие на оборудовании. Прерывания обрабатываются ядром Windows, которое сначала выполняет некоторые служебные действия перед передачей контроля исполнения драйверам железа, которые в свою очередь регистрируют ISR (функции обработчика прерывания). IDT (Interrupt Descriptor Table) - это основная структура, задействованная в обработке исключений и её формат устанавливает разработчик процессора. IDT должна быть заполнена на этапе загрузки и соответственно должна использоваться процессором для обработки прерываний, приходящих с устройств
У процессоров есть встроенный регистр, называемый IDTR, который Windows заполняет виртуальным адресом IDT в ядре, который он устанавливает для каждого процессора на этапе загрузки.
Значение регистра IDTR для каждого процессора. На мульти процессорной системе каждый процессор имеет свой IDTR регистр, который указывает на локальную приватную копию IDT
IDT содержит всего 256 значений, некоторые из которых используются для исключений, некоторые для программных прерываний, а остальные для прерываний железа. Индекс в IDT, по которому выбирают конкретный элемент, называется вектором прерывания. Формат каждого элемента IDT описывается разработчиком процессора.
Ядро Windows определяет структуру KIDTENTRY64, которая представляет собой один элемент IDT на 64-битном процессоре. Используя вывод предыдущей команды "r @idtr", мы можем вывести нулевой элемент IDT, на который указывает IDTR
Комбинация OffsetHigh, OffsetMiddle и OffsetLow даёт нам виртуальный адрес, куда процессор передаст поток выполнения, когда произойдёт прерывание. В выводе выше виртуальный адрес - 0xfffff80518001c00. Это совпадает с выводом "!idt 0" и указывает на фукнцию KiDivideErrorFault(). Значение поля Type в выводе выше (0xe) показывает, что поле в IDT представляет собой Interrupt Gate
Первые N элементов в IDT нужны для обработки исключений и определены разработчиком процессора. Остальные элементы или используются для программных прерываний, или для хардварных, или не используются вовсе. В выводе "!idt" хардварные прерывания очень просто определить: у них есть указатель на структуру KINTERRUPT. "!idt -a" показывает значения всей IDT
В этой статье мы сфокусируемся на хардварных прерываниях, соответственно последние элементы IDT. hex значение в первой колонке - это вектор или индекс прерывания, по которому и находится конкретное прерывание в IDT. Как было сказано ранее, каждый элемент IDT указывает на набор инструкций, которые будут выполнены, как один из этапов обработки исключения.
Давайте возьмём второй элемент в хардварной части IDR, вектор 0x50
Выведем IDT 0x50, используя IDTR
Когда появляется прерывание, исполнение кода передаётся в 0xfffff80517ff9bf0
Этот адрес указывает на исполняемую страницу памяти в NTOSKRNL и содержит следующие инструкции:
Переменная KiIsrThunk из NTOSKRNL указывает на ядреную страницу кода, которая содержит 256 темплейтов, похожих на инструкции выше. После push interrupt vector (0x50) в этом случае и содержимого RBP регистра на стек, KiIsrThunk заглушка передаёт управление KiIsrLinkage(). Это 2 элемента на стеке используются функцией KiIsrLinkage() через структуру KTRAP_FRAME.
KiIsrLinkage() выполняет множество служебных задач:
Интересно, что большинство частей функции KiIsrLinkage() созданы из макросов, многие из которых доступны в заголовочном файле WDK kxamd64.inc, например GENERATE_INTERRUPT_FRAME, ENTER_INTERRUPT, EXIT_INTERRUPT и RESTORE_TRAP_STATE
Структура KINTERRUPT - основной ключ к обработке прерываний, она содержит всю информацию, необходимую для вызова ISR(interrupt service routine), зарегистрированной драйвером. KiIsrLinkage() определяет, где находится структура KINTERRUPT, связанная с вектором прерывания, используя его, как индекс в массиве указателей структур KINTERRUPT, находищихся в KPCR.CurrentPrcb.InterruptObject[]. Функция KiGetInterruptObjectAddress() из NTOSKRNL получает указатель на объект KINTERRUPT, показано ниже:
Поля структуры KINTERRUPT, которые относятся к обработке прерываний:
Название | Описание |
---|---|
DispatchAddress | Указатель на начальный программный обработчик прерываний в NTOSKRNL (KiChainedDispatch() ) для общих прерываний и KiInterruptDispatch() для других |
ServiceRoutine | Указатель на программный обработчик прерываний, зарегистрированный драйвером с помощью API ядра IoConnectInterrupt() или IoConnectInterruptEx() |
MessageServiceRoutine | Используется только для MSI (message signaled interrupts - прерывания, инициируемые сообщениями), т.е. прерывания, которые доставляются путём записи в зарезервированные участи памяти вместо переключения аппаратных линий. Эти прерывания показываются, как отрицательные числа в device manager'e. Для таких прерываний ServiceRoutine указывает на ядерную функцию KiInterruptMessageDispatch(), которая вызывает ISR, связанную с драйвером в MessageServiceRoutine |
MessageIndex | Индекс MSI, передаваемый, как параметр в ISR у MessageServiceRoutine |
В старых версиях Windows KINTERRUPT аллоцировалась из исполняемого невыгружаемого пула памяти, так как содержала начальный код обработки, который был зарегистрирован прямо в IDT. Из-за перехода к механизму из KiIsrThunk() и KiIsrLinkage(), описанному выше, начальная заглушка для прерывания теперь находится в исполняемой памяти в NTOSKRNL и, соответственно, структуре KINTERRUPT больше не нужно быть аллоцированной из исполняемой памяти. Структуры KINTERRUPT теперь пре-аллоцируются и хранятся в списке в KPCR.Prcb.InterruptObjectPool. Функция KeAllocateInterrupt() забирает пре-аллоцированную структуру KINTERRUPT из списка, когда вызывается для аллокации новой структуры KINTERRUPT. Когда этот список заканчивается, алооцируется ещё одна страница со структурами с помощью MmAllocateIndependentPages(), и добалвяет их в список.
Одним из важных шагов, предпринятых KiIsrLinkage, является вызов функции в KINTERRUPT.DispatchAddress, что приводит к вызову либо KiInterruptDispatch(), либо KiChainedDispatch(). Обе эти функции вызываются с указателем на структуру KINTERRUPT, как будто у них есть доступ ко всей информации, относящейся к обработке прерывания.
Новые системы используют APIC (Advanced Programmable Interrupt Controller) для обработки прерываний с устройств. Устройства отправляют свои прерывания на процессор с помощью IRQ линий. Однако, устройств больше, чем IRQ линий. Общие прерывания убирают проблему позволяя использовать одни и те же IRQ линии множеству устройств. Когда IRQ шарится, множество драйверов регистрирует свои ISR'ы для одного и того же IRQ и вектора прерывания. Из этого вытекает множественная структура KINTERRUPT, соответствующая устройствам, которые делят прерывание, и на них ссылаются вместе с помощью их полей KINTERRUPT.InterruptListEntry. Увидеть это можно с помощью "!idt -a", когда одному вектору прерывания соответствует множество структур KINTERRUPT, связанных с ним. KiChainedDispatch() обрабатывает прерывания, которые шарятся с множеством устройств, а KiInterruptDispatch() обрабатывает остальные прерывания.
Функции KiInterruptDispatch() и KiChainedDispatch меняются в зависимости от стека прерывания процессора, указатель на который хранится в KPCR.Prb.IsrStack. Этот стек аллоцируется функцией MmAllocateIsrStack(). Размер ISR стека 0x7000 байт, как определено переменными ISR_STACK_SIZE и PAGE_SIZE в заголовочном файле ksamd64.inc WDK. Непосредственный переход на стек ISR происходит с помощью макроса SWITCH_TO_ISR_STACK и также доступен в ksamd64.inc.
Как только выполнение перешло на стек ISR, функции KiInterruptDispatch() и KiChainedDispatch() передают выполнение следующей стадии, вызывая KiInterruptSubDispatch() или KiScanInterruptObjectList() соответственно.
KiInterruptSubDispatch() вызывает KiCallInterruptServiceRoutine() для одиночной структуры KINTERRUPT.
KiScanInterruptObjectList() итерируется по всем объектам KINTERRUPT, зарегистрированным для одного вектора прерывания, используя список KINTERRUPT.InterruptListEntry и вызывает KiCallInterruptServiceRoutine() для каждого KINTERRUPT в цепочке.
Драйвер, который регистрировал ISR, может сообщить вызывающей функции KiCallInterruptServiceRoutine(), забрал ли он на обработку прерывание, вернув TRUE. Это становится важным в случае пошареных прерываний, где решение вызвать ISR в следующем KINTERRUPT в цепочке или нет зависит от того, забрал ли текущий ISR прерывание на обработку.
Следующая диаграмма показывает все структуры, описанные выше и отношения между ними.
Как и в предыдущих версиях Windows 64, и IDTR, и содержимое IDT защищено PatchGuard (kernel patch protection). Делая структуру KINTERRUPT неисполняемой и удаляю код обработки из структуры, мы закрываем ещё один вектор subversion. Однако, даже с этими новыми изменениями в обработке исключений всё равно возможно для драйвера ядра хукнуть ISR в системе для реализации своего функционала, например для кейлоггера. ISR драйвера в поле KINTERRUPT.ServiceRoutine может быть заменено указателем на хук-функцию и PatchGuard этого не заметит. Так же не заметит, если KINTERRUPT, хранящийся в KPCR.Prcb.InterruptObject[] будет заменён клонированной структурой KINTERRUPT, который будет вести к выполнению кода.
IDT - Interrupt Descriptor Table (таблица дескрипторов прерываний)
Там хранятся элементы _KIDENTRY или _KIDENTRY64 соответственно
В каждой из них есть ссылка на ISR (Interrupt Service Routine) - непостредственно функция, которая вызывается
Механизм прерываний - ещё один важный элемент уровня железа
Прерывания можно рассматривать, как события уровня железа, использующиеся для сигнализирования процессору, что что-то требует немедленного внимания
Прерывания устройств
Устройства (сетевая карта, клавиатура и тд) вызовут прерывание, чтобы
сигнализировать процессору, что у них есть новая информация для обработки
(входящий сетевой пакет, нажатие на клавишу и тд)
Ловушки / исключения
Эти вещи обычно происходят, когда процессор сталкивается с ошибкой,
такой как деление на ноль или ошибка страницы
Программные прерывания
Это такие прерывания, которые генерируются программами, например INT 2E (syscall)
используется для перехода из user mode в kernel mode. INT 3 используется
для генерации программного брейкпоинта и тд
Значение, которое идёт за инструкцией INT называется вектором прерывания, это просто индекс в IDT (Interrupt Descriptor Table). IDT ассоциирует вектор прерывания с конкретной функцией, которая будет обрабатывать вызванное прерывание. В WDK (Windows Driver Kit) такая функция называется ISR (Interrupt Service Routine)
С точки зрения железа прерывания обрабатываются конкретным куском железа, называемым PIC (Programmable Interrupt Controller - контроллер прерываний). Сейчас у нас обычно стоит новая версия PIC - APIC (Advanced Programmable Interrupt Controller), встроенная прямо в процессов
Плюсы APIC:
Для каждого CPU свой APIC, и каждый APIC может коммуницировать с другими APIC'ами через IPI (Inter-processor interrupt message)
Одна большая задача в обработке прерываний, которую выполняет APIC - это управление приоритетами прерываний. Каждой линии прерываний выдан свой приоритет и APIC проверяет, что ни один входящий запрос на прерывание с приоритетом ниже или равным текущему обрабатываемому прерыванию не достигнет процессор, обычно это называют Interrupt Masking
Заметьте, что некоторые особые прерывания не могут был замаскированы и всегда будут достигать процессор, они называются NMI (Non-maskable interrupt). Они обычно предназначены для неустранимого сбоя оборудования, что означает, что у вас серьёзные проблемы с железом.
Прерывания, приходящие от устройств, сначала обрабатываются I/O APIC, специальным чипом, встроенным в чипсет, его роль - распределять прерывания по локальным APIC'ам всех CPU, таким образом включая SMP (Symmetric multiprocessing - Симметричная многопроцессорность)
Когда прерывание достигает CPU, процессор и процедура прерывания ОС сохранят стостояние значения регистров в стеке ядра, чтобы можно было восстановить предыдущий поток выполнения и продолжить исполнение кода. Этот набор сохраняемых регистров и некотороая дополнителья информация (например код ошибки) обычно называются Trap Frame (.trap в windbg)
Углубимся немного в механизм обработки прерываний. Откуда процессор знает, где расположения IDT? Ответ - в регистре IDTR. 48-битный регистр делится на две части: 16-бит - IDT limit и 32-бита - base address
Максимальное количество записей в IDT - 256. Каждая запись - 8 бит, содержит флаги, сегментные селекторы, gate type и оффсет или адрес ISR.
Оффсет тоже разделён на две части: биты 0..15 - для младших битив и 48..63 - для старших битов
В винде IDT entry - это _KIDTENTRY
Чтобы отобразить IDT в windbg есть !idt
Например разберём поближе i8042prt!I8042KeyboardInterruptService
Для проверки, что это такое вообще (ну вдруг мы по названию не догадались) поставим бряку на него
Мы нажали на любую кнопку –> наш брейкпоинт сработал
Но давайте доберёмся до кода в статике, ведь то, что написано в выводе команды **!idt **(31: 89ec9044 i8042prt! …) не совпадает с фактическим адресом ISR
Каждый элемент IDT занимает 8 байт, мы решили, что нам нужен индекс 31 (такой индекс у нужной нам функции), что нам нужно сделать?
idtr + 0x31 * 8 –>
8003f588 - 00089044 89ec8e00 0008dd14 804d8e00 –> 0x89ec9044
8003f598 - 0008dd1e 804d8e00 0008dd28 804d8e00
8003f5a8 - 0008dd32 804d8e00 0008dd3c 804d8e00
8003f5b8 - 0008dd46 804d8e00 0008fef0 806e8e00
8003f5c8 - 0008d174 89fe8e00 00084044 89f28e00
8003f5d8 - 0008d6c4 8a018e00 0008d564 89ea8e00
8003f5e8 - 0008dd82 804d8e00 0008a9d4 89fe8e00
8003f5f8 - 0008d044 8a038e00 0008dda0 804d8e00
И так наш ISR адрес 0x89ec9044, но мы же вроде бы только что дампили I8042KeyboardInterruptService и его адрес был 0xf76a7495, непонятно
Чтож, перед тем, как вызывать ISR'ры драйверов системе нужно выполнить некоторые задачи: маскирование прерываний с более низким приоритетом в APIC, поднятие уровня IRQL и тд
Так что вместо того, чтобы заполнить IDT ISR'ами, система заполняет их glue кодом или же иначе функциями-темплейтами
Каждая темплейт-функция взята (скопирована) из KiInterruptTemplate функции и динамически модифицирована, чтобы подходить соответствующему ISR'у
Давайте посмотрим на темплейт нашей KeyboardInterruptService:
Мы можем заметить, что почти весь код скопирован с оригинального KiInterruptTemplate. Однако есть одна интересная особенность:
темплейт функции клавиатуры вызывает KiInterruptDispatch и кладёт в EDI адрес 0x89EC9008
Этот адрес указывает на interrupt object с типом _KINTERRUPT:
Как видно выше, как раз в ServiceRoutine хранится адрес ISR
Если мы теперь посмотрим на KiInterruptDispatch мы увидим, что он вызывает interrupt object ServiceRoutine
Вся структура вызовов:
И как создаётся interrupt object?
Это роль драйвера заполнить структуру вызвав IoConnectInterrupt
Давайте посмотрим на прерывание и исключение деления на ноль
Оно у нас самое первое в таблице IDT
Поставим бряку
bu nt!KiTrap00
И на машине скомпилим какой-нибудь такой код:
Вводим ноль и Viola! брякаемся
Падаем в обработку
Тут всё выглядит поинтереснее, мб из-за отсутсвия дебаг символом на XP'хе, а мб и нет
Возьмём снова наш обработчик клавиатуры по оффсету a0
Найдём ISR entry point для него, теперь для 64 бит схема немного другая:
OffsetHigh + OffsetMiddle + OffsetLow
0xfffff803309f9e70
Если в табличке !idt искать KINTERRUPT не хочется, можно сделать так:
Попробуем скрыть процесс в ядре, чтобы в юзерспейсе пользователь не увидел его. Идём по мануалу Manipulating ActiveProcessLinks to unlink processes in userland
Про _EPROCESS мы уже знаем из лабы выше. Двухсвязный список, тип _LIST_ENTRY и всё такое. Задача у нас довольно простая, но хочется проделать это руками: переписать указатели так, чтобы спрятать наш процесс:
Находим адрес процесса (notepad.exe) и берём его FLINK и BLINK
Можем глянуть, куда указывают флинк и блинк:
Получим пиды окружающих наш процесс процессов:
Image | PID | EPROCESS | ActiveProcessLinks | FLINK | BLINK |
---|---|---|---|---|---|
taskhost.exe | 994 | fffffa80`070f9b00 | fffffa80`070f9c88 | fffffa80`047ce1e8 | fffffa80`070375c8 |
notepad.exe | 854 | fffffa80`047ce060 | fffffa80`047ce1e8 | fffffa80`061457d8 | fffffa80`070f9c88 |
mscorsvw.exe | a4c | fffffa80`06145650 | fffffa80`061457d8 | fffff800`02a38940 | fffffa80`047ce1e8 |
Очевидно, нам нужна немного другая табличка)
Image | PID | EPROCESS | ActiveProcessLinks | FLINK | BLINK |
---|---|---|---|---|---|
taskhost.exe | 994 | fffffa80`070f9b00 | fffffa80`070f9c88 | fffffa80`061457d8 |
fffffa80`070375c8 |
mscorsvw.exe | a4c | fffffa80`06145650 | fffffa80`061457d8 | fffff800`02a38940 | fffffa80`070f9c88 |
Будут рассмотрены две техники:
Новая ядерная структура - _TOKEN
Выходит, что последние 4 бита - это счётчик референсов, уберём их, получим адрес токена:
0xfffff8a0`01b46a5b & 0xf0 –> 0xfffff8a0`01b46a50
Его же мы получим, если сделаем
kd> eq fffffa800372cb00+0x208 fffff8a000004040
Заберём права токена процесса System себе
Видим те же права у токена, что и на картинке
Токен системы:
Service Descriptor Table - структура ядра, показанная ниже, содержит 4 System Service Table
В системе две Service Descriptor Tables:
SST структура, показанная ниже, содержит поле ServiceTable, которое является указателем на первый элемент массива указателей на рутины(функции) ядра в случае 32-битной ОС или указателем на массив адресов (и некоторой доп информации о количестве параметров), relative to the base address pointed to by it в случае 64-битной ОС
System Service Dispatch Table или SSDT - это просто таблица функций ядра.
Как говорилось выше, есть массив указателей в случае 32-битной ОС или массив относительных адресов в случае 64-битной, на который указывает поле ServiceTable SST(System Service Table), этот массив - просто SSDT
Смотря на результаты WinDBG выше, мы видем, что есть только одна явная SST таблица в случае nt!KeServiceDescriptorTable и две в случае nt!KeServiceDescriptorTableShadow. Хотя мы видим больше данных во второй, из доступной информации мы можем заключить лишь то, что из возможных 4-ёх SST элементов nt!KeServiceDescriptorTable использует только первый - он описывает SSDT для Windows Native APIs, экспортируемых ntoskrnl.exe'ом. nt!KeServiceDescriptorTableShadow использует 2 SST элемента: первый - копия nt!KiServiceTable из nt!KeServiceDescriptorTable, второй - win32k!W32pServiceTable, описывающий SSDT для User и GDI функции, экспортируемые win32k.sys.
SSDT предоставляют функциональность и приложениям пространства пользователя (разумеется, неявно), и драйверам ядра.
Когда user mode приложение вызывает, явно или неявно, некоторые функции Windows API, множество функций ядра будут вызваны в процессе. Для перехода в режим ядра из режима пользователя используется Sysenter (или Syscall для 64-бит) ассемблерная инструкция (раньше это было 0x2E прерывание, которое ещё актуально, хотя используется не так часто). Конкретная функция обработки будет вызвана по номеру, Dispatch ID, значение которого было в регистре EAX до вызова инструкции Sysenter / Syscall.
Первые 12 бит Dispatch ID - это индекс в SSDT. Биты 12-ый и 13-ый указывают, который из SSDT. Это означает, что Dispatch ID до 0xFFF будет обработан nt!KiServiceTable SSDT, а Dispatch ID между 0x1000 и 0x1FFF будет обработан win32k!W32pServiceTable SSDT.
Когда вызывается функция в пространстве пользователя, например CreateFile, в итоге управление передаётся в ntdll!NtCreateFile и с помощью сискола в ядро nt!NtCreateFile (ntoskrnl)
По сути, сисколы и SSDT (KiServiceTable) работают вместе, как мост между API функциями пространства пользователя и соответсвующими им функциями в пространестве ядра, позволяя ядру понять, которая из функций должна быть выполнена для конкретного сискола, вызванного ещё в пространстве пользователя.
Мы можем найти структуру Service Descriptor Table в KeServiceDescriptorTable. Первый элемент будет KiServiceTable - указатель на саму SSDT
Значения из SSDT
На 64-битных системах SSDT содержит относительные сдвиги на ядерные функции. Чтобы получить абсолютный адрес для сдивига, нужно использовать следующую формулу:
RoutineAbsoluteAddress = KiServiceTableAddtess + (routineOffset >>> 4)
(пример на картинке из статьи)
Возьмём для примера функцию Sleep. Её функция в nt - это NtDelayExecution
Оффсеты(сдвиги) в KiServiceTable 4-ёх байтовые, так что нам нужно посмотреть значение на 0x31-ой позиции
Используем формулу
.foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(keservicedescriptortable+0x10) }){ dp kiservicetable + ( offset >>> 4 ) L1 }
(ПИЗДЕЦ)
.foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(nt!KeServiceDescriptorTable+10)}){ r $t0 = ( offset >>> 4) + nt!KiServiceTable; .printf "%p - %y\n", $t0, $t0 }
(ОТВАЛ БАШКИ)
Всё просто, в 32-битную эру руткиты модифицировали nt!KiServiceTable ил win32k!W32pServiceTable, чтобы вызвать свой код. Также, многие антивирусы использовали хуки SSDT, чтобы получать мгновенный алёрт об атаке.
Однако с 64-битных ОС был представлен Kernel Patch Protection (PatchGuard). PatchGuard делает периодические проверки, чтобы убедиться, что критические системные структуры, включая SSDT, не были модифицированы в недавнее время.
Цель кода не хукнуть системную функцию в SSDT (это довольно просто без PatchGuard'а), об этом уже много написано
Целью и сложной частью является поиск SSDT. В особенности win32k!W32pServiceTable SSDT, на который указывает вторая запись SST nt!KeServiceDescriptorTab1eShadow. С другой стороны найти nt!KiServiceTable SSDT ждя 32-битной ОС легко, символы экспортируются, просто нужно прочитать значение. Символы для nt!KiServiceTable не экспортируются в 64-битной ОС.
В коде мы найдём nt!KeServiceDescriptorTab1eShadow. И тогда мы будем знать расположения nt!KiServiceTable и win32k!W32pServiceTable.
Билдим драйвер ядра и приложение user mode'а. Visual Studio 2015 solution - one project for the driver (both 32-bit and 64-bit) and another for the driver (both 32-bit and 64-bit)
Теперь осталось найти паатерн, вытащить RIP адрес и посчитать адрес nt!KeServiceDescriptorTableShadow
readAddress = (ULONG_PTR)(ntTable[FunctionIndex] >> 4) + SSDT(Shadow)BaseAddress;
Найти nt!KeServiceDescriptorTable и nt!KeServiceDescriptorTableShadow в функции ядра nt!KiSystemServiceStart
Как видно выше, мы можем искать по паттерну KiSystemServiceStart от начала ядра
После того, как нашли нужную функцию, мы можем получить адрес nt!KeServiceDescriptorTableShadow из инструкции:
fffff80002908879 4c8d1d40d11f00 lea r11, [nt!KeServiceDescriptorTableShadow (fffff80002b059c0)]
Затем можем получить адреса nt!KiServiceTable(SSDT) и win32k!W32pServiceTable(SSDT Shadow)
Тематически сгруппированные команды WinDBG
Common WinDbg Commands (Thematically Grouped)
Команда | Пояснение к ней |
---|---|
!peb | display formatted PEB |
dt nt!_PEB Addr | full PEB dump |
lm | list loaded and unloaded modules |
lm vm kernel32 | verbose output (incl image, syml information) |
!dlls | display loaded modules with loader info |
!imgreloc | display relocation info |
!dh kernel32 | display headers |
!gle | Get Last Error |
!process 0 4 processname.exe | print all threads of process |
!teb | display formatted teb |
dt nt!_TEB Addr | full TEB dump |
k | display call stack for current thread |
kP | P == full parameters for each function called |
kf | f == distance between adjacent frames to be displayed (useful to check stack consumption of each frame) |
kv | v == display FPO information + calling convention |
kb | b == display the first three parameters passed to each function |
d, dd, da, du… | Display memory dd == double word values da == display ASCII characters du == display Unicode characters |
f 0012ff40 L20 'A' 'B' 'C' | fill 20 elements with ABC starting at address |
!vprot MyAddr | Displays virtual memory protection information for MyAddr |
!address MyAddr | Display information (type, protection, usage, ..) about the memory specified by MyAddr |
!heap | print all heaps |
!locks | displays a list of locked critical sections for the process |
!locks -v | display all critical sections for the process |
!cs -l [CsAddr] | Displays one or more critical sections, or the entire critical section tree. -l == display only locked sections |
!cs -s [CsAddr] | -s == causes each CS’s initialization stack to be displayed |
!cs -o [CsAddr] | -o == causes the owner’s stack to be displayed |
!cs -t [CsAddr] | -t == display critical section tree -> EnterCntr, WaitCnt, … |
!avrf -cs | Display a list of deleted critical sections (DeleteCriticalSection API) |
!critsec [CsAddr] | displays the same collection of information as !ntsdexts.locks |
dt | Display information about a local variable, function parameter, global variable or data type |
dv | Display local variables |
dv /t /i /V | Display local variables /i == classify them into categories (parameters or locals) /V == show addresses and offsets for the relevant base frame register (usually EBP) /t == display type information |
dd 0046c6b0 L1 | display 1 dword at 0046c6b0 |
dd 0046c6b0 L3 | display 3 dwords at 0046c6b0 |
du 0046c6b0 | display Unicode chars at 0046c6b0 |
as Name Equivalent as /ma Name Address as /mu Name Address |
Set alias Set alias to the NULL-terminated ASCII string at Address Set alias to the NULL-terminated Unicode string at Address |
ad Name ad * |
Delete alias with Name Delete all aliases |
al | List user-named aliases |
${Alias} ${/f:Alias} ${/n:Alias} ${/d:Alias} |
${Alias} is replaced by the alias equivalent, even if it is touching other text. If the alias is not defined, the ${Alias} is not replaced Same as above except that ${/f:Alias} is replaced with an empty string if the alias is not defined Evaluates to the alias name Evaluates: 1 = alias defined; 0 = alias not defined |
bp bu ba bc be, bd |
Set Breakpoint Set Unresolved Breakpoint: defers the actual setting of the breakpoint until the module is loaded Break on Access Breakpoint Clear Breakpoint Enable, Disable |
ba r4 0012fe34 ba w2 0012fe38 |
break on access (read or write); monitor 4 bytes break on access (write); monitor 2 bytes |
bu kernel32!LoadLibraryExW 5 | Breakpoint that will starts hitting after 5 passes |
~1 bu kernel32!LoadLibraryExW | Break only if called from thread ~1 |
bp mod!myFunc* | Break at all symbols with pattern myFunc* |
.lastevent | first-change or second-chance? |
!analyze -v | Displays detailed information about the current exception |
.exr -1 | Display most recent exception |
.exr Addr | Display exception at Addr |
!cppexr Addr | Display c++ exception at address Addr |
g, gH gN |
Go with Exception Handled Go with Exception Not Handled |
.dump /ma D:\large.dmp | all possible data: full memory, code sections, PEB and TEB’s, handle data, thread time information, unloaded module lists, and more |
.dump /m d:\small.dmp | only basic information: module information (signatures), thread and stack information |
r | print all registers |
d * | view memory |
e * | edit memory |
~1 ~2 | change context to processor 1/2 |
ed nt!Kd_Default_Mask 8 | Включить DbgPrint прям в консоль windbg |