= 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[] + 根据这一机制,我们可以将远程路径的文件保存为本地路径的文件,因为函数参数中驱动文件和配置文件参数都只能是本地路径,只有数据文件参数才能是远程路径。
. 根据 <>,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::[]