Joern 入门

前言

最近手上刚好有一个需要做两个方法间可达性分析的需求,且目标是多语言的,因此 tai-e 之类单语言的工具就行不通了。当时试了试常见的多语言分析分析工具,比如 codeql、蚂蚁新出的 YASA 之类的,codeql每种语言的 api 不太一样,兼容性很低,毕竟 codeql 虽然自称是多语言的分析工具,但其实还是对每种语言专门做了一个 AST,代码复用率很低;而 YASA 虽然利用 UAST 对多语言做了大一统,代码复用率比较高,但 YASA 毕竟只是一个面向安全场景的污点分析工具,在程序分析上的能力有点不尽如意;最后问了问好大哥 yulate,给我推荐了 Joern,试了试果然不赖,既好上手又很好用,完美的解决了我当前的需求。

原理浅析

对于多语言静态分析大一统这个问题,不同的工具给出了不一样的解决思路,上面也已经简单介绍过了。

YASA 采用的方式是他们自己提出了一套新的中间表示 UAST(Unified Abstract Syntax Tree),简单来讲,他们尽可能的将不同语言的 AST 用同一种节点兼容,无论是 java、python 还是 go,大家都有 if 条件句等共有的结构,所以都能用 IfStatement 等节点做兼容,但不同语言毕竟有很多不同的差异,像 python、go等语言都有自己独特的特性,因此为了兼容这些特性,YASA 做了一些特定的语言节点,比如 python 的 YieldExpression 或 Go 的 ChanType,这就是整体的思路,在污点传播上也是同理,大家都有变量赋值、方法调用等共有的传播方式,也有一些每种语言特有的传播方式,也需要不断的做兼容。通过主干的通用逻辑尽可能的覆盖所有语言,再辅以每种语言独特的特性做支撑,YASA 的整体原理便是如此。

codeql 依赖于特定语言的提取器和数据库模式,将每种语言转化成对应的 AST 保存在数据库,将程序分析的过程(比如找 Source 到 Sink 的路径)转换成数据库的查询,即 ql 语句,变成查询一条类似 select source where sink = xxx 的语句;WALA/LLVM 的做法较为底层,将不同语言的程序分析问题抽象成底层的 SSA 形式,缺点很明显,就是难以精准捕捉动态语言的高级语义

Joern 选择的方式是利用 CPG(Code Property Graph),全称叫做代码属性图,在2014年的 S&P 上由论文《Modeling and Discovering Vulnerabilities with Code Property Graphs》提出,可以看到 CPG 诞生在网安顶会上,也能想到它的出现也是为了解决安全问题的,简单来说, CPG 把 AST + CFG + PDG 融合成一个“能跑复杂程序语义查询”的统一图模型。

  • AST(Abstract Syntax Tree,抽象语法树):AST 是“代码的语法结构”,把源码按语言语法规则解析成一棵树,用于将复杂的原始代码进行抽象
  • CFG(Control Flow Graph,控制流图):CFG 是“程序怎么跑”,把代码拆成基本块,用边表示执行顺序,利用 CFG 可以抽象出代码的执行过程
  • PDG(Program Dependence Graph,程序依赖图):PDG = 数据依赖 + 控制依赖,数据依赖关注数据是怎么流动的,比如某变量是否来自用户输入,是污点分析的核心;控制依赖关注某条语句是否依赖某个条件是否成立,即哪些判断控制了它是否能够执行

传统的静态分析总归有缺陷:

  • AST:语法很清楚,但不知道怎么执行
  • CFG:执行路径有了,但不知道数据从哪来
  • PDG / DFG:数据依赖有了,但结构上下文丢了

而对于常见的安全问题,比如污点类型的漏洞,我们关注的东西横跨了三者:某个外部输入,是否在某条可执行路径上,未经校验流入了一个危险函数,单用 AST / CFG / DFG,答案都不完整,而通过 CPG 可以完整的获取我们所有需要的信息,在查询中建立复杂的约束:在同一执行路径上是否存在 source →(无 sanitize)→ sink ,这样就很能大程度的解决挖洞的问题

至于为什么利用 CPG 就可以做多语言,理由其实也很简单,只要你能将不同语言的 AST 都统一转换成 CPG,大家当然都能用同一套规则在 CPG 中做查询,我们翻看 joern 的目录,也可以看到很多叫 xxx2cpg 的神秘可执行文件:

只要你闲得慌,为你想兼容的语言做好 AST->CPG 的转换,就可以方便的兼容新的语言,而 codeql 这么久了还没兼容 php,就是因为 codeql 的跨语言兼容其实做的很不好,不但不开源,而且对于每种语言都有自己的解析逻辑,对于 joern 而言,你只需要做好 AST->CPG 的转换,其他工作都能用统一的引擎做查询,而 codeql 还得做一次又一次的适配

实战演练

joern 的环境搭建其实很简单,开发者甚至直接打包好了,就这个 joern-cli:

https://github.com/joernio/joern/releases

解压出来就长这样,windows 用 xxx.bat,linux 直接用可执行文件:

经过我的观察,js 和 python 做源码到 CPG 转换的这一步似乎已经直接打包在 joern.bat 里面了,所以我们可以直接用 importCode("代码地址") 这一步导入源码, joern 会自动做 CPG 转换,然后你就可以开始愉快的开始查东西了;但对于一些可能是后来才兼容进来的语言,比如 java、go,目录里是直接提供了 javasrc2cpg.bat ,所以我们还得先手动转成 CPG 然后做导入

这里我随便找了一个 java 项目:https://gitee.com/opencc/JFlow,我本地的源码版本可能和线上有点不一样,大家以学习为主即可,我们的目标就是查出来下面这个典型的 SQL 注入漏洞代码:

    public final Object Demo_SFTable_EmpsByDeptNo(String token, String deptNo)
    {
        //根据token登录.
        try
        {
            Port_GenerToken(token);

            DataTable dt = DBAccess.RunSQLReturnTable("SELECT No,Name FROM Port_Emp WHERE FK_Dept='" + deptNo + "' ORDER BY No");
            dt.Columns.get(0).ColumnName  = "No";
            dt.Columns.get(1).ColumnName = "Name"; //这里是故意处理, 用于测试是否可以转化为标准格式的字典.
            return Return_Info(200, "执行成功", bp.tools.Json.ToJson(dt));
        }
        catch (RuntimeException ex)
        {
            return Return_Info(500, "执行失败", ex.getMessage());
        }
    }

很显然,参数 deptNo 被直接拼接在了 SQL 语句里然后直接做了查询,存在一目了然的 SQL 注入,那么就让我们用 Joern 来自动化的发现这类污点型漏洞吧!

首先我们需要将原始的 java 项目转成 CPG:

D:/BaiduNetdiskDownload/joern-cli/joern-cli/javasrc2cpg.bat -J-Xmx8092m D:/BaiduNetdiskDownload/jflow/jflow-web --output temp_cpg.bin.zip

接下来在当前路径下应该可以看到一个 temp_cpg.bin.zip,这就是我们的 CPG了:

然后输入 joern.bat 的路径加载进 joern 的页面:

接着用 importCpg("./temp_cpg.bin.zip") 引入 CPG(对于 js 和 python 可以直接用 importCode("代码地址") 导入源码,joern 会自动做 CPG 的转换)

接下来就可以对它进行查询了,对于传统的污点分析问题,我们需要三步:设置 Source、设置 Sink、执行查询,joern 中提供了一个叫做 reachableByFlows 的接口可以方便的做可达性分析,首先定义 Source,这里我做的限制是 所有位于 @GetMapping / @PostMapping 方法中的 String 类型方法参数

val src = cpg.method.parameter.where(_.typeFullNameExact("java.lang.String")).where(_.method.annotation.name(".*GetMapping|.*PostMapping"))

接着定义 Sink,这里我们只需要找到存在 RunSQLReturnTable 调用的节点即可:

val sink = cpg.call.name("RunSQLReturnTable")

最后一步进行查询,判断 Source 是否真的可以流向 Sink:

sink.reachableByFlows(src).p

可以看到一下查出来了很多东西,当然,有些执行路径是有洞的,有些是没洞的,下面这个就是我们想查的路径,可以很明显的看到参数 deptNo 通过拼接被 RunSQLReturnTable 执行了,存在显而易见的 SQL 注入:

我们可以用类似 cursor 的 vibe coding 工具让 LLM 做路径验证,joern 的输出中有极为详细的调试信息,对于 LLM 而言可以很精确的验证执行路径中是否真的存在漏洞:

上面的这套 CPG+LLM 的组合拳思路来自于 LLMxCPG ,虽然现在 LLM 很发达,但适合传统工具做的事也应当拿传统工具做, LLM 强在分析和研判,我们应当让它做它适合的事,即判断某个路径是否真的存在漏洞,对于查询路径这种脏活累活,还是适合 joern、codeql 这类传统程序分析工具。一个好消息是,joern 是多语言通用的,LLM 也是多语言通用的,因此 LLM+CPG 的思路也能沿用到多语言的漏洞分析场景上,通过同一套固定的规则即可在不同语言上做精确的安全分析

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇