现在的软件正在逐渐定义一切,软件的形态也逐渐多样化,不仅仅是电脑或手机中看到的应用程序或App是软件,硬件设备等很多看不到的地方也正在有软件的构建和参与,比如汽车、电视、飞机、仓库、收银台等等,除了传感器之类的电子元件之外,硬件和软件的动作和数据都需要软件的参与,代码或多或少,形态或隐或现。

无论是什么样的软件,在开发过程中都会面临着Bug的发现和修复,而大的Bug往往能够在出厂之前或部署之前被开发人员或测试人员发现并修复,安全漏洞却没能有同样的“待遇”,就像司机全力以赴地将赶飞机的乘客载往目的地的时候,后备箱没关紧这样的事是不太容易注意到的。

1992年到2005年之间的十几年里,在美国至少有52人因为丰田卡罗拉汽车的意外加速(Unintended Acceleration)问题丧生,在事后对丰田汽车的代码审计中,安全专家们发现了多种代码安全设计和实现问题,包括缓冲区溢出、无效指针引用、竞争条件等等。

当时负责该案件代码审计的安全专家Michael Barr在报告中说道:

System safety can’t be an afterthought. It must be degisned from the very beginning into a system.

系统安全不应当事后被考虑,而是应该在系统非常早期时候被设计。

在多年的安全职业生涯中,无论是渗透测试、安全测评还是代码审计,笔者发现了各种各样的安全漏洞和风险,归纳起来大致有七种。

误解安全防护技术

这个问题在于工程师在系统设计时,对于安全防护技术只是有大概的了解,但并不全面,或不先进,在开发过程中,虽然确实采用了安全防护的技术,但并没有实现有效的防护。

最常见的情况是数据加密或哈希处理上,由于MD5实在深入人心,以至于很多开发人员谈起用户口令加密(哈希),就想到MD5,且只是简单地做单次MD5计算。

在一次内部的技术分享中,有个同事分享了一个站点的前端加密技术,该站点在用户登录时会对于账户信息进行前端加密处理,而后再发送服务端,虽然JS脚本做了一丢丢混淆,但代码结构和逻辑依旧清晰可见:貌似RSA的加密方式实则只是单次DES加密,通过明文、密文字符串组合来混淆加密的过程。

这个例子中,该站点没有实现真正意义上的慢加密,且采用了早已不被推荐使用的DES加密算法,以及自行构建了加密结构,这些全都并非最佳实践。

另一种常见的情况是误解安全防护配置,比如邮件安全配置中,很多企业邮箱仅配置了SPF,而没有进一步配置DMARC,这样不全面的安全配置并不能阻止邮件被伪造(参见笔者之前的文章《邮件安全防护及溯源》)。

所以在采纳一项安全技术之前,需要真正了解和认识这项技术的适用性和最佳实践,包括安全产品也是如此(参考笔者之前的文章《安全产品落地思路》),千万不可贸贸然用当下流行的,或记忆深处懂点皮毛的。

组件引用和隐匿安全设计

在系统设计和开发过程中,集成第三方SDK的情况非常多见,无论多么简单的系统都不大可能从零开发(软件开发的效率始终是作为“工程”被人们所追求的,开发语言、开发框架、开发模式的演变都源于此),而集成或引入的三方组件或功能可能缺少在系统应用场景下的安全测评,甚至三方组件或功能自身存在安全漏洞,而开发方又缺乏能力和精力对此进行安全测试,构建在三方组件之上的系统,无异于和这些组件、功能或代码一荣俱荣、一损俱损。

比如,多年前公司开展的酒店业务计划采购人脸识别设备作为无人值守前台使用,入住用户在线上预订酒店之后,到前台只需要刷身份证、刷脸确认身份即可入住房间。在试用国内某著名AI公司设计的这款设备时,发现利用系统引入的第三方输入法可以绕过识别系统进入设备的操作系统管理端,并在系统目录下看到所有刷身份证或刷脸人员的身份证照片和脸部照片,结合该设备的使用场景,一旦有人利用,就会成为开房记录查询机。

类似的问题在公共场所的很多智能设备中存在,比如玩具/饮料售卖机、导购机等等,还包括第三方开源组件或框架,比如log4j、fastjson、struts2等等。这就是软件供应链安全之所以检测和追溯难的原因,基于供应链各个环节过去的研发、支持和漏洞情况,可以分析可靠性、安全性,并为未来的选择做参考。

另外,是在系统设计时没有充分考虑应用场景,不必要的功能被保留或暴露,比如在当前版本的代码中包含了未暴露的下一个版本的接口,或者仅仅注释了测试环境的接口等等,认为不会有人无聊到探测这些接口。事实上,很多开发人员都不太理解怎么会有人会攻击自己开发的系统(他们为什么这么做呢)。隐匿即安全(security by obscurity)是对安全防护的错误理解,要么不做,做了的早晚被人发现,古人云,若要人不知除非己莫为。

系统设计忽略安全性

这样的情况常常发生在早期的基础互联网协议中,计算机和网络的先驱们是物理学或数学出身,他们在设计基础协议时候哪会想到后来的人们会这么复杂。同样,对于网络和系统威胁知之不多的开发人员在设计和开发系统时,也常常会忽略安全的设计,完全没想到会有什么的安全威胁,或者出于业务需求和安全设计的冲突而放弃安全设计。

比如,国内某个以CRM产品著称的公司,其产品功能允许客户自己编写代码并运行,来灵活解决所需要的数据处理或业务流程,虽然这些代码编写经过了封装和简化,并非真正意义上的开发,但依然能够构造Payload触发XSS漏洞,利用该漏洞可以在神不知鬼不觉中将系统数据外传,而无需人工下载数据。

类似的问题还包括大量的基础网络协议,比如DNS、NTP、ARP、HTTP、IP、FTP等,以及大量的基础软件、服务或框架,比如Redis、MongoDB、Flask等,或是没有考虑安全设计,或是没有考虑默认安全原则。

威胁建模的价值就在于基于需求和设计分析其中潜在的安全风险,并通过安全设计将这些风险进行规避、缓解或转移,从而降低安全实现和维护成本。

异常处理导致安全风险

在安全设计原则中有一条叫失效安全(fail-safe),即在系统发生异常或出现故障时也能够保持安全状态,在系统设计和实现中,异常处理也是常常被人遗忘,对于开发人员而言,异常的处理和安全似乎关系不大,更多是为了能够帮助系统尽可能保持正常或帮助技术人员判断故障。而实际上,异常处理的设计不佳会造成拒绝服务的安全问题或其他安全问题,业务可持续本身也属于安全的范畴。

比如,某个系统在设计上需要用户输入一串数字,系统在实现中采用链表的方式来记录输入的数字,并对这些数字进行排序处理,而链表处理排序的时间复杂度是O(nlogn),假如攻击者输入一大——长——串的数字,系统的排序处理可能会让自己卡死或发生故障。

又比如,某次笔者和老婆出门旅游时,在某地机场的旅游导航屏幕上,老婆不断用手上滑、下滑滚动页面,最终触发了页面报错,并绕过了展示应用,进入Windows操作系统。

异常处理的设计不仅仅是throw一个exception这么简单,还涉及到对于input合理性的校验和处理,以及一旦发生系统故障或崩溃时的安全防护状态,以及应对措施。

信任关系不连续或不一致

在软件功能设计或实现设计中,会因为业务需求而存在功能和功能之间、组件和组件之间的信任关系,当我们在使用A功能时便假定A功能所依赖的B功能也是安全的,应用A组件时便假定A组件关联的B组件也是安全的,以此类推。实际上,面对复杂的业务场景和需求,这样的信任关系常常会因为复杂性或迭代等原因而无法维系和评估,从而造成某个功能的安全风险引发雪崩式的或隔山打牛式的安全风险。

比如,我们曾经在对某个系统进行安全评估时发现,该系统中有一项功能是文件的安全保护,即经过保护的文件是加密的,无法被其他人直接访问的,但是经过安全防护的文件一旦可以打开,便可以通过另存为的方式直接、默认保存为明文,而无需做任何处理。在安全保护和另存为两个功能之间存在信任关系不一致的问题,两个功能不能如此简单的关联。

安全的防护是一个整体的设计和考量,需要贴合系统需求、功能和设计,基于业务、数据、逻辑分析安全状态,并弥补其中的薄弱环节,信任关系的问题可能会导致安全防护功亏一篑。

依赖单点安全防护能力

有一个安全原则叫纵深防御,是指对于安全防护需要层层建设,就像男人的小金库,除了小区和家庭已有的安全防护(保安、防盗门)之外,还需要保险柜、手机密码、账户密码等等,单一的防护能力被毁坏或绕过的可能性非常大。在软件设计中也是如此,开发人员对于系统安全不仅需要考虑功能功能安全实现,还要额外考虑安全功能实现,如果假设代码运行上下文结构或环境已经具备安全防护能力,便会忽视代码自身的安全性或设计的安全性。最常见的是运行环境有物理防护的情况。

比如,之前笔者对一款国产的信号机系统做过代码审计,在其中发现了潜在的几处安全漏洞,在特定的情况下可以造成交通灯信号的篡改,但信号机自身的部署和运行环境是路口上锁的信号箱,因此需要利用则需要先打卡信号箱,这些漏洞虽然得到了交通行业专家认同,但始终被厂商回避,大概是认为这样的几率是极小的。但仅靠信号箱的一把普通锁,是很难有效防护的。

无论运行的环境和上下文是怎样的安全防护,必要的系统设计层面的安全防护措施和手段需要充分考虑纵深防御策略,以及实施成本。

对场景或环境预估不足

系统的安全风险一大来源是具体运行的环境和使用场景,如果系统设计时不清楚场景和需求,由此所造成的安全风险和漏洞有些情况下的修复成本会非常高昂,甚至根本无法修复(修复影响业务,不修复影响安全性)。因此,威胁建模的本质是建立abuse case(用例滥用),假设用户不正常使用,甚至恶意攻击会怎么样。

比如,曾经因为工作缘故需要拜访某银行客户,疫情缘故在入园之前行方会先发送入园邀请短信至每个人的手机号,受邀人再根据短信链接填写个人信息(姓名、身份证号、手机号)以及健康信息(行程码、健康码)。这个场景下,系统没有考虑到接收邀请链接的人可能会将自己的链接发给同行的人填写(比如收到短信链接无法打开或提交失败),同行的人通过该链接便可以看到转发链接的人的所有个人信息和健康信息。这个漏洞简单,且修复成本不大,但设计上未考虑这样的“恶意”场景。

又比如,曾经所在的一家公司要做年终员工抽奖,方式是每个员工到人事部排队,通过在线抽奖系统抽取奖品,奖品从高到低,且并非人人都有奖。这个系统的开发落在笔者旁边工位旁边同事的身上,利用地利,在正式抽奖之前,先看到了这个简单系统的代码,大概逻辑是用两个字典分别记录员工工号、获奖奖品以及奖品序号和奖品(含无奖品),两个字典长度一致,且奖品的顺序是随机的,点击按钮系统开始轮询奖品字典,再点击停止则停止轮询,显示对应的奖品,如果中奖,则从字典中删除该奖品。这里的问题就在于删除的设计,假设有500名员工,300个奖品,越是先抽奖,不中奖的概率越大,当奖品字典越来越小时,中奖的概率随之也逐渐增加。由于抽奖顺序不是固定的,谁有空谁去人事部抽奖,因此那一年我故意晚去,并最终抽中一个二等奖。

安全属于业务的属性,因此需要契合业务的特点,清楚了解业务的场景、环境和使用人群,安全设计的目的不仅在于防护系统自身的安全,还在于避免使用者的不安全,和业务人员评估系统安全设计的合理性和必要性本身也是合理和必要的。

软件设计和开发如果要尽可能降低安全风险和安全漏洞,不仅需要具有丰富和扎实的技术实现能力,还需要有足够的安全攻防积累,做安全的开发相比做功能的开发犹如慢工出细活,在设计和实现的同时要不断思考可能的威胁并通过设计和实现解决,事前的安全设计因此显得尤为重要。