点击上方蓝字关注我们噢~
一提到软件安全,大家自然会想到“漏洞”这个关键词。一般来说,一款软件如果出了安全问题,那大概率是因为出了安全漏洞导致的。
对于开发人员来说,觉得这有些小题大做,认为漏洞无非就是一个缺陷或一个bug,改掉就行,没有什么特别的。对于安全人员来说,一个漏洞的利用其实是一门艺术,如果从源码层面来深究,会发现更多的奥妙。本篇先介绍漏洞与缺陷的区别,阐述实施安全编码的必要性,并以内存的源码攻防之争来说明安全攻防中常见的几种思路和方式。
“漏洞”与“缺陷”
我们先来看两张图,分别是CVE(Common Vulnerabilities and Exposures)和CWE(Common Weakness Enumeration)的一个实例:从中我们可以看到,CVE实例可以形成攻击,而CWE实例可能导致问题。通过此类比,我们可以初步来区分“漏洞”和“缺陷”。
漏洞:往往由一个或多个缺陷导致,容易被攻击者利用而形成攻击。缺陷:从质量视角来看,导致软件存在安全脆弱性,不过未必能直接形成攻击。必然和或然,反映到业务团队,往往会给出两种截然不同的处置态度。面对漏洞,业务团队往往会积极配合并火速整改,甚至“刮骨疗毒”也在所不惜。
面对缺陷,业务团队往往“讳疾忌医”,抱着侥幸心理,视而不见。
案例1:冲击波蠕虫
当缺陷转化成漏洞时,或然就变成了必然。2003年的冲击波蠕虫Worm.Blaster就是这样一个例子,由于软件源码实现时未对内存边界作限制,导致出现缓冲区溢出漏洞,进而引发了一波蠕虫病毒爆发。这次的事故教训也很惨痛,蠕虫波及全球,造成的经济损失高达5亿美元!
案例2:openssl心脏滴血
除了自己写的代码,你所引用的三方库或开源库代码也有可能会有“惊喜”。2015年,openssl“心脏滴血”漏洞波及全球(某同事以前说这个漏洞的应急响应处理差点也让他“心脏滴血”),问题源自于代码中的数据越界拷贝:导致出现内存敏感数据泄露,已登录用户密码数据被窃取。通过以上2个案例,我们可以看到,要想软件安全性高,实施安全的编码是一个很好的实践,减少了缺陷的出现,自然也就能减少潜在漏洞的发生。
源码漏洞类型和常见攻击手段
“内存之争”
最开始的漏洞利用,战场实际上就是内存,攻防双方在这个战场上展开“内存之争”。
Round 1
通过各种手段控制程序指令指针,一字曰“夺”
以x86平台为例,一旦攻击者发现了缓冲区溢出漏洞,便开始从内存中搜寻并覆盖可以控制CPU执行的返回地址区域:攻击者通过覆盖缓冲区,覆盖到返回地址区域,将其内存值改为自己指定的地址,那么就可以左右CPU的执行路径了。那么防御者如何防御呢,一个比较好的思路就是阻断你的覆盖,提前在返回地址区域前加上一个“陷阱”值,一般称之为安全Cookie,它是随机的,如果被覆盖,这个Cookie值就会发生变化(如果你运气好到爆,也有可能不会变化),从而判断是否有缓冲区溢出利用的行为。
Round2
如何绕过一些防护与限制,一字曰“绕”
既然防御者设置了“陷阱”,那就绕过这个保护。当然这个“绕”,是建立在你有足够知识了解的前提下,才能进行(“屯田日久,当见其功!”)既然绕不过“陷阱”,那就去针对设置“陷阱”的对象——保护功能代码。Windows平台下一般以SEH(Structured Exception Handling,结构化异常处理,总是会被拼写检查纠错成SHE)来进行缓冲区溢出保护,类似于C++里面的try…catch…,当你足够的覆盖就有可能覆盖到SEH的堆栈区域(或者有用覆盖虚函数表的,大体也是绕开安全cookie的思路),这个时候再覆盖保护功能代码的返回地址,那么原本的保护功能机制就失效了。于是防御方又来了个SAFESEH保护SEH,尝试阻断了这种过度覆盖方式的攻击。SAFESEH也是有条件的,有些模块可能不会被保护到,运气好捡漏的又会成为攻击对象。
Round3
如何巧妙执行shellcode,一字曰“借”
前面主要讲“夺”和“绕”,那么拿到CPU执行权,要怎么利用呢,这就涉及到shellcode的执行。所谓shellcode,就是一串具有一定代码意义的字节序列,能够直接或间接执行达到攻击者不可告人目的的代码逻辑。最初始的执行shellcode,比较粗暴,写到前面,再跳过来,由于程序地址是固定不变的,跑一万年也还是重复昨天的故事,所以写死地址直接跳也都能做到。再后来程序地址可能会变化,但系统动态库(dll)的地址不会变,于是先跳到一个具备jmp esp的代码片段,就可以接着覆盖后的内容直接继续执行。这种白嫖栈空间执行代码的行为,很快被防御者识破,于是取消栈空间执行代码的权利。攻击者们将“借”的思想发挥到了极致,由于程序在系统中实在是太复杂了,复杂到能够从里面摘取不同片段也能凑成一个利用的逻辑。于是“拼图”游戏开始了。把内存空间看做是一个字典,攻击者往里面检索需要的片段,这些片段本身又可以接连执行,于是只要跳到第一个地址,就可以“蹦蹦跳跳”执行完预期想要的逻辑。
这种攻击方式又称之为ROP(Return-oriented programming,面向返回编程),面对这种攻击,防御者应当如何防御呢?
Round4
随机与预测,一字曰“猜”
面对攻击者这种在内存中“蹦蹦跳跳”的行为,防御者提出了ASLR(Address Space Layout Randomization,地址空间布局随机化),让你找不到地方去跳。整个系统没有代码片段是事先知道的,没有了第一环,所以整个ROP链条就无法顺利构建。面对这种防御,攻击者采用了“普遍撒网”的策略,把大量构建好的shellcode指令进行复制,在内存中批量覆盖,就像拿着喷射枪四处乱喷,也叫做堆喷射攻击(Heap Spray)。这种喷射的载荷中,既包含要跳转的地址,也包含要执行的shellcode。攻击者将一些无意义的指令码也同时作为可跳转的地址,比如0x0c0c0c0c(对应汇编指令为OR AL,0C)。一旦覆盖到跳转地址,CPU便飞向0x0c0c0c0c这个地址,为了能够使载荷尽可能覆盖到这个地址,顺利执行shellcode,攻击者除了大量覆盖以外,还在shellcode前将0x0c0c0c0c这个字节当做滑板指令,便于CPU顺利着陆执行。这样的好处是不用掐分算秒地计算精确地址,差不多的地址就可以,前面就让CPU顺利滑行一段,执行无意义的指令作为前曲。面对攻击者如此“流氓”的覆盖,防御者也有对策(比如通过内存特征来判断是否有溢出覆盖行为等等)。“内存之争”的介绍就到此为止,相信攻防博弈也并未因此停止,各种先进的技术也不断演进出现。关于软件源码安全攻防系列的文章......未完待续。通过以上介绍,我们可以看到,正是这些攻防相抗,强化了我们软件的安全性,安全防御也从点防提升到面防,安全设计思想也随后被提出并被各大软件厂商广泛采纳。
长按关注 更多安全技术干货等你发现