Home >Backend Development >PHP Tutorial >PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231)(连载之第三篇)

PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231)(连载之第三篇)

WBOY
WBOYOriginal
2016-06-20 12:26:531045browse

0x00 前言

  • 作者: Cigital公司的安全顾问Qsl1pknotp
  • 题目: Exploiting memory corruption bugs in PHP Part 3: Popping Remote Shells
  • 地址: http://www.inulledmyself.com/2015/05/exploiting-memory-corruption-bugs-in.html

这片文章所花的时间比我想象中的要长, 不过这时间花得值! 我想通过视频的方式来讲解如何利用这漏洞, 所以这片文章没有之前两篇描述得详细.

可能会让一些人失望的是, 这篇文章只是介绍了如何写一个 POC (并没有实际放出 POC). 文章末尾的视频会给你们展示我的自动化远程利用工具, 以及在现实环境中为了能让这 POC 成功执行所使用的 tips & trick.

0x01 查找数据

为了能更简单的解释如何利用这个漏洞, 我使用下面的代码作为演示, 具体情况具体分析.

#!php<?phpecho serialize(unserialize(base64_decode($_GET['data'])));?>

我们的目的是能够执行任意 PHP 代码. 当然啦, 我们可以尝试去注入 shellcode 来达到目的, 但是这种方法既没有创造性, 也不优雅(高版本的 PHP 可能不会成功). 如果你还记得Part1, 为了能够执行任意 PHP 代码, 我们需要调用 php_execute_script 和 zend_eval_string . 然而, 我们希望能够进行远程攻击, 所以我们必须找到 executor_globals 以及 JMP_BUF . 至于为什么要这样做, 后面会详细介绍.

简而言之, 我们需要找到 (没有特别的顺序):

  • executor_globals
  • zend_eval_string
  • JMP_BUF
  • 将任意数据写入 stack 的方法

比较幸运的是, 上面所列举的要求, 有些还是比较好找的, 因为它们就在 binary 中. 我们直接 dump PHP binay 的 strtab.

Great! 我们直接从里面找到 zend_eval_string 的地址, 然后在 GDB 中验证这个地址是否正确.

  1. 查找 zend_eval_string 的地址

  2. 在 gdb 中查看对应的地址

  3. 查找 executor_global 的地址

  4. 在 gdb 中查看对应的地址

Awesome! 现在我们要怎么找到 JMP_BUF 呢? 通过阅读代码, 我们找到了 _zend_executor_globals 对象, 并在其中发现了一个 JMP_BUF 指针, 名为 bailout . 让我们挂上 GDB 看看地址是否正确.

  1. 查看 zend_executor_globals 对象

  2. 打印 zend_executor_globals->bailout

好, 我们拿到了这个地址, 但是这个地址指向的是什么? 有什么用? 好吧, 在 PHP 中 JMP_BUF 被用来实现 PHP 的 "try{} - catch{}" 机制. 后面会详细介绍这个.

0x02 利用方法 1 - ROP

我们现在还只差一样东西: 将任意数据写入 stack. 利用方法 2 会详细讨论 Stefan 在 2010 Syscan 公布的方法. 既然我们可以释放任意内存, 那我们下一步该干什么呢? 我们该怎么写数据到 stack 中? 怎么确保写入的数据以后不会被覆盖? (Google is your friend :))

RFC, 更确切的说是: RFC 1867

这个 RFC 指明允许带有 multipart/form-data 的 POST 请求的数据写入到 stack 中, 而且完全不会被 php 覆盖(由于各种各样的原因). 让我们上传一个"普通"的文件吧.

Awesome! 我们可以写入任意数据到 stack 中. 但是我们要写什么到 stack 中?

Hint: 我们之前所寻找的东西 : )

既然之前我们花了那么多时间去找那么多地址, 该时候使用它们了! 所以, 我们应该如何使用之前的地址呢? 经过一些研究, 我们需要这样布局:

从最简单的开始: 一开始我们就用 readelf 获取到了 zend_eval_string 的地址. Ret Pointer 和 Zend_Bailout 我们都不需要理会(会导致 PHP crash). 我们在 stack 中填写两次指向 eval_string 的指针, 下面就是我们现有的数据:

  • POP; RET - ?????
  • XCHG EAX, ESP; RET - ????
  • Zend_Eval_String - 0x082da150
  • Zend_Bailout - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
  • Ret Pointer - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04

Sweet! 我们快填写完这些东西了, excellent! 但是, 看起来我们还需要一些 ROP gadgets. 我个人比较喜欢用 ROPGadget, 不过其它工具也是可以的. 我们需要查找 XCHG EAX, ESP; RET (0x94 0xc3), 还需要找 POP EBP; RET (0x5d 0x3c). 一旦找到了这些 gadgets, 我们就可以继续下一步了.

我们拿到了这两个地址了(为啥这些地址相差那么大, 因为它们是相对地址), 我们可以继续完成 stack 中的数据:

  • POP; RET - 0x000e8e68
  • XCHG EAX, ESP; RET - 0x000057b7
  • Zend_Eval_String - 0x082da150
  • Zend_Bailout - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
  • Ret Pointer - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04

好了, 该是时候测试了.

Hmmm, 这不是我想要的结果, 现在怎么办? 看起来好像我们的代码尝试跳到我们的 gadget (c394). 不幸的是, 你还需要知道一些事情. SPLObjectStorage 要求这些 gadget 在 php 是可以访问的, 所以我们还需要修改一下. 经过修改之后:

0x03 利用方法 2 - Stefan

方法 1 就到此为止了, 方法 1 只能影响老版本的 PHP. 我们继续研究新版本的 PHP 利用方法.

比较走运的是, 之前找到 php_execute_script 和 jmp_buf 地址, 在新 exploit 中都会被用到.

jmp_buf 在 setjmp & longjmp 中被用来保存 "环境" 以预防 "不可恢复" 的错误. 在 32 位系统中, jmp_buf 是一个存储 6 个 int 的数组, 在 64 位系统中, jmp_buf 存储的是 8 个 int 的数组. 不幸的是, 需要自己查看代码来判断 jmp_buf 保存的寄存器的顺序. 这里有个 jmp_buf 样例布局. 让我们看一下 PHP 中的内容...

在我的机器上, 寄存器的顺序是: ebx, esp, ebp, esi, edi, eip. 值得完成的事情一般都不怎么容易完成, 在这里也一样, 我们的 edi & eip 看起来貌似被 Glibc 混淆了, Glibc 有个宏叫 PTR_MANGLE , 在视频中, 我们会讲解如何破解 JMPBUF.

一旦破解出了 edi & eip, 我们就可以继续重写和释放内存了. 幸运的是, 我们可以继续利用 SPLObjectStorage 远程释放内存. 剩下的事情就是将如何写到 stack 中. 和Part 2, 我们可以任意操纵 PHP 内存. 我们先释放一些内存, 然后再写 7 byte 数据填充, 当 php 重写我们的数据时, 再重复之前的操作. 第二次重写能够让我们写入任意长度的数据到 stack 中 (我测试的时候, 这个长度大概可以达到 2048 byte). 我们写入的数据和之前使用 ROP 的那个例子差不多. 我们还要继续 "加密" 我们写入 stack 中的数据. 这是攻击效果:

0x04 视频地址

video , 自备梯子

视频笔记

  • x86 Instruction Chart - http://sparksandflames.com/files/x86InstructionChart.html
  • Elf Header lowest 3 bits are 000
  • Elf layout - http://geezer.osdevbrasil.net/osd/exec/elf.txt
  • PMAP is your friend when trying to find the "Magic"
  • A look at PTR_MANGLE http://hmarco.org/bugs/CVE-2013-4788.html

0x05 译者总结

就和作者说的一样, 这篇文章没有之前两篇写得详细.

1. 作者那个 PHP binary 文件从哪来的?

原文下有评论, 作者说他通过 memory leak 获取了整个 php binary 文件.

正常情况下, 一般的套路就是:

  1. 查找 ELF magic header \x7fELF 找到起始地址
  2. 通过 strtab , symtab 找到 zend_eval_string , php_execute_script , executor_globals 地址.

2. jmpbuf 是什么?

jmpbuf 是 setjmp, longjmp 所使用的数据结构, 以实现 try--catch 机制的东西, 和 goto 语法效果差不多, setjmp 相当于在某个位置的 label, longjmp 相当于 goto, 但是 goto 语法并不能跨函数跳转. jmpbuf 主要保存着 caller 的寄存器信息以方便 longjmp 恢复. 另外 glibc 会混淆一些寄存器的值(除了有漏洞的 glibc ).

3. 如何解出 jmpbuf?

先查看 setjmp 代码:

#!bash(gdb) disassemble setjmpDump of assembler code for function setjmp:   0xb7c94410 <+0>: mov    eax,DWORD PTR [esp+0x4]   0xb7c94414 <+4>: mov    DWORD PTR [eax],ebx        # 1. 保存 ebx   0xb7c94416 <+6>: mov    DWORD PTR [eax+0x4],esi    # 2. 保存 esi   0xb7c94419 <+9>: mov    DWORD PTR [eax+0x8],edi    # 3. 保存 edi   0xb7c9441c <+12>:    lea    ecx,[esp+0x4]   0xb7c94420 <+16>:    xor    ecx,DWORD PTR gs:0x18   0xb7c94427 <+23>:    rol    ecx,0x9   0xb7c9442a <+26>:    mov    DWORD PTR [eax+0x10],ecx # 4. 保存 esp   0xb7c9442d <+29>:    mov    ecx,DWORD PTR [esp]   0xb7c94430 <+32>:    xor    ecx,DWORD PTR gs:0x18   0xb7c94437 <+39>:    rol    ecx,0x9                     0xb7c9443a <+42>:    mov    DWORD PTR [eax+0x14],ecx # 5. 保存 eip   0xb7c9443d <+45>:    mov    DWORD PTR [eax+0xc],ebp  # 6. 保存 ebp   0xb7c94440 <+48>:    push   0x1   0xb7c94442 <+50>:    push   DWORD PTR [esp+0x8]   0xb7c94446 <+54>:    call   0xb7c943c0 <__sigjmp_save>   0xb7c9444b <+59>:    pop    ecx   0xb7c9444c <+60>:    pop    edx   0xb7c9444d <+61>:    ret

上面的寄存器保存的都是 caller 的寄存器状态, 其中 esp, eip 都被混淆过了(作者自己的图也是 esp 和 eip 被混淆), 就是使用 PTR_MANGLE 进行混淆.

PTR_MANGLE 和 PTR_DEMANGLE 宏定义如下:

#!cpp#  define PTR_MANGLE(reg)   xorl %gs:POINTER_GUARD, reg;              \                 roll $9, reg#  define PTR_DEMANGLE(reg) rorl $9, reg;                     \                 xorl %gs:POINTER_GUARD, reg

其中 gs:0x18 就是上面的 POINTER_GUARD

setjmp() 使用 PTR_MANGLE 进行混淆寄存器, longjmp() 使用 PTR_DEMANGLE 解出正常的寄存器. 为了后续能过正常覆盖 jmpbuf, 所以我们需要获得 POINTER_GUARD 的值, 由于 jmpbuf 数据结构可以越界读, caller 的 eip 也可以拿到, 所以通过 PTR_DEMANGLE 就可以获得 POINTER_GUARD 的值.

4. 如何获取到 setjmp caller 的 eip ?

通过阅读代码, 我们可以知道 php_execute_script 调用了 setjmp, 并将 jmpbuf 保存到 EG(bailout) 中, 通过泄漏 php_execute_script 地址 即可知道调用 setjmp 时到 eip.

5. 如何覆盖到 jmpbuf ?

jmpbuf 地址向前搜索数值 XX 00 00 00 (XX>0x0c and XX

先看看 ZMM 的几个结构体:

#!cpp/* mm block type */typedef struct _zend_mm_block_info {    size_t _size;    size_t _prev;} zend_mm_block_info;

.

#!cpptypedef struct _zend_mm_free_block {    zend_mm_block_info info;    struct _zend_mm_free_block *prev_free_block;    struct _zend_mm_free_block *next_free_block;    struct _zend_mm_free_block **parent;    struct _zend_mm_free_block *child[2];} zend_mm_free_block;

.

#!cppstruct _zend_mm_heap {    int                 use_zend_alloc;    void               *(*_malloc)(size_t);    void                (*_free)(void*);    void               *(*_realloc)(void*, size_t);    size_t              free_bitmap;    size_t              large_free_bitmap;    size_t              block_size;    size_t              compact_size;    zend_mm_segment    *segments_list;    zend_mm_storage    *storage;    size_t              real_size;    size_t              real_peak;    size_t              limit;    size_t              size;    size_t              peak;    size_t              reserve_size;    void               *reserve;    int                 overflow;    int                 internal;#if ZEND_MM_CACHE    unsigned int        cached;    zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];#endif    zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];    zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];    zend_mm_free_block *rest_buckets[2];    int                 rest_count;};

我们需要关注的是 _zend_mm_heap 中的 cached. ZMM 会将 0x10 大小的内存块放进 cached 中, 所以当我们找到一个可以当做 memory block 之后, 最后几个字节(7 byte 数据)伪造一个 memory header (_zend_mm_block_info), 然后再用 string 重用这个伪造后的 memory block, 如果写入的长度不足以覆盖 jmpbuf, 继续伪造 memory header 相关的操作, 直到能够覆盖 jmpbuf 为止.

6. 能够覆盖 jmpbuf 之后 ?

将 eip 设置为 zend_eval_string , 将 esp 设置为一个直接可控的 stack(比如说 jmpbuf 之后), 填充好 jmpbuf, 该混淆的寄存器继续混淆. 然后在这个可控的 stack 上设置好 zend_eval_string 的参数, zend_eval_string 的定义如下:

#!cZEND_API int zend_eval_string(char *str, zval *retval_ptr, char *string_name TSRMLS_DC)

最后触发一个 exception, 即可执行我们想要的代码.

7. PHP7 的变化 ?

php7 的 zval 格式有很大的变化, 通过字符串数据覆盖 zval 结构没法再做到读取任意地址数据了, 只能向后读取数据(drops 这篇文章的作者 libnex 说他有办法, 期待新文章).

#!cstruct _zval_struct {    zend_value        value;            /* value */    union {        struct {            ZEND_ENDIAN_LOHI_4(                zend_uchar    type,         /* active type */                zend_uchar    type_flags,                zend_uchar    const_flags,                zend_uchar    reserved)     /* call info for EX(This) */        } v;        uint32_t type_info;    } u1;    union {        uint32_t     var_flags;        uint32_t     next;                 /* hash collision chain */        uint32_t     cache_slot;           /* literal cache slot */        uint32_t     lineno;               /* line number (for ast nodes) */        uint32_t     num_args;             /* arguments number for EX(This) */        uint32_t     fe_pos;               /* foreach position */        uint32_t     fe_iter_idx;          /* foreach iterator index */    } u2;};

.

#!cstruct _zend_string {    zend_refcounted_h gc;    zend_ulong        h;                /* hash value */    size_t            len;    char              val[1];};

如果通过数据去覆盖 zval_struct , 只能通过修改 len 来实现向后读取.

总结 exploit 利用步骤

  1. 利用 part 2 介绍的方法可以泄漏 std_object_handlers 信息, 随便找一个数值较小的地址
  2. 利用 part 2 介绍的任意地址读取的方法向前读取数据, 直到出现 \x7FELF .
  3. 通过 strtab, symtab 可以泄漏 zend_eval_string , php_execute_script , executor_globals (作者图省事, 文章直接本地 readelf)
  4. 通过 excutor_globals 可以拿到 bailout 地址 (也就是 jmpbuf 地址)
  5. 通过 php_execute_script 获取到调用 setjmp 时的 eip
  6. 获取到了 setjmp caller 的 eip, 再获取到 jmpbuf 地址中 eip 混淆后的值, 通过 PTR_DEMANGLE 即可获得 POINTER_GUARD 的值.
  7. 通过反复释放重用内存, 直到能过覆盖 jmpbuf
  8. 将 zend_eval_string 的地址与之前的 POINTER_GUARD 进行 PTR_MANGLE 写入到 jmpbuf 的 eip 中.
  9. 将 esp 设置为一个我们可写的 stack 范围, 比如说 jmpbuf 之后的内存, 进行 PTR_MANGLE 之后写入到 jmpbuf 的 esp 中.
  10. 在刚刚能覆盖 jmpbuf 的内存块后面依次写入 返回地址, php 代码地址, php 函数名, php 结果返回地址, php 文件名, php 代码.
  11. 触发一个 exception.

如果还有疑惑的地方, 可以去看看作者的视频以及树人的 paper. 如果我补充的有不正确的地方, 请不吝赐教.

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn