注:公众号无法引用外链,可点击文末原文链接跳转到先知社区进行阅读。
自从 2019 年 Github 开放了 CodeQL 项目以来,因其出色的实用性及灵活性,CodeQL 就成为了安全行业里经久不衰的热点话题。经过两年开源社区以及资本主义的支持,目前 CodeQL 的学习资料已经非常多了,但是对于 Android 系统源码(AOSP)分析的资料却寥寥无几,本文将简单的介绍 CodeQL,并且记录使用其对 AOSP 进行分析的过程。
CodeQL 简介
通常认为,CodeQL 是一个开源的代码分析平台或者说漏洞扫描工具,其通过介入代码编译过程(编译型语言)或者进行静态程序分析(解释型语言)来获取程序代码的语义信息(Extractor),并且生成数据库(CodeQL Database),最后使用领域专用语言(这里指 QL language)编写查询语句来发现漏洞风险。
CodeQL 的前身是 Semmle 的 LGTM 平台,其以 SaaS 的形式提供服务,普通用户无法下载到本地分析工具,2019 年 Github 收购了 Semmle 之后开放了本地工具并且开源了部分的代码。目前 CodeQL 开源的部分主要是“查询”的内容,包括了漏洞规则以及程序分析的查询,让(白)安全社区(嫖)可以更好的参与到漏洞规则的贡献中来,为此 Github 也花了很多精力尝试手把手教会广大安全从业者编写 CodeQL 查询。
CodeQL 继承了 Semmle 所有的技术资产,不过比如各前端的 Extractor、QL(编译器、Datalog 实现等)、数据库、CLI 等配套项目其实依然是闭源的(但是因为以前是 SaaS 所以似乎没有考虑过二进制保护,很多模块逆向跟看源码也没什么区别),因此从开源的角度来讲,目前开源的 CodeQL 主仓库也许(因为还开源了少量的其他模块)只能称之为一个 "Query Library",不过这并不影响 CodeQL 为安全社区带来的卓越贡献:
-
最直观的是通过 CodeQL 发现过众多 Zero-Day 漏洞。 -
整合了大量的流行漏洞模型,CodeQL 可能是目前覆盖最广的公开规则包。 -
带来了一个大众级的现代化代码审计解决方案(同时也盘活了 Semmle)。 -
为静态程序分析行业带来了一些热度,相信也会间接的带动更多学术成果落地。
CodeQL 相关资料
-
【官方】CodeQL 主仓库 -
【官方】CodeQL 主页 -
【官方】CodeQL 文档 -
【官方】LGTM 文档 -
【官方】QL 语法手册 -
【官方】CodeQL 手把手培训以及示例 -
【官方】CodeQL 课程 U-Boot Challenge (C/C++) -
【官方】CodeQL Action -
【官方】CodeQL 命令行工具 -
【官方】CodeQL Extractor & Libraries for Go -
【官方】CodeQL Extractor for C# -
【官方】CodeQL 的 VSCode 扩展 -
【翻译】CodeQL 部分文档中文翻译(原作者 Xsser) -
【整合】https://github.com/SummerSec/learning-codeql -
【整合】https://github.com/Firebasky/CodeqlLearn
分析 AOSP
AOSP 大部分的代码使用 Java 或者 C 系列的语言编写,CodeQL 对于这些编译型的语言需要介入编译器,从编译器中获取到代码的语义信息,意味着需要将我们需要的代码进行编译之后 CodeQL 才能生成分析数据库。由于 AOSP 的代码以及编译系统太过庞大,所以我建议先将 AOSP 整体编译一遍,然后清除目标分析代码的编译结果,最后再使用 CodeQL 重新编译一遍。
整体编译 AOSP
[*] 重要提示:AOSP 已经不再支持使用 macOS 进行编译了,请安装 Ubuntu!!!
虽然网上有很多 AOSP 的编译教程,不过对于 master 分支我建议还是看官方文档来吧,毕竟现在 AOSP 的编译系统方便很多,一般都不需要配置很多复杂的环境,但如果搜到了一些乱七八糟的教程可能反而会浪费很多时间,以下是我推荐的步骤:
-
操作系统选择 64 位的 Ubuntu 18.04 或者 Ubuntu 16.04,准备 16+ G 内存、500+ G 磁盘。
-
国内推荐使用清华的镜像源的每月初始化包来拉取最新代码,当然如果你以前拉取过那只需要直接同步即可,此步骤大概需要下载 100G 代码,最终占用硬盘 200+G。
-
下载每月初始包: wget -c https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar
-
解压: tar xf aosp-latest.tar
-
更新 repo 工具: cd AOSP/.repo/repo; git pull origin master
-
同步代码: cd ../../; ./.repo/repo/repo sync
-
根据官方文档使用 apt 安装相关的依赖包,16.04 可以将 18.04 以及 14.04 的依赖都安装上,目前 AOSP 编译系统里非常贴心的自带了 JDK,不用自己再去安装对应的 JDK 版本了。
-
根据官方文档开始编译。
-
source build/envsetup.sh
, 初始化环境。 -
lunch aosp_x86_64-eng
, 这里是编译 x86_64 版本的工程镜像,运行 lunch 可以查看并选择其他版本。 -
m -j16
, 这里是 16 个线程,根据自己 CPU 线程调节。 -
然后就是摸鱼时间,时间长短取决于你机器配置,一般需要 2-8 个小时不等。
假如你的硬盘空间足够,不出意外的话,编译结束应该会占有 300+G 的空间,并且最后出现绿色的编译成功提示:
#### build completed successfully (08:57:26 (hh:mm:ss)) ####
并且在 out/target/product/generic_x86_64/
目录下就可以看到完整的镜像文件:
$ ls -la out/target/product/generic_x86_64/ | grep img
-rw-rw-r-- 1 aosp aosp 67108864 Feb 1 18:15 boot-5.10-allsyms.img
-rw-rw-r-- 1 aosp aosp 67108864 Feb 1 18:15 boot-5.10.img
-rw-rw-r-- 1 aosp aosp 11173895 Feb 1 18:15 ramdisk.img
-rw-rw-r-- 1 aosp aosp 2123583488 Feb 1 21:06 system.img
-rw-rw-r-- 1 aosp aosp 4096 Feb 1 21:06 vbmeta.img
生成 CodeQL 数据库
本节以分析 Frameworks 为例,将记录生成数据库其中遇到的坑以及调试 CodeQL 的过程,若想直接得到最终的分析方法请拉到本节的结尾。
根据 CodeQL CLI 的文档,使用 --command
参数可以指定启动编译系统的命令,比如 AOSP 里使用 mmm
命令可以编译指定的模块,那么生成数据库的命令类似如下:
codeql database create ../codeql_frameworks
--language=java
--command="mmm frameworks/base"
--source-root frameworks/base
--overwrite
同时使用了 --source-root
参数指定了要分析的源码根目录,否则 CodeQL 将对 AOSP 整个目录树进行代码统计,速度非常之慢。使用 --overwrite
是允许覆盖旧的数据库。
然而不出意外的话,将得到以下的报错:
Initializing database at /home/aosp/codeql_frameworks.
Running build command: [mmm, frameworks/base]
[2022-02-01 22:26:22] [ERROR] Spawned process exited abnormally (code 1; tried to run: [/home/aosp/codeql/tools/linux64/preload_tracer, mmm, frameworks/base])
[2022-02-01 22:26:22] [build-stderr] Runner failed to start 'mmm': No such file or directory
A fatal error occurred: Exit status 1 from command: [mmm, frameworks/base]
意思是找不到 mmm
命令,因为这个命令来自于 source build/envsetup.sh
,只对于当前的终端(sh/bash/zsh/...)会话生效,从报错信息中可以看到 CodeQL 使用了一个 preload_tracer
程序来启动编译进程,而新的进程里是无法使用终端命令的,所以无法找到 mmm
。
通过逆向分析可以得知 preload_tracer
的作用是利用 LD_PRELOAD
(macOS 下为 DYLD_INSERT_LIBRARIES
)环境变量来向编译系统注入 libtrace
:
libtrace
实际上是一个 lua
解释器,用来解释 tools/tracer/base.lua
以及各语言下用于注入编译器的 tracing-config.lua
,比如 java/tools/tracing-config.lua
:
function RegisterExtractorPack(id)
local pathToAgent = AbsolutifyExtractorPath(id, 'tools' .. PathSep ..
'codeql-java-agent.jar')
-- inject our CodeQL agent into all processes that boot a JVM
return {
CreatePatternMatcher({'.'}, MatchCompilerName, nil, {
jvmPrependArgs = {
'-javaagent:' .. pathToAgent .. '=ignore-project,java',
'-Xbootclasspath/a:' .. pathToAgent
}
})
}
end
-- Return a list of minimum supported versions of the configuration file format
-- return one entry per supported major version.
function GetCompatibleVersions() return {'1.0.0'} end
这段脚本的作用就是往所有 JVM 进程注入 codeql-java-agent.jar
, 而在这个 agent 中将会调用 semmle-extractor-java
来完成代码编译信息提取的工作。
了解完 codeql create database
的工作原理,就能很容易的想到如何解决找不到命令的问题了,这里有两种方法:
-
参考 codeql-cli-binaries issues#47,使用直接调用 soong 的命令进行编译:
codeql database create ../codeql-frameworks
--language=java
--command="`pwd`/build/soong/soong_ui.bash --make-mode -j1 framework-minus-apex"
--source-root frameworks/base
--overwrite
-
使用一个编译脚本让 preload_tracer
启动bash
进行编译:
$ cat mmm.sh
#!/bin/bash
cd $1
source ./build/envsetup.sh
lunch aosp_x86_64-eng
mmm $2
$ codeql database create ../codeql-frameworks
--language=java
--command="`pwd`/mmm.sh `pwd` frameworks/base"
--source-root frameworks/base
--overwrite
选择其中一种编译方法运行之后,应该就可以进入正常的编译环节(编译之前可以先删除一下之前的缓存,比如:rm -rf ./out/soong/.intermediates/frameworks/base/*
),不出意外的话,编译结束之后会看到 codeql-cli-binaries#47#840106244的同款报错:
Initializing database at /home/aosp/codeql-frameworks.
Running build command: [/home/aosp/repo/mmm.sh, /home/aosp/repo]
...
[2022-02-02 12:49:17] [build-stdout] ninja: no work to do.
[2022-02-02 12:49:17] [build-stdout] No need to regenerate ninja file
[2022-02-02 12:49:18] [build-stdout] No need to regenerate ninja file
[2022-02-02 12:49:18] [build-stdout] No need to regenerate ninja file
[2022-02-02 12:49:18] [build-stdout] Starting ninja...
[2022-02-02 12:49:24] [build-stdout] ninja: no work to do.
[2022-02-02 12:49:25] [build-stdout] #### build completed successfully (10 seconds) ####
Finalizing database at /home/aosp/codeql-frameworks.
No source code was seen and extracted to /home/aosp/codeql-frameworks.
This can occur if the specified build commands failed to compile or process any code.
- Confirm that there is some source code for the specified language in the project.
- For codebases written in Go, JavaScript, TypeScript, and Python, do not specify an explicit --command.
- For other languages, the --command must specify a "clean" build which compiles all the source code files without reusing existing build artefacts.
大概就是在编译过程中没有发现代码的意思,这个问题可以在 codeql-cli-binaries issues#50 中得到解决方案,设置如下环境变量即可:
export ALLOW_NINJA_ENV=true
根据关键词 ALLOW_NINJA_ENV
,在 Rules executed within limited environment 中可以找到此问题的(大概率)原因:默认情况下,soong 会限制传播环境变量到 ninja,而根据上面的分析,CodeQL 需要通过 LD_PRELOAD
(以及其他 Semmle 的环境变量)来注入编译系统,如果 soong 调用 ninja 的过程中阻断了环境变量,那么就会中断 CodeQL 的介入,导致 CodeQL 无法正常感知到编译过程,也就无法发现代码了。
随后再次执行 codeql database create
,解锁下一个报错:
[2022-02-02 13:42:34] [build-stdout] [ODASA Javac] Failed to execute ODASA javac builder: java.lang.IllegalArgumentException: FileUtil.makeUniqueName(/home/aosp/codeql-frameworks/log/ext,"javac.orig"): directory /home/aosp/codeql-frameworks/log/ext does not exist.
[2022-02-02 13:42:34] [build-stdout] Exception in thread "main" java.lang.Error: Fatal extractor error detected. Attempting to abort build commands.
[2022-02-02 13:42:34] [build-stdout] at com.semmle.extractor.java.interceptors.JavacMainInterceptor.javacMainResult(JavacMainInterceptor.java:48)
[2022-02-02 13:42:34] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.SEMMLE_INTERCEPT$9(Main.java)
[2022-02-02 13:42:34] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:323)
[2022-02-02 13:42:34] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
[2022-02-02 13:42:34] [build-stdout] at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
[2022-02-02 13:42:34] [build-stdout] at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
[2022-02-02 13:42:34] [build-stdout] ninja: build stopped: subcommand failed.
[2022-02-02 13:42:34] [build-stdout] 13:42:34 ninja failed with: exit status 1
[2022-02-02 13:42:34] [build-stdout] #### failed to build some targets (49 seconds) ####
[2022-02-02 13:42:34] [ERROR] Spawned process exited abnormally (code 1; tried to run: [/home/aosp/codeql/tools/linux64/preload_tracer, /home/aosp/repo/mmm.sh, /home/aosp/repo])
A fatal error occurred: Exit status 1 from command: [/home/aosp/repo/mmm.sh, /home/aosp/repo]
非常离谱的报了一个 /home/aosp/codeql-frameworks/log/ext does not exist
,尝试手动创建这个文件夹 mkdir /home/aosp/codeql-frameworks/log/ext
会产生另外一个报错:
[ODASA Javac] Failed to execute ODASA javac builder: java.lang.RuntimeException: Failed to create a unique file in /home/aosp/codeql-frameworks/log/ext
不过检查文件系统的权限并没有什么问题,即使 chmod 777
权限也无济于事。异常里也没有别的原因提示,好在还有逆向大法;根据日志提示,真正的异常应该抛在了 FileUtil.makeUniqueName
, 但是被 javacMainResult
捕获了,导致没有打印真正的调用栈。搜索 codeql/java/tools/
里的 semmle-extractor-java.jar
以及 codeql-java-agent.jar
里面都存在这个方法,通过初步的走读代码后发现 javac.orig
是在 codeql-java-agent.jar
中使用的。具体异常代码位于 com.semmle.extractor.java.Utils$FileUtil
类的 createUniqueFileImpl
:
这几行代码里就包含了上面遇到的两个报错,第一个报目录不存在,但在调用此方法之前,实际上已经做过一次 mkdirs
:
然而 mkdirs
并不会抛异常并且开发者也没有处理的返回结果,所以如果 mkdirs
没有成功那么到了这个地方就会抛出上面看到的这个 IllegalArgumentException
第二个异常是 Failed to create a unique file
,从代码中可以看出是因为创建文件失败而抛出的(jadx 的反编译结果有问题,根据其他反编译器的结果,这个 try-catch 其实是包在 while 外面的),但是开发者是自己抛的异常并且忽略了系统的错误信息,导致从报错上无法看出创建失败的真实原因。
Patch CodeQL-Java-Agent
找到了异常位置,但是依然不知道异常原因是什么,所以这里要祭出补丁大法,将系统抛出的真实异常信息打印出来。这里使用 Recaf 工具,在 createUniqueFileImpl
方法名上面右键 Edit with assembler
之后找到
修改汇编指令,将其修改成直接抛出 IOException e
:
保存之后菜单 File -> Export progarm 导出文件,替换掉 codeql/java/tools/codeql-java-agent.jar
然后再次运行编译,得到以下信息:
[2022-02-15 13:55:14] [build-stdout] [ODASA Javac] Failed to execute ODASA javac builder: java.io.IOException: Read-only file system
[2022-02-15 13:55:14] [build-stdout] Exception in thread "main" java.lang.Error: Fatal extractor error detected. Attempting to abort build commands.
[2022-02-15 13:55:14] [build-stdout] at com.semmle.extractor.java.interceptors.JavacMainInterceptor.javacMainResult(JavacMainInterceptor.java:48)
[2022-02-15 13:55:14] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.SEMMLE_INTERCEPT$9(Main.java)
[2022-02-15 13:55:14] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:323)
[2022-02-15 13:55:14] [build-stdout] at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
[2022-02-15 13:55:14] [build-stdout] at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
[2022-02-15 13:55:14] [build-stdout] at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
[2022-02-15 13:55:14] [build-stdout] nWrite to a read-only file system detected. Possible fixes include
[2022-02-15 13:55:14] [build-stdout] 1. Generate file directly to out/ which is ReadWrite, #recommend solution
[2022-02-15 13:55:14] [build-stdout] 2. BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST := <my/path/1> <my/path/2> #discouraged, subset of source tree will be RW
[2022-02-15 13:55:14] [build-stdout] 3. BUILD_BROKEN_SRC_DIR_IS_WRITABLE := true #highly discouraged, entire source tree will be RW
Read-only file system
我是万万没想到的。所幸最下面提供了解决方案,通过搜索 BUILD_BROKEN_SRC_DIR_IS_WRITABLE
可以定位到,这是属于编译系统 Soong 的一个沙盒功能,默认情况下编译产生的文件只允许写在 ./out/
目录下面,若希望写在其他目录则需要使用 BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST
指定白名单,或者使用 BUILD_BROKEN_SRC_DIR_IS_WRITABLE
关闭 ReadOnly
。
知道原因之后只需要使用最朴素的方法:将 CodeQL 数据库路径设置为 ./out/codeql-frameworks
即可,所以最终的分析命令为:
codeql database create out/codeql-frameworks
--language=java
--command="`pwd`/build/soong/soong_ui.bash --make-mode -j1 framework-minus-apex"
--source-root frameworks/base
--overwrite
如果发生 OOM 的话,再使用 Patch 大法,修改 com.semmle.extractor.java.Utils
类里的 invokeOdasaJavac
方法中的 JVM 选项:
这个地方似乎不受 CodeQL CLI 的参数影响,默认 1g 偶尔会 OOM,修改为 -Xmx4g 之后足够使用。
不出意外的话,现在可以正确生成 CodeQL Database 了,最后输出:
Successfully created database at /home/aosp/repo/out/codeql-frameworks.
原文始发于微信公众号(秃头的逆向痴想):使用 CodeQL 分析 AOSP
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论