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

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

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](http://www.inulledmyself.com/2015/05/exploiting-memory-corruption-bugs-in.html)

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

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

0x01 查找数据
=========

* * *

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

“`

“`

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

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

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

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

![DumpElfData.png](http://drops.javaweb.org/uploads/images/6b2710ba95bb50f43f8d269cd817331c597cf3d4.jpg)

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

1. 查找`zend_eval_string`的地址

![GetZendEvalAddress.png](http://drops.javaweb.org/uploads/images/750f0abefea3376c03e58844ee97b34de541911d.jpg)

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

![VerifyingZendEvalString.png](http://drops.javaweb.org/uploads/images/8c0eab8585a52943e01e153ba67e2d14580d898d.jpg)

3. 查找`executor_global`的地址

![GetExecGlobalAddress.png](http://drops.javaweb.org/uploads/images/3c267bc5d25595a9b89dd2424624a6404661d8e9.jpg)

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

![PrintGlobalExec.png](http://drops.javaweb.org/uploads/images/5e3fcf713da609bb002af529dc5eefef0d06e25d.jpg)

Awesome! 现在我们要怎么找到`JMP_BUF`呢? 通过阅读代码, 我们找到了[`_zend_executor_globals`](https://github.com/php/php-src/blob/9a20323e1946dff57eae8cd054e0893aefe83092/Zend/zend_globals.h#L151)对象, 并在其中发现了一个`JMP_BUF`指针, 名为[`bailout`](https://github.com/php/php-src/blob/9a20323e1946dff57eae8cd054e0893aefe83092/Zend/zend_globals.h#L164). 让我们挂上 GDB 看看地址是否正确.

1. 查看`zend_executor_globals`对象

![VerifyingStruct+DataLeakage.png](http://drops.javaweb.org/uploads/images/344cae6f69713834c7c04cb5d137676402d3b658.jpg)

2. 打印`zend_executor_globals->bailout`

![FindingJMPBUF.png](http://drops.javaweb.org/uploads/images/7226f291a1760b5e19a330cee138cc18a84661a0.jpg)

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

0x02 利用方法 1 – ROP
=================

* * *

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

RFC, 更确切的说是:[RFC 1867](https://www.ietf.org/rfc/rfc1867.txt)

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

![OverwritingStack.png](http://drops.javaweb.org/uploads/images/29e3f9f4c4cbbfd57ec6f31cab9bb940f9d68f6f.jpg)

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

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

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

![stack.png](http://drops.javaweb.org/uploads/images/fae1791c0eac58f659463fdad49c5c3b50a91c32.jpg)

从最简单的开始: 一开始我们就用 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, 我们就可以继续下一步了.

![Xchg.png](http://drops.javaweb.org/uploads/images/45bdd4000dece6a62b060c19ae8633c896fc0963.jpg)

![EBP.png](http://drops.javaweb.org/uploads/images/ae48501fc5ee377722d6b664b5a57bd998e76e42.jpg)

我们拿到了这两个地址了(为啥这些地址相差那么大, 因为它们是相对地址), 我们可以继续完成 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

好了, 该是时候测试了.

![Segfault.png](http://drops.javaweb.org/uploads/images/0e7104620d6e43930b334a24c32516e5a07ea735.jpg)

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

![phpInfoImage.png](http://drops.javaweb.org/uploads/images/f24f54b99f21556a9368d33a19da6d22bda376c7.jpg)

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`](http://www.scs.stanford.edu/histar/src/pkg/uclibc/libc/sysdeps/linux/cris/bits/setjmp.h)样例布局. 让我们看一下 PHP 中的内容…

![phpJMPBUF.png](http://drops.javaweb.org/uploads/images/408034763ab64939ccaed05e6e10ed84710147ca.jpg)

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

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

![phpinfo.png](http://drops.javaweb.org/uploads/images/aba2c3e3f9aa110ee1137461191deb817c33d206.jpg)

0x04 视频地址
=========

* * *

(https://youtu.be/EidBm7-zgSM), 自备梯子

### 视频笔记

* x86 Instruction Chart -[http://sparksandflames.com/files/x86InstructionChart.html](http://sparksandflames.com/files/x86InstructionChart.html)
* Elf Header lowest 3 bits are 000
* Elf layout -[http://geezer.osdevbrasil.net/osd/exec/elf.txt](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](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](http://hmarco.org/bugs/CVE-2013-4788.html)).

### 3. 如何解出 jmpbuf?

先查看 setjmp 代码:

“`
(gdb) disassemble setjmp
Dump 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`宏定义如下:

“`
# 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<0x8f), 搜索到一个这样的值之后, 可以把这个值当作一个 memory block. 先看看 ZMM 的几个结构体: ``` /* mm block type */ typedef struct _zend_mm_block_info { size_t _size; size_t _prev; } zend_mm_block_info; ``` . ``` typedef 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; ``` . ``` struct _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`的定义如下: ``` ZEND_API int zend_eval_string(char *str, zval *retval_ptr, char *string_name TSRMLS_DC) ``` 最后触发一个 exception, 即可执行我们想要的代码. ### 7. PHP7 的变化 ? php7 的 zval 格式有很大的变化, 通过字符串数据覆盖 zval 结构没法再做到读取任意地址数据了, 只能向后读取数据([drops](http://drops.wooyun.org/papers/16036)这篇文章的作者 libnex 说他有办法, 期待新文章). ``` struct _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; }; ``` . ``` struct _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. 如果我补充的有不正确的地方, 请不吝赐教.

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容