在分析CodeQL Python 污点分析时候发现,有用到shared 的新模块typetracking。从前面Python 的Extractor 分析中,我们知道它的DB 内是没有类型信息的,没有类型信息是怎么做污点呢?
Java 是在编译的时候创建的DB,这一过程中可以拿到了AST 节点对应的类型信息,例如call 节点函数信息,可以直接关联callable,知道调的是哪个函数。但是纯AST 要怎么做?
联想之前做Java 反射Patch 的时候,印象中也需要用到类型跟踪,但是是自己实现的,改动颇大,不知道这个typetracking 是否能够更优雅的解决,分析下。
1. 抽象模块
在shared/typetracking/internal/TypeTracking.qll 中,定义了抽象模块
- CallGraphConstruction::Simple::InputSig
- CallGraphConstruction::Simple::Make
InputSig
InputSig 可以简单理解为dataflow 中的Config,它是一个signature module
,类似接口类的概念。
1 | /** The input to call graph construction. */ |
其定义了两个接口
- start:起始节点,需要根据不同的场景,统一转换成Node 类型,比如:
- classInstance 类型的,class 为ClassInstance,需要
- filter:类似sanitizer,过滤方法
2. Python 内置Tracers
实际上,有很多内置Tracker,以Python 为例,其内置了以下实现
- classTracker
- classInstanceTracker
- selfTracker
- clsArgumentTracker
- superCallNoArgumentTracker
- superCallTwoArgumentTracker
2.1 ClassTracer
2.1.1 TrackClassInput
start
1 | private module TrackClassInput implements CallGraphConstruction::Simple::InputSig { |
释义:
- 普通的类class,那么取其Expr 对应的Node 作为start
1
Class Foo: // 整个作为 start Expr
- 当一个类class 存在修饰符的时候,那么选择其最后一个装饰器作为start
1
2
3
Class Foo: - 如果有个表达式type(obj) ,其中obj 是class 类型的,那么整个
type(obj)
作为start1
2
3
4Class Foo:
...
foo = Foo()
type(foo) // type(foo) 作为start Expr这里用到了classInstanceTracker,后面有详细解释
filter
1 | predicate filter(Node n) { |
2.1.2 Make API
1 | Node classTracker(Class cls) { |
释义:
2.2 classInstanceTracker
- 以类实例化作为起点
- 跟踪寻找其传播可能
2.2.1 TrackClassInstanceInput
start
1 | private module TrackClassInstanceInput implements CallGraphConstruction::Simple::InputSig { |
释义:
- resolveClassCall
1
2
3class Foo:
...
bar = Foo() // Foo() 作为start - getADirectSuperclass 是获取cls 的父类
1
2
3class Foo:
def __new__():
bar = super().__new__(cls) // super().__new__(cls) 作为start
resolveClassCall
1 | // ------------------------------------- |
注意:
- CallNode.getFunction 并不是字面上的意思,并不是找到call 对应的Function,而只是找到call,举例解释:
CallNode: foo.bar(arg)
getFunction: foo.bar
1
2
3
4
5
6
7
8
9
10
11
12/** A control flow node corresponding to a call expression, such as `func(...)` */
class CallNode extends ControlFlowNode {
CallNode() { toAst(this) instanceof Call }
/** Gets the flow node corresponding to the function expression for the call corresponding to this flow node */
ControlFlowNode getFunction() {
exists(Call c |
this.getNode() = c and
c.getFunc() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
)
}
filter
1 | predicate filter(Node n) { |
2.2.2 Make API
1 | /** |
2.3 SelfTracker
2.3.1 TrackSelfInput
start
1 | private module TrackSelfInput implements CallGraphConstruction::Simple::InputSig { |
释义:
- 存在Function,是当前class 的方法,并且不是静态方法和内置classmethod 方法,那么该方法的第一个数self 就是start
1
2class Foo:
def bar(self): // self 参数作为 start expr
filter
1 | predicate filter(Node n) { |
2.3.2 Make API
1 | /** |
2.4 ClassArgumentTracker
- 以类中方法第一个参数作为起始点
- 跟踪寻找其传播可能
2.4.1 TrackClsArgumentInput
start
1 | private module TrackClsArgumentInput implements CallGraphConstruction::Simple::InputSig { |
释义:
- 存在Function func,属于class 的方法,那么start 节点就是这个方法的第一个形参
ps: python 这api 命名规范。。。形参实参不分。。。吐槽
1
2
3Class Foo():
def func(cls): // start = cls - type(self) 场景
1
2
3Class Foo():
def bar(self):
type(self) // start = type(self)
filter
1 | predicate filter(Node n) { |
2.4.2 Make API
1 | /** |
3. 自定义Trackers
3.1 AttrReadTracker
在分析resolveCall 方法中,其功能是定位Call 和其对应的Function 定义关系,遇到了AttrReadTracker,调用逻辑如下:
1 | resolveCall(CallNode call, Function target, CallType type) |
详细分析下其实现
顾名思义,是针对属性读的跟踪,分两步
- 构造CallGraphConstruction::Simple::InputSig 的实现类TrackAttrReadInput
- 重写start/filter
- make AttrReadTracker,从
CallGraphConstruction::Simple::Make<TrackAttrReadInput>
- 调用track 接口
3.1.1 TrackAttrReadInput
start
1 | // ============================================================================= |
释义:
- 起点是AttrRead,attr 是能够被跟踪定位对应class 的
1
2
3class Foo:
bar = 'test'
foo.bar // foo.bar 是AttrRead,attr.getObject 中的attr,也就是foo 是能够被跟踪的
从结果来讲
filter
1 | /** Gets a reference to the attribute read `attr` */ |
Make API
1 | /** Gets a reference to the attribute read `attr` */ |