= Print Nightmare 分析报告 :imagesdir: Figures :toc: :icons: font :figure-caption: 图 :xrefstyle: short :pdf-theme: basic-theme.yml
2021 年 6 月 29 日,一个非常严重的 Windows 打印服务漏洞作为 0day 被曝光,漏洞基础评分 8.8 分,发布于 GitHub 上(现已删除)。 该漏洞就是著名的 PrintNightmare: CVE-2021-34527,危险程度甚至高于永恒之蓝。
== 漏洞基本信息
34527 漏洞几乎作用于 Windows 7、Windows Server 2008 之后的所有版本,详细信息参见 <> 。
从危害角度来说,攻击者可以使用普通用户认证,以管理员权限远程执行任意代码。 就漏洞利用难度而言,该漏洞非常容易利用,因此危害极大。
从漏洞特征来说,34527 漏洞基于漏洞 CVE-2021-1675。 而 1675 漏洞是一个本地提权和远程代码执行漏洞,它和 34527 漏洞有着非常相似的地方。
在了解漏洞的工作原理之前,我们应该对 Windows 打印后台处理程序的体系架构有个大致的了解,这样能够方便我们理清漏洞涉及的各个模块之间的关系。
== CVE-2021-1675 调用流程
=== Windows 打印后台处理程序体系架构
spooler 体系架构可以用 <<spooler_arch>> 来表示:
[[spooler_arch]] .Print Spooler Architecture image::Print Spooler Architecture.png[]
具体来说,后台打印程序用于管理打印任务,它由以下部件组成:
winspool.drv:: 提供给用户的动态链接库文件。 该文件定义了 spooler 相关的 Win32 API 供用户调用。 里面的 API 均采用远程过程调用的形式来获取服务。
spoolsv.exe:: spoolsv.exe 担当体系中服务器的角色,作为第一个处理 API 调用的程序。 如此设计是为了让 print spooler 既能处理本地打印作业,又能无差别处理远程打印作业。
spoolsv.dll:: 路由程序。 它将 spoolsv.exe 接收的打印请求,向各个打印提供程序发起,并决定由哪个打印提供程序最终处理该请求。 它的作用就是区分打印任务是远程任务还是本地任务。 在远程机器上它就会将任务固定分配给本地打印提供程序。
localspl.dll:: 本地打印提供程序。 打印提供程序主要任务是解决打印任务管理的需求,绝大多数的 API 都是在这个模块内实现的。
根据上述理论来继续研究,举个例子,当我调用 AddPrinterDriverEx 函数时(CVE-2021-1675),会经过以下流程:
=== 函数版本选择
首先该函数实际上是个宏,会根据本地的编译环境选择 Unicode 版本(W)或 Ansi 版本(A),如 <>:
[[AddPrinterDriverEx]] .AddPrinterDriverEx image::AddPrinterDriverEx.png[]
但不管是宽字符还是窄字符版本,其实结果都没差,因为 Windows 内核的字符串是使用 Unicode 编码的,所以最终 Ansi 版本的调用都会转换成 Unicode 版本的调用,如 <>:
[[AnsiToUnicode]] .AnsiToUnicode image::AnsiToUnicode.png[]
当 Ansi 函数的参数均转换成 Unicode 版本后,就调用了一个函数(<>):
[[AnsiCallUnicode]] .AnsiCallUnicode image::AnsiCallUnicode.png[]
而该函数实际上就是 Unicode 版本的 AddPrinterDriverEx(<>):
[[GetUnicodeProcAddress]] .GetUnicodeProcAddress image::GetUnicodeProcAddress.png[]
=== API 函数发送 RPC 请求到 spooler 服务器上
进入 Unicode 版本的函数内部后,首先会根据 Level 的数值对函数参数类型做一个选择:
image::pDriverInfo.png[]
该漏洞中我们会设置 Level 为 2,也就是选择参数 pDriverInfo 的类型为 DRIVER_INFO_2 结构体。 然后接下来 Windows 就会对函数参数做一个处理,处理完成后会通过远程过程调用继续处理该 API:
image::set arguments.png[]
image::NdrClientCall3.png[]
=== MSRPC 机制
微软的远程过程调用机制基于 DCE 标准进行构建。 通俗来解释,远程过程调用就是在远程系统上运行进程,这些进程都是程序员或者系统预先定义好的。
RPC 的具体做法就是将想要远程调用的函数序列化,通过网络将它传送至远程系统上,远程系统再将之反序列化,并执行之。 在微软构建的体系中,TCP/IP 和 SMB 是通常被选择来传输 RPC 调用的协议。
想要使用 MSRPC,首先得定义调用的函数的 IDL 接口描述,然后使用 MIDL 工具生成客户端和服务端相应的序列化程序 stub。 而对一些 Win32 API 来说,它本身就定义好了服务端 stub,我们就可以只生成并使用客户端 stub 就够了。
MSRPC 使用 UUID 来标识某一类型的协议,如 MS-RPRN 就用来描述远程打印协议,所有跟远程打印相关的函数均属于该协议的一部分,MSPRC 使用 UUID 12345678-1234-ABCD-EF00-0123456789AB 来标识该协议(<<rprn_uuid>>):
[[rprn_uuid]] .MS-RPRN UUID image::spoolss uuid.png[]
接着,可以继续在此连接基础上,使用 operator number 来标识协议内的函数,以此来远程调用,比如 AddPrinterDriverEx 就是使用 89 来标识自己(<<addPrinterDriverEx_opnum>>):
[[addPrinterDriverEx_opnum]] .AddPrinterDriverEx Opnum image::AddPrinterDriverEx Opnum.png[]
在使用 MSRPC 的过程中,有两点需要值得注意:
"As you can see in your output, the scripts are trying to connect to port 135 (endpoint mapper) in order to get the TCP/IP port where the DCOM endpoint is listening (that is a dynamic port)." -- SecureAuthCorp/impacket issue #412
=== spoolsv.exe 处理 API 请求
[[call_flow]] .RpcAddPrinterDriverEx Call Flow image::Function Calls.png[]
从 <<call_flow>> 中可以看出,spoolsv.exe 会调用这些函数,而从函数内部分析来看,该模块内除了初始化外并没有完成什么操作。 最后,该模块会调用 pLocalProvidor 指向的函数,也就是 localspl.dll 模块内的函数 LocalAddPrinterDriverEx。 localspl 作为一个本地打印提供程序,确实是实现 API 功能的模块。
=== 本地打印提供程序的函数实现逻辑
[[LocalAddPrinterDriverEx]] .LocalAddPrinterDriverEx image::LocalAddPrinterDriverEx.png[]
首先 <> 说明了该模块会验证 spooler 是否正常运行,然后就跳转到函数 SplAddPrinterDriverEx。
[[SplAddPrinterDriverEx]] .SplAddPrinterDriverEx image::SplAddPrinterDriverEx.png[]
<> 函数内部是判断 AddPrinterDriverEx 函数能否执行成功的重要位置。 前半部分不用看,因为 WPP 是跟日志相关的技术,暂且跳过。
后半部分微软定义了一个变量 v12,是个标志位来判断函数是继续执行还是直接退出。
[[bittest]] .bittest dwFileCopyFlags image::bittest in spl.png[]
从 <> 可以看出,继续执行的条件有两种:一是 v12 为 0 也就是 bittest 判断成功,二是 Validate 成功。 而 Validate 是对权限的校验,没有办法很好地绕过。 里面有一个 API 是 OpenProcessToken,表示需要在接下来的进程中提升权限,如果不是管理员身份是做不到这点的。
所以想要继续执行就只能绕过 bittest 的判断了,而被判断的数 a4——函数的第 4 个参数,就是在官网中有着说明的参数 <> :
|=== |Name/value |Description
|APD_STRICT_UPGRADE
0x00000001
|Add the replacement printer driver only if none of the files of the replacement driver are older than any corresponding files of the currently installed driver.
|APD_STRICT_DOWNGRADE
0x00000002
|Add the replacement printer driver only if none of the files of the currently installed driver are older than any corresponding files of the replacement driver.
|APD_COPY_ALL_FILES
0x00000004
|Add the printer driver and copy all the files in the driver directory. File time stamps MUST be ignored.
|APD_COPY_NEW_FILES
0x00000008
|Add the printer driver and copy the files in the driver directory that are newer than any of the corresponding files that are currently in use.
|APD_COPY_FROM_DIRECTORY
0x00000010
|Add the printer driver by using the fully qualified file names that are specified in the _DRIVER_INFO_6 structure. If this flag is specified, one of the other copy flags in this bit field MUST be specified.
|APD_DONT_COPY_FILES_TO_CLUSTER
0x00001000
|When adding a printer driver to a print server cluster, do not copy the driver files to the shared cluster disk.
|APD_COPY_TO_ALL_SPOOLERS
0x00002000
|Add the printer driver to cluster spooler servers.
|APD_INSTALL_WARNED_DRIVER
0x00008000
|Add the printer driver, even if it is in the server's List of Warned Printer Drivers.
|APD_RETURN_BLOCKING_STATUS_CODE
0x00010000
|Specifies the implementation-specific error code to return if the printer driver is blocked from installation by server policy.
|===
bittest 16 就是验证变量的第 16 位是否为 1,对应参数的值就是 0x8000(APD_INSTALL_WARNED_DRIVER)。 根据释义也可以看出,该参数的意思是不加验证的添加打印机驱动到服务器上。
据说 1675 被修复之前,该参数还没有出现在官方文档中,这也不难看出漏洞的所在。
=== 漏洞的利用方法
当添加打印机驱动这个方法真正执行时,如果选择的是 DRIVER_INFO_2 结构体,则会发生下面几件事:
. 分别打开 DriverFile、ConfigFile 和 DataFile,确认这三个文件是否存在。 其中只有 DataFile 允许是 UNC 路径。
. 如果三个文件均存在,则将它们拷贝至目录 C:\Windows\System32\spool\drivers\x64\3\New 下面,如 <<cp_conf_file>>、<<cp_data_file>> 所示: + [[cp_conf_file]] .Copy Config File image::copy config file.png[] + [[cp_data_file]] .Copy Data File image::copy data file.png[]
. 拷贝至该目录下的原因是要执行相应的文件,3 代表了这个打印机驱动是 v3 类型的驱动。 先将新文件拷贝至 New 目录下面,防止覆盖掉 3 目录下的文件。 如果 3 下面有着同名文件,则把同名文件放到 Old 目录下面做一个备份,再将 New 下面的文件拷贝至 3 下面覆盖文件。 这点我们可以在第二次执行 RpcAddPrinterDriverEx 方法的时候就能观察到: + .Second Time RPC Call image::second time call.png[] + .Backup Files To Old Directory image::backup file.png[] + .Copy New Files To Destination image::copy file.png[] + 根据这一机制,我们可以将远程路径的文件保存为本地路径的文件,因为函数参数中驱动文件和配置文件参数都只能是本地路径,只有数据文件参数才能是远程路径。
ifdef::backend-pdf[] 恶意程序运行结果详见 https://github.com/hahaleyile/my-CVE-2021-1675[我的仓库],演示动图就是 Figures 目录下的 gif 文件。 endif::[]
== 微软对 1675 漏洞的修补
2021 年 6 月 8 日,微软对 CVE-2021-1675 漏洞进行了修补,具体的修改如下:
[[path_1675]] .CVE-2021-1675 Patch image::IsElevated.png[]
[[YIsElevationRequired]] .YIsElevationRequired image::YIsElevationRequired.png[]
[[YIsElevated]] .YIsElevated image::YIsElevated.png[]
[[unset_1675]] .Unset APD_INSTALL_WARNED_DRIVER image::JudgeIsElevated.png[]
微软在 RpcAddPrinterDriverEx 函数中加入了用户提升权限的判断,并且用户可以在注册表中将这个限制移除(<<path_1675>>)。 用户只需在注册表指定位置,创建一个名为 NoWaringNoElevationOnInstall 的键(<>),或者 RPC 账户能够获得 TOKEN_QUERY 进程 Token(<>),就能绕过该修补。 如果修补生效,dwFileCopyFlags 参数的第 16 位就会被与操作置为0,也就是说 APD_INSTALL_WARNED_DRIVER 这个参数值会无效化(<<unset_1675>>) 。
== 对 1675 补丁的绕过
尽管微软在 RpcAddPrinterDriverEx 函数上打了补丁,但是我们依然可以通过 RpcAsyncAddPrinterDriver 远程调用进行绕过。 如 <<async_send>> 所示,客户端的该函数是直接对服务端的远程调用:
[[async_send]] .RpcAsyncAddPrinterDriver Send image::RpcAsyncAddPrinterDriver Send.png[]
服务端会先给线程分配空间,然后继续调用,如 <<async_receive>> 所示:
[[async_receive]] .RpcAsyncAddPrinterDriver Receive image::RpcAsyncAddPrinterDriver Receive.png[]
继续调用的函数中会将参数都压入堆栈中,以线程的方式运行 YAddPrinterDriverEx(<<async_yadd>>):
[[async_yadd]] .thread start YAddPrinterDriverEx image::thread start YAddPrinterDriverEx.png[]
这样就成功绕过微软对 RpcAddPrinterDriverEx 的补丁了。
也就是说,通过远程调用 RpcAsyncAddPrinterDriver 函数,就能继续以管理员身份执行任意代码。
另外,据 <> 声明,该补丁对 Token 的检验还存在着问题,同时在彻底禁用了 UAC 的机器上该补丁将会无效。 不过我本人对这方面的机制不是很了解,因此不做过多的评价。
== 微软对 34527 漏洞的修复
2021 年 7 月 6 日,微软通过新一轮的补丁暂时解决了这次的后台打印漏洞问题。
据官方声明,这次的补丁将会使在打印服务器上安装打印机驱动只能由管理员来完成。 并且,微软添加了一个组策略和两个注册表键来供用户自定义该策略。
通过 <<restrict_async>> 的 ida 反编译我们可以看到,微软在 Async 函数上新加了对 Token 和注册表项的验证。
[[restrict_async]] .restrict in async function image::restrict in async.png[]
通过 <<restrict_rpcadd>> 我们可以看出,微软在 RpcAddPrinterDriverEx 函数上加入了对用户组和它的注册表键的验证。
[[restrict_rpcadd]] .restrict in RpcAddPrinterDriverEx Function image::restrict in rpcadd.png[]
因此,大概 1675 漏洞的修复确实还存在 Token 验证方面的问题吧。
== 项目计划
. 添加对 RpcAsyncAddPrinterDriver 远程调用的支持,以绕过微软 6 月 8 日的补丁,实现 CVE-2021-34527 exp。
. 理解 UAC 机制,完善这篇漏洞分析。
[bibliography] == 参考文献
[[[a,官方网站]]] Windows Print Spooler Remote Code Execution Vulnerability https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-34527
. 根据 <>,pConfigFile 是设备驱动的配置动态链接库,因此需要加载一次来初始化。 这一点从实际运行情况中也得到了证实。 + .Load pConfigFile image::Load Image.png[] + 如此一来,只要编写一个恶意 dll,将恶意代码放到 dll 入口点来执行,就能执行任意代码,并且还是管理员权限。 + .spoolsv.exe is under administrator privilege image::spoolsv user.png[]
. CreateInternalDriverFileArray()函数根据文件操作标志来决定是否检查spool驱动目录。 如果a5 flag被标志为False,驱动加载函数只会检查用户目录中是否包含要拷贝的驱动文件。 否则,函数会尝试到spool驱动目录寻找目标驱动。 这就要求 dwFileCopyFlags 需要同时设置参数 APD_COPY_FROM_DIRECTORY。 + image::APD.png[] + image::APD_1.png[]
== 利用程序的使用方法
该程序基于 docker 构建,可以有效地解决环境依赖问题。
首先使用者下载 compose 文件到自己的目录下,然后在该目录下建立一个 share 的文件夹,作为挂载目录使用。 使用者可以将恶意程序放在 share 文件夹内,该文件夹会作为 samba 路径 smb 共享出去。
接着,使用者在终端输入命令
即可启动集群(一个容器)。 用户可自行进入容器内操作,容器中的环境均已经配置好。
或者也可以直接在终端输入命令
来执行命令。
== 利用程序的运行结果
感谢 <> 开源的代码供我参考!
ifndef::backend-pdf[] .Program running result image::exploit.gif[] endif::[]
[[[b,dwFileCopyFlags]]] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/b96cc497-59e5-4510-ab04-5484993b259b
[[[c,Windows PrintNightmare 漏洞(CVE-2021-34527) 和补丁分析]]] https://www.freebuf.com/vuls/279876.html
[[[d,gentilkiwi/mimikatz]]] https://github.com/gentilkiwi/mimikatz
[[[e,官方文档]]] DRIVER_INFO_2 structure https://docs.microsoft.com/en-us/windows/win32/printdocs/driver-info-2
[[[f,cube0x0]]] https://github.com/cube0x0
[[[g,James Forshaw]]] https://twitter.com/tiraniddo/status/1410726790994169857
[[[h,官方声明]]] KB5005010: Restricting installation of new printer drivers after applying the July 6, 2021 updates https://support.microsoft.com/en-us/topic/kb5005010-restricting-installation-of-new-printer-drivers-after-applying-the-july-6-2021-updates-31b91c02-05bc-4ada-a7ea-183b129578a7