C/C++源码扫描系列- Joern 篇
文章首发于
https://xz.aliyun.com/t/9277
概述
和 codeql
,Fortify
相比 Joern
不需要编译源码即可进行扫描,适用场景和环境搭建方面更加简单。
环境搭建
首先安装 jdk
,然后从github
下载压缩包解压即可
https://github.com/ShiftLeftSecurity/joern/releases/
解压之后执行 joern
即可
然后我们 进入sca-workshop/joern-example
通过 importCode
分析我们的示例代码
joern-example$ ~/sca/joern-cli/joern
Compiling /home/hac425/sca-workshop/joern-example/(console)
creating workspace directory: /home/hac425/sca-workshop/joern-example/workspace
██╗ ██████╗ ███████╗██████╗ ███╗ ██╗
██║██╔═══██╗██╔════╝██╔══██╗████╗ ██║
██║██║ ██║█████╗ ██████╔╝██╔██╗ ██║
██ ██║██║ ██║██╔══╝ ██╔══██╗██║╚██╗██║
╚█████╔╝╚██████╔╝███████╗██║ ██║██║ ╚████║
╚════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝
Type `help` or `browse(help)` to begin
joern> importCode(inputPath="./", projectName="example")
然后使用 cpg.method.name(".*get.*").toList
可以打印出所有函数名中包含 get
的函数
joern> cpg.method.name(".*get.*").toList
res15: List[Method] = List(
Method(
id -> 1000114L,
code -> "char * get_user_input_str ()",
name -> "get_user_input_str",
fullName -> "get_user_input_str",
isExternal -> false,
signature -> "char * get_user_input_str ()",
...........
除了使用 importCode
解析代码外,还可以通过 joern-parse
工具解析代码
joern-example$ ~/sca/joern-cli/joern-parse ./
joern-example$ ls
cpg.bin example.c Makefile system_query test.sc workspace
解析完成后默认会将解析结果保存到 cpg.bin
中,可以通过 --out
参数指定保存的文件名。
$ ~/sca/joern-cli/joern-parse --help
Usage: joern-parse [options] input-files
input-files directories containing: C/C++ source | Java classes | a Java archive (JAR/WAR)
--language <value> source language: [c|java]. Default: c
--out <value> output filename
然后进入 joern
的命令行使用 importCpg
函数即可导入之前的解析结果。
joern> importCpg("cpg.bin")
Creating project `cpg.bin8` for CPG at `cpg.bin`
Creating working copy of CPG to be safe
Loading base CPG from: /home/hac425/sca-workshop/joern-example/workspace/cpg.bin8/cpg.bin.tmp
res0: Option[Cpg] = Some(value = io.shiftleft.codepropertygraph.Cpg@5b2ac349)
然后就可以开始对代码进行检索了。
Joern语法简介
Joern 的规则脚本的开发语言是 scala
,其在代码分析阶段会将代码转换成抽象语法树、控制流图、数据流图等结构,然后在规则解析阶段会将这些图的属性、节点都封装成 Java对象,我们开发的 scala 规则脚本主要就是通过访问这些对象以及 Joern提供的API来完成查询。
Joern 使用 cpg
这个全局对象表示目标源码中所有信息,通过这个对象可以遍历源码中的所有函数调用、类型定义等,比如使用 cpg.call
获取代码中的所有函数调用
joern> cpg.call
res4: Traversal[Call] = Traversal
joern
很多方法的返回值都是 Traversal
类型,可以使用 toList
方法( l
方法是这个的缩写)转成 List
方便查看.
joern> cpg.call.toList
或者
joern> cpg.call.l
这里有一点需要注意,Joern会把 =, +, &&
等逻辑、数学运算都转换为函数调用(形式为 <operator>.xxx
)保存到语法树中
Call(
id -> 1000529L,
code -> "*xx = user",
name -> "<operator>.assignment",
order -> 4,
methodFullName -> "<operator>.assignment",
argumentIndex -> 4,
signature -> "TODO",
lineNumber -> Some(value = 259),
columnNumber -> Some(value = 9),
methodInstFullName -> None,
typeFullName -> "",
depthFirstOrder -> None,
internalFlags -> None,
dispatchType -> "STATIC_DISPATCH",
dynamicTypeHintFullName -> List()
)
上面可以看到 =
赋值符号使用 <operator>.assignment
表示。
将 Joern
的查询结果转成 List
后(使用 Traversal
也可以进行过滤),我们就可以使用 Scala
的语法来对结果进行过滤,比如 filter
方法
joern> cpg.call.l.filter(c => c.name == "system")
res7: List[Call] = List(
Call(
id -> 1000419L,
code -> "system(cmd)",
name -> "system",
order -> 1,
methodFullName -> "system",
argumentIndex -> 1,
................................
................................
这里就是对 call
的结果进行过滤,返回调用 system
函数的位置。
在开发规则的时候可以查看代码的ast
, cpg
, ddg
等图形来帮助调试
joern> var m = cpg.method.name("call_system_safe_example").l.head
joern> m.dotAst.l
res19: List[String] = List(
"""digraph call_system_safe_example {
"1000522" [label = "(METHOD,call_system_safe_example)" ]
"1000523" [label = "(BLOCK,,)" ]
"1000524" [label = "(LOCAL,user: char *)" ]
"1000525" [label = "(<operator>.assignment,*user = get_user_input_str())" ]
"1000526" [label = "(IDENTIFIER,user,*user = get_user_input_str())" ]
"1000527" [label = "(get_user_input_str,get_user_input_str())" ]
"1000528" [label = "(LOCAL,xx: char *)" ]
"1000529" [label = "(<operator>.assignment,*xx = user)" ]
"1000530" [label = "(IDENTIFIER,xx,*xx = user)" ]
"1000531" [label = "(IDENTIFIER,user,*xx = user)" ]
"1000532" [label = "(CONTROL_STRUCTURE,if (!clean_data(xx)),if (!clean_data(xx)))" ]
"1000533" [label = "(<operator>.logicalNot,!clean_data(xx))" ]
"1000534" [label = "(clean_data,clean_data(xx))" ]
"1000535" [label = "(IDENTIFIER,xx,clean_data(xx))" ]
"1000536" [label = "(RETURN,return 1;,return 1;)" ]
"1000537" [label = "(LITERAL,1,return 1;)" ]
"1000538" [label = "(system,system(xx))" ]
"1000539" [label = "(IDENTIFIER,xx,system(xx))" ]
"1000540" [label = "(RETURN,return 1;,return 1;)" ]
"1000541" [label = "(LITERAL,1,return 1;)" ]
"1000542" [label = "(METHOD_RETURN,int)" ]
"1000522" -> "1000523"
"1000522" -> "1000542"
"1000523" -> "1000524"
"1000523" -> "1000525"
"1000523" -> "1000528"
"1000523" -> "1000529"
"1000523" -> "1000532"
"1000523" -> "1000538"
"1000523" -> "1000540"
"1000525" -> "1000526"
"1000525" -> "1000527"
"1000529" -> "1000530"
"1000529" -> "1000531"
"1000532" -> "1000533"
"1000532" -> "1000536"
"1000533" -> "1000534"
"1000534" -> "1000535"
"1000536" -> "1000537"
"1000538" -> "1000539"
"1000540" -> "1000541"
}
"""
)
然后找个在线Graphviz绘图网站绘制一下即可
除了Ast
,Joern
还对代码构建一下几种结构
joern> m.dot
dotAst dotCdg dotCfg dotCpg14 dotDdg dotPdg
本节只对基础语法进行介绍,其他的语法请看下文或者官方文档。
system命令执行检测
本节涉及代码
https://github.com/hac425xxx/sca-workshop/tree/master/joern-example/system_query
漏洞代码如下
int call_system_example()
{
char *user = get_user_input_str();
char *xx = user;
system(xx);
return 1;
}
代码通过 get_user_input_str
获取外部输入, 然后传入 system
执行。
def getFlow() = {
val src = cpg.call.name("get_user_input_str")
val sink = cpg.call.name("system").argument.order(1)
sink.reachableByFlows(src).p
}
代码解释如下:
- 首先根据
cpg.call.name
对所有的call
过滤,获取所有的get_user_input_str
函数调用,保存到src
- 然后获取所有
system
函数调用,并将其第一个参数(从1
开始索引)保存到sink
。 - 最后使用
sink.reachableByFlows(src)
检索出所有从src
到sink
的结果,然后对结果使用.p
方法,把结果打印出来。
可以看到对于每个搜索到的结果,Joern
会把数据的流动过程打印出来,结果中存在一个误报
clean_data
函数会对数据进行校验,不会产生命令执行,所以需要把这个结果过滤掉
def clean_data_filter(a: Any): Boolean = {
if(a.asInstanceOf[AstNode].astParent.isCall)
{
if(a.asInstanceOf[AstNode].astParent.asInstanceOf[Call].name == "clean_data")
return true
}
return false
}
def filter_path_for_clean_data(a: Any) = a match {
case a: Path => a.elements.l.filter(clean_data_filter).size > 0
case _ => false
}
def getFlow() = {
val src = cpg.call.name("get_user_input_str")
val sink = cpg.call.name("system").argument.order(1)
sink.reachableByFlows(src).filterNot(filter_path_for_clean_data)
}
Joern
没有类似 Fortify
的 DataflowCleanseRule
功能,只能对 reachableByFlows
的结果进行过滤, reachableByFlows
返回的是一组 Path
对象,然后我们使用 filterNot
来剔除掉不需要的结果,每个 Path
对象的 elements
属性是这条数据流路径流经的各个代码节点,我们可以遍历这个来查看 Path
中是否存在有调用 clean_data
函数,如果存在就说明返回 true
表示这个结果是不需要的会被剔除掉。
clean_data
函数调用在 Path
中是以 Identifier
( xx
变量) 存在,所以在规则中需要先将Path里面的每一项强转为 AstNode
类型 ,然后获取它的父节点,最后根据父节点就可以知道是否为 clean_data
的函数调用,这个信息可以通过绘制 ast
图来确定。
joern> ci.astNode.astParent.astParent.astParent.dotAst.l
res58: List[String] = List(
"""digraph {
"1000532" [label = "(CONTROL_STRUCTURE,if (!clean_data(xx)),if (!clean_data(xx)))" ]
"1000533" [label = "(<operator>.logicalNot,!clean_data(xx))" ]
"1000534" [label = "(clean_data,clean_data(xx))" ]
"1000535" [label = "(IDENTIFIER,xx,clean_data(xx))" ]
"1000536" [label = "(RETURN,return 1;,return 1;)" ]
"1000537" [label = "(LITERAL,1,return 1;)" ]
"1000532" -> "1000533"
"1000532" -> "1000536"
"1000533" -> "1000534"
"1000534" -> "1000535"
"1000536" -> "1000537"
}
"""
)
图中标蓝的就是 Path
中 clean_data
函数调用的子节点 Identifier
.
引用计数漏洞
本节相关代码
https://github.com/hac425xxx/sca-workshop/blob/master/joern-example/ref_query/
漏洞代码
int ref_leak(int *ref, int a, int b)
{
ref_get(ref);
if (a == 2)
{
puts("error 2");
return -1;
}
ref_put(ref);
return 0;
}
ref_leak
的 漏洞是当 a=2
时会直接返回没有调用 ref_put
对引用计数减一,漏洞模型:在某些存在 return
的条件分支中没有调用 ref_put
释放引用计数。
首先可以看看代码的 AST 结构
def getFunction(name: String): Method = {
return cpg.method.name(name).head.asInstanceOf[Method]
}
getFunction("ref_leak").dotAst.l
Joern
使用 controlStructure
来表示函数中的控制结构,后续可以使用这个对象来访问函数的控制结构。
下面具体分析下如何编写规则匹配到这种漏洞,首先获取所有调用 ref_get
的地方
def search() = {
var ref_get_callers = cpg.call.name("ref_get")
ref_get_callers.filter(ref_func_filter).map(e => getEnclosingFunction(e.astNode))
}
然后对 ref_get_callers
进行过滤,把存在漏洞的函数过滤出来,过滤核心函数位于 ref_func_filter
,关键代码如下
def ref_func_filter(caller: Call): Boolean = {
var node : AstNode = caller.astNode
var block : Block = null
var func : Method = null
var ret : Boolean = false
loop.breakable {
while(true) {
if(node.isBlock) {
block = node.asInstanceOf[Block]
}
if(node.isMethod) {
func = node.asInstanceOf[Method]
loop.break;
}
node = node.astParent
}
}
var true_block = func.controlStructure.whenTrue.l
var false_block = func.controlStructure.whenFalse.l
var ref_count = 1
if(true_block.size != 0) {
.................
}
return ret
}
函数大部分都是使用的 Scala
的语法,其实 Joern
的规则开发在一些情况下就是使用 Scala
语法来搜索代码结构
由于我们这里过滤的是 ref_get_callers
,所以入参的类型是 Call
,然后通过循环地向上遍历 ast
,获取到该 Call
所在的函数和 Block
。
然后根据 func
来获取代码中的控制结构,然后获取到控制结构为 True
或者 False
时的代码块,然后对代码块遍历,搜集到 Return
之前的引用计数是否有问题。
var true_block = func.controlStructure.whenTrue.l
var ref_count : Int = 1
if (true_block.size != 0) {
var block = true_block(0)
for (elem <- block.astChildren.l) {
if (elem.isCall && elem.asInstanceOf[Call].name == "ref_put") {
ref_count -= 1
}
if (elem.isCall && elem.asInstanceOf[Call].name == "ref_get") {
ref_count += 1
}
if (elem.isReturn) {
println("func_name: " + func.name + ", detect return expr, current ref_count: " + ref_count)
if (ref_count != 0) {
ret = true
}
ref_count = refcount_bak
}
}
}
代码解释如下
- 首先根据
func.controlStructure.whenTrue
获取控制结构为 True 时执行的代码块,然后遍历它的astChildren
. - 如果是
ref_put
就把ref_count - 1
,如果在Block
里面有Return
语句就打印ref_count
,如果引用计数不为0说明就有问题.
执行结果如下:
总结
Joern
本身的功能还是不错的,此外用户还可以使用 Scala 语言来完成框架不支持的事情,也是非常灵活的一个工具,不过有时候需要遍历语法树、控制流图等结构,需要对编译原理有一定的了解。
参考链接
https://docs.joern.io/c-syntaxtree#basic-ast-traversals
C/C++源码扫描系列- Joern 篇的更多相关文章
- 源码学习系列之SpringBoot自动配置(篇一)
源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...
- Laravel 源码解读系列第四篇-Auth 机制
前言 Laravel有一个神器: php artisan make:auth 能够快速的帮我们完成一套注册和登录的认证机制,但是这套机制具体的是怎么跑起来的呢?我们不妨来一起看看他的源码.不过在这篇文 ...
- 源码学习系列之SpringBoot自动配置(篇二)
源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...
- 鸿蒙源码分析系列(总目录) | 百万汉字注解 百篇博客分析 | 深入挖透OpenHarmony源码 | v8.23
百篇博客系列篇.本篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o 百篇博客.往期回顾 在给OpenHarmony内核源码加注过程中,整理出以下 ...
- MyBatis 源码分析系列文章导读
1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...
- Spring源码解析系列汇总
相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...
- 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 百篇博客分析OpenHarmony源码 | v67.01
百篇博客系列篇.本篇为: v67.xx 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 百篇博客分析OpenHarmony源码 | v25.01
百篇博客系列篇.本篇为: v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...
- 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 百篇博客分析OpenHarmony源码 | v7.07
百篇博客系列篇.本篇为: v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...
- 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 百篇博客分析OpenHarmony源码 | v3.05
百篇博客系列篇.本篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...
随机推荐
- chatGPT能做职业规划?看完之后发现3年软测白做了!
"每天都是重复.单调的工作,收入不理想,想跳槽无力,学习又没有动力和方向,不知道未来的发展在哪里,甚至想转行·····" 做测试久了,很多人都有诸如此类的疑惑,不想一直停留在测试需 ...
- Apache-Shiro <=1.2.4 反序列化漏洞 (代码审计)
一.Apache Shiro 简介: Apache Shiro提供了认证.授权.加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码.并且在实现此目 ...
- 系统编程-文件IO-dup和dup2系统调用
在linux下,一切皆文件. 文件描述符用于操作文件. 从shell中运行一个进程,默认会有3个文件描述符存在(0.1.2):)0表示标准输入,1表示标准输出,2表示标准错误. 一个进程当前有哪些打开 ...
- [Tkey] [IOI 2018] werewolf
注意看,我耗时五个小时 AK 了 IOI 题意 给你一个图,每次给定若干询问 \((s,t,l,r)\),请你完成下述要求: 定义 \(S\) 为到 \(s\) 的最短路径不小于 \(l\) 的点构成 ...
- 【赵渝强老师】MongoDB中的索引(下)
(四)索引的类型三:复合索引(Compound Index) MongoDB支持复合索引,即将多个键组合到一起创建索引.该方式称为复合索引,或者也叫组合索引,该方式能够满足多键值匹配查询使用索引的情形 ...
- vue前端开发仿钉图系列(7)底部数据列表的开发详解
底部数据列表主要是记录图层下面对应的点线面数据,点击单元行或者查看或者编辑,弹出右侧编辑页面,点击单元行地图定位到相应的绘图位置.里面的难点1是动态绑定字段管理编辑的字段以及对应的value值,2是点 ...
- C# 新语法 switch 的简单写法
// C# 中的新语法 switch 的简写 string str = "123"; string res = str switch { "1" => & ...
- Android复习(三)清单文件中的元素——>uses-configuration、uses-library、uses-permission、uses-permission-sdk-23
<uses-configuration> 语法: <uses-configuration android:reqFiveWayNav=["true" | &quo ...
- Exchange学习非常有用的网站
Exchange学习非常有用的网站 https://docs.microsoft.com/zh-cn/exchange/plan-and-deploy/deployment-ref/network-p ...
- 云原生周刊:6 项 K8s 成本控制策略 | 2023.7.17
开源项目推荐 Base Image Finder 当使用容器扫描工具来识别已知漏洞(CVE,或常见漏洞和暴露)时,可能很难理解漏洞在容器中的位置,以及如何缓解这些漏洞.通常,最简单.最有效的缓解方法是 ...