0x01 背景
如谭老师参考【4】中的《Java指针分析综述》一文中对于Java反射的静态分析研究简述:
- Livshits 借助指针分析解析反射关键API的字符串参数进而分析出反射调用的副作用,简称字符串分析方法。
- 谭、李老师提出的Elf 方案,思路大致是不依赖调用参数必须是字符串常量,而是根据其他信息,比如反射调用的参数类型、返回值的向下类型转换等。
- 还是谭、李老师推出的Solar 方案,不是很懂,原文复述下
- 集体推导技术(collective inference)
- 懒惰堆建模(lazy heap modeling), 懒惰堆建模用于分析由反射调用Class.newInstance()或 Constructor.newInstance()创建但具体类型在创建点未知的堆对象。对于这类对象,Solar 将其传播到程序中使用它们的位置,如向下类型转换,或Method.invoke()、Field.get()、Field.set()的反射调用点等,并更充分地利用程序中这些位置的类型信息以分析反射创建对象的具体类型。
0x02 Tai-e Solar
2.1 Invoke Demo Code
先准备待测试程序InvokeDemo
1 | import java.lang.reflect.Method; |
需要指定pta模式
1 | -cp /Users/m0d9/study/codeql-home/custom/test/query-tests/invoke/ |
2.2 测试
可以看到,识别的两个Invoke调用结果都正确:
- 第一个invoke识别为echo
- 第二个是不确定的,可以是InvokeDemo任意不带参数的方法
0x03 PTA前置知识
在前文指针分析中,简单提到过反射分析、污点分析都是由Tai-e PTA的plugin机制实现的。下面来看看具体的实现
3.1 Plugin机制
1 | PointerAnalysis#analyze |
在这一过程中会加载插件
3.1.1 插件接口
并且根据plugin实现接口,归类
- onNewPointsToSet
- onNewCallEdge
- onNewMethod
- ClassInitializer
- onNewStmt
- onNewCSMethod
- onUnresolvedCall
Invoke Plugin API
何时调用的Plugin 接口?答案是在DefaultSolver中
- initialize 接口中调用plugin.onStart
- anaylze接口中调用onNewPointsToSet 和onFinish
- addCSMethod 接口中调用plugin.onNewCSMethod
- processNewMethod 接口中调用plugin.onNewMethod 和onNewStmt
- processCall 接口中调用plugin.onUnresolvedCall
- processCallEdge 接口中调用plugin.onNewCallEdge
3.1.2 Load Plugins
1 | private static void setPlugin(Solver solver, AnalysisOptions options) { |
3.2 Plugins
在PTA分析中,默认会加载这些插件:
- AnalysisTimer: 用于计算分析时间
- EntryPointHandler:入口函数处理,目前为Main函数和隐式函数,比如Thread#run
- ClassInitializer:类关系及静态变量/代码块的处理
- ThreadHandler:
- NativeModeller
- ExceptionAnalysis
- LambdaAnalysis
- Java9StringConcatHandler
- ReflectionAnalysis
- TaintAnalysis
- ResultProcessor
有些可能跟反射分析关系不大,跟踪下一些不是一眼就能看出干什么的plugin
3.2.1 CompositePlugin
CompositePlugin 可以看成是plugin的集合
1 | // 所有的插件 |
3.2.2 ClassInitializer
ClassInitializer 顾名思义是用于处理class 相关
onNewMethod
1 | public void onNewMethod(JMethod method) { |
onNewStmt
1 | public void onNewStmt(Stmt stmt, JMethod container) { |
DefaultSolver#initializeClass
看看initializeClass 具体做了什么
1 | public void initializeClass(JClass cls) { |
Java 类加载的初始化过程中,编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生
<clinit\>()
方法。 如果类中没有静态语句和静态代码块,那可以不生成<clinit>()
方法。
总结下:
- DefaultSolver#initializedClasses 属性作用:记录所有的class
- initializeClass 方法的作用:
- 处理当前类及父类,保存在DefaultSolver#initializedClasses 中,已经在该变量中的不再进行处理
- 如果该类/父类有clinit方法(有静态变量/代码块),那么处理该clinit,加入分析逻辑(addCSMethod)
- 同时也讲clinit存入结果集中:csManager.mtdManager
JClass IR
3.2.3 EntryPointHandler
EntryPointHandler 也是个plugin,而且顺序放在第二
其作用是从入口函数开始分析
DefaultSolver#addEntryPoint
1 | private void processNewMethod(JMethod method) { |
- reachableMethods 可达method 增加该method
- 每个plugin执行onNewMethod
- 针对该method 的每个子stmt,调用所有plugin 的onNewStmt
ClassInitializer#onNewStmt
从此之后进进入类似JavaComipler Visitor的逻辑
DefaultSolver$StmtProcessor$Visitor
EntryPointHandler#onStart
1 | public void onStart() { |
- 手动置顶的MainClass
- 如果设置implicit-entries项为true,那么隐式入口也会被当作入口
solver#addEntryPoint 以这几个main函数为入口,进行CFG构建
通过method.getIR() 获取所包含的IR,然后进行遍历
1 | addCallEdge:808, DefaultSolver (pascal.taie.analysis.pta.core.solver) |
3.2.4 LambdaAnalysis
3.2.5 ThreadHandler
待分析
0x4 反射分析
反射分析有两个目录
- reflection
- invokedymanic
4.1 ReflectionAnalysis
ReflectionAnalysis 是用于处理Java反射的插件Plugin,分析具体的逻辑。
4.1.1 field
有几个特殊的field单独讲解下。
- csManager: 上下文管理
Model:
- logBasedModel:用于统计反射分析结果记录日志
- inferenceModel:推断反射调用,可能有以下取值
- StringBasedModel:通过String判断
- SolarModel:Solar算法
- DummyModel:do nothing
- reflectiveActionModel:和inferenceModel类似,老版本?
- annotationModel:处理注解
- othersModel:
4.1.2 setSolver
1 | public void setSolver(Solver solver) { |
4.1.3 on API
大致就是调用对应的Model API,顺序一般是
- inferenceModel: SolarModel
- reflectiveActionModel
- othersModel
onNewStmt
当遇到新的Stmt,调用以上的
1 | public void onNewStmt(Stmt stmt, JMethod container) { |
onNewPointsToSet
当新Var指向新PTS,调用Model的handleNewPointsToSet接口
1 | public void onNewPointsToSet(CSVar csVar, PointsToSet pts) { |
onNewCSMethod
1 | public void onNewCSMethod(CSMethod csMethod) { |
onUnresolvedCall
1 | public void onUnresolvedCall(CSObj recv, Context context, Invoke invoke) { |
onNewCallEdge
1 | public void onNewCallEdge(Edge<CSCallSite, CSMethod> edge) { |
4.2 Model & AbstractModel
Provides common functionalities for implementing API models.
个人理解是用于处理特殊API的接口。
4.2.1 Field
- isRelevantVar: 存的是<Var, Invoke>,用于判断是否处理过
- relevantVarIndexes: 存的是Method和参数地址
- handlers: 存的是待处理的InvokeHandler
isRelevantVar
判断是否是相关变量,逻辑是Model 接口存在
1 | // 存的是变量Var和Invoke调用 |
1 | public boolean isRelevantVar(Var var) { |
relevantVarIndexes
1 | // 存的是method 和相关参数index |
1 | // 存method和index |
以ReflectiveActionModel为例,relevantVarIndexes存的是InvokeHandler
4.2.2 InvokeHandler注解
1 | public abstract class AbstractModel extends SolverHolder implements Model { |
在初始化时,就会把带有InvokeHandler注解的方法,放在处理handlers中。
以SolarModel为例,当处理forName 方法时,就会调用Solar#classForName 方法进行处理。
4.2.3 其他API
handleNewInvoke
默认处理NewInvoke,再往上一层是ReflectionAnalysis#onNewStmt,结果是在relevantVars 里新增对应记录。
1 | ReflectionAnalysis#onNewStmt |
1 | public void handleNewInvoke(Invoke invoke) { |
handleNewPointsToSet
默认当处理CSVar指向新pts,调用对应handler处理
1 | ReflectionAnalysis#onNewPointsToSet |
1 | public void handleNewPointsToSet(CSVar csVar, PointsToSet pts) { |
4.3 SolarModel
最主要的两个Model
- SolarModel
- ReflectiveActionModel
先看SolarModel
4.3.1 fields
- typeMatcher:类型判断所用, todo
- typeSystem
- typeInfos
- handlers:上文AbstractModel解释的通过InvokeHandler注解注册的各种API处理方法
- solver: 原Solver
- csManager
- unsoundInvokes: 存的是暂时无法确定的invoke调用,会在每次invoke中尝试去解析
4.3.2 handlers
以Class.forName 为例
Class.forName(java.lang.String)
总结:根据参数string是否是常量,如果是常量,那么转换成常量对应的类obj,如果不是常量,那么生成一个unknow的obj。
1 | // 上文提到的InvokeHandler,遇到Class.forName 会调用当前method进行处理 |
疑问:对于
Class.forName(java.lang.String)
这类invoke,普通指针分析如何的逻辑?
可以看看DefaultSolver#processCall 的逻辑。
getMethods() & getDeclaredMethods()
思考:与上面的Class.forName
类似,针对r=o.getMethods()
,getMethods()要做的就是加上r var到o.getMethods 的所有可能method 对应的obj。
总结:因为返回是个Array,所以还需要考虑Array内部元素
- r 指向一个array,具体由helper.getMetaObjArray(invoke) 生成
- array[i]指向o 里的每个method 对应生成的obj
1 |
|
Method.getMethod & Method.getDeclaredMethod
针对r=o.getMethod(a1,a2)
总结:增加以下的指向
- var r 指向由o.method生成的obj
1 |
|
问题是如何根据a1和a2确定o对应的method?
1 | protected void classGetMethodKnown(Context context, Invoke invoke, |
疑问,没分析a2吗?
1 | Obj getMetaObj(Object classOrTypeOrMember) { |
Method.invoke(java.lang.Object,java.lang.Object[])
针对r=m.invoke(o,a)
总结:
1 |
|
Object.newInstance()
r=class.newInstance()
总结:增加一个指向关系,Var为r,obj为unknownObj
1 |
|
Object.newInstance(java.lang.Class,int)
r=class.newInstance(a1,a2)
总结:插入unsoundInvokes
1 |
|
4.4 ReflectiveActionModel
4.4.1 fields
- allTargets: MultiMap<Invoke, Object> 类型
4.4.2 handlers
前文分析已经得知,具体是通过InvokeHandler 注册handler 来进行处理匹配的method 逻辑。
1. Object.newInstance()
1 |
|
isInvalidTarget: 如果日志对反射调用 {@code invoke} 进行了注解、而给定的 {@code metaObj} 并非由日志生成,那么我们将{@code metaObj} 将被视为 {@code invoke} 的无效目标。
1 | private boolean isInvalidTarget(Invoke invoke, CSObj metaObj) { |
具体如何的逻辑?
newReflectiveObj
1 |
Object.newInstance(java.lang.Class,int)
1 |
|
Field.set(java.lang.Object,java.lang.Object)
1 |
|
Field.get(java.lang.Object)
1 |
|
Object.invoke(java.lang.Object,java.lang.Object[])
思考:针对r=m.invoke(o,a)
总结:
1 |
|
最主要的逻辑addReflectiveCallEdge: 增加一条反射调用边
1 | private void addReflectiveCallEdge( |
0x05 步骤跟踪
5.1 SolarModel#methodInvoke 第一次
调用栈
1 | methodInvoke:199, SolarModel (pascal.taie.analysis.pta.plugin.reflection) |
疑问:relevantVars 何时input值的
解读:
- var r3 即tt
- pts 为22行的new InvokeDemo()
- invoke 为jvm invokevirtual 动态调用
1 |
|
所以总体看看,干了啥
- unsoundInvokes,满足一定条件的invoke,会被放在这里
- 中间更新了obj、method、arg的pts
所以这里并没有直接解析出invoke的映射关系。
AbstractModel#getArgs
1 | /** |
- 第0个参试是$r6,不是csVar的$r3,那么通过csManager获取
1 | getCSVar:172, MapBasedCSManager$PointerManager (pascal.taie.analysis.pta.core.cs.element) |
疑问:csManager何时维护的?
- 第0个参试是$r3,是csVar,那么直接返回csVar的pts
typeMatcher.hasTypeInfo(invoke)
TypeMatcher.java
1 | boolean hasTypeInfo(Invoke invoke) { |
TypeMatcher#computeTypeInfo
作用是:执行程序内分析,计算反射调用 {@code invoke} 的可用类型信息。
难不成这里是关键?
1 | private static TypeInfo computeTypeInfo(Invoke invoke) { |
并未看到
通过对SolarModel的分析,我们并未发现对invoke 方法的推断逻辑
调试发现,这个关系在worlList的callEdges中
WorkList#addEntry 方法提供对callEdges的增加操作,其有一处usage
1 | DefaultSlover#processCall |
5.2 SolarModel#methodInvoke 第二次
1 | methodInvoke:199, SolarModel (pascal.taie.analysis.pta.plugin.reflection) |
此刻mtdObjs 已经有值了,并且是关联上了正确的pts:echo method
中间发生了啥?
5.3 SolarModel#getMethod
在处理getMethod时,如果发现method可以被定位,那么会调用classGetMethodKnown,进而还会把result重新入栈workList进行计算
注意此时的result 为method
对应的是invoke 的method,重新入栈之后会再次触发SolarModel#invoke,而这次method 的pts已经是已知的了。
1 | <init>:49, MockObj (pascal.taie.analysis.pta.core.heap) |
workList 中新增一条记录,invoke调用的返回Var$r6,Obj为该invoke调用
0x06 其他探索
6.1 编译优化
代码改成"ec"+"ho"
表达式
1 | // 1. 测试污点为参数 |
发现IR中仍然会将其拼接成”echo”,这步是在哪实现的呢?
- 编译阶段
- IR
答案是编译的时候进行了优化
6.2
拆分为变量
1 | String t = "ec"; |
此刻编译结果没有优化
IR:
看看Tai-e Solar反射分析结果
结论:未被识别出来
6.3
1 | String t = "ho"; |
此刻编译结果没有优化
IR:
看看Tai-e Solar反射分析结果
结论:未被识别出来
6.4
1 | String t = "echo"; |
此刻编译结果没有优化
IR:
看看Tai-e Solar反射分析结果
结论:可以被识别
0x07 总结与思考
- 指针分析需要知道入口点,如何能够保证全部类文件都分析到?