<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/binary/16366

            0x00 前言


            這片文章所花的時間比我想象中的要長, 不過這時間花得值! 我想通過視頻的方式來講解如何利用這漏洞, 所以這片文章沒有之前兩篇描述得詳細.

            可能會讓一些人失望的是, 這篇文章只是介紹了如何寫一個 POC (并沒有實際放出 POC). 文章末尾的視頻會給你們展示我的自動化遠程利用工具, 以及在現實環境中為了能讓這 POC 成功執行所使用的 tips & trick.

            0x01 查找數據


            為了能更簡單的解釋如何利用這個漏洞, 我使用下面的代碼作為演示, 具體情況具體分析.

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

            我們的目的是能夠執行任意 PHP 代碼. 當然啦, 我們可以嘗試去注入 shellcode 來達到目的, 但是這種方法既沒有創造性, 也不優雅(高版本的 PHP 可能不會成功). 如果你還記得 Part1, 為了能夠執行任意 PHP 代碼, 我們需要調用 php_execute_scriptzend_eval_string. 然而, 我們希望能夠進行遠程攻擊, 所以我們必須找到 executor_globals 以及 JMP_BUF. 至于為什么要這樣做, 后面會詳細介紹.

            簡而言之, 我們需要找到 (沒有特別的順序):

            比較幸運的是, 上面所列舉的要求, 有些還是比較好找的, 因為它們就在 binary 中. 我們直接 dump PHP binay 的 strtab.

            DumpElfData.png

            Great! 我們直接從里面找到 zend_eval_string 的地址, 然后在 GDB 中驗證這個地址是否正確.

            1. 查找 zend_eval_string 的地址

              GetZendEvalAddress.png

            2. 在 gdb 中查看對應的地址

              VerifyingZendEvalString.png

            3. 查找 executor_global 的地址

              GetExecGlobalAddress.png

            4. 在 gdb 中查看對應的地址

              PrintGlobalExec.png

            Awesome! 現在我們要怎么找到 JMP_BUF 呢? 通過閱讀代碼, 我們找到了 _zend_executor_globals 對象, 并在其中發現了一個 JMP_BUF 指針, 名為 bailout. 讓我們掛上 GDB 看看地址是否正確.

            1. 查看 zend_executor_globals 對象

              VerifyingStruct+DataLeakage.png

            2. 打印 zend_executor_globals->bailout

              FindingJMPBUF.png

            好, 我們拿到了這個地址, 但是這個地址指向的是什么? 有什么用? 好吧, 在 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 覆蓋(由于各種各樣的原因). 讓我們上傳一個"普通"的文件吧.

            OverwritingStack.png

            Awesome! 我們可以寫入任意數據到 stack 中. 但是我們要寫什么到 stack 中?

            Hint: 我們之前所尋找的東西 : )

            既然之前我們花了那么多時間去找那么多地址, 該時候使用它們了! 所以, 我們應該如何使用之前的地址呢? 經過一些研究, 我們需要這樣布局:

            stack.png

            從最簡單的開始: 一開始我們就用 readelf 獲取到了 zend_eval_string 的地址. Ret PointerZend_Bailout 我們都不需要理會(會導致 PHP crash). 我們在 stack 中填寫兩次指向 eval_string 的指針, 下面就是我們現有的數據:

            Sweet! 我們快填寫完這些東西了, excellent! 但是, 看起來我們還需要一些 ROP gadgets. 我個人比較喜歡用 ROPGadget, 不過其它工具也是可以的. 我們需要查找 XCHG EAX, ESP; RET (0x94 0xc3), 還需要找 POP EBP; RET (0x5d 0x3c). 一旦找到了這些 gadgets, 我們就可以繼續下一步了.

            Xchg.png

            EBP.png

            我們拿到了這兩個地址了(為啥這些地址相差那么大, 因為它們是相對地址), 我們可以繼續完成 stack 中的數據:

            好了, 該是時候測試了.

            Segfault.png

            Hmmm, 這不是我想要的結果, 現在怎么辦? 看起來好像我們的代碼嘗試跳到我們的 gadget (c394). 不幸的是, 你還需要知道一些事情. SPLObjectStorage 要求這些 gadget 在 php 是可以訪問的, 所以我們還需要修改一下. 經過修改之后:

            phpInfoImage.png

            0x03 利用方法 2 - Stefan


            方法 1 就到此為止了, 方法 1 只能影響老版本的 PHP. 我們繼續研究新版本的 PHP 利用方法.

            比較走運的是, 之前找到 php_execute_scriptjmp_buf 地址, 在新 exploit 中都會被用到.

            jmp_buf 在 setjmp & longjmp 中被用來保存 "環境" 以預防 "不可恢復" 的錯誤. 在 32 位系統中, jmp_buf 是一個存儲 6 個 int 的數組, 在 64 位系統中, jmp_buf 存儲的是 8 個 int 的數組. 不幸的是, 需要自己查看代碼來判斷 jmp_buf 保存的寄存器的順序. 這里有個 jmp_buf 樣例布局. 讓我們看一下 PHP 中的內容...

            phpJMPBUF.png

            在我的機器上, 寄存器的順序是: 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 中的數據. 這是攻擊效果:

            phpinfo.png

            0x04 視頻地址


            video, 自備梯子

            視頻筆記

            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 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_MANGLEPTR_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;
            

            .

            #!cpp
            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;
            

            .

            #!cpp
            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 的定義如下:

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

            最后觸發一個 exception, 即可執行我們想要的代碼.

            7. PHP7 的變化 ?

            php7 的 zval 格式有很大的變化, 通過字符串數據覆蓋 zval 結構沒法再做到讀取任意地址數據了, 只能向后讀取數據(drops 這篇文章的作者 libnex 說他有辦法, 期待新文章).

            #!c
            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;
            };
            

            .

            #!c
            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. 如果我補充的有不正確的地方, 請不吝賜教.

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线