ホームページ  >  記事  >  バックエンド開発  >  PHP のメモリ破損の脆弱性 (CVE-2014-8142 および CVE-2015-0231) の悪用 (シリーズのパート 3)

PHP のメモリ破損の脆弱性 (CVE-2014-8142 および CVE-2015-0231) の悪用 (シリーズのパート 3)

WBOY
WBOYオリジナル
2016-06-20 12:26:531007ブラウズ

0x00 はしがき

  • 著者: Qsl1pknotp、Cital Company のセキュリティ コンサルタント
  • タイトル: PHP のメモリ破損バグの悪用 パート 3: リモート シェルのポップ
  • アドレス: http://www.inulledmyself.com/2015/05/exploiting-memory-corruption-bugs-in.html

この記事は思ったより時間がかかりました、長いです、しかし、せっかくの時間なので、この脆弱性を悪用する方法をビデオで説明したいと思います。そのため、この記事は前の 2 つほど詳細ではありません。

一部の人はがっかりするかもしれません。はい、この記事ではその方法を紹介するだけです。 POC を作成します (実際に POC をリリースするわけではありません)。記事の最後にあるビデオでは、自動化されたリモート活用ツールと、実際の環境でこの POC を正常に実行するために使用する手順を示します。

0x01 Find data

この脆弱性の悪用方法をより簡単に説明するために、次のコードをデモンストレーションとして使用し、特定の状況を詳細に分析します。

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

私たちの目標は、任意の PHP コードを実行できるようにすることです。もちろん、目標を達成するためにシェルコードを挿入することもできますが、この方法は創造的でもエレガントでもありません (パート 1 を覚えている場合は、より上位のバージョンでは成功しない可能性があります)。 、任意の PHP コードを実行できるようにするには、php_execute_script と zend_eval_string を呼び出す必要があります。ただし、リモート攻撃を実行できるようにしたいので、executor_globals と JMP_BUF を見つける必要があります。詳細は後ほど紹介します。

つまり、(順不同) 以下を見つける必要があります。

  • executor_globals
  • zend_eval_string
  • JMP_BUF
  • スタックに入る方法

幸いなことに、上記の要件の一部はバイナリに直接ダンプされるため、比較的簡単に見つけることができます。 PHP binay の strtab。

素晴らしいですね。そこから zend_eval_string のアドレスを直接見つけて、このアドレスが GDB で正しいかどうかを確認します。

zend_eval_string のアドレスを検索します
  1. gdb で対応するアドレスを表示します
  2. executor_global のアドレスを検索します
  3. gdb で対応するアドレスを表示します
  4. すごいですね! JMP_BUF を見つけるにはどうすればよいでしょうか? コードを読むと、_zend_executor_globals オブジェクトが見つかり、その中に bailout という名前の JMP_BUF ポインターが見つかりました。GDB を終了して、アドレスが正しいかどうかを確認してみましょう。
  5. zend_executor_globals オブジェクトを表示します

  1. print zend_executor_globals->bailout

  2. わかりました, このアドレスを取得しましたが、このアドレスは何に使われますか? さて、PHP では JMP_BUF は PHP の「try{} - catch{}」メカニズムを実装するために使用されます。これについては後で詳しく説明します。

    0x02 利用方法 1 - ROP

  3. 残ることは 1 つだけです。任意のデータをスタックに書き込む方法です。利用方法 2 では、Syscan 2010 で Stefan によって公開された方法について詳しく説明します。任意のメモリを解放できます。次に何をすべきでしょうか? 書き込まれたデータが将来上書きされないようにする方法は? RFC、より正確には: RFC 1867

この RFC は、multipart/form-data を使用した POST リクエストからのデータをスタックに書き込むことができ、(さまざまな理由により) PHP によって上書きされないことを指定します。 「通常の」ファイルをアップロードします。

素晴らしいですね。スタックに何を書き込むことができますか?

ヒント:以前に探していたもの : )

以前は非常に多くのアドレスを見つけるのに非常に多くの時間を費やしたので、今度はそれらを使用することにしました。では、いくつかの調査を経て、以前のアドレスをどのように使用すればよいでしょうか?次のように配置する必要があります。 eval_string へのポインタを 2 回入力します。

POP - ????

XCHG ESP; ; RET - ??

Zend_Eval_String - 0x082da150

Zend_Bailout - 0x00000000

Pointer_To_Eval_String - 0xbfffda04

Ret ポインタ000000 0
  • Pointer_To_Eval_String - 0xbfffda04
  • これでほぼ完成です。個人的には ROP ガジェットがまだ必要なようですが、他のツールでも機能します。 XCHG EAX、ESP (0x94 0xc3) を探す必要があり、POP EBP (0x5d 0x3c) も見つける必要があります。
  • 我们拿到了这两个地址了(为啥这些地址相差那么大, 因为它们是相对地址), 我们可以继续完成 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<0x8f), 搜索到一个这样的值之后, 可以把这个值当作一个 memory block.

    先看看 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. 如果我补充的有不正确的地方, 请不吝赐教.

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。