代码审计方法论

代码审计方法论

一、定义

软件代码审计是在一个编程中对源代码旨在发现错误、安全漏洞或违反编程约定的项目。它是防御性程序设计范例,它试图在软件发布之前减少错误。C、C++、php源代码是最常见的审计代码,因为许多高级语言,如Python,具有较少的潜在易受攻击的函数(例如,不检查边界的函数)——维基百科

代码检查是审计工作中最常用的技术手段,实际应用中,采用“自动分析+人工验证”的方式进行。通常检查项目包括:系统所用开源框架、源代码设计、错误处理不当、直接对象引用、资源滥用、API滥用、后门代码发现等,通常能够识别如下代码中的风险点:

跨站脚本漏洞、跨站请求伪装漏洞、SQL注入漏洞、命令执行漏洞、日志伪造漏洞、参数篡改、密码明文存储、配置文件缺陷、路径操作错误、资源管理、不安全的Ajax调用、系统信息泄露、调试程序残留、第三方控件漏洞、文件上传漏洞、远程命令执行、远程代码执行、越权下载、授权绕过漏洞。

二、问题

1、代码与架构复杂

几十万、几百万行代码、一个业务分几十个模块几十个代码仓库家常便饭;开发语言多种多样,各种自研框架、流行框架应接不暇、架构还非常复杂。

以上两个问题对审计人员、SAST工具来说无疑都是很大的挑战。

2、工具准召率

没有工具是所谓银弹,官方规则、插件准召率很低,需要根据开发语言、编码风格自定义;工具对逻辑漏洞的无力,与业务逻辑漏洞大量曝光的漏洞态势之间的矛盾,工具、系统的运营也需要专门人力投入,从而不断提高工具的准召率。

心态 审计人员出于KPI的考虑,想着既然花了很长时间做了代码审计,为了体现工作量就必须说点什么,如果系统本来没有问题却在那挑刺,开发者会更加不信任你。对于甲方代码审计人员,审计任务多、代码庞大是常态,如果不考虑后果的只提高速度,这种方式会遗漏掉细节,导致不能全面的审查。

三、审计整体思路

准备工作 获得源码   要审计当然要先获得相应的源码,最好有相关文档等,资料越多越易于理解源码。

安装网站   在本地搭建网站,一边审计一边调试。通过跟踪各种动态变化了解源码的作用。

网站结构   浏览源码文件夹,了解该程序的大致目录

入口文件 index、admin文件一般是整个程序的入口,详细读一下index文件可以知道程序的架构、运行流程、包含那些配置文件,包含哪些过滤文件以及包含那些安全过滤文件,了解程序的业务逻辑。

配置文件   一般类似config等文件,保存一些数据库相关信息、程序的一些信息。先看看数据库编码,如果是gbk则可能存在宽字节注入。如果变量的值用双引号、则可能存在双引号解析代码执行的问题。

过滤功能 通过详读 公共函数文件 和 安全过滤文件 等文件,清晰掌握用户输入的数据,哪些被过滤,哪些无过滤,在哪里被过滤了,如何过滤的,能否绕过过滤的数据。过滤的方式是替换还是正则?有没有GPC?有没有使用addslasher()处理?

2、审计方法

2.1、自动手动结合

代码扫描工具->ast/正则表达式->复核

词法分析->语法分析->(语义分析->中间代码)->AST

按照预定的规则合并成一个个的标识tokens。同时,它会移除空白符,注释,等。最后,整个代码将被分割进一个tokens列表(或者说一维数组)。将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。解析器会删除一些没必要的标识tokens(比如不完整的括号),因此AST不是100%与源码匹配的,解析器100%覆盖所有代码结构生成树叫做CST(具体语法树)。以下介绍一些主流web开发语言的ast parser:

• 在线parser

• php-parsesr

• python-parser

• go-parser

• js-parser

• java-parser

结合开源、商业sast工具进行漏洞和风险扫描输出初步检测报告,然后人工复核准确性,不断优化sast扫描规则,同时提高漏洞挖掘速度。

2.2、黑白盒结合

黑盒求覆盖->白盒求重点

通过黑盒来全部覆盖所有业务cgi,同时通过白盒测试覆盖重要业务cgi,黑盒快速验证、白盒挖掘根因或者研究绕过策略,从而实现实现效率与重要性的平衡。另外,这里建议使用被动扫描器进行快速流量回放,实现流量采集与漏洞测试的同步执行,大大提高测试效率。同时也建议将cgi与功能整理出来,不然白盒审计在理解业务和代码逻辑上的成本会非常高。

2.3、正反向跟踪

• 正向:变量->函数/敏感操作->输出

$_GET->$param->eval($param)->return $param

从参数接收入口开始跟踪数据流,是一种正向追踪的思路。优点是方便理解程序整体框架,相对容易定位逻辑漏洞。但是在代码量较大、代码结构化不强的情况下人工审计成本很高,ROI可能不明显,这种情况下结合SAST工具进行审计效果会更好。

• 反向:函数/敏感操作回溯->输出

eval($param)->$param->$GET->return $param

根据敏感关键字来回溯传入的参数,是一种逆向追踪的思路。优点是仅需要搜索相应敏感关键字,就可以快速挖掘漏洞,可定向挖掘,高效,高质量,但也因为没有通读代码,对程序整体框架不熟悉,定位时会花费时间,逻辑漏洞挖掘较为困难。同理,这种情况下也需要结合SAST工具来做,因为SAST工具对敏感函数的控制流跟踪效果较好,方便人工后续回溯变量。

2.4、动静结合

(1)静态审计

通过静态分析源码,发现源码中的逻辑、数据处理、函数使用不当来确认源码中可能存在的漏洞。可以通过规则匹配和代码解析法开展,不过这块可以交给SAST工具去做。

(2)规则匹配

通过编写正则表达式匹配开发语言或者开发框架的默认变量与高危函数进行匹配,比如php的默认变量$GET、$POST,高危函数eval()、exec()等。

(3)代码解析法

通过解析代码的语法,分析出代码执行流程,这块如果采用自动化手段便属于前文所说AST,所以说不同思路之间也有相通之处。

(4)动态审计

通过运行需要审计的代码,结合使用断点调试的方法跟踪数据的流转来判断系统中是否存在漏洞。这块可以交给DAST或者IAST工具去做。注意一开始只需要关注我们关心的漏洞类型:高危文件、高危函数以及常规Web漏洞。

2.5、过往与当下结合

• 是否采用存在漏洞的框架版本;

• 是否出自同一个存在不良编码习惯或安全意识薄弱的开发者之手;

• 是否出现过漏洞,可能没彻底修复或者其他业务有类似的问题;

• 这个功能、模块、类、函数出现的漏洞是否可能出现在其他功能模块、类函数。

2.6、checklist与安全编码规范结合

梳理常见漏洞场景、常见漏洞类型、常见功能安全编码模板,对比实现方式,编码规范可以参考《OWASP安全编码规范》,相关checklist可以下一部分的内容。

2.7、通读与走读结合

类似英语阅读文章的阅读技巧,先粗略读一遍,再根据漏洞类型与业务场景跳着读。

• 首先要看程序的大体代码结构

如主目录有哪些文件,模块目录有哪些文件,插件目录有哪些文件,除了关注有哪些文件,还要注意文件的大小、创建时间。查看版本管理历史记录,大概了解整套代码更新迭代过程。

• 关注框架文件、核心功能

函数集文件(公共库类函数)、配置文件(看单引号、双引号,看全局配合或者路由)、安全过滤文件(防护代码)、index文件(入口文件),也看身份认证、权限控制、数据库操作、文件上传之类的核心功能实现逻辑。

四、业务落地

1、依赖代码仓库和业务基础信息

(1)安全资产管理:进行代码审计前必须有完善的资产大盘,了解业务、责任人、组织架构的对应关系,这样才能在明确安全态势的基础上选择代码审计的对象、优先级、方法、手段;

(2)代码获取:明确审计对象后,需要通过资产大盘找到开发团队,通过代码仓库的权限申请流程获取代码;

(3)架构、cgi接口、业务逻辑 审计前需要了解业务架构来初步评估审计范围,了解cgi接口和业务逻辑可加快审计速度,快速定位漏洞和风险。

2、平台化建设

不断增多的代码以及架构的复杂性决定了代码审计必须走平台化建设道路,通过将SAST工具与CI系统打通,实现自动化漏洞扫描,在发现漏洞后可自动创建漏洞工单,并按照已有的审计流程进行复核以及开展人工审计。

(1)CI系统与SAST工具:例如SAST工具SonarQube及其插件Find Security Bug与CI系统Jenkins就是一套低成本的实现方案;

(2)漏洞管理系统(JIRA):进入到代码审计阶段的企业,通常已经有相对完善的漏洞管理系统和工单处理流程,可以将SAST生成的报表解析、过滤后再推到漏洞管理系统;

(3)审计流程管理:这块主要明确审计过程的任务跟踪、检查覆盖率、ROI评价。

五、审计技巧

1、如何快速看懂代码

简单来说,便是关注开发模式、开发框架,读尽可能少的代码、从路由与cgi的映射关系入手,结合IDE去阅读代码。

(1)用mvc或者mvvm架构思维去理解:大部分web系统都是按照model(数据库模型)-view(用户交互)-controller(后台业务逻辑)的架构去设计开发的,近年来越来越流行的mvmm(model-view-viewmodel)架构也是在此基础上演变而来,两种区别在于mvvm的ViewControl抽离了mvc的Controller中的业务逻辑,实现业务逻辑组件的复用。审计时可以直接去Controller或者ViewModel中找相应的业务逻辑;

(2)区分前后端代: 前后端分离也是近年来流行的开发模式,前端部分通常只会出现xss漏洞,通过区分前后端代码,可以避免在前端代码部分浪费时间去定位sql注入、越权漏洞之类的后端逻辑才会出现的漏洞;

(3)先找路由与逻辑代码对应关系:路由通常出现在配置或者mvc架构的controller中,通过文件目录或者类函数的public属性来定位,或者在url路由表中直接定义;

(4)论IDE的重要性:IDE通常会显示类或者函数的Definition和Reference,通过链接可以快速跟踪代码调用,也可以使用内嵌的搜索功能快速定位关键字,比起命令行下查找关键字方便很多。

2、业务思维

这里强调业务思维,是说越了解业务,越能从开发者和用户角度去思考设计和使用上可能存在的安全问题,对逻辑漏洞挖掘非常重要。

(1)让业务提供应用程序的设计文档、架构文档,以便从功能上快速理解业务逻辑;

(2)始终记住用户可以控制请求的每一个方面,他们可以按照任意顺序访问多阶段功能,可以提交畸形数据,可以忽略某些参数,可以伪造某些参数,可以修改某些参数。因此在设计的时候一定尽可能要面面俱到;

(3)从各个角度考虑两个因素:应用程序如何处理用户的反常操作和输入的,不同代码组件与应用程序功能之间的相互依赖操作可能造成不利影响;

(4)考虑设计过程中做的每一个假设,并想象假设被违背的每种情况,尤其注意用户可以完全控制的假设条件。

3、如何提高准召率

主要关于如何提高准确性和完整性的问题。

(1)现代编程中关注代码调度、脚手架、统一框架、高度封装的问题:过滤和预编译等安全防护都在框架层或者公共class/function完成的可忽略;

(2)传入危险函数的参数用户不可控的可忽略;

(3)依赖组件扫描只是提供一种思路或者缩小审计范围,工具扫出后需要复核,如何存在问题的函数或者类业务并没有使用,不作为漏洞,只作风险提醒;

(4)接入层已使用https的,不需要关注HSTS等安全header异常,但仍建议程序自行配置安全header,特别是secure/httponly/X-Content-Type-Options:nosniff等不适合在接入层统一配置的;

(5)对于常见的应用场景,如文件操作、命令行操作、数据库操作、用户权限及认证等,我们需要了解框架的实现,给出相应的安全编码范例。框架文档中给出的例子并不一定就是最好的。安全侧必须对开发进行安全意识的培训,让他知道如何利用框架的API去安全的组合出常用功能。

对于应用漏洞挖掘,我们需要扩充字典。框架的封装,可能引入更多的危险API或危险特性。在代码审计的过程中,需要将这些内容加入到危险词字典中。

updatedupdated2023-01-012023-01-01