浅析洞态iast产品

背景

之前的工作中处理过一些洞态iast的漏报误报案例,也逐渐了解这个项目。

本文记录我对洞态iast基本原理的理解,毕竟不记一下过段时间就忘了。

本文内容包括:

  • 洞态做漏洞检测的原理
  • 洞态中的污点是什么
  • 源码分析java-agent的业务逻辑
  • 举个例子:洞态怎么检测mybatis写的sql是否存在sql注入

怎么做漏洞检测?

如上,用户可以在server端配置四类规则:

  • 污点源方法:是获取api、rpc请求信息的接口或者类签名,比如javax.servlet.ServletRequest.getParameter(java.lang.String)
  • 传播方法:是字符串拼接、编码等接口或类签名,比如java.lang.String.<init>(java.lang.String)
  • 危险方法:是高危函数,比如javax.naming.Context.lookup(java.lang.String)

源码中有三个重要的数据结构,TAINT_POOL存放污点对象,TAINT_HASH_CODES存放污点对象的hashCode值,TRACK_MAP存放调用关系

当代码执行到被hook的传播方法时,会根据用户配置的”污点来源”规则,拿到对象(一般是函数的某个参数)去TAINT_POOLTAINT_HASH_CODES搜索匹配。如果能匹配上,就会根据用户配置的”污点去向”规则,生成污点对象并放到TAINT_POOL中,并将污点对象的hashCodes存放到TAINT_HASH_CODES中,最后将传播方法的调用关系存放到TRACK_MAP

当代码执行到被hook的危险方法时,和传播方法的逻辑比较类似,不过没有”污点去向”。

这里的”污点”是什么呢?

污点是什么?

最重要的概念是对象的hashcode/identifyHashCode,hashcode/identifyHashCode作为数据的唯一跟踪方法会被加入到污点池中,也会被用来判断是否在污点池中。

下面我带你通过一个我遇到过的误报案例来理解这个概念。

因为Java中相同字符串对象的hashcode/identifyHashCode是不变的,如下

1
2
3
4
String a = "123";
String b = "123";
System.out.println(System.identityHashCode(a)); // 1289696681
System.out.println(System.identityHashCode(b)); // 1289696681

所以有时候即使危险函数的参数完全不可控,也会报警。如下代码中的iast17接口之前会误报(现已修复),因为iast会认为f.getName()返回的字符串对象123是污点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ResponseBody
@RequestMapping("/iast17")
public String iast17(@RequestParam("name") String name) {
ArrayList<String> a = new ArrayList<>();
a.add("123");
a.add(name); // a对象会被标记成污点

Iterator<String> b = a.iterator();
System.out.println(b.next());
System.out.println(b.next()); // "123"会被标记成污点

File f = new File("123");
return f.getName(); // 返回值"123"被认为是可控的,会产生误报
}

iast为什么会认为”123”是污点呢?

因为执行a.add(name)时,下面的传播规则会使得a对象变成污点

在执行b.next()时,iterator.next()传播规则会让123字符串变成污点

流程浅析

collectMethodPool方法串联了”最重要”的业务流程。当java-agent启动时,会拉取server端规则,然后根据规则hook类,确保在被hook的方法执行前或者执行后能调用到collectMethodPool方法。在处理http请求时,collectMethodPool方法会判断当前是属于哪一类规则,并做对应的动作。

你可以从java-agent启动时和请求过来时两个场景来看业务逻辑。

java-agent启动时会找到所有jvm已经加载的类并重写字节码,如下

1
2
3
4
5
6
7
8
9
10
11
12
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/IastClassFileTransformer.java#L250

public void reTransform() {
...
Class<?>[] waitingReTransformClasses = findForRetransform(); // 找到所有待重写的类
...
for (Class<?> clazz : waitingReTransformClasses) {
...
inst.retransformClasses(clazz); // 用asm重新生成字节码
...
}
}

因此实现了对污点源方法、传播方法、危险方法的hook,并且使得执行方法前或者执行方法后,调用captureMethodState方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 污点源方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/SourceAdviceAdapter.java#L26
public class SourceAdviceAdapter extends AbstractAdviceAdapter {
...
@Override
protected void after(int opcode) {
...
captureMethodState(opcode, HookType.SOURCE.getValue(), true);
...
}

// 传播方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/PropagateAdviceAdapter.java#L31
public class PropagateAdviceAdapter extends AbstractAdviceAdapter {
...
@Override
protected void after(final int opcode) {
...
captureMethodState(opcode, HookType.PROPAGATOR.getValue(), true);
...
}

// 危险方法: https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/core/adapter/SinkAdviceAdapter.java#L31
public class SinkAdviceAdapter extends AbstractAdviceAdapter {
...
@Override
protected void before() {
...
captureMethodState(-1, HookType.SINK.getValue(), false);
...
}

captureMethodState 最终会调用collectMethodPool方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/plugin/AbstractAdviceAdapter.java#L103

protected void captureMethodState(
final int opcode,
final int hookValue,
final boolean captureRet
) {
...
invokeInterface(ASM_TYPE_SPY_DISPATCHER, SPY$collectMethodPool);
pop();
}

// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/bytecode/enhance/asm/AsmMethods.java#L131

Method SPY$collectMethodPool = InnerHelper.getAsmMethod(
SpyDispatcher.class,
"collectMethodPool",
...
);

请求过来时,就会执行到collectMethodPool方法,方法中根据hookType处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// https://github.com/HXSecurity/DongTai-agent-java/blob/v1.7.7/dongtai-core/src/main/java/io/dongtai/iast/core/handler/hookpoint/SpyDispatcherImpl.java#L462

@Override
public boolean collectMethodPool(Object instance, Object[] argumentArray, Object retValue, String framework,
String className, String matchClassName, String methodName, String methodSign, boolean isStatic,
int hookType) {
// hook点降级判断
...
// 尝试获取hook限速令牌,耗尽时降级
...

...
MethodEvent event = new MethodEvent(0, -1, className, matchClassName, methodName,
methodSign, methodSign, instance, argumentArray, retValue, framework, isStatic, null);
if (HookType.HTTP.equals(hookType)) {
HttpImpl.solveHttp(event);
} else if (HookType.RPC.equals(hookType)) {
solveRPC(framework, event);
} else if (HookType.PROPAGATOR.equals(hookType) && !EngineManager.TAINT_POOL.isEmpty()) { // 处理传播方法
PropagatorImpl.solvePropagator(event, INVOKE_ID_SEQUENCER);
} else if (HookType.SOURCE.equals(hookType)) { // 处理污点源方法
SourceImpl.solveSource(event, INVOKE_ID_SEQUENCER);
} else if (HookType.SINK.equals(hookType)) { // 处理危险方法
SinkImpl.solveSink(event);
}
...
}

举个例子:怎么检测接口是否存在SQL注入风险?

后端服务用mybatis时,${变量}的sql写法容易造成sql注入,而#{变量}底层会使用预编译通常不会产生sql注入问题,如下

1
2
3
4
5
// 第一个sql:存在sql注入
select * from user where name=${name}

// 第二个sql:不存在sql注入
select * from user where name=#{name}

当用户请求/user?name=admin时,iast是怎么检查出第一种接口存在SQL注入风险,而不会对第二种接口误报呢?

实际上如果我们调试一下,就知道#$的写法调用的sql接口是有区别的,如下

1
2
3
4
5
6
// 使用${name}时
conn.prepareStatement("select * from user where name="admin")

// 使用#{name}时
pstmt=conn.prepareStatement("select * from user where name=?)
pstmt.setString(1, "admin")

洞态iast默认有一个危险方法规则是java.sql.Connection.prepareStatement(java.lang.String),当第一个参数是污点时,就会告警,规则如下。

所以使用${name}时,admin字符串对象是污点,"select * from user where name="admin"字符串对象也会被标记成污点,于是命中危险方法规则,产生告警。

总结

学习iast时阅读官方文档和代码调试很有用,java-agent调试可以看 https://doc.dongtai.io/docs/development/dongtai-java-agent-doc/agent-debug