[crash分析]栈破坏分析总结

栈破坏的两种表现

1. 栈帧被破坏

栈帧被破坏,从寄存器上可以看到栈帧寄存器rbp异常。因为返回地址正常,所以我们能够看到返回地址函数。

//x86_64汇编
//栈帧被写坏后的栈表现
[New LWP 31418]
Core was generated by `./test1'.
Program terminated with signal 11, Segmentation fault.
#0  0x000000000040053b in test1 ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.5.x86_64
(gdb) bt
Python Exception  Cannot access memory at address 0xd00000014: 
(gdb) info reg
rax            0xd      13
rbx            0x0      0
rcx            0x400560 4195680
rdx            0xd      13
rsi            0x7ffd1a1ea4d8   140725041669336
rdi            0x1      1
rbp            0xd0000000c      0xd0000000c		//rbp被写坏
rsp            0x7ffd1a1ea3c0   0x7ffd1a1ea3c0
r8             0x7fd04605be80   140532504706688
r9             0x0      0
r10            0x7ffd1a1e9f20   140725041667872
r11            0x7fd045cb62e0   140532500882144
r12            0x4003e0 4195296
r13            0x7ffd1a1ea4d0   140725041669328
r14            0x0      0
r15            0x0      0
rip            0x40053b 0x40053b 
eflags         0x10202  [ IF RF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

2. 栈帧和返回地址都被破坏

栈帧和返回地址都被写坏。这种情况下栈层级函数展示都是??。

//x86_64汇编
//栈帧和返回地址都被写坏的表现
[New LWP 31551]
Core was generated by `./test2'.
Program terminated with signal 11, Segmentation fault.
#0  0x0000000f0000000e in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.5.x86_64
(gdb) bt
#0  0x0000000f0000000e in ?? ()
#1  0x0000000000000000 in ?? ()
(gdb) info reg
rax            0xf      15
rbx            0x0      0
rcx            0x400560 4195680
rdx            0xf      15
rsi            0x7fffd510d608   140736768038408
rdi            0x1      1
rbp            0xd0000000c      0xd0000000c		//栈帧被写坏
rsp            0x7fffd510d4f0   0x7fffd510d4f0
r8             0x7fe87dcc8e80   140636519698048
r9             0x0      0
r10            0x7fffd510d060   140736768036960
r11            0x7fe87d9232e0   140636515873504
r12            0x4003e0 4195296
r13            0x7fffd510d600   140736768038400
r14            0x0      0
r15            0x0      0
rip            0xf0000000e      0xf0000000e		//返回地址被写坏
eflags         0x10212  [ AF IF RF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

3. 不破坏栈帧和返回地址,破坏栈上局部变量

即只是覆写了栈上部分局部变量,并没有破坏到栈帧和返回地址。也经常会出现各种不可预期的错误。

栈破坏的造成原因

1. 数组越界

这个大家相对容易理解,函数中的局部变量数组,因为写越界后会向上覆写栈空间。当覆写了保存栈帧和返回地址后,会导致函数返回后指令异常,从而造成程序或系统崩溃。

上面的coredump就是用数组越界写的。

//示例代码
#include 
#include 
#include 

void test2(void)
{
    int a = 0;
    int b[8] = {0};
    int i = 0;

//i 循环14 则只覆写栈帧, 循环16则覆写栈帧和返回地址
    for (i = 0; i < 16; i++)
    {
        b[i] = i;
    }
}

void test1(void)
{
    int x = 0;
    int y = 0;

    test2();
}

int main(int argc, char *argv[])
{
    test1();
    return 0;
}

2. 错误的memset

//ARM 64汇编
#0  _TEST_StatReport (Fd=, Event=, 
    Arg=<error reading variable: Cannot access memory at address 0x20>) at testStat.c:2786
2786    testStat.c: No such file or directory.
[Current thread is 1 (LWP 4678)]
(gdb) info reg
x0             0x795398            7951256
x1             0x2                 2
x2             0x1                 1
x3             0x0                 0
x4             0x2                 2
x5             0x0                 0
x6             0xffffffbb          4294967227
x7             0x0                 0
x8             0x62                98
x9             0xffffffffffffb8aa  -18262
x10            0x6b9680            7050880
x11            0xc                 12
x12            0xffffffffffffffed  -19
x13            0xfffffffffffffe08  -504
x14            0x13                19
x15            0xffffffffffffffed  -19
x16            0x771608            7804424
x17            0xffff9310ee1c      281473149103644
x18            0x13                19
x19            0x478c44            4688964
x20            0x3                 3
x21            0xffff93134000      281473149255680
x22            0x412784            4269956
x23            0x0                 0
x24            0xffff92db2010      281473145577488
x25            0xffff9158c9e0      281473120258528
x26            0xffff93138318      281473149272856
x27            0x1000              4096
x28            0x801000            8392704
x29            0x0                 0		//栈帧被写坏
x30            0x4124b4            4269236
sp             0xffff9158c480      0xffff9158c480
pc             0x412750            0x412750 <_TEST_StatReport+852>
cpsr           0x60000000          [ EL=0 C Z ]
fpsr           0x10                16
fpcr           0x0                 0
(gdb)

对于栈帧被写坏,一般是子函数的临时变量操作越界,将保存栈帧的栈写坏导致。
我们需要排查 _TEST_StatReport 的子函数中,临时变量有没有操作不当导致越界的地方。

排查了数组越界可能后,继续查找存在错误memset的地方。

发现在子函数中存在这样一处错误。结构定义是TEST_CONF_WAN,但是memset时传入的却是sizeof(TEST_WanNode)。TEST_WanNode size被 TEST_CONF_WAN大了很多。导致将栈上局部变量和栈帧都覆写为0。在函数返回时栈帧弹出到x29栈帧寄存器中,因为栈帧异常,导致Segmentation fault。

bool TEST_Policy(int id)
{
    boll policy = false;
    TEST_CONF_WAN wan;
//...
    memset(&wan, 0, sizeof(TEST_WanNode));
//...
}

3. 本地变量地址作为入参,但变量类型不匹配

这个比较少见,之前公司mac客户端程序,调用curl库函数去下载网页,然后在释放curl的时候异常crash了。

//示例代码
int TEST_Download(const char *Url)
{
    int ret = 0;
    CURL *curl = NULL;
    int responseCode = 0;    

    curl = curl_easy_init();
    if (NULL == curl)
    {
        return -ENOMEM;
    }
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
    curl_easy_setopt(curl, CURLOPT_URL, Url);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _LW_WriteJsonfunc);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, AccessUnitJsonStr);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
     if (strstr(Url, "https"))
    {
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
    }
    ret = curl_easy_perform(curl);
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
 
  curl_easy_cleanup(curl);
  //...省略
}

因为curl地址是通过库函数创建的,在使用过程中也是调用库函数,并且没有修改curl指针的地方。为何到释放的时候,就地址异常crash了呢?

当时用了笨办法,在函数的每一行都加了log,打印curl指针地址。结果发现
调用了 curl_easy_getinfo 后,curl地址变了。

再仔细看了curl_easy_getinfo 库函数定义和当前栈布局后才明白问题原因。

CURLcode curl_easy_getinfo(CURL *handle, CURLINFO_RESPONSE_CODE, long *codep);

curl_easy_getinfo 的第三个参数定义是long型指针,而我们传入的是整型变量responseCode的地址。这会导致 curl_easy_getinfo 会将栈上 responseCode上面的变量的前4个字节覆写。而这正好是curl指针的前4个字节。curl指针被写坏,从而释放时异常crash。

后记:
其实栈被写坏就是局部变量操作越界导致。但常见的主要是数组越界和memset越界。
局部变量作为入参,因为数据类型不一致被覆写,实际上和memset越界有点类似。只不过这种表现更隐蔽一些,更不容易定位。

一旦遇到栈被写坏的情况,就要条件反射的去寻找局部变量的数组和memset操作,大多数问题便可以快速的找到原因并解决。

你可能感兴趣的:(Linux,Kernel,Crash,C语言,linux,crash,栈破坏)