格式化字串漏洞沉浸式理解

uuuwind發表於2024-10-27

格式化字串漏洞總結

利用的是2024 shctf 中的
fmt_fmt
開啟pie

放到ida中看看反彙編
mian函式


無條件迴圈,根據輸入的值不同呼叫不同的函式
show_flag函式

這個函式會將dest中的內容列印出來,這裡就有格式化字串漏洞,如果能夠修改ptr指標的話就能控制dest的內容,那這樣就可以利用格式化字串漏洞
talk函式

這個函式會根據讀入引數的不同,選擇不同的緩衝區,一開始以為會有棧溢位漏洞,但是發現緩衝區觸發不了這個漏洞,然後會向這個緩衝區讀入資料,上面提到的控制ptr指標,但是這些緩衝區裡並沒有ptr的位置,哎?那怎麼辦呢?這個時候你就進入了出題人的節奏,仔細觀察就看到只有3,1,2這三個選項,那麼如果輸入別的引數呢?可以進入gdb看看
這是正常情況下,選擇了1可以看到我向這個緩衝區存入的資料

隨便輸入一個7,再去看看棧空間,這個時候我們就能很清楚的現在存入的是ptr指標的地方,太好了那麼現在就可以控制ptr指標,那就可以控制ptr,利用格式化字串漏洞

然後又找到了shell後門
那現在就可以整理一下大體的思路,透過talk函式控制ptr指標,然後透過show_flag利用格式化字串將返回地址寫入sh
那下面就要進入重點了!
要重點講一下格式化字串漏洞
一般來說出格式化字串,就是因為程式設計師的疏忽,在使用printf函式時偷懶,就像上面的這個程式沒有加格式化字串
什麼是格式化字串
在使用到printf這類函式時,printf的第一個引數就是格式化字串,利用佔位符,指定格式輸入,在一個程式執行過程中,執行到某個位置輸出,可以用佔位符代替,在輸出時會按照我們想要的格式輸出

如圖,第四個%s沒有對應引數,會輸出嗎?

檢視棧之後就明白這是把第四個引數的輸出


常用的佔位符,在pwn中主要用的%p來洩露地址
通常透過數字+佔位符的格式來洩露

將上面的程式碼改一下,會是怎麼呢,這時候會先輸出cccc bbbb aaaa
那這樣就可以造成任意地址洩露
利用這個題看一下這個功能的實現

這是洩露地址的指令碼
在除錯中可以看到,已經寫入了payload,那看一下列印的情況



這裡洩露的是後面要用的地址,那你怎麼知道是29和21呢?
這裡有一個理解就是暫存器傳參(64位)優先向rdi rsi rdx rcx r8 r9 中傳入資料,然後在向棧上傳入資料,也就是說棧上的第一個就是6,看下面的這個圖更容易理解點

至於32位,就是在棧中傳參,棧上第一個就是1

我們可以驗證一下,看看讓他返回一下9的內容


這樣現在就能理解這個偏移的計算,就可以洩露我們想要的地址,也就是上面的21和29處,
一個是棧地址,一個是mian函式地址
因為這個題開了pie保護所以偏移是不變的,我們知道了mian的地址,就可以計算出shell後門


在接受棧地址時我+0x8,為什麼?

看一下下面的除錯就知道,我們要修改的是rdp的下一個地址(ret),+0x8就是

我們現在已經講棧地址和mian地址洩露了,那麼下面就要想辦法把ret改成shell地址,那就要引出格式化字串的另一種高階用法,任意地址寫入
這裡重點理解一下%n的作用

#include <stdio.h>

void main()
{
    int s = 0;
    printf("The value of s is %n",&s);
        printf("%d\n",s);
}
// The value of s is 18

在這個程式碼中,%n前面有18個字元,包括空格,就把18透過%n賦值給s,下面有利用printf列印出來``
也就是說%n可以用於賦值
%n,不輸出字元,但是會將成功輸出的字元個數寫入對應的整型指標引數所指向的變數
%Nc:中N最大值為0xffff即為記憶體中的兩個位元組,
%n是賦值到棧上的地址

當我們就可以知道某些棧地址的位置
那麼現在我們可以將%n和$c結合使用,實現對特定地址的賦值
%overoffset c%overaddr$n
Overoffset :要覆蓋的值
overaddr:要覆蓋的地址(在這裡是棧上的第幾個)
一般這種手法都用$hn,修改低位兩位元組的內容(後四位) $n就是修改4位元組
例如 :%777c%n$n;就是將第n個棧上的內容給成777,結合這裡理解一下賦值地址

那就放到題看看,先看看在棧中讀入資料的偏移,是6(看61被讀入的位置)

也就是說我們從引數6的位置就可以修改了,但是前幾個不能修改,為什麼?結合上面修改777的例子

我們可以修改21
成功在後面寫入了7

但是我們想要修改的就是ret地址,就要先把20位置的“地址改掉”,還有一個思路,可以在前面那些沒有地址的地方加上我們要修改的地址
這裡我採用第2種思路
想了很久感覺還是放上指令碼好講一些

透過指令碼分析,sh_part[2]是什麼?
因為我們要修改stack指向的地方,在上面指令碼接受棧資料就能看出來,一開始的的ret指向和sh的地址,只是相差了後3個位,但是我們也沒辦法修改後3個位,只能是去修改後4位(低2位元組)

因此,講sh分成了3段 2位元組(4位)一段,看下圖就很明白了

def address_cut_to_3(int_address):
    hex_address = hex(int_address)[2:]
    part1 = '0x' + hex_address[:4]
    part2 = '0x' + hex_address[4:8]
    part3 = '0x' + hex_address[8:]
    return [int(part1,16), int(part2,16), int(part3,16)]

就是利用的上的定義的函式,也能瞭解是0x之後分的,用了列表的格式返回因此從左往右是0、1、2
那麼要就修改原本ret的後4位那就要選用2
再回到指令碼,那麼為什麼下面有三個p64(stack)?
首先這個p64(stack)是向這個棧上寫入要修改的地址,寫一個不就行了?,因為這個read函式讀入位元組是0x30

我在寫入語句的時候,前面的修改語句我補充到了0x18個位元組,那剩下0x18個位元組,而棧第11個就在後面0x18位元組中,而我又不想去確認具體位置,就想前面確認61一樣,那就再後面0x18個位元組中全寫入stack就行了,就有了保障,這就是輸入payload之後的棧結構,就很清晰了

那這裡就有一個擴充,如果要修改後4位元組呢?,就是將下圖畫箭頭的兩個地方修改了

那這個就需要分兩次寫入
先將stack+2

這樣我們再去修改低2位元組就可以了,看一下除錯
上面是沒修改之前,下面是下修改後


然後看一下18這個時候是什麼樣的?
可以看到中間的四位就被篡改成6258了

以此類推,我們就能修改整個地址
然後再總結一下
上面我們之所以能夠修改ret就是利用棧上的某個空間指向ret的內容

類似這樣,不同的是,我們沒有直接在棧上利用這個結構,而是自己構造了這個結構,透過對棧中11處的修改控制了ret

就醬,拜~

相關文章