0x1 背景知识
IAST 概念虽然很早就提出了,但是在实际使用情况中,因为其生产无侵入性与漏误报情况,实际使用效果还是不过的。
1.1 IAST
- DAST (Dynamic Application Security Testing)
- IAST (Interactive Application Security Testing)
- RASP(Runtime Application Self-protection)
- SAST (Static Application Security Testing)
属IAST 最模糊,与三者都有纠缠。
1.2 主动 与 被动
1.2.1 主动IAST
主动IAST很简单,可以简单理解为 动态IAST = DAST + RASP
1.2.2 被动IAST
被动型IAST是指使用动态污点分析技术,不需要扫描端,直接判断敏感参数是否为用户可控。插桩程序需要实现动态污点分析逻辑,实时监控程序污点数据变化,检测污点是否由source传播到sink点。
单看解释是有些疑问的:
- 如果经历了安全函数,理论上用户输入会被过滤,变得不可控,这个怎么在被动上实现?
- 如果只是判断用户输入是否可控,为什么需要整个source 到sink 的stack,只看source 和sink 不行吗?
0x2 DongTai IAST
洞态IAST,原灵芝IAST,是国内首款开源的IAST,采用的也是“被动IAST” 技术,带着以上的问题,我们探究探究DongTai IAST的实现。
建议先看看su18师傅的《洞态 IAST 试用》,参考【1】,su18师傅的文章写的极好。
2.1 前提
DongTai 开源的有很多安全项目,这里只关注这两个
- HXSecurity/DongTai: Server
- HXSecurity/DongTai-agent-java:Java Agent
DongTai 支持Java/Python/PHP/GO,这里仅对Java Agent 进行分析。
一开始以为Server功能比较简单,其实不是,Server也实现了一套污点逻辑,通过Agent上报的调用链进行判断,不过这里暂时先不关注。
2.2 Server
Server 采用Django + Celery,Celery broker 用的Redis,Django 数据库用的MySQL。重点关注自定义规则
2.2.1 规则
- 污点源方法 Source
- 传播方法 Propagate
- 过滤方法 Filter
- 危险防范 Sink
- 规则详情:方法的signature
- 污点输入:O/Px
- 污点输出:O/Px/R
- Hook深度:是否也允许Hook子类的方法
2.2.2 传播方法Propagate
如果分析过GadgetInspector 或者Tabby 的污点传播,那么对这些规则应该不会陌生。
传播方法描述的是一个污点在函数的路径,其中
- 污点输入可以是:
- this,也就是object
- args,方法的参数
- 污点输出可以是:
- this,可以是object、field
- return,返回值
- args,注意这里,也就是Tabby 对GI的改进点
Source、Filter、Sink 都可以看作一种特殊的传播方法propagate。
2.2.3 污点分析逻辑
其实在Server 上,也是有一套污点判断逻辑,@TODO 待补充。
2.3 Agent
2.3.1 Java Instrumentation 插桩
Java 两种插桩方式
- premain agent 方式
- agentmain attach 方式
1 | public class AgentMainTest { |
在transfrom 中实现动态修改类,从而实现方法的Hook。
在DongTai中,具体为io.dongtai.iast.agent.AgentLauncher
1 | io.dongtai.iast.agent.AgentLauncher#premain/agentmain |
install 会从server下载
- dongtai-core.jar
- dongtai-spy.jar
然后调用其中的install进行安装
1 | public boolean install() { |
1 | com.secnium.iast.core.AgentEngine#_init |
1 | IastClassFileTransformer#retransfrom |
2.3.2 retransfrom: Hook预处理
IastClassFileTransformer#retransfrom 中实现了对Hook类的修改。具体逻辑如下:
findForRetransform 发现所有的待Hook类,具体通过inst.getAllLoadedClasses 获取所有加载的类
通过configMatcher.isHookClassPoint(clazz) 简单筛选是否需要Hook,过一遍黑名单
通过classDiagram 获取该class的父类、接口
如果其中有在IastHookRuleModel.isHookClass()的,则会进入hook。isHookClass
1
2
3
4
5
6
7
8
9
10
11
12public boolean isHookClass(String className) {
return hookClassnames.contains(className) || superClassHookPoints.contains(className) || hookBySuffix(className);
}
private static boolean hookBySuffix(String classname) {
for (String suffix : instance.suffixHookPoints) {
if (classname.endsWith(suffix)) {
return true;
}
}
return false;
}
问题:可以看到,Hook点内置,并没有控制台配置的sources/propagate/filter/sink,是在哪里实现hook的呢?
答案:不要被hook这个词迷惑,这里的isHookClass只有在findForRetransform中被用到,用来标记需要重载的类,并不是所有需要hook的类,需要hook的泪是在transfrom中的plugins dispatch中实现的。
- 随后调用 retransformClasses() 会让类重新加载,从而使得注册的类修改器能够重新修改类的字节码,这要就会调用之前通过 addTransformer() 注册的 IASTClassFileTransformer 中重写的 transform() 方法。
2.3.3 transfrom
具体逻辑如下:
如果是iast的类,则不进行任何处理
设置DONGTAI_STATE 标志,表示是否是IAST 内部代码
实现SCA,这个后续再跟
判断当前类是否在hook点黑名单。在在blacklist.txt 里维护了个7W多的hook黑名单:
- agent自身的类
- 已知的框架类、中间件类
- 类名为null
- JDK内部类且不在hook点配置白名单中
- 接口
创建 ClassWriter,依然是使用 COMPUTE_FRAMES 自动计算帧的大小,并且重写了getCommonSuperClass() 方法,在计算两个类共同的父类时指定ClassLoader。
创建 IASTContext 上下文,初始化 PluginRegister,这个类中包含了一个全局常量 PLUGINS,里面保存了很多的处理插件,这些类都实现了 DispatchPlugin 接口,这个接口包含两个方法:
- dispatch():分发不同的 classVisitor 处理对应的类
- isMatch():判断是否命中当前插件
在 ClassVisitor 中又通过重写 visitMethod() ,注册继承至 AbstractAdviceAdapter 的实现类,这些类重写父类的 before()/after() ,实际上是 AdviceAdapter 的 onMethodEnter()/onMethodExit() 实现了字节码的插入。详见下面plugins 一节。
2.3.4 Plugins
默认内置的Plugins:
1 | public PluginRegister() { |
以DispatchShiro为例,重写了readSession方法,在内部增加了SpyDispatcherHandler#getDispatcher SpyDispatcher#isReplayRequest等逻辑调用,具体逻辑在SpyDispatcherImpl 中。
Plugins中,DispatchClassPlugin 比较特殊,dispatch中逻辑交由 PropagateAdviceAdapter/SinkAdviceAdapter/SourceAdviceAdapter处理,也就是控制台配置的Source、Sink、Propagate逻辑。
2.3.5 SpyDispatcherImpl
大部分的插桩逻辑都在SpyDispatcherImpl 中实现,比如SourceAdviceAdapter 的before。
IastClassFileTransformer 在初始化的时候进行了SpyDispatcherHandler的初始化SpyDispatcherImpl,以供全局使用。
0x3 功能分析
3.1 SCA
通过sendReport接口发送给server
3.2 漏洞判断
这块su18师傅已经讲了个大概逻辑,但是没具体讲污点传播,这里详细讲下。
以普通http漏洞为例,这个过程会经历
1 | DispatchClassPlugin#dispatch |
3.2.1 污点跟踪逻辑
重点关注全局变量EngineManager
EngineManager.TRACK_MAP
- 存的污点传播路径
- 当开始准备跟踪时,进行初始化,如SourceImpl#sloveSource.
- 当跟踪完成时,进行remove,如SpyDispatcherImpl#leaveHttp
EngineManager.TAINT_HASH_CODES
- 存的污点值的hash
EngineManager.TAINT_RANGES_POOL
- 存的污点值所在范围,污点一般为string,例如为hash所对应的string的子串
重点关注变量MethodEvent
其中
source = event.inValue / event.inValueString
target = event.outValue / event.outValueString
source:O = event.object
source:P = event.argumentArray
target:O = event.
target:P = event.
target:R = event.returnValue/event.OutValue
具体的流程如下:
1. HttpImpl#solveHttp
一次请求到达了应用程序,首先进入 http 节点处理逻辑,进行标记和预处理。
- 重放处理/标记
- 黑名单/后缀
- 增加http 头
2. SourceImpl#solveSource
请求进入到 source 点,将 event 放入 EngineManager.TRACK_MAP 中,将 source 的结果放入了 EngineManager.TAINT_POOL 污点池中。
Source的传播比较单一,一般只有
- O-R
- P-R
具体逻辑如下:
- 如果method return为空或者为int之类的无效污染,退出
- 如果method 为getAttrribute,那么仅允许白名单内的arg,这两步是为了执行效率考虑
- 将event 加入EngineManager.TRACK_MAP 中
- 将source 的结果放入了 EngineManager.TAINT_HASH_CODES/TAINT_RANGES_POOL 污点池中:如果source的结果是Map、Collection、Array之类的,会进一步遍历其所有值,都加入TAINT_HASH_CODES/TAINT_RANGES_POOL
疑问:solveSource 中只有P-R,没有处理O-R的情况,是漏掉了?
SourceImpl 是起点,起点的inValue 对结果并没有什么影响。重要的只是outValue,是整个污染连的起点。因此简单讲inValue设置成argumentArray并不会影响结果。
3. PropagatorImpl#solvePropagator
请求进入propagator 节点时,根据配置判断传播节点的参数是否存在于污点池中,如果是,则将传播节点 event 放入 EngineManager.TRACK_MAP 中。
Propagate有很多种传播逻辑:
- P-R
- P-O
- P-P 这个少见
- O-P
- O-O 这个也少见,只有一条规则java.lang.StringBuffer.setLength(int)
- O-R
除此之外,Propagate 还可以由多个条件组合,因此逻辑比较复杂。
入污点的处理逻辑如下:
如果source 为O,则inValue 为event.object,判断inValue 是否在历史的EngineManager.TAINT_HASH_CODES 中,如果在,则
- 将event 加入EngineManager.TRACK_MAP
- 调用setTarget,设置出污点
如果source 为Px,则inValue 为数组,其中每个值为event.argumentArray[x],判断event.argumentArray[x] 是否在历史的EngineManager.TAINT_HASH_CODES 中,如果在,则组成新的Array,赋值给inValue,同样
- 将event 加入EngineManager.TRACK_MAP
- 调用setTarget,设置出污点
如果有多条件组合
- 将每个条件分拆,存入inValues数组
- 每一个条件的inValue值过一遍污点池,不在的则剔除
- 这里多条件处理取巧了,如果有and,那么必须每个条件都满足conditionSources.length == condition
- 最后同样的将event.setInValue(inValues.toArray)
- 将event 加入EngineManager.TRACK_MAP
- 调用setTarget,设置出污点
出污点的处理逻辑如下:
如果target 为O
- event.setOutValue(event.object)
- trackTaintRange
如果target 为R
- event.setOutValue(event.returnValue);
- trackTaintRange
如果target 为P,与inValues类似,outValues 为每一个对应的argumentArray[x]
- event.setOutValue(outValues.toArray());
- 针对每一个x,trackTaintRange(propagator, event)
疑问:其中trackTaintRange 比较疑惑,还未发现其具体功能?
随着程序的多次调用,程序还会再次进入多次传播节点,这些节点也会被放入 EngineManager.TRACK_MAP 中。
疑问:P-P 逻辑中,都是从event.argumentArray 中获取参数,但是此时是在after中获取的,也就是执行完成,source P 其实已经变成了target P,这里可能存在问题,待验证。
TODO:这里有条规则,待验证。
4. SinkImpl#solveSink
应用程序走到最后的 sink 点时,根据 sink 点的配置,判断 sink 点的参数是不是在 TAINT_POOL 中,如果是,则将 sink 点写入 EngineManager.TRACK_MAP 中。
Sink传播逻辑也很简单,只有source:
- P
- O
具体逻辑如下:
- 如果是P,则获取对应P的值event.argumentArray[x],判断是否在污点池内,如果在则
- event.setInValue(sourceValue)
- 将event 加入EngineManager.TRACK_MAP
- 如果是O,则判断event.object 是否在污点池内,如果在则同样
- event.setInValue(event.object)
- 将event 加入EngineManager.TRACK_MAP
5. SpyDispatcherImpl#leaveHttp
在应用程序执行完,回到http 节点,最后执行到 leaveHttp 时,会调用 GraphBuilder 构造污点调用图并发送至云端。
3.3 其他漏洞检测
- 越权检测
- CRYPTO_WEEK_RANDOMNESS
- CRYPTO_BAD_MAC
- CRYPTO_BAC_CIPHERS
- COOKIE_FLAGS_MISSING
0x4 测试
暂时不关注,待后续有需求再补充
0x5 总结
被动IAST 的核心逻辑是整个污点链的跟踪,维护了一个污点hash表,从污点source,再通过默认收集的propagate 规则,收集其传播之后的污点值,都收集到污点hash表中,最终在sink中判断是否是污点成功传播至此。
分析过程中发现的一些问题:
- filter 规则貌似当作了普通的propagate 规则,并没有做其他处理,这个难道不会造成误报吗?
- propagate P-P传播的时候,都是取得event.argumentArray,这里可能存在问题,待测试。
- 比较依赖配置的propagate 规则,这里包含了所有的可能导致污点变化的传播,如果不全可能导致漏报。
- Array/Map 之类在传播过程中,会导致误报,因为规则仅支持P级粒度的映射。
在分析DongTai IAST之后,发现自己之前对被动IAST 完全误解了。
0x6 参考
- [1] 洞态 IAST 试用
- [2] 悬镜技术分享笔记——灰盒测试
- [3] HXSecurity/DongTai
- [4] HXSecurity/DongTai-agent-java