1. 程式人生 > 其它 >GDB除錯CVE-2018-5711 PHP-GD拒絕服務漏洞

GDB除錯CVE-2018-5711 PHP-GD拒絕服務漏洞

下載、編譯PHP原始碼

從github的PHP-src克隆下含有漏洞的版本,最好採取7.0以上版本,編譯時候會比較簡單,本次選用PHP7.1.9。編譯環境為 阿里雲 Ubuntu 16.04 LTS

git clone --branch PHP-7.1.9 https://github.com/php/php-src
Cloning into 'php-src'...
remote: Counting objects: 725575, done.
remote: Compressing objects: 100% (34/34), done.
remote: Total 725575 (delta 11), reused 12 (delta 3), pack-reused 725538
Receiving objects: 100% (725575/725575), 301.72 MiB | 11.96 MiB/s, done.
Resolving deltas: 100% (562883/562883), done.
Checking connectivity... done.

由於下載的原始碼是沒有configure檔案的,首先要編譯buildconf檔案

./buildconf --force

可以使用-h選項看到幫助

./configure -h

為了方便快速編譯,我編寫了一個指令碼。 因為我們要來除錯gd,所以要加上 with-gd

#!/bin/sh
make distclean
./buildconf --force
./configure 
        --enable-maintainer-zts 
        --enable-debug 
        --enable-cli 
        --with-gd 
        "$@"

配置好後一般會有錯誤提示,如果無錯誤提示可以使用 make -j2 來編譯原始碼。由於我採用的是阿里雲單核主機,所以使用j2引數。檢測安裝情況

sapi/cli/php -v
PHP 7.1.9 (cli) (built: Feb  2 2018 11:28:48) ( ZTS DEBUG )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies

可以看到目前PHP和gd庫已經正常安裝。

安裝GDBGUI

為了方便我們的除錯,我們安裝一個很方便的GDBGUI,具體的網址可以GOOGLE一下。 快速安裝的命令為

pip install gdbgui

以後我們可以執行gdbgui來進行遠端除錯。記得加上—auth選項,-r選項開啟外網訪問。

screen gdbgui -r --auth  
View gdbgui at http://0.0.0.0:5000
exit gdbgui by pressing CTRL+C

開始除錯

執行後PHP後可以發現觸發漏洞

然後我們來部署一下POC環境

/usr/src/php-src/sapi/cli/php

在命令列輸入引數 開始執行

 run /root/poc.php

根據之前的問題分析,我們定位到問題出現在原始碼/usr/src/php-src/ext/gd/libgd/gd_ gif_in.c 中我們在左側輸入地址,並在 gdImageCreateFromGifCtx 函式放置斷點

執行到斷點處

Breakpoint 3, php_gd_gdImageCreateFromGifCtx (fd=0x7ffff4077000) at /usr/src/php-src/ext/gd/libgd/gd_gif_in.c:135

觀察一下上下文目前並沒什麼特別。一路執行到 第214行 ,前面全部是GIF頭和color table的解析,如果檔案結構不合理,會返回並提示 invalid GIF file.

可以看到目前處理字元是 逗號 (0x2c),如果我們檢視 poc.gif 的話。已經處理到了20位置,

繼續向後讀取9個 bytes之後到達 29h 位置。後面的03 FF為觸發漏洞的第一處關鍵。

繼續向下執行,進入到ReadImage方法,到程式碼 568行,讀取了一個byte (03) 去與 MAX_LWZ_BITS 做比較,只有小約等於MAX_LWZ_BITS的時候才會繼續進行。MAX_LWZ_BITS定義為12.

一路步入LWZReadByte_方法, 第一次呼叫是在做一些初始化操作。

隨後進入while loop中,flag為0且sd->fresh為true, 進入第458行。 (如果為了方便可以直接在這裡下斷點。 隨後進入真正觸發漏洞的do while loop)

此個do while loop的終止條件為sd->firstcode != sd->clear_code ,

其中 sd->clear_code = 8 , sd->codesize = 4 , *ZeroDataBlockP = 0 。

first code為GetCode的返回值。當first_code為8的時候,此處迴圈就會繼續進行

步入GetCode 首先是一系列判斷資料長度的對迴圈終止條件的判斷。

此處發現,如果 scd->done 為true時候會返回-1 ,同時結束外層do while loop。

接下來走到了第398行,此處為本次漏洞的第一現場,由於count為unsigned char,而GetDataBlock 讀取檔案完畢後會返回-1。 而count不會被至於-1。

截止至此,其他的大多文章都分析到這裡。我對GIF的構造和具體的問題成因還是很感興趣,所以我們在深入一層,結合GIF的構造,探索造成問題的根源。水平有限,如果有問題請盡情指正

步入GetDataBlock_ 方法。 繼續步入ReadOK, ctx->getBuf 通過一個動態指標呼叫到 fileGetbuf 方法。

fileGetbuf 中會 fread 方法讀取檔案內內容。

size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;

對引數進行分析。 buf為儲存資料的目標, size為1, count為1,最後一個為資料來源。

即是此處從stream中讀取1個1byte資料到buffer中。 並返回成功讀取的大小。

此處我們可以看到 fctx->f 即將被讀取的為 ff 。

執行後 返回數值為1. buf中第一個byte為 ff. 回到 GetDataBlock領空之後的stack狀態為 count = 255 (ff)

進入下一個if條件,再次進入 fileGetbuf 其中 size為 255. 讀取的內容即為 ff 後面的 垃圾資料

載入位置為 之前 ff所在位置(0x7ffffffe9877)+255 = 0x7fffffff9b62

再次回到GetDataBlock領空 此時count為 255(0xff), buf中儲存的是隨後的GIF中的資料(0x88)。

一路讀取資料 直到 scd->buf 裝滿我們構造的資料。

繼續執行若干次後進入了死迴圈,觸發漏洞。

對跳出迴圈條件進行分析,除了其中一個跳出條件為異常處理外,另外兩處

  1. Line 398: scd->done = TRUE; 這個理論上來說應該是檔案讀取完成後,由GetDataBlock返回-1從而退出迴圈,但是本次漏洞根源即為count無法為負值,因此不可行
  2. Line 411: ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j; 此處提取了buf中的資料的一個byte中的第一位。 如在我們poc.gif中均為0x88, 每次ret運算結果即為8。而sd->clear_code即為8 從而導致無法退出迴圈。

我們也可以嘗試,如果我們使用77替換POC.gif中的88後,並無法觸發漏洞。因為ret處將取值為7,從而滿足 sd->first_code != sd->clear_code。 因此精心構造的poc可以在利用條件1的基礎上,構造滿足條件2的情況即可觸發漏洞。

修補方法

可以看到,官網對本漏洞的修補即為將count的型別變更為int。 即保證當檔案讀取完成後可以退出迴圈。