Off-by-One 溢出攻击分析
Off-by-One 溢出攻击分析
背景
上周,我参加了一门安全课程,其中包含了一个 off-by-one 溢出漏洞的示例。以下是原始代码:
1 | /* 简单的 off-by-one 溢出示例 */ |
利用此漏洞的攻击命令是:
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 | mov esp, 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
函数返回时,执行了leave
和ret
指令,由于返回地址被覆盖,程序跳转到bar
函数,打印出成功的消息多次。
进一步分析:确定有效的覆盖位置
为了成功执行攻击,关键在于精确确定需要覆盖哪些字节,以有效地操控控制流。在这个例子中,溢出发生在执行 buf[sizeof(buf)] = '\0'
时,导致保存的帧指针 (ebp
) 的最低有效字节被设置为 0
。因此,需要调整 ebp
的值,确保其指向缓冲区区域,从而使执行流程按预期进行,最终跳转到 bar
函数。
基于进一步的分析和测试,得出以下见解:
要准确确定覆盖位置,
ebp
的值至关重要。然而,获得这个值是有挑战的,因为:- GDB 调试会影响地址布局。
- 输入参数的长度会影响地址布局。
在 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 调试下是有效的,但需要注意以下细节:
- 输入参数的长度必须始终为 256 字节,否则
ebp
的值会发生变化,因为输入参数会占用栈空间,从而影响帧的起始位置,进而影响ebp
的值。 - 填充必须使用非零值,如
0x01
,因为如果遇到0
值,strncpy
会提前终止。
- 输入参数的长度必须始终为 256 字节,否则
直接执行程序时(即不使用 GDB),内存布局不同,导致偏移位置也不同。通过实验,发现偏移位置在第 235 个位置。对应的攻击命令是:
1
./obo $(perl -e 'print "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')
这样就实现了找到准确覆盖位置并成功执行攻击的效果。