查看原文
其他

软件源码安全攻防之道(上)

没羽箭 vivo千镜 2022-11-05

点击上方蓝字关注我们噢~


一提到软件安全,大家自然会想到“漏洞”这个关键词。一般来说,一款软件如果出了安全问题,那大概率是因为出了安全漏洞导致的

对于开发人员来说,觉得这有些小题大做,认为漏洞无非就是一个缺陷或一个bug,改掉就行,没有什么特别的。

对于安全人员来说,一个漏洞的利用其实是一门艺术,如果从源码层面来深究,会发现更多的奥妙。

本篇先介绍漏洞与缺陷的区别,阐述实施安全编码的必要性,并以内存的源码攻防之争来说明安全攻防中常见的几种思路和方式



01

“漏洞”与“缺陷”


我们先来看两张图,分别是CVE(Common Vulnerabilities and Exposures)和CWE(Common Weakness Enumeration)的一个实例:



从中我们可以看到,CVE实例可以形成攻击,而CWE实例可能导致问题。通过此类比,我们可以初步来区分“漏洞”“缺陷”

漏洞:往往由一个或多个缺陷导致,容易被攻击者利用而形成攻击。

缺陷:从质量视角来看,导致软件存在安全脆弱性,不过未必能直接形成攻击。

必然和或然,反映到业务团队,往往会给出两种截然不同的处置态度。

面对漏洞,业务团队往往会积极配合并火速整改,甚至“刮骨疗毒”也在所不惜。


面对缺陷,业务团队往往“讳疾忌医”,抱着侥幸心理,视而不见。


这两种态度都存在一定问题,难以长久处之。



案例1:冲击波蠕虫


当缺陷转化成漏洞时,或然就变成了必然。2003年的冲击波蠕虫Worm.Blaster就是这样一个例子,由于软件源码实现时未对内存边界作限制,导致出现缓冲区溢出漏洞,进而引发了一波蠕虫病毒爆发。


这次的事故教训也很惨痛,蠕虫波及全球,造成的经济损失高达5亿美元


案例2:openssl心脏滴血


除了自己写的代码,你所引用的三方库或开源库代码也有可能会有“惊喜”。

2015年,openssl“心脏滴血”漏洞波及全球(某同事以前说这个漏洞的应急响应处理差点也让他“心脏滴血”),问题源自于代码中的数据越界拷贝

导致出现内存敏感数据泄露,已登录用户密码数据被窃取。


通过以上2个案例,我们可以看到,要想软件安全性高,实施安全的编码是一个很好的实践,减少了缺陷的出现,自然也就能减少潜在漏洞的发生。




02

源码漏洞类型和常见攻击手段

“内存之争”


最开始的漏洞利用,战场实际上就是内存,攻防双方在这个战场上展开“内存之争”。


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顺利滑行一段,执行无意义的指令作为前曲。

如下就是利用堆喷射攻击的一个案例:


面对攻击者如此“流氓”的覆盖,防御者也有对策(比如通过内存特征来判断是否有溢出覆盖行为等等)。


“内存之争”的介绍就到此为止,相信攻防博弈也并未因此停止,各种先进的技术也不断演进出现。关于软件源码安全攻防系列的文章......未完待续。

通过以上介绍,我们可以看到,正是这些攻防相抗,强化了我们软件的安全性,安全防御也从点防提升到面防,安全设计思想也随后被提出并被各大软件厂商广泛采纳。



更多精彩阅读

Soot在Android组件NPE拒绝服务检测中的应用
如何用lint扫出不安全代码
如何用OLLVM来保护你的关键代码




长按关注  更多安全技术干货等你发现 


好文!在看吗?点一下鸭!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存