长亭百川云 - 文章详情

初识CodeQL原理与漏洞挖掘过程

小陈的Life

58

2024-07-13

点击上方“蓝字”关注我们

正文共: 7618字 25图

预计阅读时间: 20分钟

前言

近期接触到的热词CodeQL比较多,花了点时间来了解下多次映入眼帘的热词,到底是什么。

codeql是github security lab开发的一种代码查询语言,可以利用codeql方便的进行代码的污点追踪分析,通过像SQL查询语言一样的对代码的查询方式,可以让使用者不用去过于关心污点追踪的实现细节,简而言之就是将代码比作一个大型的数据库,利用查询语句QL对数据库进行检索,定位到关键代码点。

具体的工作原理可以看下图:

CodeQL会将代码通过提取器(Extractor)将源文件转换成一个关系表达形式的层次结构,并将多个这样的层次结构导入到数据库中,生成为一个快照数据库,然后用QL查询语言和相关库类进行编译查询、计算并得出最后的查询结果(Query Results)。

污点分析技术介绍

污点分析技术(Taint Analysis)是信息流分析技术的一种实践方法,通过对系统中敏感数据进行标记,继而跟踪该标记在程序中的传播路径,以检测系统安全问题。这里引用由中国科学院几位研究员公布的《污点分析技术的原理和实践应用》中的案例分析。

污点分析的处理过程可以分为3个阶段:

  1. 识别污点源(Source)和汇聚点(Sink)

  2. 污点传播分析

  3. 无害处理(Sanitizer)

污点分析的首要目的就是识别明确敏感数据的污点源(Source),即用户可能输入的接口,并进行标记。如上图所示代码的第4行passwordText接口定义为Source,并对pwd变量进行污点标记。如果该污点变量进行相关传播(称之为污点传播)将继续标记其他变量。例如图中pwd变量在第5行的leakedPwd和第6行的leakedMessage变量都满足相关传播规则,则都将会被标记上。当被标记的变量到达漏洞处(Sink,污点汇聚点),就依据对应的安全策略进行检测。相反,如果pwd经过Encrypt方法处理过后,继续传播给sanitizedPwd就不会产生相关问题。所以如果污点变量经过一个使数据不再携带隐私信息的接口处理(Sanitizer,无害处理)后就可以移除该污点数据的标记。

污点分析技术也分有静态分析和动态分析:静态污点分析技术可以通过分析源码或字节码中语句或指令之间的静态依赖关系来判断污点标记所有可能的传播途径;动态污点分析技术可以借助程序插桩, 结合定制的硬件来跟踪污点标记的传播。可见, 污点分析既不必改变应用程序原有的编程模型或语言特性, 又可以提供精确的数据流传播跟踪。

在Eclipse上搭建CodeQL环境

在Eclipse IDE中打开Help->Install New Software并选择Add一条下载源:

CodeQL for Eclipse - http://update.semmle.com/ql-for-eclipse/latest

添加后出现下拉框,选择下列几个重要的

  • CodeQL for Eclipse

  • CodeQL for Eclipse Graph/Treemap Visualization and Reporting

  • CodeQL for Eclipse Java Libraries(根据自己的查询语言来选择)

  • CodeQL for Eclipse License

选择后安装,紧接着就能在新建项目中的Other找到CodeQL的项目了

但是到这里还没结束,还需要安装一个CodeQL专用数据库,可以通过两种方式选择创建数据库:

  • 利用CodeQL CLI工具创建数据库

  • 从LGTM上下载相对于语言的数据库

如果只是学习CodeQL的语法,建议可以去LGTM上下载一个数据库,如果是需要分析自己的代码,需要用CLI工具生成相关代码的数据库,感兴趣的可以看官网的使用文档:https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html

首先打开LGTM的搜索项目地址https://lgtm.com/search搜索某个项目,例如我搜索的java

随便点开一个项目

点击Query this project旁边的小箭头下载数据库工程

跳转到如下图页面

选择Java语言的数据库(依据自己所需查询的代码来选择)

随后打开eclipse选择File->Import->Existing Projects into Workspace

导入数据库工程

导入后会在Project Explorer中看到数据库的项目工程,此时再点击右键->CodeQL->Use This Database,当数据库工程名称前面出现[Current DB]时,说明数据库已经选中完毕。

至此数据库环境已经搭建完毕,现在再创建一个新的项目,将其转换成CodeQL项目!

按照正常步骤创建完一个新的项目后,右键工程项目,选中Configure->Convert to QL project

然后在项目中新建一个CodeQL Query文件,我这里命名为NewQuery.ql

进行简单的查询,关于查询语法可以看官方文档:https://help.semmle.com/QL/learn-ql/

如上就可以编写简单的CodeQL语句了,但是本文是介绍加漏洞分析,切记一定要自己使用CLI生成自己代码相关的数据库!(不然查不出数据就别找小编了/(ㄒoㄒ)/~~)

我先是在Eclipse上创建了一个Maven webapp架构的项目,目的为的就是等会方便转换成数据库。

命令:

`cd codeql-project``codeql database create ./database -s . --language=java --command="mvn install"`

不出意外终端会显示字样:

Successfully created database at D:\Program\Project\java\database.

最终结构如上图所示。

得到这个结构还不行,我们需要将其转换成支持eclipse解析的格式

codeql database bundle --include-uncompressed-source -o ./database.zip ./database

随后会生成一个database.zip文件,这个就和之前写的导入到eclipse中即可。

CodeQL数据流分析反序列化

CodeQL在DataFlow中定义了Node来表示数据可以流经的任何元素类,可以用成员函数asExpr()和asParameter()来进行映射。

`class Node {`  `/** Gets the expression corresponding to this node, if any. */`  `Expr asExpr() { ... }``   `  `/** Gets the parameter corresponding to this node, if any. */`  `Parameter asParameter() { ... }``   `  `...``}`

而在污点跟踪的过程中,使用的是全局数据流跟踪。其必须继承扩展类:DataFlow::Configuration

同时还需要重写isSource和isSink两个方法

`import semmle.code.java.dataflow.DataFlow``   ``class MyDataFlowConfiguration extends DataFlow::Configuration {`  `MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" } //定义Name``   `  `override predicate isSource(DataFlow::Node source) {`    `...`  `}``   `  `override predicate isSink(DataFlow::Node sink) {`    `...`  `}``}`

当然,如果从网上搜索的资料可以看到大都数代码都导入了一个semmle.code.java.security.DataFlow的包中RemoteUserInput来获取Source的输入,并通过成员谓词flowsTo来检测source能否流到sink处,但是在codeQL v1.20.0之后的版本在security目录下没有DataFlow.qll了。

局部数据流跟踪了解下就行,而平时对于代码审查我们使用的都是全局污点跟踪技术,同样需要继承自TaintTracking::Configuration库类。

`import semmle.code.java.dataflow.TaintTracking``   ``class MyTaintTrackingConfiguration extends TaintTracking::Configuration {`  `MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }``   `  `override predicate isSource(DataFlow::Node source) {`    `...`  `}``   `  `override predicate isSink(DataFlow::Node sink) {`    `...`  `}``}`

这里在CodeQL中已经封装好众多安全漏洞,在目录Security/CWE/下。这里直接引用CWE-502的UnsafeDeserialization.qll,在该封装库中已经定义了ObjectInputStream的readObject和readUnshared方法,以及常见的XMLDecoder等反序列化漏洞点检测。

这里先不说怎么找到的Sink点,后续在详细的做个解释。

我在这里编写了个反序列化的Filter,具体代码如下:

`public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {`    `HttpServletRequest httpRequest = (HttpServletRequest)request;`        `java.security.Principal user = httpRequest.getUserPrincipal();`        `if(user == null && readOnlyContext != null)`        `{`            `javax.servlet.ServletInputStream sis = request.getInputStream();`            `ObjectInputStream ois = new ObjectInputStream(sis);`            `Object mi = null;`            `try`            `{`                `mi = (Object)ois.readObject();`            `}`            `catch(ClassNotFoundException e)`            `{`                `throw new ServletException("Failed to read MarshalledInvocation", e);`            `}`            `request.setAttribute("MarshalledInvocation", mi);`        `}`        `chain.doFilter(request, response);`  `}`

这是之前Jboss的反序列化漏洞关键代码,直接搬过来做测试,因为是request请求,Source直接定义为所有的GET/POST请求内容用RemoteFlowSource来set。至于关键漏洞点(Sink),就是CodeQL的CWE-502封装好的UnsafeDeserializationSink类。

整体查询QL语句为:

`/**` `* @name Deserialization of user-controlled data` `* @description Deserializing user-controlled data may allow attackers to` `*              execute arbitrary code.` `* @kind path-problem` `* @problem.severity error` `* @precision high` `* @id java/unsafe-deserialization` `* @tags security` `*       external/cwe/cwe-502` `*/``   ``import java``import semmle.code.java.dataflow.FlowSources``import semmle.code.java.security.UnsafeDeserialization``import DataFlow::PathGraph``   ``class UnsafeDeserializationConfig extends TaintTracking::Configuration {`  `UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" }``   `  `override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }``   `  `override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink }``}``   ``from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf``where conf.hasFlowPath(source, sink)``select sink.getNode().(UnsafeDeserializationSink).getMethodAccess(), source, sink,`  `"Unsafe deserialization of $@.", source.getNode(), "user input"`

这是CodeQL中已经写好的查询语句,其中用@kind path-problem表示使用标准的CodeQL库的路径查询(附属说明,CodeQL查询时上面的/**/内并不是注释,而是给解释器看的属性)

也可以按照自己的想法定义返回格式,如下:

`from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf``where conf.hasFlowPath(source, sink)``select` `"Sink点:", sink.getNode().asExpr().getLocation(),``"Source点:",source.getNode().asExpr().getLocation(),``"Sink Method:",sink.getNode().(UnsafeDeserializationSink).getMethodAccess()`

如何定位Sink

之前有看到CodeQL是有封装好定位反序列化Sink的类,具体关键代码如下:

`class ObjectInputStreamReadObjectMethod extends Method {`  `ObjectInputStreamReadObjectMethod() {`    `this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and`    `(this.hasName("readObject") or this.hasName("readUnshared"))`  `}``}`

可能一时不太理解,这里我再分批次慢慢刨析讲

Method

首先需要理解的就是Method类,该类用于定位相关方法,例如下所述查询:

`import java``   ``from Method method``where method.hasName("readObject")``select method, method.getDeclaringType()`

查询到相关方法属性

而光靠Method定位还不足以准确定位,于是又引入了Class Name的方式辅以定位

具体方法hasQualifiedName:

`import java``   ``from Method method``where method.hasName("readObject") and method.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream")``select method,method.getDeclaringType()`

这样就定位到ObjectInputStream Class的readObject方法

通常查询的时候也会获取该类的直接或间接的父类,可以用getAnAncestor()

`import java``   ``from Method method``where method.hasName("readObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("java.io", "ObjectInputStream")``select method,method.getDeclaringType()`

所以查询可以根据不同的情况(例如有些方法并不是显示ObjectInputStream定义的,就可以通过查询直接或间接父类/子类的方法来定位)来选择查询函数。

MethodAccess

通常这里是用MethodAccess.getMethod()和Method查询结果做比较

用这种方法方便的是可以有类似超链接的方式跳转到关键代码处

`import java``   ``from MethodAccess call, Method method``where method.hasName("readObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("java.io", "ObjectInputStream") and call.getMethod() = method``select call`

查询后如下类似于一个蓝色的超链接

说到这里,聪明的你应该看得懂之前封装好的代码是如何定位到Sink的吧。如果还要不太理解的可以看我的Reference[4]。

Reference

[1].http://www.jos.org.cn/html/2017/4/5190.htm

[2].https://help.semmle.com/ql-for-eclipse/

[3].https://securitylab.github.com/research/insecure-deserialization

[4].https://xz.aliyun.com/t/7789


             

喜欢本文点个在看

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2