我有一个非常奇怪的代码,据我所知,它替换了函数b的返回地址,从而从它调用函数f。但我不太明白为什么在函数 f 运行后,执行返回到函数 main 并从那里再次调用 b 。 P.s.:该代码仅适用于 32 位系统
#include <iostream>
int f() {
std::cout << "Hello";
return 2;
}
int b() {
int *m[1];
m[3] = (int *)&f;
return 1;
}
int main() {
return b();
}
我尝试通过汇编程序,但它没有给出任何特殊结果。
组装主要:
int main() {
004724E0 push ebp
004724E1 mov ebp,esp
004724E3 sub esp,0C0h
004724E9 push ebx
004724EA push esi
004724EB push edi
004724EC mov edi,ebp
004724EE xor ecx,ecx
004724F0 mov eax,0CCCCCCCCh
004724F5 rep stos dword ptr es:[edi]
004724F7 mov ecx,offset _666773A0_main@cpp (047E068h)
004724FC call @__CheckForDebuggerJustMyCode@4 (0471389h)
00472501 nop
return b();
00472502 call b (0471456h)
}
00472507 pop edi
00472508 pop esi
00472509 pop ebx
0047250A add esp,0C0h
00472510 cmp ebp,esp
00472512 call __RTC_CheckEsp (0471294h)
00472517 mov esp,ebp
00472519 pop ebp
0047251A ret
组装b:
int b() {
004722A0 push ebp
004722A1 mov ebp,esp
004722A3 sub esp,0CCh
004722A9 push ebx
004722AA push esi
004722AB push edi
004722AC lea edi,[ebp-0Ch]
004722AF mov ecx,3
004722B4 mov eax,0CCCCCCCCh
004722B9 rep stos dword ptr es:[edi]
004722BB mov ecx,offset _666773A0_main@cpp (047E068h)
004722C0 call @__CheckForDebuggerJustMyCode@4 (0471389h)
004722C5 nop
int *m[1];
m[3] = (int *)&f;
004722C6 mov eax,4
004722CB imul ecx,eax,3
004722CE mov dword ptr m[ecx],offset f (0471172h)
return 1;
004722D6 mov eax,1
}
004722DB push edx
004722DC mov ecx,ebp
004722DE push eax
004722DF lea edx,ds:[472300h]
004722E5 call @_RTC_CheckStackVars@8 (047122Bh)
004722EA pop eax
004722EB pop edx
004722EC pop edi
004722ED pop esi
004722EE pop ebx
004722EF add esp,0CCh
004722F5 cmp ebp,esp
004722F7 call __RTC_CheckEsp (0471294h)
004722FC mov esp,ebp
004722FE pop ebp
004722FF ret
00472300 add dword ptr [eax],eax
00472302 add byte ptr [eax],al
00472304 or byte ptr [ebx],ah
00472306 inc edi
00472307 add al,bh
00472309 ?? ??????
0047230A ?? ??????
组装f:
int f() {
00472400 push ebp
00472401 mov ebp,esp
00472403 sub esp,0C0h
00472409 push ebx
0047240A push esi
0047240B push edi
0047240C mov edi,ebp
0047240E xor ecx,ecx
00472410 mov eax,0CCCCCCCCh
00472415 rep stos dword ptr es:[edi]
00472417 mov ecx,offset _666773A0_main@cpp (047E068h)
0047241C call @__CheckForDebuggerJustMyCode@4 (0471389h)
00472421 nop
std::cout << "Hello";
00472422 push offset string "Hello" (0479B30h)
00472427 mov eax,dword ptr [__imp_std::cout (047D0C8h)]
0047242C push eax
0047242D call std::operator<<<std::char_traits<char> > (04711A9h)
00472432 add esp,8
return 2;
00472435 mov eax,2
}
0047243A pop edi
0047243B pop esi
0047243C pop ebx
0047243D add esp,0C0h
00472443 cmp ebp,esp
00472445 call __RTC_CheckEsp (0471294h)
0047244A mov esp,ebp
0047244C pop ebp
0047244D ret
如果我需要提供更多代码我可以
拆解后更新发布:Jester 评论了答案:
一旦
尝试返回,它将从堆栈中弹出下一个项目并跳转到那里。查看汇编代码;这将是 main 中f
处push edi
的结果。004724EB
当时不知道 edi 包含什么,但听起来你很幸运,这是一个有效的代码地址,最终碰巧再次调用 b。
就像我下面所说的那样,通常我们希望它会崩溃,因为在返回地址正上方的堆栈上有另一个有效的代码地址并不常见。 但在这种情况下,我们这样做,可能是因为
main
的调用者必须处理地址并且可能将它们放在寄存器中,并且 MSVC 调试模式的 main
使用 EDI 进行 rep stosd
来毒害一些堆栈内存,因此它必须保存/恢复它。
什么编译器有什么选项,针对什么 ISA?
(更新回复:您的编辑:那是 32 位 x86。来自
mov eax,0CCCCCCCCh
/ rep stosd
,这是调试版本中的 MSVC,毒害堆栈内存,因此未初始化的变量具有可识别的位模式。并且 MSVC 在函数之间填充 int3
指令,而不是像 GCC/Clang 使用的 NOP,因此函数之间的失败更少似乎是合理的,除非它恰好是 16 机器代码字节的倍数。)
此代码具有在编译时可见的未定义行为,因此不能保证编译器将其编译为实际存储到堆栈上的数组并返回的 asm。
对于某些类型的 UB,某些编译器(尤其是 Clang,有时是 GCC)假设执行路径无法访问并停止为其发出指令,包括省略函数末尾的
ret
,以便执行进入接下来的状态二进制文件。 特别是在启用优化的情况下,尽管即使没有优化,这个 UB 也是可见的。
如果它确实按编写的方式编译,则您将使用函数地址覆盖堆栈上的某些内容。 如果
m[3]
处的内容是函数的返回地址,那么您将返回到 f
,而不是最初设置返回地址的调用站点。
所以事情已经很奇怪了,我通常预计当
f
尝试返回时它会崩溃。 我不知道为什么在 b
弹出的地址上方会有另一个有效的退货地址以到达 f
。
该代码仅适用于32位系统
我猜是 32 位 x86? 还有其他 32 位 ISA,包括在业余爱好板上广泛使用的 ARM 和 RISC-V。
至少在 x86-64 上,在 x86-64 调用约定中需要调用之前,堆栈指针在进入
f
时会错位: RSP % 16 == 0
在 x86-64 调用约定中需要调用之前,因此 RSP % 16 == 8
在 call
指令推送之后的函数入口处8 字节返回地址。 在许多其他 ISA 上; call
/ bl
/ jal
只是将返回地址放入寄存器(“链接寄存器”)中,因此函数入口处的堆栈对齐方式与另一个函数内的调用点处的堆栈对齐方式相同。
并且
cout<<
函数很可能会执行依赖于 16 字节 RSP 对齐的操作,例如使用 movaps
将 16 字节复制到堆栈变量和/或从堆栈变量复制 16 字节。 Glibc printf
和 scanf
可以。
但是,当然,x86-64 使用 8 字节指针,而
int
仅是 32 位,因此您最多只能覆盖部分返回地址,即使 m[]
具有不同的偏移量。 该 ROP 演示仍然可以在 Linux 非 PIE 可执行文件中工作 (gcc -fno-pie -no-pie -fno-stack-protector
),因为静态地址(包括代码)将位于虚拟地址空间的低 31 位中。