Пожалуйста, обратите внимание, что пользователь заблокирован
Введение
В этой заметке я хотел бы описать процесс выполнения 64-битного шеллкода в ядре NT. Идея возникла после того, как я случайно нашел интересный MSR-регистр -
Поиск базового адреса ntoskrnl.exe
Регистр
Если верить исходникам
Если проанализировать память, то можно найти значительное количество указателей на функции ядра и константу
Это значение уже будет меняться от версии к версии Windows. Для
На С такая функция могла бы выглядеть следующим образом:
Пишем шеллкод для функции GetNtBase
Наверняка существуют более элегантные способы написания ядерных шеллкодов и кому-то мой подход покажется избыточным. Интересно мнение экспертов по этому поводу.
Алгоритм был намечен следующий:
Шеллкод состоит ровно из 30 байт.
Создаем исполняемый пул, передаем управление на шеллкод
Драйвера могут создавать потоки с помощью функций PsCreateSystemThread и
Перед тем как передавать управление на шеллкод, мы должны сначала выделить исполняемый пул в ядре, т.к. в противном случае мы получим багчек с ошибкой
Далее копируем шеллкод в исполняемую область памяти и вызываем
На листинге ниже мы можем видеть, что в
Шеллкод успешно выполнился и теперь в
.
Конечно, с точки зрения любого вменяемого разработчика, такое программирование является некорректным. Как уже упоминалось ранее, новый поток должен явно вызывать функцию
В этой заметке я хотел бы описать процесс выполнения 64-битного шеллкода в ядре NT. Идея возникла после того, как я случайно нашел интересный MSR-регистр -
IA32_GS_BASE, чтение которого с некоторыми дополнениями позволяет получить базовый адрес ядра. На звание эксперта не претендую, поэтому любая критика приветствуется. Также, если есть какие-то вопросы, то можно их задавать в комментариях. В качестве окружения для разработки я использую Visual Studio 2019.Поиск базового адреса ntoskrnl.exe
Регистр
IA32_GS_BASE находится по адресу 0xC0000101.
Код:
0: kd> rdmsr 0xC0000101
msr[c0000101] = fffff804`07fc6000
Windows XP SP1, то регистр содержит указатель на область памяти MM_KSEGN_BASE
Код:
Virtual Memory Layout on the AMD64 is:
+------------------------------------+
0000000000000000 | User mode addresses - 8tb minus 64k|
| |
| |
000007FFFFFEFFFF | | MM_HIGHEST_USER_ADDRESS
+------------------------------------+
000007FFFFFF0000 | 64k No Access Region | MM_USER_PROBE_ADDRESS
000007FFFFFFFFFF | |
+------------------------------------+
.
+------------------------------------+
FFFF080000000000 | Start of System space | MM_SYSTEM_RANGE_START
+------------------------------------+
FFFFF68000000000 | 512gb four level page table map. | PTE_BASE
+------------------------------------+
FFFFF70000000000 | HyperSpace - working set lists | HYPER_SPACE
| and per process memory management |
| structures mapped in this 512gb |
| region. | HYPER_SPACE_END
+------------------------------------+ MM_WORKING_SET_END
FFFFF78000000000 | Shared system page | KI_USER_SHARED_DATA
+------------------------------------+
FFFFF78000001000 | The system cache working set | MM_SYSTEM_CACHE_WORKING_SET
| information resides in this |
| 512gb-4k region. |
| |
+------------------------------------+
.
.
Note the ranges below are sign extended for > 43 bits and therefore
can be used with interlocked slists. The system address space above is NOT.
.
.
+------------------------------------+
FFFFF80000000000 | Start of 1tb of | MM_KSEG0_BASE
| physically addressable memory. | MM_KSEG2_BASE
+------------------------------------+
FFFFF90000000000 | win32k.sys |
Если проанализировать память, то можно найти значительное количество указателей на функции ядра и константу
ExNode0, указатель на которую находится по смещению 0x240. Это значение не подвержено рандомизации, поэтому мы будем использовать ее для нашего шеллкода. Далее необходимо найти RVA для ExNode0, чтобы найти NtBase.Это значение уже будет меняться от версии к версии Windows. Для
20H2 19042.572 это значение будет 0xd25440.
Код:
0: kd> dps fffff804`07fc6000 + 0x240
fffff804`07fc6240 fffff804`0d325440 nt!ExNode0
0: kd> ? fffff804`0d325440 - 0xd25440
Evaluate expression: -8778705534976 = fffff804`0c600000
0: kd> ? nt
Evaluate expression: -8778705534976 = fffff804`0c600000
На С такая функция могла бы выглядеть следующим образом:
C:
#include <intrin.h>
#define EX_NODE0_OFFSET 0x240
#define EX_NODE0_RVA 0xd25440
#define IA32_GS_BASE 0xc0000101
int NtBase;
int ExNode0;
int GetNtBase()
{
ExNode0 = __readmsr(IA32_GS_BASE) + EX_NODE0_OFFSET;
NtBase = ExNode0 - EX_NODE0_RVA;
return NtBase;
}
Пишем шеллкод для функции GetNtBase
Наверняка существуют более элегантные способы написания ядерных шеллкодов и кому-то мой подход покажется избыточным. Интересно мнение экспертов по этому поводу.
Алгоритм был намечен следующий:
- Написать минимальный драйвер на С, состоящий из функций
DriverEntryиDriverUnload. - Создать файл
shellcode.asm, который реализует функциюGetNtBaseи вызвать ее вDriverEntry. - Скомпилировать драйвер
- Открыть драйвер в
IDA Proи скопировать нужные опкоды. - Отключить
shellcode.asmи добавить опкоды в буфер.
Код:
PUBLIC GetNtBase
.data
.code
GetNtBase PROC
int 3
mov ecx, 0C0000101h
rdmsr
shl rdx, 20h
or rax, rdx
add rax, 240h
mov rax, [rax]
sub rax, 0D25440h
ret
GetNtBase ENDP
END
Шеллкод состоит ровно из 30 байт.
Создаем исполняемый пул, передаем управление на шеллкод
Драйвера могут создавать потоки с помощью функций PsCreateSystemThread и
IoCreateSystemThread (начиная с Windows 8). Драйвер должен удалять поток с помощью PsTerminateSystemThread.Перед тем как передавать управление на шеллкод, мы должны сначала выделить исполняемый пул в ядре, т.к. в противном случае мы получим багчек с ошибкой
PAGE_FAULT_IN_NONPAGED_AREA.Далее копируем шеллкод в исполняемую область памяти и вызываем
PsCreateSystemThread с указателем на шеллкод.
C:
#pragma once
#include <ntddk.h>
#include <intrin.h>
// constants
#define IA32_GS_BASE 0xc0000101
#define EX_NODE0_RVA 0xd25440
#define EX_NODE0_OFFSET 0x240
// prototypes
NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject);
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath);
C:
#include "getntbase.h"
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE, DriverUnload)
#endif
// Pool tag for our shellcode
#define SHC_TAG "dchS"
// Unload driver
NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("The DriverUnload routine called\n"));
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
PVOID P;
NTSTATUS status;
HANDLE hThread;
__debugbreak();
/*
mov ecx, 0C0000101h
rdmsr
shl rdx, 20h
or rax, rdx
add rax, 240h
mov rax, [rax]
sub rax, 0D25440h
ret
*/
UCHAR shellcode[] = {0xB9, 0x01, 0x01, 0x00, 0xC0, 0x0F, 0x32, 0x48, 0xC1, 0xE2,
0x20, 0x48, 0x0B, 0xC2, 0x48, 0x05, 0x40, 0x02, 0x00, 0x00,
0x48, 0x8B, 0x00, 0x48, 0x2D, 0x40, 0x54, 0xD2, 0x00, 0xC3};
// Allocate executable pool
P = ExAllocatePoolWithTag(NonPagedPoolExecute, sizeof(shellcode), SHC_TAG);
// Here is should be a check for STATUS_INSUFFICIENT_RESOURCES if ExAllocatePoolWithTag failed
// Initialize pool memory
RtlZeroMemory(P, sizeof(shellcode));
// Copy shellcode to the pool
RtlCopyMemory(P, shellcode, sizeof(shellcode));
// Create system thread. Here is a problem for our shellcode. We need call PsTerminateSystemThread for the shellcode thread context.
status = PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)P, NULL);
if (status != STATUS_SUCCESS)
{
KdPrint(("Can't create PsCreateSystemThread\n"));
}
// Close handle
ZwClose(hThread);
// Free pool memory
ExFreePoolWithTag(P, SHC_TAG);
// Initialize DriverUnload
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
На листинге ниже мы можем видеть, что в
rax находится наш шеллкод и страница является исполняемой.
Код:
2: kd> u @rax
ffffe005`5f302a50 b9010100c0 mov ecx,0C0000101h
ffffe005`5f302a55 0f32 rdmsr
ffffe005`5f302a57 48c1e220 shl rdx,20h
ffffe005`5f302a5b 480bc2 or rax,rdx
ffffe005`5f302a5e 480540020000 add rax,240h
ffffe005`5f302a64 488b00 mov rax,qword ptr [rax]
ffffe005`5f302a67 482d4054d200 sub rax,0D25440h
ffffe005`5f302a6d c3 ret
2: kd> !pte @rax
VA ffffe0055f302a50
PXE at FFFFAD56AB55AE00 PPE at FFFFAD56AB5C00A8 PDE at FFFFAD56B80157C8 PTE at FFFFAD7002AF9810
contains 0A00000005032863 contains 0A00000005235863 contains 0A0000000483A863 contains 0A0000013CA2C863
pfn 5032 ---DA--KWEV pfn 5235 ---DA--KWEV pfn 483a ---DA--KWEV pfn 13ca2c ---DA--KWEV
Шеллкод успешно выполнился и теперь в
rax находится базовый адрес ядра. При этом система даже не упала в BSOD и мы можем спокойно выгрузить драйвер
Код:
3: kd> r @rax
rax=fffff8040c600000
3: kd> ? nt
Evaluate expression: -8778705534976 = fffff804`0c600000
Конечно, с точки зрения любого вменяемого разработчика, такое программирование является некорректным. Как уже упоминалось ранее, новый поток должен явно вызывать функцию
PsTerminateSystemThread, чего в нашем случае не происходит, впрочем, этот вызов можно реализовать и в самом шеллкоде. На самом деле я был даже удивлен, что такой наглый подход в принципе сработал.
Последнее редактирование: