• XSS.stack #1 – первый литературный журнал от юзеров форума

[ЗАМЕТКА 2] Исполняем шеллкод в ядре при помощи PsCreateSystemThread (x64)

varwar

El Diff
Забанен
Регистрация
12.11.2020
Сообщения
1 383
Решения
5
Реакции
1 537
Пожалуйста, обратите внимание, что пользователь заблокирован
Введение

В этой заметке я хотел бы описать процесс выполнения 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

Наверняка существуют более элегантные способы написания ядерных шеллкодов и кому-то мой подход покажется избыточным. Интересно мнение экспертов по этому поводу.
Алгоритм был намечен следующий:
  1. Написать минимальный драйвер на С, состоящий из функций DriverEntry и DriverUnload.
  2. Создать файл shellcode.asm, который реализует функцию GetNtBase и вызвать ее в DriverEntry.
  3. Скомпилировать драйвер
  4. Открыть драйвер в IDA Pro и скопировать нужные опкоды.
  5. Отключить 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 байт.

16.png


Создаем исполняемый пул, передаем управление на шеллкод

Драйвера могут создавать потоки с помощью функций 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

19.png


Шеллкод успешно выполнился и теперь в rax находится базовый адрес ядра. При этом система даже не упала в BSOD и мы можем спокойно выгрузить драйвер :).

Код:
3: kd> r @rax
rax=fffff8040c600000
3: kd> ? nt
Evaluate expression: -8778705534976 = fffff804`0c600000

Конечно, с точки зрения любого вменяемого разработчика, такое программирование является некорректным. Как уже упоминалось ранее, новый поток должен явно вызывать функцию PsTerminateSystemThread, чего в нашем случае не происходит, впрочем, этот вызов можно реализовать и в самом шеллкоде. На самом деле я был даже удивлен, что такой наглый подход в принципе сработал.
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Простите, продолжаю извращаться томными вечерами.

А как выполнить шеллкод без использования PsCreateSystemThread? На x86 можно было бы обойтись таким макросом*:

C:
#define ExecCode(address) __asm MOV edx,address __asm call edx

И после вызвать ExecCode с указателем на шеллкод в исполняемом пуле.

Поскольку MSVC не поддерживает ассемблерных вставок для x64, то необходимо использовать внешний ассемблерный файл (я использовал ранее shellcode.asm) и написать свою функцию.
Перед этим я скомпилировал драйвер и посмотрел в IDA Pro где вообще хранится шеллкод. Забегу немного вперед - шеллкод передается через регистр RAX.

Сама функция ExeсCode в файле shellcode.asm выглядит следующим образом:

Код:
; This function perform a bare-bones jump, ignoring a stack frame and other 
; C-based function call conventions
PUBLIC ExecCode
.CODE
ExecCode PROC
    call rax
    ret
ExecCode ENDP
END

Вернусь к тому, как передается шеллкод. В дизасемблере функция DriverEntry копирует шеллкод через регистр RAX и наша функция вызывается по регистру. Опять же, никаких проблем с последующей выгрузкой драйвера обнаружено не было.
ExecCode2.png


Код:
2: kd> t
GetNtBaseeAddress!ExecCode:
fffff805`7d341000 ffd0            call    rax
2: kd> !pte @rax
                                           VA ffffe485fbb02a50
PXE at FFFF9A4D26934E48    PPE at FFFF9A4D269C90B8    PDE at FFFF9A4D39217EE8    PTE at FFFF9A7242FDD810
contains 0A00000005235863  contains 0A00000004838863  contains 0A0000000483C863  contains 0A0000013CA2B863
pfn 5235      ---DA--KWEV  pfn 4838      ---DA--KWEV  pfn 483c      ---DA--KWEV  pfn 13ca2b    ---DA--KWEV

2: kd> u @rax
ffffe485`fbb02a50 b9010100c0      mov     ecx,0C0000101h
ffffe485`fbb02a55 0f32            rdmsr
ffffe485`fbb02a57 48c1e220        shl     rdx,20h
ffffe485`fbb02a5b 480bc2          or      rax,rdx
ffffe485`fbb02a5e 480540020000    add     rax,240h
ffffe485`fbb02a64 488b00          mov     rax,qword ptr [rax]
ffffe485`fbb02a67 482d4054d200    sub     rax,0D25440h
ffffe485`fbb02a6d c3              ret
2: kd> t
ffffe485`fbb02a50 b9010100c0      mov     ecx,0C0000101h

Результат выполнения шеллкода - в RAX находится базовый адрес ядра.

Код:
2: kd> ? @rax
Evaluate expression: -8772474896384 = fffff805`7fc00000

Сверяем значения.

Код:
2: kd> ? nt
Evaluate expression: -8772474896384 = fffff805`7fc00000

Ниже листинг с запуском, остановкой и удалением сервиса.

Код:
C:\WINDOWS\system32>sc start test

SERVICE_NAME: test
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 4  RUNNING
                                (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0
        PID                : 0
        FLAGS              :

C:\WINDOWS\system32>sc stop test

SERVICE_NAME: test
        TYPE               : 1  KERNEL_DRIVER
        STATE              : 1  STOPPED
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0

C:\WINDOWS\system32>sc delete test
[SC] DeleteService SUCCESS

Таким образом, мы выполняем шеллкод в контексте текущего потока и не нужно думать об освобождении ресурсов через PsTerminateSystemThread и возможных непредвиденных последствий в случае игнорирования этого вызова.

И все же стоит переписать шеллкод для поиска базы, используя динамический поиск сигнатуры ядра, вместо захардкоженных оффсетов.

* Этот трюк для x86 был найден в книге "The Rootkit Arsenal".
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Переписал в общем шеллкод для поиска базы, используя побайтовое сканирование памяти (scandown техника). Итоговый шеллкод вышел размером 47 байт, но я не мастер ассемблера, возможно кто-то сможет его уменьшить. Код функции и шеллкод приложены ниже в спойлерах shellcode.asm и getntbase.c. Хочу поделиться некоторыми особенностями поиска MZ-сигнатуры. Старые техники, основанные на поиске двух, трех байт MZ-заголовка для x64 не работают, т.к. получаются ложные срабатывания.

Пример такого срабатывания:

Код:
1: kd> db @rax
fffff805`808f547c  4d 5a 90 00 00 00 00 00-00 00 00 00 00 00 00 00  MZ..............
fffff805`808f548c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f549c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f54ac  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f54bc  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f54cc  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f54dc  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`808f54ec  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Поэтому я просто добавил вторую проверку PE-заголовка. В итоге базовый адрес ядра обнаруживается правильно.

Код:
3: kd> bp ffffe485`fbb02a7e
3: kd> g
Breakpoint 1 hit
ffffe485`fbb02a7e c3              ret
3: kd> db @rax
fffff805`7fc00000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
fffff805`7fc00010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
fffff805`7fc00020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff805`7fc00030  00 00 00 00 00 00 00 00-00 00 00 00 10 01 00 00  ................
fffff805`7fc00040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
fffff805`7fc00050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
fffff805`7fc00060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS
fffff805`7fc00070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

Сверяем значение.

Код:
3: kd> ? nt
Evaluate expression: -8772474896384 = fffff805`7fc00000

Адрес ядра - fffff805`7fc00000.

Код отрабатывает довольно быстро, ~ 12 МБ адресного пространства сканирует < 1 сек. На сканировании больших участков памяти нужно использовать другие методы. Преимущества этой техники по сравнению с захардкоженными RVA очевидны - нет привязки к версии ядра. Технику с использованием IA32_GS_BASE я нигде не видел раньше, хотя концептуально она ничем не отличается от техники с использованием SYSENTER_EIP_MSR. Оба регистра указывают куда-то в область памяти ядра.

Код:
PUBLIC ExecCode
.CODE
ExecCode PROC

    call rax
    ret

ExecCode ENDP

PUBLIC GetNtBase
.CODE
GetNtBase PROC

    mov ecx, 0C0000101h
    rdmsr
    shl rdx, 20h
    or rax, rdx
    add rax, 240h
    mov rax, [rax]

find_mz_sig: ; loop untill return nt base address
    dec rax
    cmp DWORD PTR [rax], 905a4dh
    jnz find_mz_sig
    cmp DWORD PTR [rax+110h], 4550h
    jnz find_mz_sig

    ret

GetNtBase ENDP
END

C:
#include "getntbase.h"

#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE, DriverUnload)
#endif

#define SHC_TAG "dchS"

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;
    
    /*
        mov ecx, 0C0000101h
        rdmsr
        shl rdx, 20h
        or rax, rdx
        add rax, 240h
        mov rax, [rax]
    find_mz_sig:
        dec rax
        cmp DWORD PTR [rax], 905a4dh
        jnz find_mz_sig
        cmp DWORD PTR [rax+110h], 4550h
        jnz find_mz_sig
        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, 0xFF, 0xC8, 0x81, 0x38, 0x4D, 0x5A,
      0x90, 0x00, 0x75, 0xF5, 0x81, 0xB8, 0x10, 0x01, 0x00, 0x00,
      0x50, 0x45, 0x00, 0x00, 0x75, 0xE9, 0xC3
    };
    P = ExAllocatePoolWithTag(NonPagedPoolExecute, sizeof(shellcode), SHC_TAG);   
    RtlZeroMemory(P, sizeof(shellcode));
    RtlCopyMemory(P, shellcode, sizeof(shellcode));
    ExecCode(P);

    ExFreePoolWithTag(P, SHC_TAG);
    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

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);
VOID ExecCode(PVOID Address);
//VOID GetNtBase();
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх