杂谈

最近都在搞工作上的事情,涉及到的东西没法分享出来,因此原计划的博客更新计划又被搁置了。国庆时想起来原本打算分享下毕设的项目PHPVulFinder也没有进行,左右一合计,刚好就把这个事儿了结了。

在谈及具体的技术之前,需要说明的一点是,这个项目是我的毕设题目,并且由于比较严重的拖延症,各种实现也是能拖就拖,直到快答辩时,才草草糊了个危险函数调用识别的部分,因此项目本身还存在很多不足,如果大家觉得写的挺菜,也不用奇怪,你的感觉是正确的。

总体思路

PHPVulFinder这个项目从名字上来看,和@隐形人真忙师傅的phpvulhunter很接近,因为我做这个项目的初衷也是看到了这个项目以及他的这两篇文章:PHP自动化白盒审计技术与实现浅谈PHP自动化代码审计技术。但是在实际实现时,除了对函数预定义直接借用了隐形人师傅的成果外,后续的分析思路差异很大,基本没有可见联系。

上面是有关于选择PHP静态自动化分析作为毕设题目的初衷,第二个就是有关具体的设计思路了,同样于隐形人师傅关系密切,其来源是知乎上的一个问题:如何使用AST生成程序的控制流图(CFG)?,目前该问题下有三个回答,我认为都值得一看,即使是从未接触过编译原理的朋友,我相信仔细看完后也能受益匪浅。

长话短说,对PHPVulFinder这个项目影响最大的是这段内容:

所以正统做法推荐的是在做数据流分析之前,先把AST转换为一种更细粒度的、把控制流显式暴露出来的中间表示(IR) …

对数据流分析来说,IR无论是树形、DAG还是线性形式都没关系,只要控制流和数据依赖易于分析就好。SSA形式的IR可以把两者都显式暴露出来,特别是use-def关系(并且有些IR会额外维护def-use关系),所以在现代编译器和程序分析器里比较流行。

因此PHPVulFinderphpvulhunter最大的区别在于,phpvulhunter尽量直接复用AST来完成CFG的构造,而PHPVulFinder中,我将AST又处理了一次,转换成了更便于分析的四元组。

除此之外,整个项目的设计思路也就大同小异,大致包括以下三步:

IR生成

严谨来说,从PHP-Parser得到的AST也能算是树形IR,为了我自己的描述习惯,此处小标题的IR生成指的是对AST进行处理进而得到四元组。

通常来讲,四元组会包括四个组成部分:

  1. 类型
  2. 左操作数
  3. 右操作数
  4. 结果

从PHP-Parser得到的AST转四元组,第一个要处理的就是类型如何给值,庆幸的是,PHP-Parser在设计时就已经考虑了相关的问题,可以直接通过getType()来获取类型。

当然,并不是所有类型都可以通过getType()来实现类型的赋值,举一个例子,对于一个switch-case结构,必然会拆分成多个四元组,并且每一个case字句都将跳转至不同的流程,此时便不能简单地借助getType()来完成跳转四元组的类型赋值,而是需要创建一个专门用于表示跳转关系的类型,在PHPVulFinder中,名为JUMP,对于JUMP类型的四元组而言,甚至只需要一个操作数来表示跳转地址即可:

1
$this->quads[$this->quadId] = new Quad(1,$this->quadId,"JUMP",$this->quads[$now_id]->result);

PHP-Parser生成的AST类型复杂,比如就包括了PhpParser\Node\ScalarPhpParser\Node\StmtPhpParser\Node\Expr等等,其中又包含了许多细分情况,从项目代码中也能看出来:

1
2
3
4
5
6
if( $expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
$expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
$expr instanceof PhpParser\Node\Expr\BinaryOp\Equal ||
$expr instanceof PhpParser\Node\Expr\BinaryOp\Concat ||
$expr instanceof PhpParser\Node\Expr\BinaryOp\Smaller
)

这一块基本是个体力活,需要不断根据PHP-Parser的AST类型来补足不同类型的四元组生成考虑。

除了类型之外,左操作数以及右操作数这两个组成部分的处理可以放在一块说明,以最简单的加法操作而言,左、右操作数无非就是两个数字的相加,但是如果两个函数的结果相加呢?那么这时候的左、右操作数就不再是一个具体的数值了。

对于两个函数结果相加这种情况,首先需要获取这两个函数的计算结果所在的四元组ID,然后将该四元组ID直接作为相加操作的左右操作数来记录,因此在进行后续的回溯等操作时,可以通过这个ID指向相应的四元组取值。

因此,在生成四元组时,如果遇到了隐含跳转关系的执行,都可以使用四元组ID来替代左右操作数,这些隐含跳转关系的执行包括但不限于switch-casex=y?z:qiffor等等。

还有一种特殊情况需要处理:函数调用。函数一般可以分为用户自定义函数以及系统内置函数,对于系统内置函数,PHP-Parser可以直接将其识别为具体的调用,比如echo就是PhpParser\Node\Stmt\Echo_exit就是PhpParser\Node\Expr\Exit_。但对于用户自定义函数而言,一个函数就包括了一个独立的作用域,因此在四元组阶段就需要体现出独立的作用域,我的处理办法是在函数调用前以及函数调用后各自增加一个四元组用于和其他的四元组分隔,代码如下:Code In Github

一个生成完的四元组样例如下:

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
33
34
35
36
object(Quad)#631 (6) {
["label"]=>
int(0)
["id"]=>
int(1)
["op"]=>
string(11) "Expr_Assign"
["arg1"]=>
NULL
["arg2"]=>
object(PhpParser\Node\Scalar\String_)#602 (2) {
["value"]=>
string(3) "123"
["attributes":protected]=>
array(3) {
["startLine"]=>
int(15)
["endLine"]=>
int(15)
["kind"]=>
int(2)
}
}
["result"]=>
object(PhpParser\Node\Expr\Variable)#601 (2) {
["name"]=>
string(1) "t"
["attributes":protected]=>
array(2) {
["startLine"]=>
int(15)
["endLine"]=>
int(15)
}
}
}

整个IR的生成中,基本都是此类细节的考虑以及重写,因此仅举几个例子作为示范,如果有兴趣可以自行分析代码。

基本块划分以及变量依赖

基本块的划分主要按照慕课上哈工大的《编译原理》课程中提及的思路进行,参考地址:代码优化1-流图

变量依赖的主要目的是标记出可能受污染变量以及后续的传播途径,此时就需要通过四元组中记录的信息来进行回溯,最终目的是将存在赋值关系的变量都进行污染分析,当到达sink点时就直接触发危险调用报警。

比如对于下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function a($s){
$b = $s."...";
$c = "ss";
$d = array($b,$c);
}
function b($ss){
echo $ss;
$a = "asd";
$b = "dfg";
system($b);
$para = $_GET['w'];
$c = "ppp";
$cd = "qwe";
system($para);
return "success";
}
$t = "123";
a($t);
$d = "sss";
$e = $_GET['p'];
$f = b($e);

第一个基本块的提取效果:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
object(BasicBlock)#696 (3) {
["inedge"]=>
object(Quad)#660 (6) {
["label"]=>
int(0)
["id"]=>
int(1)
["op"]=>
string(11) "Expr_Assign"
["arg1"]=>
NULL
["arg2"]=>
object(PhpParser\Node\Scalar\String_)#629 (2) {
["value"]=>
string(3) "123"
["attributes":protected]=>
array(3) {
["startLine"]=>
int(21)
["endLine"]=>
int(21)
["kind"]=>
int(2)
}
}
["result"]=>
object(PhpParser\Node\Expr\Variable)#628 (2) {
["name"]=>
string(1) "t"
["attributes":protected]=>
array(2) {
["startLine"]=>
int(21)
["endLine"]=>
int(21)
}
}
}
["outedge"]=>
object(Quad)#662 (6) {
["label"]=>
int(1)
["id"]=>
int(3)
["op"]=>
string(13) "Expr_FuncCall"
["arg1"]=>
object(PhpParser\Node\Name)#632 (2) {
["parts"]=>
array(1) {
[0]=>
string(1) "a"
}
["attributes":protected]=>
array(2) {
["startLine"]=>
int(22)
["endLine"]=>
int(22)
}
}
["arg2"]=>
int(1)
["result"]=>
int(12)
}
["entry"]=>
int(1)
}

字符串变量以及HTTP参数的提取效果:

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
33
34
object(Variable)#710 (5) {
["BasicBlockId"]=>
int(0)
["Varname"]=>
string(1) "t"
["linenum"]=>
array(2) {
[0]=>
int(21)
[1]=>
int(21)
}
["from"]=>
array(2) {
[0]=>
NULL
[1]=>
object(PhpParser\Node\Scalar\String_)#629 (2) {
["value"]=>
string(3) "123"
["attributes":protected]=>
array(3) {
["startLine"]=>
int(21)
["endLine"]=>
int(21)
["kind"]=>
int(2)
}
}
}
["tainted"]=>
int(0)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[11]=>
object(Variable)#716 (5) {
["BasicBlockId"]=>
int(3)
["Varname"]=>
string(1) "w"
["linenum"]=>
array(2) {
[0]=>
int(14)
[1]=>
int(14)
}
["from"]=>
string(4) "_GET"
["tainted"]=>
int(1)
}

变量$ss的污染分析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[8]=>
object(Variable)#719 (5) {
["BasicBlockId"]=>
int(3)
["Varname"]=>
string(2) "ss"
["linenum"]=>
array(2) {
[0]=>
int(9)
[1]=>
int(9)
}
["from"]=>
array(1) {
[0]=>
int(16)
}
["tainted"]=>
int(1)
}

在污点变量的分析中,做的还不是很完善,比如当遇到不支持的赋值、拼接函数时,变量会被污染但是却无法识别,这个项目中存在不少类似的问题,主要原因是毕设的时间紧迫,只能先实现几个基本情况的识别用于交付。

危险调用识别就不单独列出来讲,一个是这一部分做的十分粗糙,甚至可以说是稀烂,另一个是我暂时没什么很好的思路,只能在污点变量到底sink点时触发危险调用识别。

PHPVulFinder之外

这个项目起初的目标是能简单支持对类、命名空间等情况进行分析,但是有点高估自己的开发速度,导致最终留出的开发时间十分紧迫,只勉强做完了这个Demo,但是也还是有一些大致的思路,比如通过类哈希表以及函数哈希表建立映射关系,在生成四元组时通过哈希表来完成,同样的,变量也需要考虑到这种情况下由于作用域的不同所带来的重名、取不到值等情况。

如果能把这两个问题解决,我想,对于类的静态自动化处理应该还是能做不少事,可惜看不到RIPS新版的代码,我还是比较好奇他们是如何解决这个问题的。