运行时和编译时的安全性检查

在研究HOOK API/PE INJECT等技术的时候经常会碰到需要将某个特性关闭的, 比如/RTCs, /INCREMENTAL等等. 究其原因很简单, 编译器在编译/链接的时候添加了一些我们不希望的代码, 而这些代码也正是我们在反汇编中经常碰到的. 为了方便今后的工作, 我想我们有必要找出这些隐藏在C/C++代码后面的东西, 看看vc在背后到底为我们做了哪些工作. 在本文中涉及到的vc特性有/RTC(c,u,s)、/GS、/INCREMENTAL.

 

 

RTC 表示运行时检查。RTC 有若干子选项。/RTC 设计用于调试版本,而不用于优化代码。当运行时检查失败的时候会又系统对话框提示,如果您不喜欢默认对话框,可以编写自己的处理程序。

 

              

/RTCc - 检测导致数据丢失的分配

当运行到ch = s的时候就会有错误,因为加了RTCc之后系统认为ch = s这个转化会发生截断,我们看汇编:

注释已经写的比较清楚了,不多解释。如果想避免这样的情况发生:ch = (char)(s & 0FF00)

 

           

/RTCu - 报告使用了未初始化的变量

我们还是通过一个例子:

当ch != 'o'的时候就会发生一个错误,因为我们使用了未初始化的变量。我们来看一下编译器是如何做到的:

  

  

/RTCs - 此选项在保护堆栈不被破坏方面采取了若干措施

        

  • 在每次调用函数时,将所有局部变量初始化为非零值。这样可以防止以前的调用对堆栈中的值的无意使用。
  • 在Debug模式下我们经常会看到这样的代码:

    这个代码其实就是在将栈上所有的局部变量全部初始化为CCCCCCCC, 这个汇编代码对应的源代码很简单: int t[10];

     

    那么为什么要用CCCCCCCC呢?

    CC其实就是int 3对应的指令,万一代码出错执行到这里就会引发一个异常。

     

      

  • 验证堆栈指针能够检查到堆栈破坏,例如,在一个位置将函数定义为 __stdcall,而在另一个位置将函数定义为 __cdecl 可导致堆栈破坏。
  • 我们通过下面的例子分析一下编译器是如何实现的:

     

    汇编代码如下:

    我们看到, 在这个例子中有两个地方对堆栈进行了检查, 分别是func和main函数返回之前. 为什么在func中有一条_asm{}却没有任何代码呢?原因是并不是任何函数在返回前都会进行检查, 据我观察如果一个函数没有内嵌汇编或者没有其他函数的调用那么就不进行堆栈检查. 而一条空的_asm{}足以让他进行堆栈检查.

      

  • 检测局部变量的溢出和不足。这与 /GS 不同,因为它仅适用于调试版本,并且检测缓冲区的两端以及所有缓冲区是否遭到破坏。
  • 如果我们使用了RTCs以后, 在Debug模式下这段代码就会弹出一个对话框告诉我们:  Run-Time Check Failure #2 - Stack around the variable 'arr' was corrupted. 我们还是来看汇编吧:

    再研究_RTC_CheckStackVars之前我们有必要研究一下t1,arr,t2的变量分布情况: cc cc cc cc 22 22 22 22 cc cc cc cc cc cc cc cc 00 00 00 00 00 00 cc cc cc cc cc cc cc cc cc cc 11 11 11 11 cc cc cc cc

    黄颜色标注的地方用于4字节对齐. 红颜色标注的表示跟没有使用RTCs不同的地方:局部变量并不是连续存放的, 在每一个局部变量前后都会有4个字节的填充, 这些额外填充的变量将在之后的_RTC_CheckStackVars中起到关键作用. 其实我们也大概可以猜到, 在_RTC_CheckStackVars中只要判断这个数组的前后四个字节是否依旧是CCCCCCCC就可以知道是不是越界访问了.

     

    _RTC_CheckStackVars的实现稍有复杂, 我把它转化成伪代码后供大家参考:

    基本上这个函数的实现我们已经了解, 但是对第二个参数似乎还不是很明白, 我们再回头看一下_RTC_CheckStackVars是怎么被调用的:

    看完下面数据, 我们应该很清楚_RTC_CheckStackVars的来龙去脉了:

    所有的这一切都是编译器在编译的时候悄无声息的安插在我们的代码中!

      

    /GS -- 缓冲区安全性检查

            如果使用 /GS 进行编译,将在程序中插入代码,以检测可能覆盖函数返回地址的缓冲区溢出。如果发生了缓冲区溢出,系统将向用户显示一个警告对话框,然后终止程序。这样,攻击者将无法控制应用程序。用户也可以编写自定义的错误处理例程,以代替默认对话框来处理错误。

            在返回地址之前将插入一个专门的 cookie(系列字节),以使得任何缓冲区溢出都将更改该 cookie。在函数返回之前,将测试 cookie 的值。如果 cookie 值已被更改,将会调用处理程序。

            此 cookie 由 C 运行库在程序启动时生成,攻击者将无法知晓 cookie 值,并且在每次运行程序时,该值都不相同。

            此编译器选项适用于已发布的代码。

    其实我们在之前的例子中已经接触到了, 我们删掉不相关的代码:

    其实大概的思想我们通过这段短短的代码已经知晓, 将一个特殊的数值保存ebp - 4的位置也就是所有局部变量之前. 然后在函数退出之前判断该数值有没有被修改, 如果被修改了就意味着返回地址遭到破坏, 报错.

     

    那么有几个问题值得我们思考:

    1. 这个特殊的数值是哪里来的?

    2. 为什么要拿这个数值跟ebp异或一下?

     

    我们就顺着这两个问题研究一下. 首先我们找到的是如下定义:

    从名字上分析似乎跟我们讨论的东西相近, 都是security_cookie相关的, 难道就是这个数值保存在ebp-4中么?当然要不是, 要不然hack起来毫无难度, 由此可见真正写入ebp-4的数值应该是随机的, 通过某种特殊的算法动态生成. 接下来我们有找到了这个函数:

    在这里我们看到在mainCRTStartup最开始调用了一个叫__security_init_cookie的函数, 这个正是之前我们讨论的算法, 也就是产生随机数的地方. 这里可以看出来, 为了取得好的随机性, 先是取出时间, 异或之, 然后是分别跟其他一些列具有随机性的数据(进程ID, 线程ID, TickCount和性能计数器)进行异或运算. 这个变量是全局的, 不会再改变。如果想要查看这个cookie变量的值. 可以再调试的时候拉出"即时窗口(Immediate)"在里面输入__security_cookie回车就能看到了.

     

    但是在汇编代码中我们看到并不是直接将这个数值写到了ebp-4中, 而是于ebp异或以后再保存到ebp-4中, 这样做有什么好处?

    1. 于ebp异或已经可以保证每个函数的cookie变量都是随机的, 更好的保证了随机性.

    2. 因为ebp的值也是不允许变的, 通过与ebp异或我们还能检查ebp是否被破坏, 一举两得!

     

    到此为止我们已经把vc运行时检查相关的内容介绍了一遍, 在结束本文之前, 我还想对另外一个vc特性提一下: 增量链接. 尽管这个特性跟运行时检查没有关系, 但是因为最近研究的一些东西经常跟这个特性有关.

           

    /INCREMENTAL(增量链接)

    那么在研究微软的实现细节之前, 我们先了解一下关于增量链接的基础知识:

    1. 默认情况下增量链接是打开的, 如果您确定您不需要这个特性, 请设置为/INCREMENTAL:NO

    2. 使用增量链接与为什么增量链接对程序的功能性没有影响.

    3. 使用增量链接的目的是为了加快链接的速度, 也就是为开发人员提供的.

    4. 使用了增量链接之后, 程序的大小会增加, 因为会有代码和数据的填充.

    5. 为了确保最终发布版本不包含填充或 thunk,请非增量链接您的程序.

     

    接下来我们还是通过一个简单的例子观察一下使用增量链接到底给我们带来了什么.

    这个例子再简单不过, 我们先来看看不使用增量链接时的情况:

    这个不需要多解释, 简单的说就是该干嘛干嘛. 现在让我们看一下使用增量链接的情况:

    使用增量链接以后, 当我们调用一个函数的时候并没有跳转的这个函数真正的地址去执行, 而是在内部维护了一个jmp table. 通过这个jmp table再jmp到真正的函数地址. 那么为什么说链接的速度加快了?我们先来考虑一下正常情况下链接的过程:

    比如有两个函数a(),b(), 在release版本下面为了使程序尽量紧凑, 这两个函数会连续的分布在代码段, 所谓连续就是说这两个函数之间没有空隙. 现在假设a的入口0x0400, 长度100. 这样b的入口就是400+100=500. 由于在开发过程中会不断地修改, 假设经过某一次修改以后a的大小变为150, 那么b的入口点也要往后挪50个字节, 也就是550. 这样程序中所有调用b的地方我们都需要做相应的修改, 由于这样的地方可能会很多, link的时间也会增加. 在Debug模式下, 情况略有不同, 在每一个函数后面都会相应地加上一些padding, 也就是最常见的CCCCCCCC(int 3). 加上这些padding以后, 之前说的情况有所缓解, 但是如果函数增加的内容超过了padding, 这时增量链接就发挥了作用. 增量链接把所有的函数调用集中到一个表里(ILT, 在debug的时候我们经常看到的@ILT+305(...) 就是源自这个表), 这样我们就不需要把每个调用的地方一一修改, 唯一需要修改的就是ILT中对应的函数地址, 因为所有对该函数的调用都要通过到这个表.

     

    启用增量链接以后有一个问题需要注意: 函数的地址不是真正的地址了!这个问题尤其是在研究Hook API/CreateRemoteThread等东东的时候尤其值得注意. 看下面的例子:

    ThreadFunc即将被我们写入另外的进程中. 如果启用了增量链接以后我们写入的不是真正ThreadFunc的内容, 而是ILT中从jmp ThreadFunc开始的大小为cbCodeSize个字节的内容.

     

    虽然基本上增量链接的知识都了解了, 但是我不得不承认还有一个问题我没搞明白. 假设有10个CPP, 已经编译出10个obj和最终的exe文件. 现在我们修改了其中一个CPP中的一个函数, 编译以后一个新的obj就生成了(原exe文件还存在). 这个时候链接跟exe不存在的时候链接有什么区别?为了使增量链接有效果, 肯定不是从零生成一个exe, 而是对原有的exe进行一些修改. 但是这个过程我不了解, 我不知道链接器是如何对现有exe进行更新的. 如果有朋友明白这个过程, 请务必指点. 感激不尽!

     

     

     

     

      

    参考:

    http://msdn.microsoft.com/en-us/aa289171(zh-cn,VS.71).aspx

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值