前言 Joern在解析php源码时,会先生成Control Flow Graph,然后生成UDG(一个过渡Graph),接着生成Data Dependency Graph,最后处理Call Graph。现在就先来学习下Jeorn是怎么生成Control Flow Graph的。
Switch Demo 官方手册:https://www.php.net/manual/zh/control-structures.switch.php
利用Joern生成下面switch语句的CPG代码属性图:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php switch ($i) {case "apple" : echo "i is apple" ; break ; case "bar" : echo "i is bar" ; break ; case "cake" : echo "i is cake" ; break ; } ?>
对应的CPG图,下图中蓝色AST结点中的数字是lineno:
而下图蓝色AST结点中的数字代表的则是id:
其中不存在PDG图,也就是数据流图,没有REACHES
关系边。从中抽出CFG图为:
nodes.csv 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 $ cat nodes.csv id:int labels:label type flags:string_array lineno:int code childnum:int funcid:int classname namespace endlineno:int name doccomment 0 Filesystem File "a.php" 1 AST AST_TOPLEVEL TOPLEVEL_FILE 1 "" 13 "a.php" 2 Artificial CFG_FUNC_ENTRY 1 "a.php" 3 Artificial CFG_FUNC_EXIT 1 "a.php" 4 AST AST_STMT_LIST 1 0 1 "" 5 AST AST_SWITCH 2 0 1 "" 6 AST AST_VAR 2 0 1 "" 7 AST string 2 "i" 0 1 "" 8 AST AST_SWITCH_LIST 3 1 1 "" 9 AST AST_SWITCH_CASE 3 0 1 "" 10 AST string 3 "apple" 0 1 "" 11 AST AST_STMT_LIST 3 1 1 "" 12 AST AST_ECHO 4 0 1 "" 13 AST string 4 "i is apple" 0 1 "" 14 AST AST_BREAK 5 1 1 "" 15 AST NULL 5 0 1 "" 16 AST AST_SWITCH_CASE 6 1 1 "" 17 AST string 6 "bar" 0 1 "" 18 AST AST_STMT_LIST 6 1 1 "" 19 AST AST_ECHO 7 0 1 "" 20 AST string 7 "i is bar" 0 1 "" 21 AST AST_BREAK 8 1 1 "" 22 AST NULL 8 0 1 "" 23 AST AST_SWITCH_CASE 9 2 1 "" 24 AST string 9 "cake" 0 1 "" 25 AST AST_STMT_LIST 9 1 1 "" 26 AST AST_ECHO 10 0 1 "" 27 AST string 10 "i is cake" 0 1 "" 28 AST AST_BREAK 11 1 1 "" 29 AST NULL 11 0 1 "" 30 AST NULL 1 1 1 ""
附上更清晰的图片:
rels.csv 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 $ cat rels.csv start end type 1 2 ENTRY 1 3 EXIT 6 7 PARENT_OF 5 6 PARENT_OF 9 10 PARENT_OF 12 13 PARENT_OF 11 12 PARENT_OF 14 15 PARENT_OF 11 14 PARENT_OF 9 11 PARENT_OF 8 9 PARENT_OF 16 17 PARENT_OF 19 20 PARENT_OF 18 19 PARENT_OF 21 22 PARENT_OF 18 21 PARENT_OF 16 18 PARENT_OF 8 16 PARENT_OF 23 24 PARENT_OF 26 27 PARENT_OF 25 26 PARENT_OF 28 29 PARENT_OF 25 28 PARENT_OF 23 25 PARENT_OF 8 23 PARENT_OF 5 8 PARENT_OF 4 5 PARENT_OF 4 30 PARENT_OF 1 4 PARENT_OF 0 1 FILE_OF
cpg_edges.csv 输入nodes.csv
文件和rel.csv
文件,最后输出的文件cpg_edges.csv
内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 start end type var 2 6 FLOWS_TO 6 12 FLOWS_TO 6 19 FLOWS_TO 6 26 FLOWS_TO 6 30 FLOWS_TO 12 14 FLOWS_TO 14 30 FLOWS_TO 19 21 FLOWS_TO 21 30 FLOWS_TO 26 28 FLOWS_TO 28 30 FLOWS_TO 30 3 FLOWS_TO
生成的edges.csv中只有控制流上的关系。
Joern源码分析 CFG生成入口 下面是Joern在生成PHP代码属性图时的入口函数Main函数:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 package tools.php.ast2cpg;import java.io.FileReader;import java.io.IOException;import org.apache.commons.cli.ParseException;import ast.php.functionDef.FunctionDef;import cfg.ASTToCFGConverter;import cfg.CFG;import cfg.PHPCFGFactory;import cg.CG;import cg.PHPCGFactory;import ddg.CFGAndUDGToDefUseCFG;import ddg.DDGCreator;import ddg.DataDependenceGraph.DDG;import ddg.DefUseCFG.DefUseCFG;import inputModules.csv.KeyedCSV.exceptions.InvalidCSVFile;import inputModules.csv.csvFuncExtractor.CSVFunctionExtractor;import outputModules.common.Writer;import outputModules.csv.MultiPairCSVWriterImpl;import outputModules.csv.exporters.CSVCFGExporter;import outputModules.csv.exporters.CSVCGExporter;import outputModules.csv.exporters.CSVDDGExporter;import udg.CFGToUDGConverter;import udg.php.useDefAnalysis.PHPASTDefUseAnalyzer;import udg.useDefGraph.UseDefGraph;public class Main { static CommandLineInterface cmdLine = new CommandLineInterface(); static CSVFunctionExtractor extractor = new CSVFunctionExtractor(); static ASTToCFGConverter ast2cfgConverter = new ASTToCFGConverter(); static CFGToUDGConverter cfgToUDG = new CFGToUDGConverter(); static CFGAndUDGToDefUseCFG udgAndCfgToDefUseCFG = new CFGAndUDGToDefUseCFG(); static DDGCreator ddgCreator = new DDGCreator(); static CSVCFGExporter csvCFGExporter = new CSVCFGExporter(); static CSVDDGExporter csvDDGExporter = new CSVDDGExporter(); static CSVCGExporter csvCGExporter = new CSVCGExporter(); public static void main (String[] args) throws InvalidCSVFile, IOException { parseCommandLine(args); String nodeFilename = cmdLine.getNodeFile(); String edgeFilename = cmdLine.getEdgeFile(); FileReader nodeFileReader = new FileReader(nodeFilename); FileReader edgeFileReader = new FileReader(edgeFilename); extractor.setInterpreters(new PHPCSVNodeInterpreter(), new PHPCSVEdgeInterpreter()); extractor.initialize(nodeFileReader, edgeFileReader); ast2cfgConverter.setFactory(new PHPCFGFactory()); cfgToUDG.setASTDefUseAnalyzer(new PHPASTDefUseAnalyzer()); MultiPairCSVWriterImpl csvWriter = new MultiPairCSVWriterImpl(); csvWriter.openEdgeFile( "." , "cpg_edges.csv" ); Writer.setWriterImpl( csvWriter); FunctionDef rootnode; while ((rootnode = (FunctionDef)extractor.getNextFunction()) != null ) { PHPCGFactory.addFunctionDef( rootnode); CFG cfg = ast2cfgConverter.convert(rootnode); csvCFGExporter.writeCFGEdges(cfg); UseDefGraph udg = cfgToUDG.convert(cfg); DefUseCFG defUseCFG = udgAndCfgToDefUseCFG.convert(cfg, udg); DDG ddg = ddgCreator.createForDefUseCFG(defUseCFG); csvDDGExporter.writeDDGEdges(ddg); } CG cg = PHPCGFactory.newInstance(); csvCGExporter.writeCGEdges(cg); csvWriter.closeEdgeFile(); } private static void parseCommandLine (String[] args) { try { cmdLine.parseCommandLine(args); } catch (RuntimeException | ParseException e) { printHelpAndTerminate(e); } } private static void printHelpAndTerminate (Exception e) { System.err.println(e.getMessage()); cmdLine.printHelp(); System.exit(0 ); } }
其中第37行的:
1 static ASTToCFGConverter ast2cfgConverter = new ASTToCFGConverter();
和第64行的:
1 ast2cfgConverter.setFactory(new PHPCFGFactory());
以及76行至79行的CFG处理流程:
1 2 3 4 PHPCGFactory.addFunctionDef( rootnode); CFG cfg = ast2cfgConverter.convert(rootnode); csvCFGExporter.writeCFGEdges(cfg);
若上面代码没有行号,可以看下图:
CFGFactory初始化 在整个CFG解析过程中,从ASTToCFGConverter
类开始,将AST对象转成CFG对象:
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 package cfg;import ast.functionDef.FunctionDefBase;public class ASTToCFGConverter { private CFGFactory factory; public void setFactory (CFGFactory factory) { this .factory = factory; } public CFG convert (FunctionDefBase node) { return factory.newInstance(node); } }
在从根节点构建CFG图之前,首先确定使用的CFGFactory为PHPCFGFactory
。PHPCFGFactory
类继承自jpanlib 中的父类CFGFactory
,它是joern-php 中PHPCFGFactory
的基类,同时也是joern-fuzzyc 中CCFGFactory
的基类。前者是生成php源码CFG的,后者是用来生成fuzzy-c源码CFG的。
CFGFactory
:joern/projects/extensions/joern-fuzzyc/src/main/java/cfg/CCFGFactory.java
PHPCFGFactory
:joern/projects/extensions/joern-php/src/main/java/cfg/PHPCFGFactory.java
CCFGFactory
:joern/projects/extensions/joern-fuzzyc/src/main/java/cfg/CCFGFactory.java
它们之间的继承关系如下图所示:
CFG根节点 结点类型为TOPLevelFUnctionDef
,是ASTNode
类型的结点中特殊的结点之一。整个过程是比较比复杂的,所以我打算在另一篇文章中再阐述。
跟进PHPCFGFactory.addFunctionDef()
:
直接返回null。
CFG对象实例化 Control Flow Graph显式地描述了执行代码语句的顺序,以及执行特定执行路径所需要满足的条件。因此,statements和predicates都应该用结点来表示,并且这些结点需要由有向边连接来表示控制流的传递。CFG可以由抽象语法树转化而来 。首先,考虑结构化控制语句(constructed control statements),例如,if
,while
,for
等等来初步构建控制流图。然后,再考虑像goto
,break
,continue
之类的非结构化控制语句(unstructured control statements)来修正初步控制流程图。(来自S&P’14, Modeling and Discovering Vulnerabilities with Code Property Graphs ,本文的研究成果就是joern工具)
这里用一张思维导图稍微补充一下流程控制语句中的结构化语句和非结构化语句
参考C语言5种程序语句(1)——流程控制语句中的结构化语句(条件语句和循环语句)
在第77行,使用ASTToCFGConverter
类的实例化对象ast2cfgConverter
,从每个function definition node开始,一步一步生成CFG图:
跟进ASTToCFGConverter.convert()
(注意区分和后面CFGFactory.convert()
函数之间的区别),调用factory
对象的newInstance()
方法,该factory
已经被初始化为PHPCFGFactory
:
Class:FunctionDefBase FunctionDefBase 是FunctionDef 的父类,但是无论是父类还是子类,它们都是一种ASTNode ,继承关系为:
FunctionDefBase
:
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 package ast.functionDef;import ast.ASTNode;import ast.logical.statements.CompoundStatement;public abstract class FunctionDefBase extends ASTNode { protected ParameterList parameterList = null ; protected CompoundStatement content = null ; public abstract String getName () ; public abstract String getFunctionSignature () ; public ParameterList getParameterList () { return this .parameterList; } public void setParameterList (ParameterList parameterList) { this .parameterList = parameterList; super .addChild(parameterList); } public CompoundStatement getContent () { return this .content; } @Override public String getEscapedCodeStr () { setCodeStr(getFunctionSignature()); return getCodeStr(); } public void setContent (CompoundStatement content) { this .content = content; super .addChild(content); } }
FunctionDefBase有两个protected成员变量,ParameterList
和CompoundStatement
。CompoundStatement 是什么呢?它定义在:
joern/projects/extensions/jpanlib/src/main/java/ast/logical/statements/CompoundStatement.java
继承自类Statement
,也是一种ASTNode结点。继承关系如下图所示:
CompoundStatement是一种比较特殊的Statement类型,是一种复合类型的statement,比如一个for循环,其内部所有的语句,我们都可以将其认为是CompoundStatement
。
至于ParameterList
,我猜测是这样的,比如有一个函数,它有一些参数,ParameterList
中就会维护这些参数。当然这个case中没有体现出这些,是我的猜测,比如一个函数为:
1 2 3 4 5 6 <?php function foo ($a, $b) { $c = $a + $b; }
那该FunctionDefBase对象对应的parameterList
为:
PHPCFGFactory.newInstace 回到前面的执行路径,继续跟踪,又了调用PHPCFGFactory.newInstance(node)
。我们可以看到在PHPCFGFactory
中,存在很多newInstance()
同名函数,因为不同语句的结构存在差别:
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 package cfg;import ...public class PHPCFGFactory extends CFGFactory { private static final int CFG_ENTRY_OFFSET = 1 ; private static final int CFG_EXIT_OFFSET = 2 ; static { structuredFlowVisitior = new PHPStructuredFlowVisitor(); } public PHPCFGFactory () { structuredFlowVisitior = new PHPStructuredFlowVisitor(); } @Override public CFG newInstance (FunctionDefBase functionDefinition) { ... } public static CFG newInstance (IfStatementBase ifStatement) { ... } private static CFG convertIfElem (Iterator<IfElement> iterator) { ... } public static CFG newInstance (SwitchStatement switchStatement) { ... } public static CFG newInstance (ForEachStatement forEachStatement) { ... } public static CFG newInstance (TryStatement tryStatement) { ... } }
但是我们很容易发现,还有一些语句,比如while
,for
,break
语句都没有出现,它们则是在PHPCFGFactory
的父类CFGFactory
中被定义,像源码中的break语句就是交由newInstance(BreakStatement breakStatement)
来处理的:
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 package cfg;import ...public class CFGFactory { protected static StructuredFlowVisitor structuredFlowVisitior; public CFG newInstance (FunctionDefBase functionDefinition) { ... } public static CFG newInstance (ASTNode... nodes) { ... } public static CFG newErrorInstance () n { ... } public static CFG newInstance (WhileStatement whileStatement) { ... } public static CFG newInstance (ForStatement forStatement) { ... } public static CFG newInstance (DoStatement doStatement) { ... } public static CFG newInstance (TryStatement tryStatement) { ... } public static CFG newInstance (SwitchStatement switchStatement) { ... } public static CFG newInstance (ParameterList paramList) { ... } public static CFG newInstance (CompoundStatement content) { ... } public static CFG newInstance (ReturnStatement returnStatement) { ... } public static CFG newInstance (GotoStatement gotoStatement) { ... } public static CFG newInstance (Label labelStatement) { ... } public static CFG newInstance (ContinueStatement continueStatement) { ... } public static CFG newInstance (BreakStatement breakStatement) { ... } public static CFG newInstance (ThrowStatement throwStatement) { ... } public static CFG convert (ASTNode node) { ... } public static void fixBreakStatements (CFG thisCFG, CFGNode target) { ... } public static void fixContinueStatement (CFG thisCFG, CFGNode target) { ... } private static void fixBreakOrContinueStatements (CFG thisCFG, CFGNode target, Iterator<CFGNode> it) { ... } public static void fixGotoStatements (CFG thisCFG) { ... } public static void fixReturnStatements (CFG thisCFG) { ... } }
继续跟进该函数CFGFactory.newInstance(FunctionDefBase functionDefinition)
:
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 package cfg;import ...public class PHPCFGFactory extends CFGFactory { private static final int CFG_ENTRY_OFFSET = 1 ; private static final int CFG_EXIT_OFFSET = 2 ; static { structuredFlowVisitior = new PHPStructuredFlowVisitor(); } public PHPCFGFactory () { structuredFlowVisitior = new PHPStructuredFlowVisitor(); } @Override public CFG newInstance (FunctionDefBase functionDefinition) { CFG cfg = super .newInstance(functionDefinition); Long id = functionDefinition.getNodeId(); ((CFGEntryNode)cfg.getEntryNode()).setNodeId( id + CFG_ENTRY_OFFSET); ((CFGExitNode)cfg.getExitNode()).setNodeId( id + CFG_EXIT_OFFSET); return cfg; } ...... }
PHPCFGFactory.newInstance(FUnctionDefBase functionDefinition)
重写了父类CFGFactory
的newInstance(FunctionDefBase functionDefinition)
方法。先调用父类的方法,即super.newInstance()
来生成初步的CFG。
CFGFactory.newInstance 看下CFGFactory.newInstance(FunctionDefBase functionDefinition)
是如何初始化并且构建一个CFG图的:
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 package cfg;import ...public class CFGFactory { protected static StructuredFlowVisitor structuredFlowVisitior; public CFG newInstance (FunctionDefBase functionDefinition) { try { CFG function = newInstance(); CFG parameterBlock = convert( functionDefinition.getParameterList()); CFG functionBody = convert(functionDefinition.getContent()); parameterBlock.appendCFG(functionBody); function.appendCFG(parameterBlock); fixGotoStatements(function); fixReturnStatements(function); if (!function.getBreakStatements().isEmpty()) { System.err.println("warning: unresolved break statement" ); fixBreakStatements(function, function.getErrorNode()); } if (!function.getContinueStatements().isEmpty()) { System.err.println("warning: unresolved continue statement" ); fixContinueStatement(function, function.getErrorNode()); } if (function.hasExceptionNode()) { function.addEdge(function.getExceptionNode(), function.getExitNode(), CFGEdge.UNHANDLED_EXCEPT_LABEL); } return function; } catch (Exception e) { return newErrorInstance(); } } }
首先,先创建一个CFG块,将其赋给function
变量,它是一个最小单位的CFG图,仅仅由entry node和exit node组成,一般表示为:
1 2 [ENTRY] { [ENTRY] ==[]==> [EXIT] , } [EXIT] { }
其中这里的function
最后会变成cpg图中类型为AST_FUNC_DECL
或是AST_TOPLEVEL
之类的结点,如果它的类型是AST_TOPLEVEL
,那么它的id是1,它的类型在PHPCSVNodeTypes 类中有定义,有4种:
1 2 3 4 5 6 7 8 public class PHPCSVNodeTypes { public static final String TYPE_TOPLEVEL = "AST_TOPLEVEL" ; public static final String TYPE_FUNC_DECL = "AST_FUNC_DECL" ; public static final String TYPE_METHOD = "AST_METHOD" ; public static final String TYPE_CLOSURE = "AST_CLOSURE" ; }
跟入无参的newInstance()
方法:
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 public class CFGFactory { public static CFG newInstance (ASTNode... nodes) { try { CFG block = new CFG(); CFGNode last = block.getEntryNode(); for (ASTNode node : nodes) { CFGNode container = new ASTNodeContainer(node); block.addVertex(container); block.addEdge(last, container); last = container; } block.addEdge(last, block.getExitNode()); return block; } catch (Exception e) { return newErrorInstance(); } } }
首先初始化一个CFG block,然后获取该控制流块的entrynode和exitnode,形成一个仅有[Entry] ==[]==> [Exit]
的初始CFG block:
Class:CFG 跟入CFG类 的构造函数CFG()
:
先稍微介绍下CFG类,位于基础类库jpanlib/ 目录下,具体位置为joern/projects/extensions/jpanlib/src/main/java/cfg/CFG.java
。
文件中存在注释:
说明CFG图就是CFGFactory的目标格式。
CFG类继承自joern/projects/extensions/jpanlib/src/main/java/graphutils/IncidenceListGraph.java
中的IncidenceListGraph
类,源码中存在注释:
是基于两个关联列表的图implementation。在IncidenceListGraph
中定义了很多的图操作。
继续回到源码,执行到第36行,它这里用了this 关键字来调用本类的重载构造器 (因为构造方法在类里不能像其他方法一样调用,只能通过this(参数列表)
调用)。
补充1:重写Override和重载Overload的区别
https://www.runoob.com/java/java-override-overload.html
重写 :是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写! 重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。
重载 :重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载 。
补充2:Java使用this关键字调用本类重载构造器
https://www.cnblogs.com/wanghongyun/p/6132083.html
在构造器中是可以直接调用本类的其他重载构造器的,但是不能直接使用构造器名称来调用另一个构造器,而是需要使用this
关键字。
this(...)
方法必须出现在构造器的第一行,用来调用其他重载构造器,调用时参数必须严格匹配。这种调用方式的有点在于一个构造器可以不必重复编写其他构造器中已有的代码,而是通过调用其他构造函数以实现复用,从而提供良好的类代码结构。
在上图public CFG(CFGNode entry, CFGNode exit)
函数中,接受的两个参数类型都是CFGNode
类型的,不过通过this
关键字传入的两个参数分别是new CFGEntryNode()
和new CFGExitNode()
,我们知道CFGNode
和CFGEntryNode
与CFGExitNode
之间存在某种继承关系。在IDEA中查看继承关系,CFGNode
是一个接口,AbstractCFGNode
是实现了该接口的抽象类(抽象类是只能被用于继承的),然后CFGEntryNode
和CFGExitNode
分别继承自该抽象类:
在public CFG(CFGNode entry, CFGNode exit)
中是最最初始的CFG图,目前的CFG图中存在两个结点,就是CFG的entry结点和exit结点,但是它们的id
都为-1 ,因为这并不代表任何具象的AST结点,只是标记某个CFG块的entry和exit位置:
然后初始化存储非结构化控制语句的List:
CFG
对象实例化结束后, 程序返回到CFGFactory.newInstance()
,并将该CFG
对象赋给block
参数。接着读取block.entry
和block.exit
:
但是nodes
是空的list,所以后面的for循环会被跳过,直接进入第89行调用CFG.addEdge()
:
该函数会设置outNeighborhood
和inNeiborhood
。这样就合成了一个最基本的CFG图,由一个entry结点和exit结点组成。接着开始处理AST树的叶子结点,将其一点点加入到CFG图中。继续跟入:
对于CFG图上的起始结点(即flags: TOPLEVEL_FILE
的结点),它的content内容就是它的子结点:
故functionDef.getContent()
返回的是一个类型为CompoundStatement
的AST结点。接着调用CFGFactory.convert
函数,该函数的作用就是将ASTNode转化成CFG:
1 2 3 4 5 6 7 8 public static CFG convert (ASTNode node) { if (node == null ) return newInstance(); node.accept(structuredFlowVisitior); return structuredFlowVisitior.getCFG(); }
首先newInstance()
会初始化一个最小的CFG块,接着处理后面的CompoundStatement。
通过content.getStatements()
返回子结点之后,先将statement
转化为子控制流图(对应代码是convert(statement)
,convert函数的返回类型是CFG
),为了方便起见,我称其为child-CFG ,将其都加入CFG中。可以看到两个子结点的类型分别是:
NullNode :一个空结点node类型,该类型结点往往是CFG中EXIT结点的parent结点
SwitchStatementPHP :继承自SwitchStatement
SwitchStatementPHP 有两个成员变量:
1 2 3 4 5 public class SwitchStatementPHP extends SwitchStatement { private Expression expression = null ; protected SwitchList switchList = null ; }
继续跟入CFGFactory.convert
:
因为node结点的类型是SwitchStatementPHP
,所以调用相应的visitor:PHPStructureFlowVisitor
,然后调用PHPCFGFactory.newInstance(node)
创建child-CFG,跟入该函数:
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 70 71 public static CFG newInstance (SwitchStatement switchStatement) { try { SwitchStatementPHP phpSwitchStatement = (SwitchStatementPHP)switchStatement; CFG switchBlock = new CFG(); CFGNode conditionContainer = new ASTNodeContainer( phpSwitchStatement.getExpression()); switchBlock.addVertex(conditionContainer); switchBlock.addEdge(switchBlock.getEntryNode(), conditionContainer); boolean defaultExists = false ; for ( SwitchCase switchCase : phpSwitchStatement.getSwitchList()) { CFG caseBody = convert(switchCase.getStatement()); switchBlock.appendCFG(caseBody); String label = switchCase.getValue() != null ? switchCase.getValue().getEscapedCodeStr() : "default" ; if ( label.equals("default" )) defaultExists = true ; if ( !caseBody.isEmpty()) { for ( CFGEdge caseEntryEdge : caseBody.outgoingEdges( caseBody.getEntryNode())) { switchBlock.addEdge( conditionContainer, caseEntryEdge.getDestination(), label); } } else { switchBlock.addEdge( conditionContainer, switchBlock.getExitNode(), label); } } if ( !defaultExists) { switchBlock.addEdge( conditionContainer, switchBlock.getExitNode(), "default" ); } fixBreakStatements( switchBlock, switchBlock.getExitNode()); return switchBlock; } catch (Exception e) { return newErrorInstance(); } }
接下来利用SwitchStatementPHP.getExpression()
的结果合成CFGNode:
返回一个后继结点,它的类型是Variable,也是ASTNode的子类类型之一:
然后将其实例化为一个ASTNodeContainer
对象,它是一种CFG的一种Node类型:
ASTNodeContainer
的作用主要是保存一个CFG结点的ASTNode属性:
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 package cfg.nodes;import ast.ASTNode;public class ASTNodeContainer extends AbstractCFGNode { private ASTNode astNode; public ASTNodeContainer (ASTNode node) { if (node == null ) { throw new IllegalArgumentException("node must not be null" ); } setASTNode(node); } private void setASTNode (ASTNode astNode) { this .astNode = astNode; this .astNode.markAsCFGNode(); } ...... }
因为是CFGNode,所以它的isInCFG
属性应为true
,调用ASTNode.markAsCFGNode
将其标记为true:
然后将新的CFGNode保存到CFG图的vertex-list中:
CFG.addEdge 接着调用CFG.addEdge()
为entry结点和该CFGNode结点添加一条CFGEdge,即[ENTRY] ==[]==> [6 (Variable)]
:
这里会维护outNeighborhood
和inNeighborhood
:
接着为每一个switch case块实例化一个caseBody CFG,switchCase.getStatement()
得到的statement类型是一个CompoundStatement
,它们的结构如下图所示:
然后调用CFGFactory.convert
函数将switch.getStatement
得到的CompoundStatement转为child CFG,执行完的结果:
这里可以看到的是这里的CompoundStatement
其实就对应我们源码中的echo和break语句,因此是复合类型 的:
1 2 echo "i is apple" ;break ;
然后调用CFG.appendCFG()
将caseBody
添加进main switchblock switchBlock
中。
CFG.appendCFG CFG.appendCFG
已经在之前将基本CFG块合成较大的CFG块时出现好几次了,现在来看下它到底做了什么事情。
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public class CFG extends IncidenceListGraph <CFGNode , CFGEdge > { private CFGNode entry; private CFGNode exit; private CFGNode error; private List<CFGNode> parameters; private List<CFGNode> breakStatements; private List<CFGNode> continueStatements; private List<CFGNode> returnStatements; private HashMap<CFGNode, String> gotoStatements; private HashMap<String, CFGNode> labels; private CFGExceptionNode exceptionNode; public void addCFG (CFG otherCFG) { addVertices(otherCFG); addEdges(otherCFG); getParameters().addAll(otherCFG.getParameters()); getBreakStatements().addAll(otherCFG.getBreakStatements()); getContinueStatements().addAll(otherCFG.getContinueStatements()); getReturnStatements().addAll(otherCFG.getReturnStatements()); getGotoStatements().putAll(otherCFG.getGotoStatements()); getLabels().putAll(otherCFG.getLabels()); if (this .hasExceptionNode() && otherCFG.hasExceptionNode()) { CFGExceptionNode oldExceptionNode = getExceptionNode(); CFGExceptionNode newExceptionNode = new CFGExceptionNode(); setExceptionNode(newExceptionNode); addEdge(oldExceptionNode, newExceptionNode, CFGEdge.UNHANDLED_EXCEPT_LABEL); addEdge(otherCFG.getExceptionNode(), newExceptionNode, CFGEdge.UNHANDLED_EXCEPT_LABEL); } else if (otherCFG.hasExceptionNode()) { setExceptionNode(otherCFG.getExceptionNode()); } } public void appendCFG (CFG otherCFG) { addCFG(otherCFG); if (!otherCFG.isEmpty()) { for (CFGEdge edge1 : incomingEdges(getExitNode())) { for (CFGEdge edge2 : otherCFG .outgoingEdges(otherCFG.getEntryNode())) { addEdge(edge1.getSource(), edge2.getDestination(), edge1.getLabel()); } } removeEdgesTo(getExitNode()); for (CFGEdge edge : otherCFG.incomingEdges(otherCFG.getExitNode())) { addEdge(edge.getSource(), getExitNode(), edge.getLabel()); } } } private void addVertices (CFG cfg) { for (CFGNode vertex : cfg.getVertices()) { if (!(vertex.equals(cfg.getEntryNode()) || vertex.equals(cfg.getExitNode()))) { addVertex(vertex); } } } private void addEdges (CFG cfg) { for (CFGNode vertex : cfg.getVertices()) { for (CFGEdge edge : cfg.outgoingEdges(vertex)) { if (!(edge.getSource().equals(cfg.getEntryNode()) || edge.getDestination().equals(cfg.getExitNode()))) { addEdge(edge); } } } } }
当程序执行到下图时:
先记录下switchBlock
和caseBody
的状态:
1 2 3 4 5 6 7 8 9 10 // switchBlock [ENTRY] { [ENTRY] ==[]==> [(6) Variable] , } [EXIT] { } [(6) Variable] { } // caseBody [ENTRY] { [ENTRY] ==[]==> [(12) EchoStatement] , } [EXIT] { } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [EXIT] , }
对照neo4j中的可视化图形,switchBlock为:
caseBody为:
经过CFG.appendCFG
合成的new switchBlock为:
首先将otherCFG的vertex都加入到main CFG中,但是每个CFG块都有的entry结点和exit结点就不必重复加入了:
然后将otherCFG
的edges也都加入到main CFG中,这个main CFG就是调用appendCFG的对象。其中IncidenceListGraph.outgoingEdges(vertex)
会返回从该vertex出发的关系边,并且一条关系边中的两个结点不能存在entry结点或是exit结点:
返回后:
得到的CFG图目前是这样的:
1 2 3 4 5 [ENTRY] { [ENTRY] ==[]==> [(6) Variable] , } [EXIT] { } [(6) Variable] { } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { }
[(6) Variable]
和[(12) EchoStatement]
及[(14) BreakStatement]
是没有什么关联的,但是[(12) EchoStatement] ==[]==> [(14) BreakStatement]
是存在关系边的,所以后面需要将id为6的结点和这两个结点连接起来。
然后将otherCFG中一些必要的Statement也加入到main CFG中,比如加入了一个BreakStatement
:
然后重新组合otherCFG
和main CFG:
可以看到其对main CFG造成的改变就是多了一条[(14) BreakStatement] ==[]==> [EXIT]
的边。
但是目前[(6) Variable]
并没有与[(12) EchoStatement]
连接起来,所以接下去需要将caseBody的每个CFG头结点与switchBlock连接:
如此得到的switchBlock就变成了:
1 2 3 4 5 [ENTRY] { [ENTRY] ==[]==> [(6) Variable] , } [EXIT] { } [(6) Variable] { [(6) Variable] ==[apple]==> [(12) EchoStatement] , } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [EXIT] , }
现在的CFG就是下面的标红部分:
其中下面几个id的结点都是break结点:
id = 14
id = 21
id = 28
default case 接着需要确定switch语句中是否存在default标签,如果没有后面需要特殊处理下:
fixBreakStatements 最后,还需要对breakStatements
进行最后的处理,调用CFGFactory.fixBreakStatements()
处理:
传入的参数thisCFG
则是:
1 2 3 4 5 6 7 8 9 [ENTRY] { [ENTRY] ==[]==> [(6) Variable] , } [EXIT] { } [(6) Variable] { [(6) Variable] ==[apple]==> [(12) EchoStatement] ,[(6) Variable] ==[bar]==> [(19) EchoStatement] ,[(6) Variable] ==[cake]==> [(26) EchoStatement] ,[(6) Variable] ==[default]==> [EXIT] , } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [(19) EchoStatement] , } [(19) EchoStatement] { [(19) EchoStatement] ==[]==> [(21) BreakStatement] , } [(21) BreakStatement] { [(21) BreakStatement] ==[]==> [(26) EchoStatement] , } [(26) EchoStatement] { [(26) EchoStatement] ==[]==> [(28) BreakStatement] , } [(28) BreakStatement] { [(28) BreakStatement] ==[]==> [EXIT] , }
用一张更清晰的图来表示为:
从上图很容易发现多了两条边:
1 2 [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [(19) EchoStatement] , } [(21) BreakStatement] { [(21) BreakStatement] ==[]==> [(26) EchoStatement] , }
按照上图的语义,对照源码有:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php switch ($i) {case "apple" : echo "i is apple" ; break ; case "bar" : echo "i is bar" ; break ; case "cake" : echo "i is cake" ; break ; } ?>
当第5行break之后,还会继续执行到case "bar"
中的内容。
来分析下fixBreakOrContinueStatements
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static void fixBreakOrContinueStatements (CFG thisCFG, CFGNode target, Iterator<CFGNode> it) { while (it.hasNext()) { CFGNode breakOrContinueNode = it.next(); ASTNodeContainer nodeContainer = (ASTNodeContainer) breakOrContinueNode; BreakOrContinueStatement statement = (BreakOrContinueStatement) nodeContainer.getASTNode(); Integer depth = statement.getDepthAsInteger(); if (depth != 0 && depth != 1 ){ statement.decrementDepth(); continue ; } thisCFG.removeEdgesFrom(breakOrContinueNode); thisCFG.addEdge(breakOrContinueNode, target); it.remove(); } }
除去CFG中break结点后多余的edge:
最后将该break结点连接到exit结点上:
最后返回得到一个switchBlock:
得到的CFG结构为:
1 2 3 4 5 6 7 8 9 [ENTRY] { [ENTRY] ==[]==> [(6) Variable] , } [EXIT] { } [(6) Variable] { [(6) Variable] ==[apple]==> [(12) EchoStatement] ,[(6) Variable] ==[bar]==> [(19) EchoStatement] ,[(6) Variable] ==[cake]==> [(26) EchoStatement] ,[(6) Variable] ==[default]==> [EXIT] , } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [EXIT] , } [(19) EchoStatement] { [(19) EchoStatement] ==[]==> [(21) BreakStatement] , } [(21) BreakStatement] { [(21) BreakStatement] ==[]==> [EXIT] , } [(26) EchoStatement] { [(26) EchoStatement] ==[]==> [(28) BreakStatement] , } [(28) BreakStatement] { [(28) BreakStatement] ==[]==> [EXIT] , }
返回CFGFactory.newInstance 接着层层返回到CFGFactory.newInstance
:
其中function
,parameterBlock
和functionBody
分别为:
返回PHPCFGFactory.newInstance 最后返回到PHPCFGFactory.newInstance()
对ENTRY
结点和EXIT
结点的id进行分配,分别为2和3:
最后51行返回的CFG就是最终的CFG。
1 2 3 4 5 6 7 8 9 10 [(2) ENTRY] { [(2) ENTRY] ==[]==> [(6) Variable] , } [(3) EXIT] { } [(6) Variable] { [(6) Variable] ==[apple]==> [(12) EchoStatement] ,[(6) Variable] ==[bar]==> [(19) EchoStatement] ,[(6) Variable] ==[cake]==> [(26) EchoStatement] ,[(6) Variable] ==[default]==> [(30) NullNode] , } [(12) EchoStatement] { [(12) EchoStatement] ==[]==> [(14) BreakStatement] , } [(14) BreakStatement] { [(14) BreakStatement] ==[]==> [(30) NullNode] , } [(19) EchoStatement] { [(19) EchoStatement] ==[]==> [(21) BreakStatement] , } [(21) BreakStatement] { [(21) BreakStatement] ==[]==> [(30) NullNode] , } [(26) EchoStatement] { [(26) EchoStatement] ==[]==> [(28) BreakStatement] , } [(28) BreakStatement] { [(28) BreakStatement] ==[]==> [(30) NullNode] , } [(30) NullNode] { [(30) NullNode] ==[]==> [(3) EXIT] , }
可以看到和此前不一样的地方在于,[ENTRY]
和[EXIT]
变成了[(2) ENTRY]
和[(3) EXIT]
,它们有了自己的id,但注意它们的类型和其他结点不一样,它们是CFGEntryNode
和CFGExitNode
,其他的结点类型是ASTNodeContainer
,不过都是一种CFGNode
。
CSVCFGExpoter.writeCFGEdges 该函数的作用就是就是将CFGEdges写入到cpg_edges.csv
文件中。
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 70 71 72 package outputModules.csv.exporters;import java.util.Map;import cfg.CFG;import cfg.CFGEdge;import cfg.nodes.ASTNodeContainer;import cfg.nodes.AbstractCFGNode;import cfg.nodes.CFGEntryNode;import cfg.nodes.CFGExitNode;import cfg.nodes.CFGNode;import databaseNodes.EdgeTypes;import databaseNodes.NodeKeys;import outputModules.common.CFGExporter;import outputModules.common.Writer;public class CSVCFGExporter extends CFGExporter { @Override protected void writeCFGNode (CFGNode statement, Map<String, Object> properties) { properties.put(NodeKeys.FUNCTION_ID, String.format("%d" , Writer.getIdForObject(currentFunction))); Writer.addNode(statement, properties); } @Override protected void addFlowToLink (Object srcBlock, Object dstBlock, Map<String, Object> properties) { long srcId = Writer.getIdForObject(srcBlock); long dstId = Writer.getIdForObject(dstBlock); Writer.addEdge(srcId, dstId, properties, EdgeTypes.FLOWS_TO); } public void writeCFGEdges (CFG cfg) { for ( CFGEdge cfgEdge : cfg.getEdges()) { CFGNode src = cfgEdge.getSource(); CFGNode dst = cfgEdge.getDestination(); if ( (src instanceof ASTNodeContainer || src instanceof CFGEntryNode || src instanceof CFGExitNode) && (dst instanceof ASTNodeContainer || dst instanceof CFGEntryNode || dst instanceof CFGExitNode)) { Long srcId = (src instanceof ASTNodeContainer) ? ((ASTNodeContainer)src).getASTNode().getNodeId() : ((AbstractCFGNode)src).getNodeId(); Long dstId = (dst instanceof ASTNodeContainer) ? ((ASTNodeContainer)dst).getASTNode().getNodeId() : ((AbstractCFGNode)dst).getNodeId(); Writer.setIdForObject(src, srcId); Writer.setIdForObject(dst, dstId); addFlowToLink( src, dst, null ); } } Writer.reset(); } }
小结 本小节仅仅涉及的是switch源码的CFG生成,还有一些其他的可继续挖掘的信息我都是没有跟下去的。因为这些打算放在其他的文章里,进行专项学习。
之前在phith0n大大开设的知识星球里看到过,说现在的漏洞分析文章大多都是一步一步跟踪流程,贴了一张张截图,但是这样的文章大多是”没有灵魂的“,因为缺少了我们从中学习到的更加深层,更加通用的,我们在调试漏洞时思考的内容。这些内容只能对这一个漏洞适用,缺少了”可迁移性“,我们应该在文章体现的是更加本质 内容,我深以为然。但是我离这样的水平还有很长的距离,所以现在我还是将我跟踪调试的步骤一步一步记录下来。期望在结束的时候,我能学习到更加深层次的东西。