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 裝滿我們構造的資料。
繼續執行若干次後進入了死迴圈,觸發漏洞。
對跳出迴圈條件進行分析,除了其中一個跳出條件為異常處理外,另外兩處
- Line 398: scd->done = TRUE; 這個理論上來說應該是檔案讀取完成後,由GetDataBlock返回-1從而退出迴圈,但是本次漏洞根源即為count無法為負值,因此不可行
- 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。 即保證當檔案讀取完成後可以退出迴圈。