解讀Linux安全機(jī)制之棧溢出保護(hù)
一、概述
棧溢出保護(hù)是一種緩沖區(qū)溢出攻擊緩解手段,當(dāng)函數(shù)存在緩沖區(qū)溢出攻擊漏洞時,攻擊者可以覆蓋棧上的返回地址來讓shellcode能夠得到執(zhí)行。當(dāng)啟用棧保護(hù)后,函數(shù)開始執(zhí)行的時候會先往棧里插入cookie信息,當(dāng)函數(shù)真正返回的時候會驗(yàn)證cookie信息是否合法,如果不合法就停止程序運(yùn)行。攻擊者在覆蓋返回地址的時候往往也會將cookie信息給覆蓋掉,導(dǎo)致棧保護(hù)檢查失敗而阻止shellcode的執(zhí)行。在Linux中我們將cookie信息稱為canary(以下統(tǒng)一使用canary)。
gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all編譯參數(shù)以支持棧保護(hù)功能,4.9新增了-fstack-protector-strong編譯參數(shù)讓保護(hù)的范圍更廣。以下是-fstack-protector和-fstack-protector-strong的區(qū)別:
Linux系統(tǒng)中存在著三種類型的棧:
- 應(yīng)用程序棧:工作在Ring3,由應(yīng)用程序來維護(hù);
- 內(nèi)核進(jìn)程上下文棧:工作在Ring0,由內(nèi)核在創(chuàng)建線程的時候創(chuàng)建;
- 內(nèi)核中斷上下文棧:工作在Ring0,在內(nèi)核初始化的時候給每個CPU核心創(chuàng)建一個。
二、 應(yīng)用程序棧保護(hù)
1. 棧保護(hù)工作原理
下面是一個包含棧溢出的例子:
- /* test.c */
- #include <stdio.h>
- #include <string.h>
- int main(int argc, char **argv)
- {
- char buf[16];
- scanf("%s", buf);
- printf("%s\n", buf);
- return 0;
- }
我們先禁用棧保護(hù)功能看看執(zhí)行的結(jié)果
- [root@localhost stackp]# gcc -o test test.c -fno-stack-protector
- [root@localhost stackp]# python -c "print 'A'*24" | ./test
- AAAAAAAAAAAAAAAAAAAAAAAA
- Segmentation fault <- RIP腐敗,導(dǎo)致異常
當(dāng)返回地址被覆蓋后產(chǎn)生了一個段錯誤,因?yàn)楝F(xiàn)在的返回地址已經(jīng)無效了,所以現(xiàn)在執(zhí)行的是CPU的異常處理流程。我們打開棧保護(hù)后再看看結(jié)果:
- [root@localhost stackp]# gcc -o test test.c -fstack-protector
- [root@localhost stackp]# python -c "print 'A'*25" | ./test
- AAAAAAAAAAAAAAAAAAAAAAAAA
- *** stack smashing detected ***: ./test terminated
這時觸發(fā)的就不是段錯誤了,而是棧保護(hù)的處理流程,我們反匯編看看做了哪些事情:
- 0000000000400610 <main>:
- 400610: 55 push %rbp
- 400611: 48 89 e5 mov %rsp,%rbp
- 400614: 48 83 ec 30 sub $0x30,%rsp
- 400618: 89 7d dc mov %edi,-0x24(%rbp)
- 40061b: 48 89 75 d0 mov %rsi,-0x30(%rbp)
- 40061f: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax <- 插入canary值
- 400626: 00 00
- 400628: 48 89 45 f8 mov %rax,-0x8(%rbp)
- 40062c: 31 c0 xor %eax,%eax
- 40062e: 48 8d 45 e0 lea -0x20(%rbp),%rax
- 400632: 48 89 c6 mov %rax,%rsi
- 400635: bf 00 07 40 00 mov $0x400700,%edi
- 40063a: b8 00 00 00 00 mov $0x0,%eax
- 40063f: e8 cc fe ff ff callq 400510 <__isoc99_scanf@plt>
- 400644: 48 8d 45 e0 lea -0x20(%rbp),%rax
- 400648: 48 89 c7 mov %rax,%rdi
- 40064b: e8 80 fe ff ff callq 4004d0 <puts@plt>
- 400650: b8 00 00 00 00 mov $0x0,%eax
- 400655: 48 8b 55 f8 mov -0x8(%rbp),%rdx <- 檢查canary值
- 400659: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
- 400660: 00 00
- 400662: 74 05 je 400669 <main+0x59> # 0x400669
- 400664: e8 77 fe ff ff callq 4004e0 <__stack_chk_fail@plt>
- 400669: c9 leaveq
- 40066a: c3 retq
我們看到函數(shù)開頭(地址:0x40061f)處gcc編譯時在棧幀的返回地址和臨時變量之間插入了一個canary值,該值是從%fs:0x28里取的,棧幀的布局如下: 在函數(shù)即將返回時(地址:0x400655)檢查棧中的值是否和原來的相等,如果不相等就調(diào)用glibc的_stackchk_fail函數(shù),并終止進(jìn)程。 2. canary值的產(chǎn)生 這里以x64平臺為例,canary是從%fs:0x28偏移位置獲取的,%fs寄存器被glibc定義為存放tls信息的,我們需要查看glibc的源代碼: 結(jié)構(gòu)體tcbheadt就是用來描述tls的也就是%fs寄存器指向的位置,其中+0x28偏移位置的成員變量stackguard就是canary值。另外通過strace ./test看到在進(jìn)程加載的過程中會調(diào)用arch_prctl系統(tǒng)調(diào)用來設(shè)置%fs的值, 產(chǎn)生canary值的代碼在glibc的dlmain和_libcstart_main函數(shù)中: dlrandom是一個隨機(jī)數(shù),它由dlsysdepstart函數(shù)從內(nèi)核獲取的。dlsetupstackchkguard函數(shù)負(fù)責(zé)生成canary值,THREADSETSTACK_GUARD宏將canary設(shè)置到%fs:0x28位置。 在應(yīng)用程序棧保護(hù)中,進(jìn)程的%fs寄存器是由glibc來管理的,并不涉及到內(nèi)核提供的功能。 3. x32應(yīng)用程序棧保護(hù) 解讀完了x64的實(shí)現(xiàn),我們來看看x32下面的情況,我們還是使用上面例子的代碼在x32的機(jī)器上編譯,得到下面的代碼: