为什么在 b 用 &f(32 位 MSVC 调试版本)覆盖其返回地址后,函数 b 和 f 在此代码中被调用*两次*?

问题描述 投票:0回答:1

我有一个非常奇怪的代码,据我所知,它替换了函数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  

如果我需要提供更多代码我可以

c++ assembly visual-c++ x86 buffer-overflow
1个回答
3
投票

拆解后更新发布:Jester 评论了答案:

一旦

f
尝试返回,它将从堆栈中弹出下一个项目并跳转到那里。查看汇编代码;这将是 main 中
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 位中。

© www.soinside.com 2019 - 2024. All rights reserved.