官网文档:joern
程序分析常常需要生成一些程序的中间表示,中间表示包括但不限于
- Abstract Syntax Tree(AST, 抽象语法树)
- Control Flow Graph(CFG,控制流图)
- Control Dependence Graph(CPG,数据依赖图)
- Data Dependence Graph (DDG,数据依赖图)
- Program Dependence graph (PDG,程序依赖图, = CPG + DDG)
- Code Property Graphs (CPG,程序属性图, = 前面5种图的总和)
而Joern则是一款很好的生成这些中间表示的工具,可以从github上下载joern-cli命令行工具,Joern支持C/C++和Java编程语言,支持Windows/Mac/Linux平台,不过joern-cli种的bat脚本不太好用,不知道windows下应该怎么用。
Joern-cli命令行交互模式
joern-cli的目录下包括
joern, joern-parse等均是可执行文件,执行 ./joern
会进入命令行交互模式
这里以生成下面C语言代码的AST等中间表示为目标
示例代码1
int myfunc(int b) {
int a = 42;
if (b > 10) {
foo(a);
}
bar(a);
return a;
}
由于该示例代码并无控制依赖(按道理说foo(a)
应该依赖于b>10
,可能有解析错误)和数据依赖,故没有列出。
命令行输入importCode.c.fromString( """ int myfunc(int b) { int a = 42; if (b > 10) { foo(a); } bar(a); } """ )
生成AST
输入cpg.method("myfunc").plotDotAst
生成CFG
输入cpg.method("myfunc").plotDotCfg
生成CPG
输入cpg.method("myfunc").plotDotCpg14
可以看到这段代码并没有明确的控制依赖和数据依赖关系,控制流图也是以行(也可以解释为statement)来生成,不过这些中间表示中foo(a);
去哪了就不清楚了。
示例代码2
#include<stdio.h>
#include<string.h>
int main() {
unsigned int first_len = UINT_MAX - 256;
unsigned int second_len = 256;
unsigned int buf_len = 256;
char first[first_len], second[second_len], buf[buf_len];
int new_len = (first_len+second_len); // <- IDB (negative)
if(new_len <= 256) {
memcpy(buf, first, first_len);
memcpy(buf + first_len, second, second_len); /* <=== */
}
return 0;
}
由于示例代码的AST和CPG过于庞大,故没画出。
CFG
这里可以看出CFG中
- 变量声明语句(如
char first[first_len], second[second_len], buf[buf_len];
)不会出现在CFG中 - CFG中一个大的statement会被拆分为几个小的(如
unsigned int first_len = UINT_MAX - 256;
可拆分为 变量声明unsigned int first_len
,减法UINT_MAX - 256
和赋值first_len = UINT_MAX - 256
,其中变量声明会被无视,而减法又在赋值之前执行),每个小的statement会作为一个控制流结点出现在CFG中。
CDG
可以看到CDG中,if
语句块中的语句会对if
条件中的语句有控制依赖。
示例代码3
#include <stdio.h>
int main () {
int i = 0;
char c;
while (i < 10){
getchar(c);
++i;
}
printf("%d",i);
}
CFG
可以看出循环语句除了循环体最后一个statement有一个边连回循环条件之外其它跟if
语句差不多
CDG
可以看出与if
条件的CDG相比,循环语句的CDG多了一条循环条件statement连回自己的边
示例代码4
#include <stdio.h>
int main () {
int i;
char c;
for (i = 0; i < 10; ++i){
getchar(c);
printf("%c",c);
}
printf("%d",i);
}
CFG
CDG
可以看到生成CFG和CDG时,
for (i = 0; i < 10; ++i){
dosomething();
}
和
i = 0;
while(i < 10){
dosomething();
++i;
}
的CFG一样。
不过目前如何用joern-cli像joern一样导出代码的图表示的csv文件还不清楚,官网给的是用scala写脚本导出json,不过graph-func.sc脚本好像总是有编译错误,不知道是什么问题。
Joern
joern的目录如下,文档参考
这是老版本的Joern,现在好像停更了,与Joern-Cli不同的是,这个版本的Joern会对每个输入的.c文件生成一个edges.csv和nodes.csv。
用法如下:./joern-parse output SourceDir
-
SourceDir为存放源代码的文件夹,joern会解析该文件夹下所有.c,.cpp等文件。
-
output存放结果
针对test1.c生成的edges.csv和nodes.csv
test1.c内容
#include "stdio.h"
#include "string.h"
#include <stdio.h>
void foo()
{
int x = source();
if (x < MAX)
{
int y = 2 * x;
sink(y);
}
}
int main()
{
foo();
}
这里nodes.csv有几个内容
重要的有key, type, code, location, isCFGnode
- key为该CPG图结点的索引
- type表示是什么类型的结点,IdentifierDeclStatement,ArgumentList等等
- code为该结点对应的代码
- location包含该结点对应的行号
- isCFGnode为
True
表示这是一个CFG结点,为空或False表示AST结点
以上面test1.c的foo
函数来说
void foo()
{
int x = source();
if (x < MAX)
{
int y = 2 * x;
sink(y);
}
}
它有4个statement。int x = source();
, if (x < MAX)
, int y = 2 * x;
, sink(y);
。
CPG如下:
那么ENTRY,DECL,PRED,DECL, CALL, EXIT即CFG结点。CFG结点包含的code为一个完整的statement的code。而剩下的即AST结点,AST结点通常只包含一个statement的部分code。
edges.csv包含的域如下:
- start表示开始结点的索引,即key
- end为结束结点的索引
- type为边类型,这里有12种边
- IS_AST_PARENT
- IS_CLASS_OF
- FLOWS_TO
- DEF
- USE
- REACHES
- CONTROLS
- DECLARES
- DOM
- POST_DOM
- IS_FUNCTION_OF_AST
- IS_FUNCTION_OF_CFG
- var表示边的值,比如如果两个结点关于变量
buf
存在数据依赖,那值就是buf
。
其中
- FLOWS_TO 表示控制流图(control flow),只连接CFG结点。
- CONTROLS 表示控制依赖图(control dependency),只连接CFG结点。
- REACHES 表示数据依赖图(data dependency),只连接CFG结点。