CVE-2018-8120漏洞是Windows 7 及 Windows Server 2008系列中的一个经典的提权漏洞,这篇文章中我们将一步步分析漏洞成因,并构造利用POC。
Win7 x86虚拟机(Vmware)
该漏洞存在于SetImeInfoEx函数中;在一处访问内核对象的数据时,没有判断是否合法即进行访问。
交叉引用查找发现,只有在系统调用函数NtUserSetImeInfoEx中调用该函数
查看这部分代码,可以知道SetImeInfoE函数的第一个参数来源是GetProcessWindowsStation
想要搞清楚pWinStation的结构,需要找到GetProcessWindowStation函数原型,在MSDN上对该函数的定义如下:
HWINSTA GetProcessWindowStation(); If the function succeeds, the return value is a handle to the window station.
这样,我们就知道了pWinStation是Window Station的Handle。在Windows中Handle实际也就是内核的对象引用,这里是WindowsStation对象。在Windbg中查看得到该对象的信息
对比漏洞触发时引用的位置v3[5]实际是WindowStation结构偏移0x14的数据对象即spklList;而spklList默认为NULL!
另外在查询该API时,同是也能查到关于Window Station其余的API信息,其中关系最紧密的就是
//CreateWindowStation创建一个Window Staion HWINSTA CreateWindowStationA( LPCSTR lpwinsta, DWORD dwFlags, ACCESS_MASK dwDesiredAccess, LPSECURITY_ATTRIBUTES lpsa ); //SetProcessWindowStation 为当前进程设置Window Station BOOL SetProcessWindowStation( HWINSTA hWinSta );
根据上面的漏洞点、溯源成因分析。我们可以用相关的API接口构造一个简单的POC。
其中NtUserSetImeInfoEX函数属于系统调用,没有直接的导出函数可以调用,所以需要根据调用号自定义实现。
该函数的定义在Win7上没有找到(在Win10后win32k.sys的系统调用可以在win32u.dll中找到)但是有公开的文档
在这里可以找到Windows系统调用在各个版本号上的系统调用号。
还可以在ntdll.dll中找到系统调用如何自定义实现
(其中mov eax, 0x….是系统调用号)
据此我们可以实现自定义的NtUserSetImeInfoEx函数
接下来我们需要编译并在目标Windows7上运行。
运行之后windbg收到异常,断下状态如下
继续运行,目标系统蓝屏
根据上述内容,我们知道SetImeInfoEx函数的第一个参数是pWinStation,第二个参数实际就是NtUserSetImeInfoEx的参数(char*)的前0x15c的内容。也即都是可控的。
而我们发现在SetImeInfoEx中,最终将执行qmemcpy操作,而且目的地址、源地址在某种程度都是可控的。只要在访问NULL时不会触发异常即可!
这一部分代码还可以从Windows泄漏的部分源码中查到,可以辅助分析。win2000源码
在用户层应用程序中,NULL引用可能是无法导致代码执行的。但是在内核态,NULL引用却有意向不到的效果(Win7上)
这里不得不提Windows的一个未公开API(在ntdll.dll中)
NTSTATUS NtAllocateVirtualMemory( _In_ HANDLE ProcessHandle, _Inout_ PVOID *BaseAddress, _In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_T RegionSize, _In_ ULONG AllocationType, _In_ ULONG Protect );
该函数可以在指定进程内分配一块指定大小的空间;同时第二个参数BaseAddress可以指定为优先选择的分配的空间(当该处有足够的大小空闲时,就直接分配该地址【页对齐】)。这就意味着,我们有机会分配一块包含NULL的地址,并向其中写入构造的数据。
如下利用NtAllocateVirtualMemory分配包含NULL的地址空间
在windbg调试器中查看效果
通过上述 分析过程,我们发现可以在NULL分配空间,填充合适的结构,最终利用qmemcpy实现任意地址写。
构造如下的数据,用于最终执行到qmemcpy
最终调试到qmemcpy处状态,可以造成任意地址写
这里发现虽然可以任意地址写,但是注意到目的地址(edi)偏移0x18的位置需要为0。
到目前我们拥有了任意地址写的能力(内核或用户态),我们需要把写的能力转为权限的提升。在Windows上,所有的权限控制都有一个存在Kernel中的Token对象来控制。每个登入系统的用户会生成一个User Token,所有启动的进程会继承用户的Token,子进程继承父进程Token。更多详细的介绍建议查阅MSDN文档。
下面我们在Windbg里查看下不同进程的Token
Lsass.exe进程(SYSTEM权限)
在看一下cmd.exe的Token(普通用户)
可以看到Token在_EPROCESS结构偏移0xf8的位置,且最终向下8byte对齐。
我们用lsass.exe的Token复写cmd.exe 的Token,看下情况会如何
不出意外的cmd.exe将拥有SYSTEM的权限!
根据这种思路,我们可以利用任意地址写漏洞修改当前进程Token。但是需要直到SYSTEM的Token,但是普通用户没有SYSTEM进程的访问权限。
另一种关于任意地址写漏洞(www)的利用方式中,在Windows上用的最为经典的就是Bitmap的滥用,也称“PvScan0 Technique” ,该方法可以将任意地址写扩展到任意地址读以泄漏信息。
这里建议查阅Window MSDN官方文档(与Windows 绘图相关),与之相关的API重点有
// 创建Bitmap HBITMAP CreateBitmap( int nWidth, int nHeight, UINT nPlanes, UINT nBitCount, const VOID *lpBits ); // 将bitmap bits拷贝到指定缓冲区 LONG GetBitmapBits( HBITMAP hbit, LONG cb, LPVOID lpvBits ); // 设置bitmap的bits LONG SetBitmapBits( HBITMAP hbm, DWORD cb, const VOID *pvBits );
CreateBitMap创建的结构SURFACE OBJECT
头部是一个BASEOBJECT结构(Windows 未公开,但是Reactos有文档BASEOBJECT
接下来是_SRFOBJ MSDN的定义如下:
这其中的pvScan0指向bitmap的Pixel Data数据区,也即是GetBitmapBits和SetBitsMaps直接操作的数据区。
我们在windbg中观察下这里有什么特别之处:
在进程PEB中有一个结构GdiSharedHandleTable保存着GDICELL结构数组(该结构并没有公开)
其中的pKernelAddress指向SURFACEOBJECT结构(BASEOBJECT首字节)
这几个结构体之间的关联,下面这幅图可以阐述:
在windbg中查看(标出了pKernelAddress和wType)
通过上面的结构我们可以获知
PEB.GdiSharedHandleObejct = 0x00300000
Gdiaddr = 0x003000a0
BASEOBJECT中的hMgr = 0x0104000a(CreateBitMap返回值)
这三者之间的关系:
gdiCell_Addr = PEB.GdiSharedHandleObejct + (hMgr & 0xffff) * sizeof(GDICELL)
其中PEB.GdiSharedHandleObject的地址用户层可以得到,hMgr是CreateBitMap返回的Handle,也就是我们可以计算得到GdiCell的位置,进而可以得到pKernelAddress。
尽管我们在用户态无法访问SURFOBJ的成员,但是由关系图2- 知道pvScan0和pKernelAddress之间的Offset是固定的:
pvScan0_Offset = pKernelAddress + 0x10 + 0x1c
这样,我们就可以根据CreateBitMap返回值得到pvScan0的地址。
pvScan0 = *( PEB.GdiSharedHandleObejct + (hMgr & 0xffff) * sizeof(GDICELL)) + 0x2C;
下面的代码可以用来测试该推理:
Windbg调试过程如下
到这里我们已经有了一个内核态的地址pvScan0,pvScan0处的Value就是用户可以直接读写的Pixel Data;通过GetBitmapBits和SetBitMaps,我们可以对该地址读写操作。
结合任意地址写漏洞,我们修改pvScan0,就可以真正达到准确的任意地址写!
在上一环节,我们可以在用户层获取内核地址的读写(通过pvScan0),这里详细介绍如何结合BitMap利用任意地址写漏洞
(1) 创建2个bitmaps(Manager/Worker)
(2) 使用CreateBitMap返回的handle获取pvScan0的地址
(3) 使用任意地址写漏洞将Worker的pvScan0地址写入Manager的PvScan0(作为Value)
(4) 对Manager使用SetBitmapBits ,也就是改写Woker的pvScan0的Value为读/写的任意地址。
(5) 对Worker使用GetBitmapBits/SetBitmapBits,以对第四步设置的地址任意读写!
下面的图示可以很好的表示上述攻击主要流程3、4:
代码实现Attack 3(只需要修改之前 2.5.4 任意地址写的POC)
Windbg调试,修改前Manager pvScan0
顺利执行到任意地址写漏洞(v4+0x18为0 的限制,pvScan0是满足的)
修改之后的mgr_pvScan0
Attack4 就是执行SetBitMapBits修改manager的PvScan值指向的内容(也就是Worker的PvScan0的值),写入目的地址。
例如下面的代码使得Worker的PvScan0指向0xdeadbeef
Windbg调试,修改前Worker PvScan0
修改后
此时Worker的pvScan0值已经指向了我们的目标地址,通过SetBitMapBits即可写入任意内容。
当有任意地址写的能力的时候,最好的代码执行思路就是重写一个函数指针指向Shellcode,在shellcode中实现Token的重写!而Windows也确实有类似的指针(一个函数分发表),在hal模块下(该模块是Windows系统启动时加载的第一个模块)有很多这样的函数指针,我们取其中一个测试下
上图中显示了函数分发表中的函数,在偏移0x4的位置hal!HaliQuerySystemInformation是Windows API NtQueryIntervalProfile运行时调用的函数,如果重写这个地址,就可以调用触发执行Shellcode。
这里的HalDispatchTable的地址我们可以直接由ntoskrnl.exe模块中查询得到,但是由于ALSR的存在,得到的地址并不是真实的地址,我们只用这个计算得到偏移,之后再使用。某些系统需要加上一个Detect Offset,调试时自行修改。
获取HalDispatchTable地址
之后可以在2.5.6的基础上修改HalDispatchTable中的函数指针
修改之后的HalDispatchTable
可以发现HalDispatchTable偏移+0x4的函数指针已经被修改为Shellcode地址。
对于Windows内核的漏洞,一般思路就是达到提权的目的。提权,就和权限相关,建议阅读Windows有关权限控制的官方文档。
提权Shellcode 本质也是一段Shellcode,不过和一般的Shellcode实现的功能不同,一般的Shellcode在于返回一个执行命令的Shell。而提权Shellcode致力于修改当前进程的Token,使其权限更高,甚至于SYSTEM权限!
这里给大家展示下Windbg遍历系统进程Token的过程
首先,在Windows内核中,fs寄存器指向nt!_KPCR结构
上图中,通过fs指向的KPCR结构找到偏移0x120处的KPRCB结构,在KPRCB偏移0x4的位置是CurrentThread的KTHREAD结构。
上述图展示了由KTHREAD获取PID、Token、FLINK的方法,在Win7 32上得到的偏移如下
网上有公开的Windows提权Shellcode,我这里只是对Shellcode的一个分析,可以尝试自己动手实现。
将提权Shellcode在修改HalDispatchTable的基础上,即可以提权!为了显示提权成功,可以在触发提权后,启动测试程序的cmd拥有了SYSTEM权限。
提权的触发路径
提权成功