Codeql学习一

前言

受形式所迫,ai被污染的比较强,因此,很多东西,最佳的选择是去官方文档进行学习,搭配实战训练,新旧版本的差别较大,这里,尽量多用Codeql去分析代码,提炼习惯,纯英文,逼自己一句一句的好好读,好好理解
java+python(以后会更)

此前学习过一遍Codeql的基本语法,但是不太满意,感觉没有完全学明白,且时常在新旧之间搞混,所以,为了提高熟练度,以及给后来者一些稍稍正确的方向,还是记录一下这个过程

简单的例子

注意

官方文档这个MethodAccess错了()之后已经重写为MethodCall了,望周知

学习

嗯,打算好好推推进度,从一个例子开始学习

1
2
3
4
5
6
7
import java

from MethodCall ma
where
ma.getMethod().hasName("equals") and
ma.getArgument(0).(StringLiteral).getValue() = ""
select ma, "This comparison to empty string is inefficient, use isEmpty() instead."

这是一个合法的查询语句
在初始导入语句之后,这个简单的查询包含三个部分,功能类似于SQL查询中的FROM、WHERE和SELECT部分,鄙人虽然sql没咋学,基本的还是能理解的
MethodCall: 某个方法被调用的位置 ,可以通过它获得调用的目标方法 getMethod() ,表达式 getQualifier() ,参数 getArgument(i) ,位置getLocation()
在where处即进行进一步过滤
where ma.getMethod().hasName("equals") and ma.getArgument(0).(StringLiteral).getValue()=""
可以看到,这里的过滤规则有两条

1
首先,该调用的方法名是"equals";其次,该方法的第一个参数是字符串字面量,并且值为空

这里用的hasName是一个谓词:强调条件是否成立;而getName是返回一个字符值,需区分,明显用谓词更简洁方便

select ma,"This comparison to empty string is in efficient,use isEmpty() instead."
这里就是输出了,ma是 代码位置(AST 节点) ,后续是描述
这里我们知道,Codeql的结果必须是一个代码位置,一个相关信息描述,这是codeql的规范

如上,我们看到了,整个功能是在优化代码,找到.equals("")的冗余代码,转为isEmpty()

我们知道,我们现在只是 为了检测 _String.equals(“”) _

1
2
3
4
5
public class TestJava {
void myJavaFun(Object o) {
boolean b = o.equals("");
}
}

这里这种也会被检测,如何提高查询精度呢?
添加一句即可ma.getQualifier().getType() instanceof TypeString_ __ _
继续介绍这几个新家伙

  • getQualifier(),我们知道ma是调用,getMethod()是方法,而这个,就是调用该方法的对象
  • getType(),返回Java编译器看到的该对象的类型,String or Object
  • TypeString, Java 中的 java.lang.String 类型,是RefType( Any reference type(类、接口);Type是 any type )的一个特化

上述,我们就完成了一个简短的ql查询,还是很简单的~学会这些,我们已经可以开始辅助我们的审计了

一些数据类型的查询方式

跟进第二讲,我们学习一下,在Codeql中,有哪些查询方式
算基础,帮助我们更精准的找到想要的结果

首先,关于Java库里,有五大类

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
用于表示“语言结构”的类
Class;Method;Field;Packag;Parameter
这些类用来查找“声明”。

用于表示源代码结构(语法树)的类
Expr(表达式)
Stmt(语句)
MethodCall(方法调用)
AssignExpr(赋值)
这是你写查询时最常用的部分

表示注解、JavaDoc、源代码注释,例如:
Annotation
Comment
DocComment
用于分析代码标注或注解话题。
用于计算代码指标,例如:
CyclomaticComplexity(圈复杂度)
CouplingBetweenObjects
LinesOfCode
适用于代码规范/质量分析。

用于分析调用图(call graph),例如:
Call
Callable
Method.getACall()
Method.getACallee()
适合做安全分析、寻找数据流路径(sources-to-sink)。

简单跟着学习一些,熟悉熟悉

Type
1
2
3
4
5
6
PrimitiveType represents a primitive type, that is, one of boolean, byte, char, double, float, int, long, short; QL also classifies void and <nulltype> (the type of the null literal) as primitive types.
RefType represents a reference (that is, non-primitive) type; it in turn has several subclasses:
Class represents a Java class.
Interface represents a Java interface.
EnumType represents a Java enum type.
Array represents a Java array type.

查询int类型的值

1
2
3
4
5
6
import java

from Variable v, PrimitiveType pt
where pt = v.getType() and
pt.hasName("int")
select v
  • PrimitiveType为基本类型,可以对一个变量getType获取

如果想要查询一些特定类呢?
这里,我们可以借助Codeql的查询

CodeQL 类 含义
TopLevelClass Java 文件里最外层的 class
NestedClass 在别的类里声明的 class
LocalClass 在方法或构造函数内部定义的 class
AnonymousClass new 接口() 或 new 类() { … } 这种匿名类

我们也可以通过下述方法查询单例类
CodeQL 提供了如:

  • TypeString
  • TypeObject
  • TypeCloneable
  • TypeSerializable
  • TypeSystem
  • TypeClass
  • TypeRuntime

这些类是 单例(singleton),每个都对应 Java 标准库中一个具体的类。

例如:

  • TypeStringjava.lang.String
  • TypeObjectjava.lang.Object

你不能 new 它们,但可以用它们做类型判断。

看几个例子

1
2
3
4
5
import java

from TopLevelType tl
where tl.getName() != tl.getCompilationUnit().getName()
select tl
  • getCompilationUnit()这个方法,很好用,就是,找到一个元素所在的源码文件

我们知道一个类的public类和文件名一致,如果不一致,即

1
2
找所有不是与文件名相同的顶层类
这些类一般是 非 public 顶层类 或 额外定义的顶层类

下一个嘞

1
2
3
4
5
import java

from NestedClass nc
where nc.getASupertype() instanceof TypeObject
select nc

所有“嵌套类(NestedClass)”中,那些“直接继承 Object” 的类。

  • instanceof,判断前一个对象是否符合后面的类型
  • getASupertype(),拿到这个类extends的那个父类

我们学Java免不了泛型(Generics)吧,关于它的查询语句又是怎么样的呢?

  • Map 这个泛型接口的声明 = GenericInterface
  • 或者如果是 class,就叫 GenericClass
  • 二者合起来叫 GenericType=> 泛型类型的声明
  • TypeVariable(类型参数):代表泛型参数本身
  • ParameterizedType:参数化后的实例类型

假设一个p,是 ParameterizedType ,我们可以.getSourceDeclaration() 会返回它的 GenericType

现在有一个例子

1
2
3
4
5
6
import java

from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
pt.getSourceDeclaration() = map
select pt

这里就是查找 all parameterized instances of java.util.Map

  • hasQualifiedName():属于谓词, 检查某个类、方法、包的完全限定名是否匹配指定名称,此匹配更加唯一,限定路径名
  • getSourceDeclaration():将一个参数实参拿到它的泛型并返回

也会有些关于泛型的更为复杂的情况,其实学到这里思考,真的有必要学吗?不过我是强迫症,跟了跟了

1
2
3
class StringToNumMap<N extends Number> implements Map<String, N> {
// ...
}

Java 中可以限制某个类型参数的上界

  • N 是一个 TypeVariable(类型变量)
  • 它有一个 上界(upper bound)Number
1
2
3
4
5
6
import java

from TypeVariable tv, TypeBound tb
where tb = tv.getATypeBound() and
tb.getType().hasQualifiedName("java.lang", "Number")
select tv

查找所有边界为 Number 的类型参数

到此为止,后面也是不太理解,且与实际不太符合

AST

这里介绍Codeql里操作AST的基本方法

Expr(表达式)与 Stmt(语句)

这是 CodeQL 里“程序执行单位”的两个基类:

  • Exprx + y, foo(), o.equals(""), 字面量等
  • Stmtif, while, return, {}, 赋值语句等

它们构成了整个 Java 程序的 AST。

子节点

Expr.getAChildExpr()

Stmt.getAChild()

父节点

Expr.getParent()

Stmt.getParent()

例子
1
2
3
from Expr e
where e.getParent() instanceof ReturnStmt
select e

这里会返回return的子表达式

1
2
3
from Stmt s
where s.getParent() instanceof IfStmt
select s

熟悉这么一些方法就行,也不是很会用到,实际感受还得在实战上

调用图(call graph)

后续我们想打污点追踪就必须学会方法调用关系图,介绍一个元素

  • Callable,表示所有可以被调用的东西,方法 & 构造函数
  • Call,表示所有调用的表达式,如obj.foo();new Foo();this(),super()
  • getCallee(),根据调用找到被调用的方法

例子

1
2
3
4
5
from Call c, Method m
where m = c.getCallee() and
m.hasName("println")
select c

这里c是一个实际的调用,通过getCallee找到它的方法并匹配指定方法名,即可找到全部该方法被调用的位置

反之,我们可以查找一个方法是否存在调用情况

1
2
3
from Callable c
where not exists(c.getAReference())
select c
  • getAReference():根据Callable找到有哪些调用指向它
MethodAccess&Call

可以注意一下二者区别,Call表示一切调用,不局限于方法调用,可以是构造,方法引用,或者super调用等等
而前者仅限于方法调用object.equals("")这种
所有 MethodAccess 都是 Call,但不是所有 Call 都是 MethodAccess。

初阶污点追踪

也是一口气学到这里了~
希望可以做一个漂亮的今日份收尾
学习Codeql我们肯定希望把它学通,而不是仅作为一个特定查询工具,学会污点,写好查询,刷的一下出了一个洞,这才是最有意思的地方

https://github.blog/changelog/2023-08-14-new-dataflow-api-for-writing-custom-codeql-queries/

有更新,未免不必要的麻烦,我们在这个上面进行一些基础的修改,如下

  • 以前,数据流分析配置是通过继承类 DataFlow::ConfigurationTaintTracking::Configuration 来实现的,现在用 模块(例如 DataFlow::ConfigSigTaintTracking::Global)来定义配置。
  • 移除了 override 关键字:新的 API 不需要像旧版那样使用 override
  • isSanitizerisSanitizerInisBarrierisBarrierIn 替代:这两个函数现在适用于数据流和污点跟踪配置。
  • 不再需要“虚拟字符串值的特征谓词”。

原文如下

1
2
3
4
5
6
7
8
9
10
11
To convert the query to the new API:
You use a module instead of a class. A CodeQL module does not extend anything, it instead implements a signature. For both data flow and taint tracking configurations this is DataFlow::ConfigSig or DataFlow::StateConfigSigif FlowState is needed.
Previously, you would choose between data flow or taint tracking by extending DataFlow::Configuration or TaintTracking::Configuration. Instead, now you define your data or taint flow by instantiating either the DataFlow::Global<..> or TaintTracking::Global<..> parameterized modules with your implementation of the shared signature and this is where the choice between data flow and taint tracking is made.
Predicates no longer override anything, because you are defining a module.
The concepts of sanitizers and barriers are now unified under isBarrier and it applies to both taint tracking and data flow configurations. You must use isBarrier instead of isSanitizer and isBarrierIn instead of isSanitizerIn.
Similarly, instead of the taint tracking predicate isAdditionalTaintStep you use isAdditionalFlowStep .
A characteristic predicate with a dummy string value is no longer needed.
Do not use the generic DataFlow::PathGraph. Instead, the PathGraph will be imported directly from the module you are using. For example, SensitiveLoggerFlow::PathGraph in the updated version of the example query below.
Similar to the above, you’ll use the PathNode type from the resulting module and not from DataFlow.
Since you no longer have a configuration class, you’ll use the module directly in the from and where clauses. Instead of using e.g. cfg.hasFlowPath or cfg.hasFlow from a configuration object cfg, you’ll use flowPath or flow from the module you’re working with.

旧版的查询语句是如何呢?

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
class SensitiveLoggerConfiguration extends TaintTracking::Configuration {
SensitiveLoggerConfiguration() { this = "SensitiveLoggerConfiguration" } // Characteristic predicate with a dummy string value (see below)

// Define the source of taint flow
override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof CredentialExpr }

// Define the sink for taint flow
override predicate isSink(DataFlow::Node sink) { sinkNode(sink, "log-injection") }

// Define sanitizers (which clean tainted data)
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer.asExpr() instanceof LiveLiteral or
sanitizer.getType() instanceof PrimitiveType or
sanitizer.getType() instanceof BoxedType or
sanitizer.getType() instanceof NumberType or
sanitizer.getType() instanceof TypeType
}

// Define barrier conditions (where taint is stopped)
override predicate isSanitizerIn(DataFlow::Node node) { this.isSource(node) }
}

import DataFlow::PathGraph

from SensitiveLoggerConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This $@ is written to a log file.", source.getNode(), "potentially sensitive information"

新版的查询语句如下

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
30
31
32
module SensitiveLoggerConfig implements DataFlow::ConfigSig {  // Module now implements the signature, no inheritance

// Define the source of taint flow
predicate isSource(DataFlow::Node source) { source.asExpr() instanceof CredentialExpr }

// Define the sink for taint flow
predicate isSink(DataFlow::Node sink) { sinkNode(sink, "log-injection") }

// Define barrier conditions (sanitizers are now unified with barriers)
predicate isBarrier(DataFlow::Node sanitizer) { // 'isBarrier' replaces 'isSanitizer'
sanitizer.asExpr() instanceof LiveLiteral or
sanitizer.getType() instanceof PrimitiveType or
sanitizer.getType() instanceof BoxedType or
sanitizer.getType() instanceof NumberType or
sanitizer.getType() instanceof TypeType
}

// Define barrier conditions for source
predicate isBarrierIn(DataFlow::Node node) { isSource(node) } // 'isBarrierIn' replaces 'isSanitizerIn'
}

// Instantiate the module with the TaintTracking::Global type
module SensitiveLoggerFlow = TaintTracking::Global<SensitiveLoggerConfig>; // TaintTracking selected

// Import the PathGraph from the module
import SensitiveLoggerFlow::PathGraph

// Using PathNode from the module and the flowPath predicate
from SensitiveLoggerFlow::PathNode source, SensitiveLoggerFlow::PathNode sink
where SensitiveLoggerFlow::flowPath(source, sink) // Use the flowPath from the module
select sink.getNode(), source, sink, "This $@ is written to a log file.", source.getNode(), "potentially sensitive information"

我们简单运行一下,看看效果

1
2
3
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking

嗯,这里仍有四处错误

  • CredentialExpr
  • sinkNode
  • LiveLiteral
  • TypeType

实际学习还是可以跟官方文章的,不过记得新版本的改变就行,这里切记,不要问ai,也减少翻阅太早的博客,基本只会误导自己,总之做好心理准备
上述报错我们直接进这个网站https://codeql.github.com/codeql-standard-libraries/java/,学python的分析也是同理噢
我们发现CredentialExpr&LiveLiteral需要import semmle.code.java.security.SensitiveLoggingQuery

同理

  • sinkNode: import semmle.code.java.dataflow.ExternalFlow
  • TypeType:这个没找到,直接删了吧(气急败坏

如上,完成了修改,开始运行(时间稍微有点长)

效果还不错,揪住了一些可疑的地方~

上述代码:

1
2
旨在分析 敏感数据流,特别是检查是否有敏感数据(例如凭证、密码)被写入日志文件。
它利用了 污点跟踪(Taint Tracking) 和 数据流分析,并结合 敏感日志查询(SensitiveLoggingQuery) 来识别潜在的安全风险。

上面我们用了一个例子进行了相关说明,但是真正查询语句的撰写还差了一些,不过没事,抓住几个要点就行了,多实战

此处刚学了一点,介绍了一些查询语句以及基本的污点追踪,之后的内容还有很多,每天学一点点趴,bye~

参考

https://blog.dvkunion.cn/2022/03/24/CodeQL-%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97/

https://codeql.github.com/docs/codeql-language-guides/analyzing-data-flow-in-java/

https://codeql.github.com/codeql-standard-libraries/java/

https://github.blog/changelog/2023-08-14-new-dataflow-api-for-writing-custom-codeql-queries/