理解對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和放在哪塊內存區域了。

修復方法

補丁libsysutils: Fix potential overwrites in FrameworkListener
給出了一個修復方法,增加了數組越界檢查。

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

參考

zergRush (CVE-2011-3874) 提權漏洞分析
從zergRush深入理解Use After Free
《Android安全攻防權威指南》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章