PWN——uaf漏洞
1.uaf漏洞原理
在C語言中,我們通過malloc族函數進行堆塊的分配,用free()函數進行堆塊的釋放。在釋放堆塊的過程中,如果沒有將釋放的堆塊置空,這時候,就有可能出現use after free的情況。這里我寫了一個demo
#include<stdio.h>
#include<stdlib.h> typedef struct demo { char *s; void(*func)(char *); }DEMO; void eval(char command[]) { system(command); } void echo(char content[]) { printf("%s",content); } int main() { DEMO*p1; DEMO*p2; p1=(DEMO*)malloc(sizeof(struct demo)); p1->s="I will tell you what's uaf."; printf("%s\n",p1->s); printf("p1 malloc address:%p\n",p1); p1->func=echo; p1->func("use after free!\n"); free(p1); p1->s="~Heihei~"; printf("%s\n",p1->s); p2=(DEMO*)malloc(sizeof(struct demo)); p2->func=eval; // p1->func=eval; printf("p2 malloc address:%p\n",p2); p2->func("whoami"); p1=NULL; printf("Now,I can't use address of p1."); p2->func("whoami"); return 0; }
運行的結果如下圖所示
可以看出,在free()堆塊之后,沒有將堆塊置空,堆塊處於懸空狀態,導致被free掉的堆塊依然可以被使用。同時,如果我們申請相同大小的堆塊的話,由於ptmalloc的堆管理機制,重新分配的堆塊的位置和我們釋放的堆塊地址是一樣的。但是,在將p1置空之后,再次使用堆塊的時候,就會報出段錯誤。
其實,一般的uaf漏洞利用,和我們上圖的demo也是類似的,都是定義一個結構體,結構體中有一個函數指針,然后free,將chunk添加進fastbin,再次分配相同大小的內存塊,這時候分配的就是剛才free掉懸空的堆塊,然后改寫函數指針,劫持數據流,配合不同的函數指針,可以實現任意地址讀,任意地址寫,以及命令執行。
注:這里第二次分配內存的時候,我們一般分配相同數據類型。比如上面的例子中,第一個釋放的堆塊p1的是DEMO類型的結構體,第二次分配的時候,我們們分配的p2也是一個DEMO類型的結構體,這種情況下,p2一定用的是之前p1的內存。但是在有些情況下,如果我們分配的數據類型不同,但是前后分配大小相同,也可能造成堆塊的再次利用。
2.例題
這道例題,是pwnable.kr上的uaf這道題目,源碼如下。
#include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std; class Human{ private: virtual void give_shell(){ system("/bin/sh"); } protected: int age; string name; public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; } }; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; }
之前沒看過C++,花了點時間把C++的類與面向對象的一些知識學習了一下。
· 先看主函數,主函數中實現了分配堆塊,釋放堆塊,以及從讀入數據的功能。然后再看定義的幾個類,Human是定義的一個基類,Man和Woman是定義的派生類,Human中public和protected定義的函數和變量可以在后面被子類繼承和訪問。但是定義的私有的give_shell虛函數,我們是無法在外部調用的,也無法通過子類來訪問。只能通過Human類中定義的函數來調用。
我們這里需要一些預備知識:C++的類中有序函數的時候,會生成一個虛表vtable,編譯器會生成一個vptr指針指向vtable(不管基類中有多少虛函數,只有一個_vptr指針指向vtable;多態繼承時,繼承了多個父類,相應就會又多個vtable)。對於共有成員變量來說,虛函數可以被子類同名函數重新調用,子類中有同名函數的時候,子類的vatble中指向基類虛函數的函數指針變為指向子類同名函數。對於私有成員來說,他的函數指針會保留在子類的vtable中(但是子類不是繼承私有成員)。所以對於上面的源碼來講,Man類和Woman類的vtable中,introduce函數指針指不同,但是give_shell函數指針是相同的。我們可以再深入思考一下:vtable保留在哪里?通過IDA我們可以看出,vtable是保留在rodata段的。vtable保留着類的虛函數指針,它應該在編譯前就完成,相應的它肯定不在代碼段,bss段和data段分別保留的是未初始化和初始化后的局部變量,全局變量以及靜態變量,所以vtable也不會在bss段或者data段。這樣看來,vtable和字符串常量一樣,保留在全局數據段是合理的。
這幾張圖片是從sakura師傅的博客取下來的,這里貼出來,大家可以看一下
對於這道題而言,類比C程序中利用的步驟,我們應改首先創建堆塊,然后通過case3釋放堆塊,但使堆指針懸空,最后通過case2,讀入文件內容,覆寫類的函數指針,劫持數據流。
打開IDA,main函數的偽代碼中可以得知,Man和Woman對象分配的堆的大小為0x18個字節。
我們首先通過調試,找到相應的虛表函數,要找到虛表函數,就要先找到Man的構造函數。
找到Man的構造函數之后進入,會發現調用Human的構造函數(因為Man作為子類要繼承父類),所以這時候繼續單步,可以在寄存器窗口看見調用give_shell函數的地址
找vtable的話,進入IDA,到rodata段。
Man類vtable表,give_shell的函數指針的地址為0x401570。現在要具體關注一下堆塊內部的內存布局,以便在后面有效地覆蓋函數指針。
0x40117a是give_shell函數地址,0x4012d2是introduce函數地址。
按照C++實例化類的內存分配的規則,類中有虛函數的時候,分配的對象的內存數據中,前8個字節是_vptr指針,這個指針指向vtable表,以后是成員變量的內存數據(這里應該只是成員變量的內存數據,不包括成員函數,所有函數都是定義在text段的)。
vptr指向的地址,是vtable表的地址,vtable表上存儲的是虛函數的地址。
case2的時候填充的數據就應該是把前8個字節修改
case1調用intreduce函數的代碼如下
v13和v14應該分別是Man對象和Woman對象的vptr指針,前面看到過,give_shell函數地址在0x401570地址處,我們向文件中寫入0x401570-8的地址。
這里要注意的是,再次分配地址的時候,由於最后delete的是Woman對象的地址,第一次寫入的是之前Woman的地址,第二次才是寫入到Man對象地址的vtable表上。
終於是寫完了這篇博客。。。
C++和堆的很多知識我絕對理解的不是很到位,是要不斷學習的,但是這道題是看着博客,跌跌撞撞做出來了。寫出來是為了加深印象,如果哪里有說的不對的地方,還希望路過的師傅們能過及時指出來,謝過師傅們了。