利用 _RTC_CheckStackVars(...) 和 windbg 发现访问越界

来源:互联网 发布:linux定时重启服务 编辑:程序博客网 时间:2024/06/03 17:39

背景:在项目 debug 过程中遇到 failwithmessage 函数,但是程序正常运行,判断这个函数应该是类似 exception 之类的功能。查看调用栈,发现是 _RTC_CheckStackVars(...) 引起 failwithmessage 的调用。网上查到这个函数是检查栈变量完整性,很可能程序当中已经发生了访问越界。项目稳定性的要求比较高,需要捉一下虫子。

通过本文可以了解:

1. RTC API (run time check) 的基本原理。

2. 实际分析调用栈的形式。

3. 初步探索windbg调试的一些方法。


代码:猜测是访问越界导致,所以单写了代码越界的代码调试用

int main(){    int a[3] = { 0 };    int b[3] = { 0 };    a[3] = 3;    return 0;}

编译,并利用windbg调试,果然出现同样的函数调用栈。第一行是我们可以通过windbg查看的关于调用栈的所有信息。这里选了Addrs和Source args。第一列是每一个函数调用栈栈顶地址。函数名紧接着传进函数的参数的值。我们的调试函数是wmain,单步跟踪可以发现,在wmain完全退出之前(位于renturn之后)调用了_RTC_CheckStackVars。这是一个编译器行为。


图中高亮的函数是我们需要仔细分析的函数。通过搜索visual studio安装文件夹,找到了对应的定义文件。

C:\Program Files\Microsoft Visual Studio 12.0\VC\include\rtcapi.h

/* These unsupported functions are deprecated in native mode and not supported at all in /clr mode */_RTCINTERNAL_DEPRECATED  void   __fastcall _RTC_CheckStackVars(void *_Esp, _RTC_framedesc *_Fd);/* Compiler generated calls (unlikely to be used, even by power users) *//* Types */typedef struct _RTC_vardesc {    int addr;    int size;    char *name;} _RTC_vardesc;typedef struct _RTC_framedesc {    int varCount;    _RTC_vardesc *variables;} _RTC_framedesc
不太明白,网络搜索知道_RTC_CheckStackVars第一个参数是函数要检查的栈顶地址,第二个参数指向结构体_RTC_framedesc。该结构体第一个参数是压栈的局部变量个数,第二个参数保存局部变量相关信息。分别是变量的栈偏移地址,变量大小和变量的名字。因此我们可以通过地址跳转最终找到出错的变量。

首先查看_RTC_framedesc。0x0002代表检查的栈中变量的个数。0x00281434代表变量信息存储的地址。


接下来查看地址0x00281434以获得变量的信息。

第一个变量的偏移地址是0xffffffec,大小是0x0c,名称存储在0x28144e,可以看到是a(0x61)。第二个变量偏移地址是0xffffffd8,大小是0x0c,名字存储在0x28144c,b(0x62)。最后要找到出错的内存地址:栈顶地址+偏移量,得到是0x16faf8

高地址对应是a的内存,低地址是b。可以看出,对于debug版本,VS不止会为变量分配空间,而且给变量周围包围4个字节的boudary,均初始化成0xcccccccc。但读写越界的时候,这四个字节中的数据被改写,_RTC_CheckStackVars就会发现异常,并调用failwithmessage函数。而且查看该函数的栈空间,会发现Stack arround the variable 'a' corrupted字样。对应图中0x16fb04位置的内存已经从0xcccccccc改成0x03。

最后可以利用上述信息定位错误。重头开始运行程序,在进入main函数入口添加断点。查看变量a的地址是0x163f24,所以a[3]的地址是0x163f30。添加断点

ba w4 0x163f30

表示从0x163f30开始四个字节内的内存被写入时,进入断点。运行程序发现断点停在a[3] = 3。由此我们定位到了访问越界的根源。


相关的知识:

1. windbg的几种断点:bu - defferred breakpoints 每次会根据符号去计算断点地址。因此即使相应的模块还没有被加载,也可以通过它来设置断点。

                                 bp - original breakpoints    通过地址设置断点。

                                 ba - data breakpoints        可以针对指定内存的读写操作(另外一种是对指定代码)设置断点。

                                 bm - set breakpoints using symbol pattern 在设置符号断点的时候可以利用通配符。比如在一个类的成员函数入口都设置断点,bm module!class::*。

2. 为了验证猜想,修该代码为a[3] = 0xcccccccc。这个时候_RTC_CheckStackVars不再报错。

3. 在VS中关闭RTC(runtime check)。

              /RTC1 /RTCu /RTCc /RTCs      -         编译器开关。

              #pragma runtime_checks( "[runtime_checks]", {restore | off} )         -          宏编译开关。