首页 > 其他分享 >追core笔记之五:如何查看一个corrupt stack的core

追core笔记之五:如何查看一个corrupt stack的core

时间:2023-01-28 14:33:59浏览次数:57  
标签:core bar mov rbp gdb 指令 之五 corrupt rsp

接触c以来有很多好奇的问题,其中一类是关于栈的。比如:栈上存储了哪些数据?函数参数怎么传递的?返回值怎么传出去的?从一个函数是怎么跳转到另外一个函数的?为何gdb可以看到函数的调用栈?为何有些栈的信息会乱?

如果要讲清楚core栈的信息为何有很多问号,想了下觉得应该先从如何确定函数的调用栈来讲。

注:本文会花大部分篇幅介绍栈上存储了什么,如果制造一个corrupt stack的core,解决方案根据经验看往往并不固定,希望对读者有用。

1. call-trace是如何确定的

我们知道栈是向下增长的,更具体些,栈顶由寄存器esp进行定位。压栈操作使栈顶地址减少,弹出操作使栈顶地址增大。

esp寄存器始终指向栈顶,而ebp则指向一个固定的位置,又称为帧指针(Frame Pointer)。我们先通过gdb看一个例子,观察下函数调用过程中ebp esp的变化,栈上数据发生了什么样的改变。

1.1. 函数调用时栈的变化

函数调用时,栈上有一些数据是我们预先定义的,比如参数、各种指令,有些则是编译器为了寻址、回溯添加进去的。

先贴下示例用的代码,使用gcc4.8.2在64位环境下编译,所以接下来使用rbp rsp来描述。

函数调用关系为:main->bar->foo

void foo(const char* arg) {
    char buf[16] = {0};
    strcpy(buf, arg);
    printf("foo arg:%s\n", buf);
}

void bar(int argc, const char* arg) {
    printf("bar argc:%d\n", argc);
    foo(arg);
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("argument count < 2.\n");
        return -1;
    }

    bar(argc, argv[1]);

    return 0;
}

那么接下来,我们使用gdb看下调用bar后的栈的变化(注意由于编译环境的不同,对应的符号地址可能会略有变化)

传入的参数为”helloworld”,我们在bar处设置下断点:

(gdb) set args helloworld
(gdb) disass main
Dump of assembler code for function main(int, char**):
   0x0000000000400778 <+0>:     push   %rbp
   0x0000000000400779 <+1>:     mov    %rsp,%rbp
   0x000000000040077c <+4>:     sub    $0x10,%rsp
   0x0000000000400780 <+8>:     mov    %edi,-0x4(%rbp)
   0x0000000000400783 <+11>:    mov    %rsi,-0x10(%rbp)
   0x0000000000400787 <+15>:    cmpl   $0x1,-0x4(%rbp)
   0x000000000040078b <+19>:    jg     0x40079e <main(int, char**)+38>
   0x000000000040078d <+21>:    mov    $0x4008a5,%edi
   0x0000000000400792 <+26>:    callq  0x400590 <puts@plt>
   0x0000000000400797 <+31>:    mov    $0xffffffff,%eax
   0x000000000040079c <+36>:    jmp    0x4007bb <main(int, char**)+67>
   0x000000000040079e <+38>:    mov    -0x10(%rbp),%rax
   0x00000000004007a2 <+42>:    add    $0x8,%rax
   0x00000000004007a6 <+46>:    mov    (%rax),%rdx
   0x00000000004007a9 <+49>:    mov    -0x4(%rbp),%eax
   0x00000000004007ac <+52>:    mov    %rdx,%rsi
   0x00000000004007af <+55>:    mov    %eax,%edi
   0x00000000004007b1 <+57>:    callq  0x400747 <bar(int, char const*)>
   0x00000000004007b6 <+62>:    mov    $0x0,%eax
   0x00000000004007bb <+67>:    leaveq 
   0x00000000004007bc <+68>:    retq   
End of assembler dump.
(gdb) b *0x00000000004007b1
Breakpoint 1 at 0x4007b1: file test_test.cpp, line 21.
(gdb) i r rbp rsp
rbp            0x7fffffffdd60   0x7fffffffdd60
rsp            0x7fffffffdd50   0x7fffffffdd50

注意我们的断点设置了地址,而没有使用b 21设置,这样可以跳过设置参数的汇编指令,也就是40079e这几条。

而使用函数名、行号等作为断点时这几条汇编指令都不会被跳过去:

类似需要注意的还有gdb自动帮我们跳过去的代码,比如接下来si调用bar

(gdb) si
bar (argc=0, arg=0x7ffff7ba55f0 <vtable for (anonymous namespace)::future_error_category+16> " 677") at test_test.cpp:10
10      void bar(int argc, const char* arg) {
(gdb) disass
Dump of assembler code for function bar(int, char const*):
=> 0x0000000000400747 <+0>:     push   %rbp
   0x0000000000400748 <+1>:     mov    %rsp,%rbp
   0x000000000040074b <+4>:     sub    $0x10,%rsp
   0x000000000040074f <+8>:     mov    %edi,-0x4(%rbp)
   0x0000000000400752 <+11>:    mov    %rsi,-0x10(%rbp)
   0x0000000000400756 <+15>:    mov    -0x4(%rbp),%eax
   0x0000000000400759 <+18>:    mov    %eax,%esi
   0x000000000040075b <+20>:    mov    $0x400898,%edi
   0x0000000000400760 <+25>:    mov    $0x0,%eax
   0x0000000000400765 <+30>:    callq  0x400570 <printf@plt>
   0x000000000040076a <+35>:    mov    -0x10(%rbp),%rax
   0x000000000040076e <+39>:    mov    %rax,%rdi
   0x0000000000400771 <+42>:    callq  0x400700 <foo(char const*)>
   0x0000000000400776 <+47>:    leaveq 
   0x0000000000400777 <+48>:    retq 

如果使用b bar,则断点停在0x0000000000400756指令处,也就是已经运行了以下这些指令:

void bar(int argc, const char* arg)
  400747:       55                      push   %rbp
  400748:       48 89 e5                mov    %rsp,%rbp
  40074b:       48 83 ec 10             sub    $0x10,%rsp
  40074f:       89 7d fc                mov    %edi,-0x4(%rbp)
  400752:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
    printf("bar argc:%d\n", argc);

言归正传,看下运行到bar汇编指令第一行后rsp rbp寄存器变化

(gdb) i r rbp rsp
rbp            0x7fffffffdd60   0x7fffffffdd60
rsp            0x7fffffffdd48   0x7fffffffdd48

callq指令运行后,可以看到栈顶指针rsp减少了8字节的空间,我们看下这个8字节存了什么?

(gdb) x /xg $rsp
0x7fffffffdd48: 0x00000000004007b6
(gdb) info symbol 0x00000000004007b6
main + 62 in section .text of /home/users/y/Training/test/test_test

从前面main的指令,也可以看到这里存储了bar函数返回后需要调用的main下一条指令,这是堆栈帧(Stack Frame)的一部分。

继续运行下一条指令,看下对rbp rsp的影响:

(gdb) ni
0x0000000000400748      10      void bar(int argc, const char* arg) {
(gdb) i r rbp rsp
rbp            0x7fffffffdd60   0x7fffffffdd60
rsp            0x7fffffffdd40   0x7fffffffdd40
(gdb) x /xg $rsp
0x7fffffffdd40: 0x00007fffffffdd60

看到push %rbp将上个rbp存储的内容写到栈上。

继续运行看下一条指令:

(gdb) ni
0x000000000040074b      10      void bar(int argc, const char* arg) {
(gdb) i r rbp rsp
rbp            0x7fffffffdd40   0x7fffffffdd40
rsp            0x7fffffffdd40   0x7fffffffdd40

mov %rsp,%rbp设置rbp存储了跟rsp相同的一段地址值0x7fffffffdd40,对应的内容从上条指令可以看到是上一个rbp的值。

接下来的几个指令,就是增长栈空间,将rsi edi存储的值,也就是参数值放到栈上,然后开始真正函数实现的调用。 接下来跟main调用bar类似,bar调用foo

我们先贴一下foo前面几行汇编代码:

void foo(const char* arg)
  400700:       55                      push   %rbp
  400701:       48 89 e5                mov    %rsp,%rbp
  400704:       48 83 ec 20             sub    $0x20,%rsp
  400708:       48 89 7d e8             mov    %rdi,-0x18(%rbp)

为了更直观的了解下rsp rbp的变化流程,省去gdb单步调试的部分,画了一张图描述了下几个指令执行后栈上的变化

call-trace-rsp-rbp

关于图形格式的一些解释:

  1. 红色为标题行,表示调用对应的assemble code后的状态。下面是当时部分栈上的状态。
  2. 由于使用的是64位系统,因此每个框表示8个字节。
  3. 绿色、蓝色分别为rbp rsp所存储的8字节栈上空间的首地址。
  4. 黄色表示此时rbp rsp存储了相同的值,指向了同一块内存
  5. 框内内容为该8字节内存的地址,如有必要,还有些其他说明。

从这张图我们可以清晰的看到每条指令执行后栈上的变化,比如bar函数调用了move %rbp %rsprbprsp存储了同一块内存地址。那么栈上为什么会写入了这些数据?rbp有什么作用?接下来想从一个最基本的问题“栈是什么”来解释下,并分析下上面这张图为什么会填充了一些诸如返回指令prev rbp这样的内容。

1.2 栈是什么

寄存器的操作导致了栈的变化,上图中我们可以看到在一级级的函数调用时,栈一直在向下增长。除了我们需要的指令、参数、变量以外,每次函数调用时栈上的数据还会发生一些自动的变化,比如push %rbp

实际上栈保存了一个函数调用所需要的上下文信息,被称为堆栈帧(Stack Frame)或者活动记录(Activate Record)。

上图可以清晰的看到rsp始终指向了栈顶,而rbp的作用还有些不够明显。先看下i386函数的标准开头

push rbp             //%rbp压入栈中,称为prev rbp, rsp += 8
mov rbp rsp          //%rbp = %rsp,也就是当前rbp存储了prev rbp的地址

函数调用开始后,本函数内rbp不再发生变化,始终存储了prev rbp的地址。这也是rbp寄存器又被称为帧指针(Frame Pointer)的原因,下图绘制了当前栈上rbp的链式关系,可以直观的看到对应链表关系,当函数调用深度增加,这个回溯的链接不会变。

chains-of-rbp

那么问题来了,最前面的rbp存储了什么,图里也给出了答案:0,从另一方面显示了main函数特殊的地位。

实际上栈上保存的数据比示例里复杂一些:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 保存的上下文: 包括在函数调用前后需要保持不变的寄存器

例如这样:

active_record.png

该照片剪切自《程序员的自我修养》一书

栈上存储返回后执行的下一条指令是很容易理解的,因为必须要在函数调用后给他下一条指令的入口。那么存储rbp的作用是什么呢?前面一直在分析进入函数时栈上数据的变化,现在我们先看下函数退出对栈的影响。

我们还是gdb看下foo函数退出时栈上的变化:

=> 0x0000000000400745 <+69>:    leaveq 
   0x0000000000400746 <+70>:    retq   
End of assembler dump.
(gdb) i r rbp rsp
rbp            0x7fffffffdd20   0x7fffffffdd20
rsp            0x7fffffffdd00   0x7fffffffdd00

这是执行leaveq前的寄存器情况,对比前面的图片可以看到rbp的值未发生变化。

看下执行leaveq后的寄存器变化

(gdb) ni
0x0000000000400746      8       }
(gdb) i r rbp rsp
rbp            0x7fffffffdd40   0x7fffffffdd40
rsp            0x7fffffffdd28   0x7fffffffdd28

leaveq实际上是两条指令:

movq %rbp, %rsp      //%rbp的值,赋值到rsp,即rsp指向的内存存储了prev_rbp
popq %rbp            //弹出栈顶的数据:prev_rbp到rbp,即rbp的值

可以看到跟函数首部的指令正好相反

push rbp             //%rbp压入栈中,称为prev rbp, rsp += 8
mov rbp rsp          //%rbp = %rsp,也就是当前rbp存储了prev rbp的地址

leaveq执行后:

rsp指向了rbp的上一条指令,也就是返回后执行的下一条指令
rbp则存储了prev_rbp的值,直观点的结果如图:

rbp_on_function_exit.png

retq将栈顶地址pop到rip,也就是下一条要执行的指令,我们验证下

(gdb) i r rbp rsp rip
rbp            0x7fffffffdd40   0x7fffffffdd40
rsp            0x7fffffffdd28   0x7fffffffdd28
rip            0x400746 0x400746 <foo(char const*)+70>
(gdb) ni
bar (argc=2, arg=0x7fffffffe124 "helloworld") at test_test.cpp:13
13      }
(gdb) i r rbp rsp rip
rbp            0x7fffffffdd40   0x7fffffffdd40
rsp            0x7fffffffdd30   0x7fffffffdd30
rip            0x400776 0x400776 <bar(int, char const*)+47>

可以看到这里rip指向了bar要执行的下一条指令,foo函数功成身退,栈上的控制权重新返回给了bar函数。

再看下rbp rsp的值,是不是有些眼熟?对照下前面的call-trace的图,bar调用foo前的栈上内容,是不是完全一致?

而这,就是rbp最重要的作用,从前面也可以看到,rbp是一个链式的结构,不断地回溯,可以找到程序最开始的地方,借助rbprsp才能找到退出函数后的下一条指令。你看,程序都是不忘初心,不是么?这也是rbp被称为帧指针(Frame Pointer)的原因。

那么,只有借助rbprsp才能找到退出函数的下一条指令吗?

1.3. fomit-frame-pointer

GCC编译器有一个--fomit-frame-pointer可以取消帧指针,即不使用任何帧指针,而是通过esp直接计算帧上变量的位置。这么做的好处是可以多出一个ebp寄存器供使用,但是坏处却很多,比如帧上寻址速度会变慢,而且没有帧指针之后,无法准确定位函数的调用轨迹(Stack Trace)。

举个例子,foo函数加了该编译选项后的汇编如下

void bar(int argc, const char* arg) {
  40074c:       48 83 ec 18             sub    $0x18,%rsp
  ... ...
  400779:       48 83 c4 18             add    $0x18,%rsp
  40077d:       c3                      retq

可以看到rsp的恢复使用了偏移量的形式

查看gcc的优化选项,-o1在不影响debug的情况下会开启该选项。

在使用tcmalloc时,需要注意这个选项,具体可以参考tcmalloc的主页

2. core栈为什么会乱

在追查core时,我们会发现很多?的情况。这类core确实难以追查,在接手模块的过程中,我遇到了两类core属于这种情况。

从前面的介绍里,我们知道了栈上的数据可以回溯追踪到函数的调用信息,而gdb之所以能够定位core栈,就是通过rbp来区分每个函数的帧,也就可以根据rbp前面的字节,即函数返回后调用函数的下条指令,逐步回溯到调用函数名。

同时根据前面的分析,当我们数组写越界时,污染的是高地址的数据,比如定义顺序在前的其他变量,或者prev_rbp、返回后的下条指令等。写坏的栈数据很难追查,不过根据前面的分析,我们可以得到一些基本的结论:

  1. rbprsp的值要大
  2. rbp的前一个地址是调用函数的下一条指令
  3. rbp遵循链式结构

3. corrupt stack的实例

我们继续用第一节里的代码看下corrupt stack的core实例

$ ./test_test helloworldabcdef
bar argc:2
foo arg:helloworldabcdef
Segmentation fault (core dumped)

gdb看下

$ gdb test_test core.22278
(gdb) bt
#0  0x00007fff1d18613d in ?? ()
#1  0x726f776f6c6c6568 in ?? ()
#2  0x666564636261646c in ?? ()
#3  0x00007fff1d185100 in ?? ()
#4  0x0000000000400776 in bar (argc=684837, 
    arg=0x206f6f6600020001 <error: Cannot access memory at address 0x206f6f6600020001>) at test_test.cpp:12
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

果然,一个没有调用栈的core出现了。

这种core实际修复要复杂的多,这里只是简单介绍下一些基本思路:

(gdb) fr 0
#0  0x00007fff1d18613d in ?? ()
(gdb) i r rsp rbp rip
rsp            0x7fff1d185110   0x7fff1d185110
rbp            0x400898 0x400898
rip            0x7fff1d18613d   0x7fff1d18613d

可以看到rip的地址明显不是一个合法的指令,通过dmesg也可以验证这点:

$ dmesg | tail -n 1
test_test[3932]: segfault at 7fff1d18613d ip 00007fff1d18613d sp 00007fff1d185110 error 15

那么,怎么定位呢?

我们先看下rbp的值是否合法

(gdb) x /xg 0x400898
0x400898:       0x6367726120726162
(gdb) info symbol 0x6367726120726162
No symbol matches 0x6367726120726162.
(gdb) x /s 0x400898
0x400898:       "bar argc:%d\n"

可以看到第一个有用的信息,是在使用字符串”bar argc:%d\n”附近。

(gdb) x /16xg $rsp
0x7fff1d185110: 0x726f776f6c6c6568      0x666564636261646c
0x7fff1d185120: 0x00007fff1d185100      0x0000000000400776
0x7fff1d185130: 0x00007fff1d18613d      0x0000000200000000
0x7fff1d185140: 0x00007fff1d185160      0x00000000004007b6
0x7fff1d185150: 0x00007fff1d185248      0x0000000200000000
0x7fff1d185160: 0x0000000000000000      0x00007fb397b10bd5
0x7fff1d185170: 0x0000000000000000      0x00007fff1d185248
0x7fff1d185180: 0x0000000200000000      0x0000000000400778
(gdb) info symbol 0x0000000000400776
bar(int, char const*) + 47 in section .text of /home/users/yingshin/Training/test/test_test

这里另一个有用的信息,是最近的函数调用指令为0x400776bar在调用foo后的下一条指令。

猜测问题是否出在foo函数,查看源码,strcpy这种很容易写栈溢出的函数,对比”helloworldabcdef”的长度,不难得到答案。这里贴一下foo的汇编:

  400704:       48 83 ec 20             sub    $0x20,%rsp
  400708:       48 89 7d e8             mov    %rdi,-0x18(%rbp)//参数拷贝到$rbp - 0x18内存开始的位置
    char buf[16] = {0};
  40070c:       48 c7 45 f0 00 00 00    movq   $0x0,-0x10(%rbp)//$rbp - 0x10即buf首地址,即如果buf写超过16个字节的话,就会写乱rbp的内存、甚至调用函数的内存
  400713:       00
  400714:       48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
  40071b:       00
    strcpy(buf, arg);
  40071c:       48 8b 55 e8             mov    -0x18(%rbp),%rdx
  400720:       48 8d 45 f0             lea    -0x10(%rbp),%rax
  400724:       48 89 d6                mov    %rdx,%rsi
  400727:       48 89 c7                mov    %rax,%rdi
  40072a:       e8 81 fe ff ff          callq  4005b0 <strcpy@plt>

当然这只是一个简化版的例子,实际上我在解决core的过程中花了很多的时间,甚至有运气的成分。

只能根据经验总结一下你要埋这么一个core的方法:

  1. strcpy等不安全的函数要多用
  2. 数组定义的时候不要解释传入的常量大小
  3. 多使用一些魔数

当然这样还是太基本了些,大部分情况要传入的参数是不会引起core的,最好是在多线程重入的情况下可能导致参数本身是非预期的。。。算了,不说了,两行泪。。。

当需要解决这种问题时,则反其道而行之。

4. 总结

这篇文章其实花了很长的篇幅来介绍栈上数据的布局,然而对于解决方案却没有明确给出来。主要还是这类core表现形式往往不同,出现问题后检查一些容易导致栈溢出的函数会比较好一些。更进一步,在平时写代码时,注意使用strncpy代替strcpy等等操作,多线程全局变量是否符合预期等等,往往比辛苦追查core要更省时间一些。而对于出现的这种core,从栈上开始分析,前面提到的一些定理,也能分析出蛛丝马迹。

标签:core,bar,mov,rbp,gdb,指令,之五,corrupt,rsp
From: https://www.cnblogs.com/lidabo/p/17070249.html

相关文章

  • 浅试netcore缓存
    1.介绍我的理解缓存好比记忆细胞,浏览器请求就好比病毒。当我们经历过一次新冠感染后,身体便会产生对应记忆细胞,下次同种新冠再次入侵,我们体内便会快速调度记忆细胞进行识别......
  • 学习Ucore_lab体会
    纸上得来终觉浅,觉知此事要躬行使能中断物理内存管理虚拟内存管理内核线程管理用户进程管理进程调度同步和互斥文件系统 这些实验的名字都会让我心动。学习uc......
  • .NET Core 面试题
    值类型和引用类型的区别?值类型继承System.ValueType,例如:int、bool、double等,string为特殊的引用类型赋值:值类型会产生一个新的数据副本,而引用类型是直接赋值引用。内存......
  • Ucore_lab 5~8 相关
    实验五博客:https://kiprey.github.io/2020/08/uCore-5/实验书:https://learningos.github.io/ucore_os_webdocs/lab5/lab5_2_1_exercises.html源码 ......
  • Ubuntu22.04 x64 下运行.NET Core3.1程序 报错 usable version of libssl was found
    问题Ubuntu22.04x64下运行.NETCore3.1程序报错:usableversionoflibsslwasfound解决办法wgethttp://security.ubuntu.com/ubuntu/pool/main/o/openssl/libss......
  • Un petit pas, encore dans l'attente
    Jeudi,26janvier2023Jeviensderecevoiraujourd'huil'octroid'autorisationdetravailenBelgiqueetj'attendsencoreuneautorisationdelapartduminis......
  • ASP.NET Core+Element+SQL Server开发校园图书管理系统(二)
    随着技术的进步,跨平台开发已经成为了标配,在此大背景下,ASP.NETCore也应运而生。本文主要基于ASP.NETCore+Element+SqlServer开发一个校园图书管理系统为例,简述基于MVC三......
  • docker部署netcore项目
    1.部署历史猿友们好,作为初来实习的我,已经遭受社会的“毒打”,所以请容许我在下面环节适当吐槽,3Q!传统部署​ 回顾以往在服务器部署webapi项目(非独立发布),dotnet环境、守......
  • 新版TinyCore Linux系统安装
      1.设置软件仓库源echo"https://mirrors.163.com/tinycorelinux">/opt/tcemirror 2.安装启动加载器及其依赖tce-load-wi grub2-multi.tcz  liblvm2.tcz ......
  • 在不受支持的 Mac 上安装 macOS Ventura、Monterey、Big Sur (OpenCore Legacy Patche
    请访问原文链接:https://sysin.org/blog/install-macos-13-on-unsupported-mac/,查看最新版。原创作品,转载请保留出处。作者主页:www.sysin.org随着OpenCoreLegacyPatch......