为了更快的解决问题,我尝试着在网上搜索相关wp,但是发现很多该系列的文章讨论的都不是在这个版本上进行的,所以导致很多WP在这个平台上已经不适用了。
然而现实中,Windows Kernel的EXP却也依然能够涵盖到非常新的win10版本上,说明即使在最新的win10上,也依然有利用的机会。所以这边的第二篇想简单讨论以下这种攻击在win10 1903上依然通用的一种解法
本篇转自安全客 https://www.anquanke.com/post/id/213428
Window Kernel Exploit 02 - Arbitrary Overwrite
任意地址写,在用户态中也是一种常见的漏洞,其形式通常存在一个可以被控制的指针,以及在之后的逻辑中,会发生一次对指针内容的修改:
|
|
这种漏洞在用户态有一些很容易能够想到的利用方法,比如修改内存中一些固定模块的函数指针,然后调用对应函数,从而跳转到指定的控制流上。在内核中的利用思路也是类似,不过会多一些防护,之后会慢慢道来。
漏洞点
用IDA可以很容易的看到问题所在:
|
|
这个UserAddr
是一个由用户态定义的结构体:
|
|
这个就是一个典型的write-what-where
的漏洞
利用思路
往哪儿写(旧思路)
在内核态,遇到这种漏洞的时候,第一个想到的应该就是需要往哪儿写。一个常见的套路的是nt!haldispatchTable
,这个地址是一个ntokrnl.exe
中的一个用来存放函数指针的全局对象,修改这个table
中一个很少被调用的APINtQueryIntervalProfile对应的位置nt!haldispatchTable+4
(64bit 为 nt!haldispatchTable+8
),此时就相当于是劫持了NtQueryIntervalProfile的调用。
要写什么(旧思路)
第一个直观的想法就是将用户态的shellcode地址写上去。然后就可以依照第一篇中写的EXP,修改一下放到这边去。但是这样的话,就有了第一章的CR4
的问题。
新的地址(旧思路)
之前的做法需要用到ROP
,我虽然找到了wjllz
大师傅的博客,但是这个方法在我的测试环境上(1903)似乎不能正常work。于是我直接找到了他本人去问。大师傅人很好,给了我很多方向,然后我找到了一篇blackhat上的文章,里面提到了一种方法:
这个方法的原理是说,在形如NtGdiDdDDICreateAllocation
,或者大佬博客里提到的NtGdiDdDDIGetContextSchedulingPriority
这类和GDI相关的API调用的处理驱动中,有一片未初始化的可写可执行区域:
上图中的win32k!NtGdiDdDDIGetContextSchedulingPriority
,这个地方就是驱动所在的导出表的位置
|
|
然而我们顺着找这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
的地址,可以找到如下内容:
|
|
这个9090f62f
正好指向一个实现在dxgkrnl.sys
的对应函数:
|
|
于是一个新的想法就产生了:能不能将这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
指向的地址给修改成我们shellcode的地址呢?
|
|
令人头痛的是,这个dxgkrnl
中记录的这API,与文章中提到的API完全不一样,逆向后发现,好像有一段判断逻辑发生了偏移:
于是现在有了两个继续研究的方向
- 找到这个初始化函数的判断依据,想办法进行原先的那些API初始化
- 在先有的函数上找到同类型的函数指针进行劫持
不过,说起来初始化的类型不同的话,应该会导致依赖中断号的上层调用的时候出现问题呀,不知道windows是怎么解决的问题。
不过,后来我仔细思考了一下这个利用方法,总结其他其目的应该是:
- 通过修改
dxgkrnl!DxgkGetContextSchedulingPriority
的地址,这样就能够劫持NtGdiDdDDIGetContextSchedulingPriority
- 第一次将这个函数劫持为
ExAllocatePoolWithTag
,然后通过调用NtGdiDdDDIGetContextSchedulingPriority
去调用ExAllocatePoolWithTag
,分配一个RWX
的空间 - 然后利用WWW漏洞,往这个空间写入shellcode
- 修改
dxgkrnl!DxgkGetContextSchedulingPriority
为ExAllocatePoolWithTag
的地址,然后跳转到shellcode
既然是这个思路的话,那么为什么不使用最初提到的HalDispathTable?不过后来我试了一下发现返回值怪怪的,查看了ReactOS
中的源码找到了原因:
|
|
原来这个位置只能接受一个传参啊。。。。
后来绕回这个dxgkrnl
的逻辑,发现依然无法修改函数地址,我搜了非常多的资料,终于在这个网站上找到了答案:
https://www.unknowncheats.me/forum/2567493-post86.html
这里提到了一个很重要的事情:windows 1903
中,大部分的函数已经不使用win32kbase作为proxy,而是直接由dxgkrnl.sys导出函数来调用。
之前我试了很多次修改这个gDxgkInterface
但是都不能work,原来是因为很多函数的逻辑里面根本就没有经过win32kbase!gDxgkInterface。。。。。。于是之前提到的所有利用手段都被堵上了。
和Linux kernel pwn的联想(新的思路)
在此期间一直在想,Linux的kernel pwn也是要找类似的跳板吗?看了一下相关ctf的writeup,发现大家其实都是再改cred
这个结构体,这个结构体相当于是Linux中的一个权限管理结构体,所以通过修改这个结构体,就能够实现某个进程的提权。那Windows中就没有类似的结构体了吗?随着工作的进行,我发现Windows中的TOKEN
这个结构体好像起到的作用就和这个cred
类似,也是类似Windows下的权限控制(具体可以看这里Windows Via C/C++ note 4),那是不是改这个地方就可以了呢?
后来,又是wjllz大佬抬了一手,给了一篇文章:https://labs.bluefrostsecurity.de/blog/2020/01/07/cve-2019-1215-analysis-of-a-use-after-free-in-ws2ifsl/,这个文章里面提到了一个2012年就被提出来的,window kernel pwn应该做什么的文章http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf,完美解释了我心中的疑惑。wjllz大佬给的文章都很有价值,大力吹一波~这些内容之后应该会趁机研究一下记录。不过这边我们先顺着这个WP中的思路走,解决一下我们当前的问题。
通过修改_TOKEN来进行提权
一个进程中的TOKEN
结构体如下:
|
|
据说这个结构体从Win7
开始就没有太多的变动了,这里我们关注一下这几个结构体成员变量:
|
|
这个结构体变量用bit位的方式记录了当前token中使用的privilege(特权)。我们都知道,token能够限制一个进程能能够对其他进程的权限控制,但是有时候我们也会需要有类似windbg之类的进程对其他各类进程进行调试,这个时候系统就会赋予调试器这个调试其他进程的特权。这个概念非常重要,我们通过给与当前进程特权,就能够往其他更高权限才能够接触到的进程中进行代码注入等,从而实现进程劫持等等,完成提权。
我们首先检查一下当前进程使用的token
是怎么样的
|
|
从上可以看到,我们当前进程的权限有一个SeDebugPrivilege
,这个特权的意思是Required to debug and adjust the memory of a process owned by another account.
,也就是说能够调试并且调整由其他账号拥有的进程中的内存。这个权限其实蛮高的(做这个实验的时候,我正在用windbg调试,所以权限才会这么高)。不过我们希望能够做到的是往其他进程中注入线程,那么这个时候我们需要的可能就不止这个权限了,一个比较无脑的方式就是能不能将这些特权全部拿到手,就能够保证我们必定提权成功了。
于是我们稍微改动一下我们的利用code:
|
|
然后试了一下。。。终于成功了!!!!
Exp分析
由于EXP是从别的地方抄过来的,有些地方其实有点云里雾里的,这里简单分析一下:
|
|
首先是这个函数,这是关键函数之一,正是用的这个函数我们找到了TOKEN的地址,不过话又说回来,这个函数做了些什么呢?函数首先尝试去call了NtSystemQueryInformation
:
|
|
这个API其实功能非常强大,能够返回大量系统中的重要信息。这里当我们传入的变量为StstemHandleInformation
的时候,返回的变量为SYSTEM_HANDLE_INFORMATION
,具体定义如下:
|
|
这个地方用了一个无限制数组的技巧:这个Handles[1]
其实长度可以超过1,但是这样写的话能够让编译器知道这是一段数组,从而可以无限加长这个结构体的长度,不过用sizeof查看的时候,这个Handles是当作一个数组指针大小在考虑
通过调用这个函数,能够拿到当前进程中所有打开的句柄。于是我们通过检查句柄类型,找到当前进程中使用的TOKEN的句柄。不过具体这个ObjectTypeNumber
具体是几表示什么意思,好像网上的资料并不多。。。不过我通过processexp
配合函数,大概总结出几个来:
|
|
于是这里通过给GetKernelPointer
传入0x5,获取到这个TOKEN
对象的真正的地址。于是之后我们就能够拿到_TOKEN
结构体中偏移地址为0x40
的结构体_SEP_TOKEN_PRIVILEGES
:
|
|
此时Present
表示的是这个token中被开启的特权,而Enabled
中表示的是当前token中被允许的特权。在之前提到的这篇文章中,作者发现,其实Windows并不会check一个token的Present
值,而是checkEnabled
这个位置上的值有没有被打开,来证明其权限是否打开了。所以这里我们就能够简单的利用这一点,将这个变量修改为-1,从而让当前的token获得最高的权限。
踩坑记录
- 写这篇文章的时候,居然正逢微软服务器炸了,搞得服务器上的符号下载不下来。(第一次真正意义上甩锅给微软了。。。),导致分析驱动的时候遇到了很大的问题。主要体现在分析
win32k.sys
的时候,正常看起来是这样的
但是我当时看起来,这段地址直接不能访问,我只好自己建立了一个节,然后访问过去是这样的
现在想起来,似乎是当时由于符号服务器炸了,ida导入了错误的符号表,导致连PE头都识别错了。具体错误原因我猜测应该和符号有关系,因为等过了两天符号服务器好了之后,莫名其妙的之前的坑都消失了。。。 - 调试的时候遇到的第一个问题就是这段内容:
|
|
这段内容中,我想要查看win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
指向的内容,检查过去发现地址是这样的
|
|
这个9090f62f
就是要跳转的内容,但是我想查看的时候看到的内容是这样的
|
|
就很奇怪。。。后来我问了wjllz大佬,他说可能是地址空间的问题,然后给了一篇博客,里面提到了一个指令
|
|
然而我这么做之后,发现地址空间依然没有被映射,后来找到另一个暴力的指令:
|
|
这个指令会强迫映射地址空间,这次成功的找到了地址内容:
|
|
- 中途一度放弃使用API
NtGdiDdDDIGetContextSchedulingPriority
,因为自己并没有去深究这个win32u.dll
是什么。第二天和同事一起看的时候意外发现,无论怎么调用这里面的API,调用都会失败。使用windbg去调试之后发现,居然是在ntdll!KiUserCallbackDispatcher
调用失败了:
|
|
经过我们查看,这段逻辑是这样的:
|
|
这就很奇怪了,进程的KernelCallbackTable
居然是空的。那什么时候会填充呢?查来查去发现,原来是user32.dll中会初始化的东西,用来处理一些Ring0的系统调用。而我之前就真的很巧,没有把user32.dll包含在进程中。。。。。所以为了做戏做全套,这边只需要调用
|
|
即可将user32.dll
也导入。(gdi32.dll的初始化是在user32.dll中完成的)
|
|
不过非常遗憾。。这个问题再Win1903上无法解决。。。我找到了一个大佬云集的论坛里面提到了这个问题:
https://www.unknowncheats.me/forum/anti-cheat-bypass/335585-communicating-mapped-driver-using-hooks-5.html
这个地方的人提到了,win1903里面这个gDxgkInterface
接口不再导出函数了,而是直接在dxgkrnl.sys
导出表里面导出,这样的话这段地址就受到了PG的保护,不能再被拿来利用了。。
- 找到秘籍之后,我第一次是无脑拷贝了shellcode,结果发生了报错:
|
|
后来发现人家的shellcode是x64平台上的,我这个是x86,所以换了一个shellcode就好了。。