1. 程式人生 > >理解對C++裸指標釋放後重用的問題

理解對C++裸指標釋放後重用的問題

本文將以Android 2.2-2.3上的一個zergRush漏洞為例,分析指標釋放後重用的問題。

zergRush是Android 2.2-2.3上的一個漏洞,主要問題就在於指標的釋放後重用。
zergRush利用了libsysutils庫提供的Framework套接字的通用介面。
程式從套接字收到的訊息中出抽取出的文字命令會導致棧緩衝區溢位,進而造成釋放後重用問題。
具體地,是vold後臺程式呼叫了libsysutils.so,bug出在FrameworkListener.cpp的dispatchCommand方法。

什麼是釋放後重用

釋放後重用(Use After Free)問題是指,程式使用指標訪問了一個已經通過free函式或者delete操作符釋放過的物件,並且這個指標沒有置空,攻擊者在這塊釋放後的記憶體中寫入了惡意的資料shellcode,當程式第2次使用這個指標的時候,控制流就轉向了攻擊者構造的惡意資料中了。

FrameworkListener中的bug

FrameworkListener.cpp中有bug的關鍵程式碼如下:

//引數cli為與使用者程序連線的socket連結;引數data為使用者程序的命令引數
void FrameworkListener::dispatchCommand(SocketClient *cli, char *data){
    FrameworkCommandCollection::iterator i;
    int argc = 0;
    //在棧上臨時分配的區域性緩衝區,用來存放從socket中解析命令引數指標
    char *argv[16];
    //棧上分配的緩衝區,存放從socket中解析命令引數資料
char tmp[255]; char *p = data; //p指向使用者資料 char *q = tmp; //q指向tmp陣列 //... //下面的迴圈遍歷輸入中的所有字元,直到遇到一個結尾\0 while(*p) { //... //將使用者輸入複製到緩衝區,引數放入tmp陣列,但是沒有檢查邊界 *q = *p++; //如果引用的字串外面還有一個空格,則將q重置到tmp的起始位置 if (!quote && *q == ' ') { *q = '\0'
; //strdup會在堆上分配空間,返回這塊堆記憶體的指標 argv[argc++] = strdup(tmp); memset(tmp, 0, sizeof(tmp)); q = tmp; continue; } q++; } argv[argc++] = strdup(tmp); for (i = mCommands->begin(); i != mCommands->end(); ++i) { FrameworkCommand *c = *i; if (!strcmp(argv[0], c->getCommand())) { //呼叫FrameworkCommand的虛擬函式 if (c->runCommand(cli, argc, argv)) { } } } int j; for (j = 0; j < argc; j++){ //因為是strdup動態分配出來的,所以需要主動釋放 free(argv[j]); } return; }

下圖是第1次呼叫dispatchCommand的記憶體佈局:

假設其中一個FrameworkCommand物件所在的記憶體地址是0x12345678,這個地址值,使用者程序可以在引數中以字串的形式提供,即\x78\x56\x34\x12,這裡要考慮到位元組序,記憶體低地址將存放小端的位元組。

假設引數data的資料為“cmd p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 p16 \x78\x56\x34\x12”
前15個引數的處理過程中,argv陣列中的元素都是正常的從strdup返回的指向堆的指標值,即指向引數字串的指標。
當p指標指向p16這個引數值,argv[16]=strdup(“p16”),這時argv[16]已經超出了argv陣列的範圍,此時&argv[16]=&tmp[0],這個引數值將覆蓋tmp陣列的頭4位元組。之後tmp清空,q指標重新指向tmp陣列的開頭,繼續讀入最後一個引數。

繼續呼叫*q = *p++,此時tmp開頭4位元組即為\x78\x56\x34\x12,同時也是argv[16]元素的值,注意到這個值有別於argv陣列中其它的元素的值,其它元素的值都是strdup動態分配返回的堆指標,而argv[16]是攻擊者惡意構造的地址值。

此時argv[16]的頭4位元組,也就是tmp頭4位元組的資料是0x78,0x56,0x34,0x12,
free(argv[16])呼叫的是free(0x12345678),即釋放掉了FrameworkCommand所在記憶體,即這塊記憶體被記憶體分配器新增到類似freelist這樣的資料結構中,供下一次動態分配使用。

這裡需要說明下strdup這個函式。char* strdup(const char *s1)函式會為s1指標指向的字串資料分配等大小的記憶體,並返回指向這塊記憶體的指標。因為是動態分配的,這塊記憶體在堆上,實際使用Android系統中Bionic lib庫內建的dlmalloc分配器來動態分配的。dlmalloc分配器在某些情況下記憶體被free後不會馬上釋放回核心,而是保留給應用程式重新申請。

下圖是第2次呼叫dispatchCommand的記憶體佈局:

當用戶程序第2次呼叫dispatchCommand,走到argv[0] = strdup(tmp)處時,strdup分配的記憶體就是上次釋放掉的FrameworkCommand所在記憶體,並把tmp的位元組資料拷貝到這塊記憶體中。這時可以構造惡意資料覆蓋vtable指標,讓它指向shellcode的記憶體地址,這樣當函式主動呼叫runCommand時,控制流就會跑到shellcode中了。比如第二次傳給dispatchCommand的命令是”AAAA param”,vtable指標會被覆蓋成0x41414141,EIP將被指向 [0x41414141+runCommand虛擬函式在虛表中的偏移]。剩下的問題就是如何巧妙的構造shellcode和放在哪塊記憶體區域了。

修復方法

+            if (argc >= CMD_ARGS_MAX)
+                goto overflow;

參考