蒸米32位ROP笔记 1
本文最后更新于:3 年前
简介
ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以 用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。
Linux常见保护技术
- Canary
- Fortify
- NX/DEP
- PIE/ASLR
- RELRO
0x01 、x86 ROP
level1——栈上执行shellcode
level1.c:
1 |
|
使用如下指令编译,关闭所有编译保护:
1 |
|
- -m32参数指定编译为32位程序;
- -fno-stack-protector参数指定不开启堆栈溢出保护,即不生成 canary;
- -z execstack参数指定允许栈执行,即不开启NX。
关闭整个linux系统的ASLR保护:
1 |
|
运行程序,输入一串字符串然后返回helloworld;file查看是个动态链接的32位文件;checksec查看所有安全编译选项都没有开:
利用pattern脚本生成测试字符串,通过gdb调试程序,输入字符串后得到内存溢出的地址,并利用pattern计算偏移(从变量写入处到eip顶内存长度),可得到溢出偏移量为140。
如果能构造一个[shellcode][“AAAAAAAAAAAAAA”….][ret]
字符串,程序执行ret地址上的代码。但是我们要知道shellcode所在的内存地址。
注意使用gdb调试程序时,查询内存中shellcode的地址是错误的,原因是gdb的调试环境会影响buf在内存中的位置,即使关闭了ALSR,解决的办法是开启core dump
1 |
|
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
运行程序并触发栈溢出,使用gdb打开core dump文件即可得知真正的code_shell地址为0xffffcf60
EXP:
1 |
|
除了本地调试,还有远程部署的方式,如下,将题目绑定到指定端口上:
1 |
|
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 |
|
开启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
。
注意:system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。
EXP:
1 |
|
level 2 - 通过 ROP 绕过 DEP 和 ASLR 防护
开启在level1中关掉的ASLR,开启ASLR后libc.so地址每次都是变化的:
1 |
|
虽然每次程序运行的地址都是变换的,但是程序在内存中的地址并不随机。
思路是:程序先泄露出libc.so某些函数在内存中的地址,再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,最后执行我们的ret2libc的shellcode。
利用objdump来查看可以利用的plt函数和函数对应的got表:
通过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 |
|
level2——Memory Leak & DynELF
当无法获得目标的libc.so的情况下,如何获得偏移地址呐?
这时候可以通过内存泄漏来搜索内存找到system()的地址。这里我们采用pwntools提供的DynELF模块来进行内存搜索。但是需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少 1 byte的数据。
1 |
|
随后将这个函数作为参数再调用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段的地址。
因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。
利用的是ROPgadget工具快速查找可用gadget,
ROPgadget --binary level2 --only "pop|ret"
思路:首先通过DynELF获取System()地址,通过read()把/bin/sh
写入到.bss
段中,通过gadget清空read()栈上参数,调用system("/bin/sh")。
EXP:
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!