GeekPwn 2016 Windows 服务提权漏洞的分析和利用

Yuebin Sun(@yuebinsun2020)

摘要

GeekPwn 2016 比赛中有一道 Windows 服务漏洞提权题目,该服务程序会创建命名管道(Named Pipe)服务端接收客户端发送的文件路径然后调用 LoadLibrary 加载,但加载之前有一系列的检查过程,我们的最终目标是绕过这些检查,加载我们指定的 DLL并以 SYSTEM 权限执行任意代码。本文笔者逐一分析该服务的各个验证环节及其绕过方法,以及如何组合他们最终启动 SYSTEM 权限的计算器。

服务端的处理逻辑:

  1. 服务端创建命名管道,等待客户端的连接。

  2. 客户端连接之后,服务端OpenProcess 打开客户端进程句柄,获得客户端进程 Image 文件路径。

  3. 验证客户端 Image 文件路径的签名。

  4. 签名验证通过之后,创建 Event 事件对象,无限等待 Event 对象直到 Signaled 状态。

  5. 通过管道接受客户端发送的 DLL 文件路径。

  6. 验证 DLL 文件路径的签名,签名通过之后调用 LoadLibrary 加载 DLL。

四个障碍

LoadLibrary 是我们的目标,但是在抵达目标之前,服务程序中有多层障碍需要绕过:

1. 管道创建时,没有赋予普通用户写权限。

管道创建时,指定的 DACL 字符串是 “D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRWD;;;WD)”,普通用户没有向管道发送数据的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c:\project\geekpwn_2016>accesschk.exe -v \pipe\GeekPwn2016

\\.\Pipe\GeekPwn2016
Medium Mandatory Level (Default) [No-Write-Up]
RW NT AUTHORITY\SYSTEM
FILE_ALL_ACCESS
RW BUILTIN\Administrators
FILE_ALL_ACCESS
RW Everyone
FILE_LIST_DIRECTORY
FILE_READ_ATTRIBUTES
FILE_READ_DATA
FILE_READ_EA
SYNCHRONIZE
READ_CONTROL
WRITE_DAC

2. 连接管道的客户端进程文件需携带有效签名。

服务端接收客户端的连接请求之后,调用 OpenProcess 获得客户端进程句柄,然后得到客户端进程 Image 路径,之后会调用 WinVerifyTrust 验证签名。注意有些 PE 文件通过附属 Manifest 文件提供签名信息,而 WinVerifyTrust 验证签名时对这类文件会视为无效。只有签名验证通过之后才会继续下一步。

3. 客户端没有能力使服务端创建的 Event 对象置位(Signaled)。

服务端创建的 “Global\GeekPwn2016” Event 对象,客户端作为普通权限进程是没有权限调用 SetEvent 置为 Signaled 状态的。

4. 客户端发送给服务端的 DLL 路径需要通过签名验证后才能加载。

客户端通过管道发送的 DLL 路径会传递给 WinVerifyTrust 验证签名,验证通过后才会传递给 LoadLibrary 加载。

Bypass 四个障碍

1. 利用 WRITE_DAC 给普通用户添加管道写权限。

前面 accesschk 对管道权限的枚举结果可以发现,普通用户虽然没有 WRITE_DATA 权限,但也有不少权限,挨个查 MSDN,发现 WRITE_DAC 可以帮我们重新改写对象的 DACL 列表。

利用 WRITE_DAC 给 Everyone 组用户添加 WRITE_DATA 权限,网上可以找到代码片段,需要注意的是,调用 SetSecurityDescriptorDacl 之后,只是生成了一个新的满足需求的 SecurityDescriptor 对象,管道对象的 DACL 并没有被修改,需要进一步调用 SetKernelObjectSecurity 并传递 pipe handle 才能真正在管道对象中生效。

DACL 修改之后,还有个地方需要注意,管道对象的 handle 需要关闭且以 WRITE_DATA 权限再次打开才行,因为用于修改 DACL 的 handle 无论如何没有 WRITE_DATA 权限。

2. 向有签名的程序注入代码,借壳绕过 WinVerifyTrust 的签名检查。

服务端的检查代码如上图,先获得客户端进程 PE 文件路径,然后验证 PE 文件路径。所以如果我们可以找个有签名的程序,向其进程注入攻击代码就可以绕过上述检查了。

向有签名的程序注入代码有很多方法,最简单的是创建签名程序作为子进程,本例中笔者选用 SysInternals 工具集中的 procexp.exe,然后调用 WriteProcessMemory 和 CreateRemoteThread 注入代码。直接注入完整程序逻辑比较困难,所以我们注入的是一段加载用于加载 DLL 的 Shellcode,这段 Shellcode 运行时会加载同目录的 hello.dll,剩下的工作都在这个 hello.dll 中完成。

Hello.dll 中调用 CreateFile 和 CreateNamedPipeW 连接服务端管道时,服务端获得进程 PE 路径是 procexp.exe 的路径,WinVerifyTrust 验证签名有效。

3. 抢先创建 Event 对象,Event Owner 就是自己了

服务端创建并且无限期等待 Event 的代码如上图,服务端创建的 Event 普通权限的客户端进程显然是没有权限修改状态的。连接返回失败,GetLastError 返回错误代码 5,提示 Access Denied。想到一个方法是,在服务端创建 Global\GeekPwn2016 Event 之前,抢先创建同名 Event,并设置该 Event 的初始状态为 Signaled。根据 MSDN 中对 CreateEvent 返回值的描述,之后服务端进程调用 CreateEvent 创建时会返回我们之前创建的这个 Event 的句柄。再之后的 WaitForSingleObject() 会直接以成功状态返回(返回值为0)。

4. LoadLibrary 的“灵活”与 WinVerifyTrust 的“死板”

服务端接收客户端发来的 DLL 路径后,验证过程如上图。还是调用 WinVerifyTrust 验证签名。这次怎么绕过签名检查呢?经过自己分析对比 LoadLibrary 和 WinVerifyTrust,发现二者在处理路径时存在标准不一致导致的漏洞。传递给 WinVerifyTrust 的路径,会当作全路径直接验证。而据 MSDN 介绍,LoadLibrary 似乎“灵活”很多,如果路径结尾没有扩展名,LoadLibrary 在真正的加载动作之前会自动补上一个 “.dll” 作为最终路径。

利用这个特性就可以绕过上面的检查了。假设我们传递的路径是 c:\test\evil。WinVerifyTrust 会直接验证 c:\test\evil。而 LoadLibrary 发现没有以 .dll 结尾,会自动追加,最终加载 c:\test\evil.dll。

注意,evil.dll 一定要编译成 64 位的,因为服务端主程序 GeekPwn2016.exe 是 64 位的。

成功迫使管道服务端加载 evil.dll 后启动 SYSTEM 权限计算器就很简单了,直接在 DLL Main 中调用 WinExec(“calc.exe”, SW_SHOW); 就好了,当然普通用户桌面中是看不到弹出计算器的,需要 procexp.exe 或者任务管理器查看。

总结

本次的服务端程序中,大部分漏洞或利用技巧都是依赖 MSDN 上一些不太显眼的特性或 Tricks。如果平时能够多收集这样的点,势必对漏洞的发现和利用大有帮助。

附录中会贴出精简后的服务端代码,完整的攻击程序源码会单独以压缩包形式提供。

参考链接

  1. 微软MSDN关于CreateEvent的文档: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms682396(v=vs.85).aspx

  2. 微软 MSDN 关于 LoadLibrary 的文档: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms684175(v=vs.85).aspx

附录 精简后的服务端代码

rekken

2017.07.23