欢迎光临
我们一直在努力

使用CodeQL分析AOSP

使用 CodeQL 分析 AOSP

自从 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 为安全社区带来的卓越贡献:

  1. 最直观的是通过 CodeQL 发现过众多 Zero-Day 漏洞。
  2. 整合了大量的流行漏洞模型,CodeQL 可能是目前覆盖最广的公开规则包。
  3. 带来了一个大众级的现代化代码审计解决方案(同时也盘活了 Semmle)。
  4. 为静态程序分析行业带来了一些热度,相信也会间接的带动更多学术成果落地。

CodeQL 相关资料

分析 AOSP

AOSP 大部分的代码使用 Java 或者 C 系列的语言编写,CodeQL 对于这些编译型的语言需要介入编译器,从编译器中获取到代码的语义信息,意味着需要将我们需要的代码进行编译之后 CodeQL 才能生成分析数据库。由于 AOSP 的代码以及编译系统太过庞大,所以我建议先将 AOSP 整体编译一遍,然后清除目标分析代码的编译结果,最后再使用 CodeQL 重新编译一遍。

整体编译 AOSP

[*] 重要提示:AOSP 已经不再支持使用 macOS 进行编译了,请安装 Ubuntu!!!

虽然网上有很多 AOSP 的编译教程,不过对于 master 分支我建议还是看官方文档来吧,毕竟现在 AOSP 的编译系统方便很多,一般都不需要配置很多复杂的环境,但如果搜到了一些乱七八糟的教程可能反而会浪费很多时间,以下是我推荐的步骤:

  1. 操作系统选择 64 位的 Ubuntu 18.04 或者 Ubuntu 16.04,准备 16+ G 内存、500+ G 磁盘。
  2. 国内推荐使用清华的镜像源的每月初始化包来拉取最新代码,当然如果你以前拉取过那只需要直接同步即可,此步骤大概需要下载 100G 代码,最终占用硬盘 200+G。

    1. 下载每月初始包: wget -c mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar
    2. 解压:tar xf aosp-latest.tar
    3. 更新 repo 工具:cd AOSP/.repo/repo; git pull origin master
    4. 同步代码:cd ../../; ./.repo/repo/repo sync
  3. 根据官方文档使用 apt 安装相关的依赖包,16.04 可以将 18.04 以及 14.04 的依赖都安装上,目前 AOSP 编译系统里非常贴心的自带了 JDK,不用自己再去安装对应的 JDK 版本了。

  4. 根据官方文档开始编译。

    1. source build/envsetup.sh, 初始化环境。
    2. lunch aosp_x86_64-eng, 这里是编译 x86_64 版本的工程镜像,运行 lunch 可以查看并选择其他版本。
    3. m -j16, 这里是 16 个线程,根据自己 CPU 线程调节。
  5. 然后就是摸鱼时间,时间长短取决于你机器配置,一般需要 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 的工作原理,就能很容易的想到如何解决找不到命令的问题了,这里有两种方法:

  1. 参考 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
  1. 使用一个编译脚本让 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.

未经允许不得转载:Caldow » 使用CodeQL分析AOSP
分享到: 生成海报

相关推荐

  • 暂无文章

切换注册

登录

忘记密码 ?

切换登录

注册

我们将发送一封验证邮件至你的邮箱, 请正确填写以完成账号注册和激活