摘要
我们将通过发送一个精心构造的proxyDoAction请求,来详细考察CVE-2018-20310(它是位于PDF Printer中的一个基于堆栈的缓冲区溢出漏洞)的攻击向量、漏洞分析和利用方法。
软件版本
文中描述的方法已经在9.3.0.912版本的Foxit Reader软件进行了测试,其中FoxitProxyServer_Socket_RD.exe二进制文件的SHA1值为:0e1554311ba8dc04c18e19ec144b02a22b118eb7。该版本是撰写本文时的最新版本。
攻击向量
PDF Printer是Foxit Reader中的一个功能,主要用于处理来自应用程序的PDF文件打印请求。安装Foxit Reader后,Foxit PDF Printer就会成为处理打印作业的默认打印机。
从Chrome打印文档
这实际上意味着FoxitProxyServer_Socket_RD.exe二进制文件启动后,会在中等完整性级别运行片刻。
从应用程序打印文档时,FoxitProxyServer_Socket_RD.exe将在中等完整性级别运行
只在这个级别运行片刻的原因是服务器默认监听localhost端口50000并且只接受一个请求。一旦发出请求,它就会关闭端口并终止执行。当用户尝试使用Foxit PDF Printer打印到PDF时,攻击者就能够在渲染选项卡中执行代码。
在对该问题进行深入考察之后,发现可以从沙盒进程发出未公开的ALPC请求,以使用默认打印机启动打印作业。这意味着,攻击者根本不需要向FoxitProxyServer_Socket_RD.exe二进制文件发送竞争请求。
漏洞分析
在从浏览器打印页面时,我们截获了许多发送到端口50000的请求样本;此后,我们又发现了一个重要的函数,即sub_41DBA0。
sub_41DBA0的代码流程
这个函数用于处理多种不同类型的请求,其中,相应的处理程序在上图中用蓝色突出加以显示,其中包括:
· proxyDoAction
· proxyPreviewAction
· proxyPopupsAction
· proxyCPDFAction
· proxyUpdatePreview
· proxyFinishPreview
· proxyCollectSysFont
· proxyGetImageSize
· proxyCheckLicence
· proxyGetAppEdition
· proxyInitLocalization
· proxyCreateDirectoryCascade
· proxyIEMoveFileEx
· proxySendFileAsEmailAttachment
虽然其中一些处理程序确实是高度可利用的,但并不总是能够到达易受攻击的API,这里,我们将以proxyIEMoveFileEx为例进行介绍。该函数接受三个参数,它实际上就是一个MoveFileExW调用,并且没有对参数进行任何检查。不过,由于它无法正确解析提供的数据包结构,因此,该函数实际上是无法利用的。通常情况下,软件开发人员在发布软件之前会进行相应的测试,以确保它们能正常工作!以下是这个底层API所在的位置:天空彩
.text:00420C85 loc_420C85: ; CODE XREF: sub_420930+331 .text:00420C85 push ebx ; dwFlags .text:00420C86 push edi ; lpNewFileName .text:00420C87 push eax ; lpExistingFileName .text:00420C88 call ds:MoveFileExW
在进行了更加深入的逆向分析之后,我们发现proxyDoAction也是一个非常让人感兴趣的函数,因为攻击者可以利用它的操作码抵达5条不同的代码路径。以下是检查请求数据包中的proxyDoAction字符串的相关代码:
sub_41DBA0函数会检查proxyDoAction请求
也就是说,只要能够提供正确格式的请求,我们最终可以到达该处理程序:
用于到达处理程序的proxyDoAction请求
在处理程序内部,我们可以看到它具有3个参数:
sub_41E190函数会对3个参数进行检查
通过深入考察该函数,处理第三个参数的代码如下所示:
.text:0041E407 mov esi, [eax] ; eax is a ptr to our buffer .text:0041E409 jmp short loc_41E421 ; take jump .text:0041E40B ; --------------------------------------------------------------------------- .text:0041E40B .text:0041E40B loc_41E40B: ; CODE XREF: sub_41E190+275 .text:0041E40B xor esi, esi .text:0041E40D test eax, eax .text:0041E40F jnz short loc_41E421 .text:0041E411 call sub_64BE4A .text:0041E416 mov dword ptr [eax], 16h .text:0041E41C call sub_65015F .text:0041E421 .text:0041E421 loc_41E421: ; CODE XREF: sub_41E190+279 .text:0041E421 ; sub_41E190+27F .text:0041E421 lea eax, [edi+4] ; calculate offset to src ptr .text:0041E424 mov [ebp+var_80_opcode], 0 ; initialize dst buffer .text:0041E42B add eax, ebx ; recalculate offset to src ptr .text:0041E42D lea ecx, [ebp+var_80_opcode] ; fixed buffer of size 0x4 .text:0041E430 push esi ; size, controlled from our buffer .text:0041E431 push eax ; src ptr to copy from .text:0041E432 mov edx, esi .text:0041E434 call sub_41CB30 ; call sub_41CB30 .text:0041E439 add esp, 8 .text:0041E43C push [ebp+var_80_opcode] ; opcode .text:0041E43F push [ebp+var_84] ; int .text:0041E445 push [ebp+lpFileName] ; lpFileName .text:0041E44B call sub_4244C0 ; proxyDoAction second handler
对sub_41CB30的调用看起来非常可疑,因为它使用长度值和源缓冲区作为参数。此外,我们可以看到,目标缓冲区是存储在ecx中的。当我们考察sub_41CB30函数时,可以看到它执行了哪些操作:
.text:0041CB30 sub_41CB30 proc near ; CODE XREF: sub_41D500+185 .text:0041CB30 ; sub_41D740+11A .text:0041CB30 .text:0041CB30 arg_0_src = dword ptr 8 .text:0041CB30 arg_4_size = dword ptr 0Ch .text:0041CB30 .text:0041CB30 push ebp .text:0041CB31 mov ebp, esp .text:0041CB33 push esi .text:0041CB34 mov esi, [ebp+arg_4_size] ; store controlled size in esi
如上所示,sub_41CB30将通过参数源缓冲区、目标缓冲区和长度来调用sub_645BD0函数。其中,源缓冲区和长度这两个参数完全处于攻击者的控制之下,而目标缓冲区则是sub_41E190函数的本地堆栈变量。
.text:0041CB61 loc_41CB61: ; CODE XREF: sub_41CB30+16 .text:0041CB61 push ebx .text:0041CB62 mov ebx, [ebp+arg_0_src] ; set the src in ebx .text:0041CB65 test ebx, ebx .text:0041CB67 jz short loc_41CB7F .text:0041CB69 cmp edi, esi .text:0041CB6B jb short loc_41CB7F .text:0041CB6D push esi ; size .text:0041CB6E push ebx ; src .text:0041CB6F push ecx ; dst .text:0041CB70 call sub_645BD0 ; call sub_645BD0
从某种程度上说,sub_645BD0函数就是memcpy函数的一种内联的自定义实现,最终,我们将执行以下代码块:
.text:00645C14 loc_645C14: ; CODE XREF: sub_645BD0+2F .text:00645C14 bt dword_932940, 1 .text:00645C1C jnb short loc_645C27 .text:00645C1E rep movsb ; stack buffer overflow! .text:00645C20 mov eax, [esp+8+arg_0] .text:00645C24 pop esi .text:00645C25 pop edi .text:00645C26 retn
触发漏洞
由于我们可以在沙箱之外运行可执行文件,因此,使用以下命令来调试应用程序的话,会更容易一些:
C:\>cdb -c "g;g" "C:\Program Files (x86)\Foxit Software\Foxit Reader\Plugins\Creator\FoxitProxyServer_Socket_RD.exe" 50000
默认情况下,该应用程序将使用端口50000,不过,我们也可以通过命令指定端口。
在沙箱外触发SRC-2019-0025/CVE-2018-20310漏洞
简单来说,这里需要发送一个精心构造的、作为操作码的请求,其缓冲区大小为0x1000字节,从而触发基于堆栈的缓冲区溢出。
漏洞利用
我们无法直接利用SEH处理程序:
使用SafeSEH选项编译FoxitProxyServer_Socket_RD.exe
此外,如果我们再次深入考察proxyDoAction处理程序,我们会发现,该函数末尾有一个对sub_43AE57的调用。
.text:0041E510 loc_41E510: ; CODE XREF: sub_41E190+8E .text:0041E510 ; sub_41E190+9E .text:0041E510 mov ecx, [ebp+var_C] .text:0041E513 mov large fs:0, ecx .text:0041E51A pop ecx .text:0041E51B pop edi .text:0041E51C pop esi .text:0041E51D pop ebx .text:0041E51E mov ecx, [ebp+var_14] .text:0041E521 xor ecx, ebp ; xor cookie with frame pointer .text:0041E523 call sub_43AE57 .text:0041E528 mov esp, ebp .text:0041E52A pop ebp .text:0041E52B retn 4 .text:0041E52E ; -----------------------
正如您所猜测的那样,它会进行cookie检查:
.text:0043AE57 sub_43AE57 proc near ; CODE XREF: sub_413FA0+5D .text:0043AE57 ; sub_413FA0+7B .text:0043AE57 cmp ecx, ___security_cookie ; bummer .text:0043AE5D repne jnz short loc_43AE62 .text:0043AE60 repne retn .text:0043AE62 ; --------------------------------------------------------------------------- .text:0043AE62 .text:0043AE62 loc_43AE62: ; CODE XREF: sub_43AE57+6 .text:0043AE62 repne jmp sub_43B739 .text:0043AE62 sub_43AE57 endp
但是,如果我们仔细研究这个易受攻击的函数,就会发现一些有趣的东西:
.text:0041E4A2 loc_41E4A2: ; CODE XREF: sub_41E190+2F0 .text:0041E4A2 mov byte ptr [ebp+var_4], 8 .text:0041E4A6 cmp [ebp+var_24], 0 .text:0041E4AA jnz short loc_41E4B8 .text:0041E4AC mov ecx, [ebp+var_28] ; code execution primitive 1 .text:0041E4AF test ecx, ecx .text:0041E4B1 jz short loc_41E52E .text:0041E4B3 mov eax, [ecx] .text:0041E4B5 call dword ptr [eax+8] ; eop .text:0041E4B8 .text:0041E4B8 loc_41E4B8: ; CODE XREF: sub_41E190+31A .text:0041E4B8 mov byte ptr [ebp+var_4], 9 .text:0041E4BC mov ecx, [ebp+var_28] ; code execution primitive 2 .text:0041E4BF test ecx, ecx .text:0041E4C1 jz short loc_41E4DB .text:0041E4C3 mov edx, [ecx] .text:0041E4C5 lea eax, [ebp+var_4C] .text:0041E4C8 cmp ecx, eax .text:0041E4CA setnz al .text:0041E4CD movzx eax, al .text:0041E4D0 push eax .text:0041E4D1 call dword ptr [edx+10h] ; eop
如果我们利用堆栈溢出覆盖var_28但不覆盖返回地址或异常处理程序,那么我们就可以伪造一个对象,并通过vtable调用来重定向代码执行流程。
这种方法是切实有效的,因为VAR_28在堆栈中的位置较低:
-00000080 var_80_opcode dd ? ; pwned -0000007C var_7C db 36 dup(?) | -00000058 var_58 dd ? | overflow direction -00000054 var_54 db 8 dup(?) | -0000004C var_4C db 36 dup(?) v -00000028 var_28 dd ? ; pwned also! -00000024 var_24 db 8 dup(?) -0000001C var_1C dq ? -00000014 var_14 dd 2 dup(?) -0000000C var_C dd 2 dup(?) -00000004 var_4 dd ?
变量var_80_opcode的堆栈大小为0x80 – 0x7c = 0x4字节。这样的话,事情就变得更容易了!下面,让我们看一下溢出之前的代码:
.text:0041E34D loc_41E34D: ; CODE XREF: sub_41E190+1A2 .text:0041E34D ; sub_41E190+1AB .text:0041E34D lea eax, [esi+1] .text:0041E350 add ebx, 4 .text:0041E353 push eax .text:0041E354 call sub_43AEAB .text:0041E359 mov [ebp+var_84], eax .text:0041E35F add esp, 4 .text:0041E362 lea eax, [ebp+var_84] .text:0041E368 mov [ebp+var_E4], offset off_8F3140 .text:0041E372 mov [ebp+var_E0], eax .text:0041E378 lea eax, [ebp+var_E4] .text:0041E37E mov [ebp+var_C0], eax .text:0041E384 lea eax, [ebp+var_4C] ; overflowed pointer loaded .text:0041E387 mov [ebp+var_28], 0 .text:0041E38E mov [ebp+var_90], eax .text:0041E394 push eax .text:0041E395 lea ecx, [ebp+var_E4] .text:0041E39B mov byte ptr [ebp+var_4], 5 .text:0041E39F call sub_421D60 .text:0041E3A4 mov [ebp+var_28], eax ; bingo! We can fake an object!! .text:0041E3A7 mov [ebp+var_24], 0 .text:0041E3AB mov byte ptr [ebp+var_4], 6 .text:0041E3AF mov ecx, [ebp+var_C0] .text:0041E3B5 test ecx, ecx .text:0041E3B7 jz short loc_41E3DA
因此,我们可以利用var_4C(它将发生溢出)来伪造一个对象,因为指向该对象的指针稍后将存储到var_28中。这就意味着我们只需溢出0x80- 0x4cC=0x34字节即可!现在,如果我们更新poc,就可以破坏堆栈上的变量并重定向执行流程:
控制eip
当然,我们还必须面对ASLR所带来的问题,不过,这里我们不打算对其深究,因为该漏洞的影响无论如何都是有限的。不过,这却是一个很好的例子,说明即使才去了适当的安全措施,仍然会出现安全问题。
此外,我们还使用@zeroSteiner提供的修改版mayhem库将poc注入到了沙盒进程(以及python)中,以向Foxit开发人员展示该漏洞的实际影响。
如果要对其进行测试,可以下载poc触发器。
小结
这不仅仅是Foxit Reader中的一个漏洞的问题,而是第三方应用程序应该在多大程度上信任安装的打印服务器的问题。另外,我们发现,研究新的或未经探索的组件通常能挖掘到高度可利用的安全漏洞,但是对于研究人员来说,获取对于接口的访问权限可能是这里最难的挑战。