修改调用堆栈

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

是否可以修改c++中的调用堆栈? (我意识到这是一个可怕的想法,我真的只是想知道——我不打算真正这样做)

例如:

void foo(){
  other();
  cout << "You never see this" << endl;  //The other() function modifies the stack to 
                            //point to what ever called this function...so this is not displayed
}
void other(){
  //modify the stack pointer here somehow to go down 2 levels
}

//Elsewhere
foo();
c++ stack-trace
2个回答
1
投票

在典型的 C 实现中,当一个函数调用另一个函数时,将使用处理器堆栈并使用调用操作码。这具有将下一个要执行的处理器指令指针推送到处理器堆栈上的效果。通常除了返回地址之外,还使用堆栈帧指针的值。 所以堆栈包含:
...free_space...[local_variables] [framePtr] [returnAddr] PREVIOUS_STACK。

因此,为了更改返回地址(您应该知道它的大小 - 如果您通过 -m64 进行编译,它将具有 64 位的大小),您可以获取变量的地址并向其中添加一些命令到达返回指针的地址并更改它。 下面的代码是用 g++ 在 m64 模式下编译的。 如果通过改变也对您有效,您可能会看到效果。

#include <stdio.h>


void changeRetAddr(long* p){
    p-=2;
    *p+=0x11;
}


void myTest(){
    long a=0x1122334455667788;
    changeRetAddr(&a);
    printf("hi my friend\n");
    printf("I didn't showed the salutation\n");
}


int main(int argc, char **argv)
{ 
    myTest();
    return 0;
}

0
投票

George Kourtis 上面的答案涉及硬编码常量,需要通过转储二进制文件来手动确定正确的值。我们不需要这样做,只要我们可以使用一些 GCC 扩展 - 特别是

&&LABEL
,它让我们获取 goto 标签的地址,以及
__builtin_frame_address(0)
,它为我们提供指向堆栈帧的指针。

这是一个测试 C 程序(尽管它在编译为 C++ 时也能工作)。它演示了类似于 Fortran 77 的“备用返回”功能,其中可以将备用返回地址作为参数传递给函数。

#include <stdio.h>

void alt_return_demo(void* return_to) {
        void *ra = __builtin_return_address(0);
        void *fa = __builtin_frame_address(0);
        printf("alt_return_demo: called with:\n");
        printf("\treturn_to=%p\n", return_to);
        printf("\treturn_address=%p\n", __builtin_return_address(0));
        printf("\tframe_address=%p\n", __builtin_frame_address(0));
        printf("\tframe_address[1]=%p\n", ((void**)__builtin_frame_address(0))[1]);
        ((void**)__builtin_frame_address(0))[1] = return_to;
}

int alt_return_tester(void) {
        void* locs[] = { &&loc_0, &&loc_1, &&loc_2 };
        int i = 0;
        loc_0: {
                printf("@loc_0 i=%d\n", i);
                void* return_to = locs[(i++) == 0 ? 0 : 1];
                alt_return_demo(return_to);
        }
        loc_1: {
                printf("@loc_1 i=%d\n", i);
                void* return_to = locs[(i++) < 3 ? 1 : 2];
                alt_return_demo(return_to);
        }
        loc_2: {
                printf("@loc_2 i=%d\n", i);
                return 0;
        }
}

int main(void) {
        return alt_return_tester();
}

运行它时,您应该看到它打印

@loc_0
两次,
@loc_1
两次,然后
@loc_2
一次。

请记住,这是完全不可移植的 – 以及依赖于 GCC 扩展,它还依赖于返回地址是帧指针之后的指针 – 我已经测试过该代码可以在 x86-64 Linux 上运行和 x86-64 macOS,但很可能不会在其他一些操作系统或 CPU 架构上。这些 GCC 扩展也由 Clang 实现,可能还有其他一些 C/C++ 编译器,但肯定不是全部。此外,越来越多的编译器和操作系统具有完整性机制来阻止漏洞利用篡改堆栈上的返回地址,并且这些机制很可能会与该技术发生冲突(无论是现在还是将来)。

上面的代码也使用了

__builtin_return_address
,但这并不是使其工作所必需的,只是为了打印一些调试信息。 (证明帧指针之后的指针与返回地址的值相同。)

在我的测试中,这只适用于

void
函数。将它与返回值的函数一起使用的问题是,对于这些行:

    return_value = alt_return_demo(return_to);
loc_1:

GCC 生成此程序集(对于 32 位返回值):

        call    alt_return_demo
        movl    %eax, -44(%rbp)
.L5:

loc_1
(在上面的汇编中变成了
.L5
)是after将返回值(在
%eax
中)复制到局部变量的指令。 Clang的汇编也是一样的。我不知道 GCC 或 Clang 中有什么方法可以创建一个指向
call
指令之后立即指向的标签,但如果没有它,您就无法可靠地获取返回值。

好吧,你可以用一个丑陋的 hack - 32 位

movl %eax, -N(%rbp)
是 3 个字节长,64 位
movq %rax, -N(%rbp)
是 4 个字节长,所以只需从
return_to
地址中减去 3 或 4 个字节即可。然而,这是非常脆弱的,因为编译器生成的汇编器输出的任何变化都会破坏它。我认为最好强制所有“备用返回”函数为
void
,并使用输出参数(C 中的指针,C++ 中的指针或引用)。

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