转载-0xCCCCCCCC,一则程序的汇编分析

作者:吉林小伙
链接:https://zhuanlan.zhihu.com/p/20697025
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

【相同主题】

Why is the stack filled with 0xCCCCCCCC - zray4u的个人空间 - 开源中国社区
http://my.oschina.net/ray1421/blog/714709

You are just seeing the code that's generated by the MSVC compiler when you use the /RTC option. Which enables runtime checks, turned on by default in the debug build. The value 0xcccccccc is magical, it is very good at crashing your program when you use an uninitialized pointer. Or generate a weird int value. Or crash your code when it goes bananas and start to execute data as though it is code. 0xcc is the x86 instruction for INT 3, it invokes a debugger break.

The "why this place" is part of the diagnostics you get from /RTC. It make the compiler allocate local variables with extra space between them. Filled by that magical value. Which makes it very simple to diagnose stack corruption caused by buffer overruns, it just needs to check if the magic values are still there when the function returns.


提到0xCCCCCCCC,大家都会联想到"烫烫烫烫",今天我们就围绕这个0xCCCCCCCC来说一说mscl干的那些事儿.

首先看代码:19213711_CfvM.png

在vs2013里Debug模式下编译这个函数,编译器会干些什么呢?我们看看汇编:

19213711_0UUv.png

注意前面的方框里的数字,是代表对应的行号产生的汇编代码,我们先来看一下19行的左大括号生成的汇编,首先注意:

mov eax,100CCh
call ConsoleApplication1!ILT+1020(__alloca_probe) (00432401)

顾名思义,就是检查一下是否可以分配出来0x100cc这么大的栈空间,具体的实现代码在VC\crt\src\intel\chkstk.asm里,注释的很清楚,为了减小篇幅,我就不贴实现了.下面简单介绍下这个chkstk.asm.

当你的函数内需要的栈空间超过已经提交的栈边界,编译器就会插入alloca_probe函数以便检查一下是否栈溢出,如果溢出,OS就会抛出_XCPT_UNABLE_TO_GROW_STACK异常,具体细节详细查看chkstk.asm里的注释.如果没有溢出,那么当alloca_probe返回时,esp就是新的stackframe.

好了,接下来就是这四条汇编指令:

lea edi,[ebp-100CCh]
mov ecx,4033h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]

首先把ebp - 0x100CC的值赋值给edi,然后把0x100CC/4(即0x4033)赋值给ecx,然后把eax赋值成0xCCCCCCCC,最后执行rep stos dword ptr es:[edi], 最后这条指令的作用就是,循环将eax里的值放到es:[edi]里,每次循环edi的值会自动加4, 循环次数ecx值会自动减1.这样当这四条指令执行完毕后,从ebp - 0x100CC到ebp这块内存地址里的值就都是0xCCCCCCCC了,也就是说我们在函数开头alloca_probe后得到的栈空间全部被初始化成了0xCCCCCCCC了,为什么要这么干呢?我们后面会分析.

我们接着看下面的汇编:

mov eax,dword ptr [ConsoleApplication1!__security_cookie (004b2010)]
xor eax,ebp
mov dword ptr [ebp-4],eax

第一条很简单,把0x004b2010里的值赋值给eax,然后将eax与ebp异或,然后将eax保存在ebp - 4的地方,ebp-4就是我们这个test函数所在frame的底部,紧挨着stack frame pointer的位置.这是做什么用的呢?看名字就能看出来了,是一个security_cookie,作用就是方便检查是否有缓冲区溢出,在函数的user code(20行生成的汇编)执行完毕后,将会调用security_check_cookie来检查ebp - 4这块内存里存放的值异或ebp后还是否等于0x004b2010里值.如果不等于,就证明ebp-4这块内存里的值别修改过,而这块内存在正常情况下是不应该被写的,造成ebp-4里的值被修改的原因可能是不小心写出了bug,也可能是有心人恶意利用缓冲区溢出进行攻击.  (note:怪不得之前看汇编代码一直感觉源代码里不可能这么些东西吧???原来是为了检查缓冲区溢出。要不编译器怎么可能有检查出这类错误并报错呢?难道是操作系统检查出来的?)

检查security_cookie的汇编代码如下:

mov ecx,dword ptr [ebp-4]
xor ecx,ebp
call ConsoleApplication1!ILT+6440(__security_check_cookie (0043392d)

最后就是这个:

add esp,100CCh
cmp ebp,esp
call ConsoleApplication1!ILT+4230(__RTC_CheckEsp) (0043308b)

这三行指令的第一条,就是传说中的"堆栈平衡"了,这样才能保证在最后ret的时候,去esp指向的地方能取到返回地址,才能继续执行下去,而这个_RTC_CheckEsp,就负责验证esp和ebp是否相等,如果不想等就会报错,以便你能发现堆栈不平衡的错误.一般情况下,堆栈平衡无需我们参与,编译器会自动平衡,但是在我们使用动态链接库的时候,如果我们声明的call convetion与动态库里的call convetion不一致,将会导致编译器生成的代码不能正确平衡堆栈,此时_RTC_CheckEsp就起作用了.

到目前只有这个_RTC_CheckStackVars没有说明了,这个函数是检查栈变量的,那么它是如何检查的呢?我在crt的代码里找到了这个函数的声明:

void __fastcall _RTC_CheckStackVars(void *_Esp, _RTC_framedesc *_Fd);

注意是__fastcall,这种call convetion前两个参数要放到ecx,edx里,由被调用者平衡堆栈.我们再看看这两个参数,第一个参数是esp的值,第二个参数是一个结构体指针,我们来看看这个结构体的声明:

 typedef struct _RTC_vardesc {
        int addr;
        int size;
        char *name;
    } _RTC_vardesc;

    typedef struct _RTC_framedesc {
        int varCount;
        _RTC_vardesc *variables;
    } _RTC_framedesc;

原来这个结构体里保存的是变量的个数和变量的描述.我们来看看test里是怎么传递的参数:

mov ecx,ebp
push eax
lea edx,[ConsoleApplication1!test+0x78 (00438508)]
call ConsoleApplication1!ILT+1535(_RTC_CheckStackVars (00432604)

前面已经说了,__fastcall的前两个参数会放入ecx,edx,对应汇编可知ecx就是ebp的值,即当前test所在frame的底部,edx是一个常量0x00438508,也就是_RTC_framedesc指针,为什么是常量呢?因为这是编译器生成并填充的结构体对象,并把对象直接写到了可执行程序里,然后当作参数并调用_RTC_CheckStackVars.

接下来我们就来看看_RTC_CheckStackVars是怎么检查的变量的:19213711_1Byi.png

我大概注释了一下这个函数的汇编代码,下面我把它写成c代码:

  1 typedef struct _RTC_vardesc
  2 {
  3     int addr;
  4     int size;
  5     char* name;
  6 }_RTC_vardesc;
  7 
  8 typedef struct _RTC_framedesc
  9 {
 10     int varCount;
 11     _RTC_vardesc *variables;
 12 }_RTC_framedesc;
 13 
 14 void __fastcall _RTC_CheckStackVars(void* _Esp, _RTC_framedesc* _Fd)
 15 {
 16     int index = 0;
 17     while(_Fd->varCount && index < _Fd->varCount)
 18     {
 19         if(*(uint32_t*)((int32_t)_Esp + _Fd->variables[index].addr - 4) != 0xCCCCCCCC ||
 20                 *(uint32_t*)((int32_t)_Esp + _Fd->variables[index].addr + _Fd->variables[index].size) != 0xCCCCCCCC)
 21         {
 22             _RTC_StackFailure(...);
 23         }
 24         index++;
 25     }
 26 }

大概是这个样子吧,其原理就是检查栈变量前后4个字节的值是否是0xCCCCCCCC了,现在你应该明白为什么函数开始要rep stos来填充成0xCCCCCCCC了吧,就是为了这里可以检查栈变量前后的内容是否还是0xCCCCCCCC(也可能是因为填充了0xCCCCCCCC,所以_RTC_CheckStackVars里才用的0xCCCCCCCC来检查);

好了,代码分析完了,我们用windbg来看一下我们分析的对不对:  (note:自己查了下windgb真的是windows下面调试程序必备的工具啊!有时间一定好好学习下。)

mov ecx,ebp
push eax
lea edx,[ConsoleApplication1!test+0x78 (00438508)]
call ConsoleApplication1!ILT+1535(_RTC_CheckStackVars (00432604)

我们前面说过,这个0x00438508就是_RTC_framedesc指针,我们这个test函数里就一个局部变量,我们试着查看一下其中的变量信息:

19213711_mI3F.png

首先查看0x00438508,第一个成员值是1,也就是varCount,第二个就是variables指针,值为0x00438510, 然后看第二个箭头,其中fffefff8就是成员addr,这是相对于ebp的偏移, 0000ffff就是size 65535, 0043851c就是名字了,看第三个箭头,恰好是arr.

我们来试着触发一下这个错误:

char a;
scanf("%x", &a);

这样随便输入一个数,就可以了,

错误如图所示,mscl真是业界良心,但是你输入的如果是cccccccc,虽然越界了,但这种机制就没办法查出来了.

ps:至于为什么填充0xCCCCCCCC,一直流传一种说法是因为int3的机器码恰好是0xCC.我觉得只是巧合而已.

转载于:https://my.oschina.net/ray1421/blog/714722

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值