调用栈被破坏的手动恢复

函数调用前言后序

一般来说,在未优化的情况下,函数的调用栈的前言和后续对应的指令都是固定的。

push %rbp
mov %rsp,%rbp
sub $10,%rsp #这里的立即数 10 表示函数需要的栈大小,与函数自身小关
​
​
leave # leave 等价于以下两条指令
#mov %rbp,%rsp
#pop %rbp
ret

push %rbp 之后,栈顶指针rsp 便指向rbpmov %rsp,%rbp 之后,rbp 也指向栈顶,所以在 callee 被调用函数的栈当中,rbp指向的栈底的内容记录的其实是 caller 调用者的 rbp,即rbp 指向的栈里的内容总是上一个函数的 rbp,这样 rbp 构成一个链表,从这个链表里就能获得调用栈的信息。

调用栈被破坏的手动恢复

调用栈被破坏的手动恢复 

 

调用栈被破坏

如果返回地址被覆盖,就可能导致程序崩溃,并且 rbp 链被破坏,即使有崩溃的 core 文件,此时 bt 看到的调用栈可能不全,很多函数都是 ?? 的状态。

调用栈被破坏的手动恢复

 

破坏的调用栈部分恢复

注意发生问题时,如果是因为返回地址被写坏,rsp寄存器的值一般是对的,因为 rsp是从 rbp恢复来的。通过 rsp 寄存器的值来手动恢复调用栈,以下边的代码为例子来说明。

void func1(int x)
{
    int y = x + 3;  //填充栈内容
    printf("func1");
    int a[2];
    a[3] = 0x1111;
    a[4] = 0x2222; //数组越界破坏调用栈
}
​
void func2(int b)
{
    int x = b + 3;
    printf("func2");
    func1(2);
}
​
void func3(int c)
{
    int y = c + 1;
    printf("func3");
    func2(3);
}
​
void func4(int d)
{
    int y = d;
    printf("func4");
    func3(4);
}
​
int main(int argc, char* argv[])
{
    func4(6);
    return 0;
}

直接执行上边的代码得到的可执行程序,会发生奔溃。

Segmentation fault (core dumped).

查看崩溃时,rsp 寄存器的内容,复制到 notepad++ 当中,因为 notepad++ 当中会高亮与选中的内容相同的内容。栈上的内容都是显示完成的 64 位的,所以进行一次替换,将第一列的地址的内容也全部显示为完成的 64 位。

调用栈被破坏的手动恢复

 

在第2列中选中(双击)看起来像是栈地址的内容,比如这里的 0x00007ffd 开头的比较像时栈上的某个地址。

选中0x00007ffd978bd000 之后,果然第 1 列的地址列当中也有 0x00007ffd978bd000 ,所以也被被高亮。

再选中地址列 0x00007ffd978bd000 对应的第2列内容 0x00007ffd978bd030,第一列的地址列中也0x00007ffd978bd030。这样,差不多断定这就是整个 rbp链。

调用栈被破坏的手动恢复

 

栈上的rbp相邻的前一个 8 字节的内容就是返回地址,所以这里依次可以得到返回的地址为

0x0000000000400627 -> 0x0000000000400654 -> 0x000000000040066f

再利用 binutilsadd2line 工具,得到最后的调用栈为 func3 <- func4 <- main,这里因为编译加了 -g 选项才能看到行号,否则只有函数名。

调用栈被破坏的手动恢复

 

这里的返回地址 0x0000000000400627 等都是 call指令的下一条指令的地址,所以通常显示的源码的行号都是实际调用链上发生调用的下一行。

后序

为啥保存的 rbp 都出现在第 2 列,而不是第 3 列呢? 这是因为abi 里要求,在call指令之前,栈顶rsp寄存器必须是 16 字节对齐的,所以 call 指令把返回地址入栈之后,接着执行call指令后的函数的第一条指令,也就是 push %rbp,所以保存 rbp 的栈地址也必然是 16 字节对齐的。

另外,能被恢复的只是部分调用栈,因为发生问题的 func1 的栈已经被展开了,被破坏的是 func2的,func2 之后的调用栈才是能够被恢复的,这样能缩小问题的范围。

上一篇:攻防世界—RE—新手区—logmein


下一篇:C语言变量名存储在哪里