利用Foxit Reader的PDF Printer实现提权

去年年中,我在一篇文章中讲述了在Foxit Reader中挖掘UAF漏洞的过程,以及如何利用该漏洞发送远程代码执行攻击。之后,我又在一篇文章中介绍了Foxit Reader SDK ActiveX中的一个命令注入漏洞。本着不放弃不抛弃的精神,在同年晚些时候我又对Foxit Reader的一个新组件进行了深入的研究。令我惊讶的是,在这个组件中又发现了几个允许有限提升权限的漏洞,其中一个漏洞尤其严重,所以,本文就诞生了。
摘要
我们将通过发送一个精心构造的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 SoftwareFoxit ReaderPluginsCreatorFoxitProxyServer_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中的一个漏洞的问题,而是第三方应用程序应该在多大程度上信任安装的打印服务器的问题。另外,我们发现,研究新的或未经探索的组件通常能挖掘到高度可利用的安全漏洞,但是对于研究人员来说,获取对于接口的访问权限可能是这里最难的挑战。