共计 9055 个字符,预计需要花费 23 分钟才能阅读完成。
作者:vivo 互联网安全团队 – Chen Haojie
本文基于笔者对 doop 动态程序剖析框架源代码和规则学习,并联合对目前破绽公开技术细节的学习,批改加强 doop app only 模式下的剖析规定后,实现通过 doop 工具辨认 commons text rce 破绽(CVE-2022-42889)。内容蕴含三局部,第一局部简略介绍 doop 剖析框架,第二局部简略介绍 commons text 破绽的原理和代码调用栈,第三局部重点介绍如何革新 doop app only 模式下的规定以辨认 commons text 破绽的污点信息流。
一、doop 动态剖析框架简介
1. doop 动态剖析框架简介
doop 动态剖析框架由希腊雅典大学 plast-lab Yannis Smaragdakis 团队设计开发,目前看是一款开源畛域的比拟先进的程序动态剖析框架,一些程序动态剖析论文的实践也有通过 doop 的规定实现后试验。
doop 整体架构简单明了,合乎通常动态代码破绽扫描工具扫描器内核的设计思路。架构上由 groovy 写的调用程序“粘合”在一起,通过调用 fact-generator 和 datalog 分析器,得出自动化的剖析后果。
上面是笔者画的 doop 整体架构图,蕴含 doop 中一些要害的组件模块:
2. doop 工作流程
- doop 的 fact generator 模块会对输出进行解析(例如 jar 包的解析或者类的 resolve 从而加载进必要的类信息到内存中)
- 调用 soot、wala 等工具生成 jimple IR,在此基础上生成后续剖析引擎须要的 facts 文件。而后 doop 应用 LogicBlox(目前 doop 已不保护)或者 Soufflé(开源的 datalog 剖析引擎)
- 基于 facts 文件和既定的 datalog 剖析规定文件进行剖析,失去最终的程序剖析后果。
doop 反对对 java 源码及字节码的剖析,不过源码的 jdk 版本受限,倡议间接应用字节码进行剖析。
doop 外围是其实现的一套 datalog 剖析规定,其中蕴含了由毛糙到精密的 context-insensitive、1-call-site-sensitive、1-call-site-sensitive+heap 的丰盛的动态程序剖析策略等等等,同时通过在 addons 中增加了额定的对信息流剖析、对 spring 等生态框架、对 java 反射个性的反对,非常弱小。
以上是对 doop 的架构和性能的简略介绍,jar 包信息的解析、规定的预处理、编译执行和解释执行、程序的并发设计或者因为大量 sootclass 加载造成的内存溢出问题等一些细节因为篇幅限度不在此介绍。
二、commons text rce 破绽简介
先对该破绽进行简略介绍。
Apache Commons Text 是一款解决字符串和文本块的开源我的项目,之前被披露存在 CVE-2022-42889 近程代码执行破绽,这个破绽目前网上的剖析文章比拟多,在此不做复述。该破绽原理上有点相似 log4j2,当然影响不可相比,其代码中存在能够造成代码执行的插值器,例如 ScriptStringLookup(当然这里提到这个插值器是因为咱们指标就是剖析这一条 sink 污点流),同时没有对输出字符串的安全性进行验证导致问题。
借用网上公开的 poc 触发 ScriptStringLookup 中的代码执行,应用 commons text 1.9 版本:
残缺的破绽调用栈如下:
从调用栈能够看出,通过调用 commons text 的字符串替换函数,能够调用到 ScriptStringLookup 类的 lookup 办法,从而调用 scriptEngine.eval 执行代码。能够看出该条破绽链路较浅,但链路要害节点也波及了接口抽象类的 cast、输出字符串的词法剖析状态机以及各种字符串的处理函数,作为试验对象十分适合。
三、commons text rce 污点信息流的 doop 辨认规定
咱们选取上述二中 commons text 中 org.apache.commons.text.StringSubstitutor replace 函数作为 source,ScriptEngine eval 函数作为 sink。
doop 设置 app only 模式去进行剖析,doop 在 app only 模式下会将!ApplicationMethod(?signature) 退出 isOpaqueMethod(?signature),这样一些剖析不会进入 jdk 的类中,能够大大提高 doop 的剖析效率。根据莱斯定理,动态程序剖析难以达到齐全的齐备 (truth 或者 perfect),也是尽可能优化 sound。相似在企业级的 SAST 部署应用也是如此,也须要在扫描精度、扫描速度以及理论可用性中进行取舍或者均衡,所以 doop 的 app only 模式下在集体看来更靠近理论嵌入到 devsecops 中的轻量级动态代码破绽扫描的利用。
3.1 doop 的 datalog 剖析规定简略介绍
因为波及 doop app only 规定的革新,首先先简略介绍 doop 应用的 datalog 规定。
doop 目前保护应用开源的 Soufflé剖析 datalog 规定。datalog 是申明式的编程语言,也是 prolog 语言的非图灵齐备子集,所以实质上也是建设在形式逻辑中的一阶逻辑上。所以根底概念也是命题推导,在 Soufflé的模式上就是体现为关系(relation)。
如下例子:
很显著能够看出该例子通过 datalog 定义的关系逻辑实现相等关系的自反性、对称性和传递性,首先定义了 equivalence 关系,该关系能够由 rel1 和 rel2 关系蕴涵失去,而 equivalence 的 a 须要满足关系 rel1,b 须要满足关系 rel2。具体语法和高阶个性能够通过 souffle-lang.github.io 网站进行理解。
3.2 doop 配置应用简略介绍
doop 能够通过 gradle 去编译应用,须要提前在类 unix 零碎中借助 cmake 编译装置 Soufflé,doop 的具体装置应用能够在 https://github.com/plast-lab/doop-mirror 中理解。
对 doop 的命令行应用进行简略,剖析,有几个要害的命令参数,- i 参数承受须要剖析的文件(例如 jar 包),- a 参数配置剖析策略(例如是抉择 context sensitive 还是 context insensitive),–app-only 参数配置开启 doop 的 app only 模式,–information-flow 开启 doop 的信息流剖析模式(能够用来做污点剖析),–platform 设置剖析须要的 jdk 平台,–fact-gen-cores 配置生成 facts 的并发性。
本文应用的 doop 命令参数:
-a context-insensitive –app-only –information-flow spring –fact-gen-cores 4 -i docs/commons-text.jar –platform java_8 –stats none
3.3 从新编译打包 commons text
这是我最后应用 doop 剖析 commos text 的办法,次要为了尽可能加重的对原生规定的侵入。doop 在应用 jackee 进行剖析事,剖析入口的确定及一些 mockobject 的构建都须要依赖于对 springmvc 注解的辨认。
下载 commons text 的源码,自定义两条 class 和 method 注解 TestctxTaintedClassAnnotation、TestctxTaintedParamAnnotation:
注解实现为一个空注解,次要是为了标注一下咱们的 source,将注解打到对应的 class 类和办法:
从新编译打包为 jar 包,失去 2 中命令参数 - i 的 commons-text.jar。
3.4 革新 doop app only 下的规定
doop 的污点信息流辨认依赖于指针剖析后果,同时也依赖污点转移函数。doop 中曾经预置了多条污点转移函数,其中蕴含了字符串、链表、迭代器等根底类办法。
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.Object)>").
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.String)>").
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.StringBuffer)>").
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.CharSequence)>").
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(char[])>").
ParamToBaseTaintTransferMethod(0, "<java.lang.StringBuffer: java.lang.StringBuffer append(char)>").
BaseToRetTaintTransferMethod("<java.lang.Float: float floatValue()>").
BaseToRetTaintTransferMethod("<java.lang.String: byte[] getBytes(java.lang.String)>").
BaseToRetTaintTransferMethod("<java.lang.String: char charAt(int)>").
BaseToRetTaintTransferMethod("<java.util.Enumeration: java.lang.Object nextElement()>").
BaseToRetTaintTransferMethod("<java.util.Iterator: java.lang.Object next()>").
BaseToRetTaintTransferMethod("<java.util.LinkedList: java.lang.Object clone()>").
BaseToRetTaintTransferMethod("<java.util.LinkedList: java.lang.Object get(int)>").
BaseToRetTaintTransferMethod("<java.util.Map: java.util.Set entrySet()>").
BaseToRetTaintTransferMethod("<java.util.Map$Entry: java.lang.Object getValue()>").
BaseToRetTaintTransferMethod("<java.util.Set: java.util.Iterator iterator()>").
BaseToRetTaintTransferMethod("<java.lang.String: char[] toCharArray()>").
BaseToRetTaintTransferMethod("<java.lang.String: java.lang.String intern()>").
然而其中没有蕴含 String split 函数的污点转移规定,须要增加上:
BaseToRetTaintTransferMethod("<java.lang.String: java.lang.String[] split(java.lang.String,int)>").
如上述,doop 自有的 jackee 规定必定没有蕴含咱们自定义的注解,所以须要在 EntryPointClass、Mockobj 等关系定义中增加对咱们自定义的 class 污点注解的辨认。
EntryPointClass(?type) :-
//...
Type_Annotation(?type, "org.apache.commons.text.TestctxTaintedClassAnnotation");
//...
MockObject(?mockObj, ?type) :-
//...
Type_Annotation(?type, "org.apache.commons.text.TestctxTaintedClassAnnotation");
同时也须要增加 param 污点的注解。doop 须要通过这些注解辨认剖析入口办法,构建污点 mockobj,建设初始的指向关系等。
//...
mainAnalysis.VarPointsTo(?hctx, cat(cat(cat(cat(?to, ":::"), ?type), ":::"), "ASSIGN"), ?ctx, ?to) :-
FormalParam(?idx, ?meth, ?to),
(Param_Annotation(?meth, ?idx, "org.springframework.web.bind.annotation.RequestParam");
Param_Annotation(?meth, ?idx, "org.springframework.web.bind.annotation.RequestBody");
Param_Annotation(?meth, ?idx, "org.apache.commons.text.TestctxTaintedParamAnnotation");
为了确保办法的可达性,咱们还增加了 ImplicitReachable(“”) :- isMethod(“”). 但后续看不肯定有必要,仅供参考。
通过注解咱们在规定中定义了 source,接下来须要定义 sink,咱们将 ScriptEngine 的 eval 办法定义为 sink:
LeakingSinkMethodArg("default", 0, method) :- isMethod(method), match("<javax.script.ScriptEngine: java.lang.Object eval[(].*[)]>", method).
正如前述,因为是在 app only 下,doop 下通过 OpaqueMethod 关系过滤了 jdk 类的辨认,这样会导致相应的上述预置的污点转移函数无奈实现污点转移,所以须要另外定制规定流去将转移函数蕴含进数据流剖析过程。
于是须要定义 OptTaintedtransMethodInvocationBase 关系。
.decl OptTaintedtransMethodInvocationBase(?invocation:MethodInvocation,?method:Method,?ctx:configuration.Context,?base:Var)
OptTaintedtransMethodInvocationBase(?invocation,?tomethod,?ctx,?base) :-
ReachableContext(?ctx, ?inmethod),
//Reachable(?inmethod),
Instruction_Method(?invocation, ?inmethod),
(_VirtualMethodInvocation(?invocation, _, ?tomethod, ?base, _);
_SpecialMethodInvocation(?invocation, _, ?tomethod, ?base, _)
).
在此基础上,为了实现新的污点转移,doop 须要依据以下自定义规定剖析出返回值的类型信息。
.decl MaytaintedInvocationInfo(?invocation:MethodInvocation,?type:Type,?ret:Var)
MaytaintedInvocationInfo(?invocation, ?type, ?ret) :-
Method_ReturnType(?method, ?type),
MethodInvocation_Method(?invocation, ?method),
AssignReturnValue(?invocation, ?ret).
.decl MaytaintedTypeForReturnValue(?type:Type, ?ret:Var, ?invocation:MethodInvocation)
MaytaintedTypeForReturnValue(?type, ?ret, ?invocation) :-
MaytaintedInvocationInfo(?invocation, ?type, ?ret),
!VarIsCast(?ret).
基于以上的污点转移过程剖析规定,利用到污点变量的转移剖析规定中。
VarIsTaintedFromVar(?type, ?ctx, ?ret, ?ctx, ?base) :-
//mainAnalysis.OptTaintedtransMethodInvocationBase(?invocation,?method,?base),
mainAnalysis.OptTaintedtransMethodInvocationBase(?invocation,?method,?ctx,?base),
MaytaintedTypeForReturnValue(?type, ?ret, ?invocation),
BaseToRetTaintTransferMethod(?method).
//mainAnalysis.VarPointsTo(_, _, ?ctx, ?base).
同时也须要从新定义 LeakingSinkVariable 关系,因为咱们这里自定义的 sink 办法也是 Opaque 办法,这样能力辨认到咱们的 ScriptEngine 的 eval 办法。
LeakingSinkVariable(?label, ?invocation, ?ctx, ?var) :-
LeakingSinkMethodArg(?label, ?index, ?tomethod),
mainAnalysis.OptTaintedtransMethodInvocationBase(?invocation,?tomethod,?ctx,?base),
//mainAnalysis.VarPointsTo(_, _, ?ctx, ?base),//here problem
ActualParam(?index, ?invocation, ?var).
从下面规定的定义能够看出,革新的流程还是比拟清晰的,并且通过关系的名字,这些关系的含意和用处也很容易了解。增加这些自定义规定到咱们的 doop 剖析中运行,在后果中能够看出,doop 实现了对 commons text 的污点信息流的辨认。
在后果集中的 LeakingTaintedInformation.csv 文件中能够找到咱们须要捕捉到的 souce-sink 流。
default default <<immutable-context>> <org.apache.commons.text.lookup.ScriptStringLookup: java.lang.String lookup(java.lang.String)>/javax.script.ScriptEngine.eval/0 <org.apache.commons.text.StringSubstitutor: java.lang.String replace(java.lang.String)>/@parameter0
LeakingTaintedInformation.csv 给出了污点信息。包含污点的标签(这里是默认的 default,能够自定义),sink 办法的调用信息,该 sink 办法对应的污点源头 souce 信息。
如上图能够看出,org.apache.commons.text.lookup.ScriptStringLookup: java.lang.String lookup(java.lang.String) 中调用到 javax.script.ScriptEngine.eval,并且污点的源头是 org.apache.commons.text.StringSubstitutor: java.lang.String replace(java.lang.String) 办法的参数 @parameter0。
同时,在后果集中的 AppTaintedVar.csv 文件也能够看到具体的利用代码中因为污点流传过程中的被净化的变量. 以下面 commons text 破绽执行办法栈中的
org.apache.commons.text.StringSubstitutor 的 resolveVariable 为例:
能够看出办法中被净化的入参 variableName、buf,还有 resolver,以及 $stack7 等 (这是通过 soot 生成 jimple 的过程中 SSA pack 局部优化新增的栈变量)。
基于这两个后果集根本能够看出破绽的触发流程或者说污点的流传过程(尽管不是特地直观),如果须要也能够再搭配生成的 CallGraphEdge.csv 去更不便的进行剖析。
四、总结
doop 间接用来剖析大型项目须要肯定的计算资源,并且无论是规定的定制还是剖析后果查看都不是特地直观,毕竟它的设计初衷就是一款剖析框架,用在理论漏扫破绽开掘中可能须要进一步包装批改。但能够看出,doop 作为一款优良的开源动态剖析框架,在算法上毋庸置疑是比拟先进和丰盛的,而且基于开源的算法规定,咱们能够任意去定制咱们须要的剖析逻辑。其与 codeql 在设计思路也较为相近,将程序信息提取后生成数据库,凋谢查问接口,将程序剖析转变为数据关系的查问,因而能够扩大出更多的用处。