从汇编层看64位程序运行——ROP攻击以控制程序执行流程

一般我们运行有调用关系的代码,就像套娃一样,一个套着一个。比如main函数调用foo7函数,foo7调用foo函数,则main函数要先进入foo7,然后进入foo。等foo执行完,会回到foo7;等foo7执行完,再回到main。
在这里插入图片描述
那么我们有办法脱离这种套娃模式,然后foo函数执行返回到main函数吗?
在这里插入图片描述
当然是有的。
这就需要使用ROP攻击。
ROP攻击全称是:Return Oriented Programming (ROP) attacks。它是指我们通过修改栈中保存的Next RIP地址,来达成控制程序运行流程的方法。
《从汇编层看64位程序运行——函数的调用和栈平衡》中,我们介绍了call指令会将本函数中下一个要执行的汇编指令地址push到栈中。这个地址就是我们需要攻击的地址。
在这里插入图片描述
如果我们将上述地址改成foo函数的入口地址,并将foo的Next RIP(后面也称ret rip)地址改成foo7本来应该返回到main函数中的地址,则就可以达成一次完整的攻击。
在这里插入图片描述
我们看下具体步骤。
一般情况下,我们找到ret rip有两种方法:

  • 无栈传递参数时,是ebp + 0x08。
  • 有栈传递参数时,即可以使用ebp + 0x08,又可以用最后一个栈传递参数地址减去0x08。

这次我们需要改变两个栈帧中的ret rip的值,于是上述方法我们各采用一次。

代码讲解

void foo7(unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e, unsigned int f, void* g) {
    void* ptr = (void*)&g;
    void* next_rip = (void*)((unsigned long long)ptr - 8);
    unsigned long long rip = *((unsigned long long*)next_rip);
    rip = rip * 1;
    memcpy(next_rip, &g, 8);
    __asm__("mov -0x8(%rbp), %rdi\n\t");
}

foo7一共有7个参数,我们在《从汇编层看64位程序运行——参数传递的底层实现》中分析过这个案例。它的前6个参数会通过rdi、rsi、rdx、rcx、r8d和r9d这几个寄存器来传递到foo中,而第7个参数g则是通过栈传递。

int main() {
    void* g = (void*)&foo;
    foo7(1, 2, 3, 4, 5, 6, g);
    return 0;
}

我们让g这个参数等于foo函数地址,然后直接调用了foo7。这样call foo7指令压入栈的ret rip就会紧挨着g这个参数在栈中的位置。因为栈的增长方向是向地址小的方向发展,所以这个g在栈上的地址减去0x08,就得到foo7执行完要返回到main函数中的地址。

void* next_rip = (void*)((unsigned long long)ptr - 8);

对于原始要返回的地址,我们需要记录下,以让foo执行完返回到main中。

unsigned long long rip = *((unsigned long long*)next_rip);
rip = rip * 1;

然后我们就可以着手将foo7栈帧中的ret rip改成foo的入口地址

memcpy(next_rip, &g, 8);

这次我们按照传递参数的规律,将foo7最后一个局部变量rip的值保存到rdi寄存器中。后面foo函数会直接从rdi中获取这个地址。这么做的原因是,我们没有通过编译器调用foo,所以它不会帮我们构建参数传递的过程。这个过程就需要我们手工进行。

__asm__("mov -0x8(%rbp), %rdi\n\t");

进入foo函数,我们就将变量取出,直接覆盖foo的ret rip。

void foo(void* return_rip) {
    __asm__("mov %rdi, 0x08(%rbp)\n\t");
    printf("Hello from foo!\n");
}

这样,下面代码的执行结果是输出“Hello from foo!”。即foo函数被执行一次。

#include <stdio.h>
#include <string.h>

void foo(void* return_rip) {
    __asm__("mov %rdi, 0x08(%rbp)\n\t");
    printf("Hello from foo!\n");
}

void foo7(unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e, unsigned int f, void* g) {
    void* ptr = (void*)&g;
    void* next_rip = (void*)((unsigned long long)ptr - 8);
    unsigned long long rip = *((unsigned long long*)next_rip);
    rip = rip * 1;
    memcpy(next_rip, &g, 8);
    __asm__("mov -0x8(%rbp), %rdi\n\t");
}

int main() {
    void* g = (void*)&foo;
    foo7(1, 2, 3, 4, 5, 6, g);
    return 0;
}

在这里插入图片描述

调试讲解

分析完代码后,我们通过调试来验证这个过程。
在这里插入图片描述
+12到+23,汇编代码将foo的地址压入栈中。我们在+23处下断点,看看没压栈时栈的状态。
在这里插入图片描述
然后单步一次,看看压栈完的状态。可以看到foo的地址被压栈,rsp的值减少了0x08。
在这里插入图片描述
一直到+58处call指令,中间的指令都没有压栈和出栈行为。我们直接跳到call指令,然后单步进去,然后查看栈的状态。
在这里插入图片描述
我们看到call指令压入的值0x00005555555551fb就是理论上foo7要返回到main函数的地址。
在这里插入图片描述

void* ptr = (void*)&g;
void* next_rip = (void*)((unsigned long long)ptr - 8);
unsigned long long rip = *((unsigned long long*)next_rip);

上面这块代码对应于
在这里插入图片描述

-0x18(%rbp)保存的是ptr;-0x10(%rbp)保存的是next_rip ;-0x8(%rbp)保存的是rip。

memcpy(next_rip, &g, 8);

上面代码对应如下面图
在这里插入图片描述
我们让程序执行到+67处,观察它和运行到+70处的栈的变化。
在这里插入图片描述
可以看到foo7的返回地址变成了foo函数的入口地址。
foo7在退出前,会把上一栈帧的rbp还原,我们再看还原后的栈。
在这里插入图片描述
此时栈顶就是foo的地址了,这样foo7的ret执行完,会跳转到foo函数
在这里插入图片描述
我们在foo函数中插入的汇编被执行后,foo函数栈帧的ret ip就会被修改成main函数的地址了。
在这里插入图片描述
这样等foo执行ret后,程序会自动回到main函数
在这里插入图片描述

  • 31
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值