SRCINV-源码审计系统的简要介绍

Contents

  • 1 - 简介
  • 1.1 - 框架
  • 1.2 - 依赖
  • 1.3 - 针对目标
  • 2 - 框架的简单使用
  • 3 - 信息收集
  • 4 - 信息解析
  • 4.1 - 信息调整
  • 4.2 - 基础信息获取
  • 4.3 - 详细信息获取
  • 4.4 - 交叉引用信息整理
  • 4.5 - 间接调用1
  • 4.6 - 间接调用2
  • 5 - 信息使用
  • 6 - 后续
  • 7 - 引用

1 - 简介

该文档将介绍SRCINV框架(beta)的使用, 以及设计思路, 和主要针对的目标.

1.1 - 框架

SRCINV是source code investigation的缩写, 是根据经验, 对源码审计工作的代码体现.

源码(或二进制代码)是提供给研究人员查看的, 也是提供给编译器(或解释器或执行单元)的,

该框架从编译器编译源码文件的中间信息, 提取解析该项目的所有源码信息提供给研究人员使用.

由于通常的gcc插件, 看到的是当前编译单元的信息, 无法看到整个项目. 所以我们将整个项目的 每个编译单元的信息提取出来再进行整合, 便于从全局角度去观察整个项目.

该框架旨在对开源项目进行高度自动化的代码审计, 以帮助开发测试/安全研究人员, 发现项目 中可能存在的代码问题, 并提供一定程度的验证样本生成和代码补丁生成等功能.

对于每个项目, 使用一个struct src结构记录所有生成的索引信息, 包含

sibuf(用于表示编译文件的索引信息)链表,

resfile(用于表示提取的结果文件)链表,

sinodes(用于表示非局部变量/函数/类型, 分两大种类:名称索引, 位置索引. 对于文件 变量(static)/文件函数/有位置信息的类型, 使用位置索引; 对于全局变量/全局函数/没有位 置信息但是有名称的类型, 使用名称索引; 对于没有位置也没有名称的类型, 记录在sibuf中)

目录结构如下:

	si_core.c	主程序
	collect/	编译器插件, 信息收集
	plugins/	信息解析及使用等各种功能plugin
	output/		索引数据等文件
1.2 - 依赖

该框架只能运行于64位GNU/Linux系统, 且需要存在personality系统调用以关闭进行aslr.

终端颜色显示, 不同的发行版可能会存在一些兼容问题.

其他依赖的库:

	clib (https://github.com/snorez/clib)
	ncurses
	readline
	libcapstone

需要的头文件:

	gcc plugin 库文件
1.3 - 针对目标

该框架预期为尽可能多种类的开源项目进行测试. 当前该框架只针对gcc编译器编译的C项目.

2 - 框架的简单使用

框架集成了一些基础命令以供使用(可查看 –[ 5 - 信息使用 节的演示视频):

	si_core:	主程序
	help:		使用帮助, 查看当前可使用的命令
	exit/quit:	退出程序
	load_plugin:	加载plugin
	unload_plugin:	卸载plugin
	reload_plugin:	重新加载plugin
	list_plugin:	显示当前所有可供使用的plugin
	do_make:	按照内核编译模式编译指定项目
	do_sh:		执行终端命令
	set_plugin_dir: 设置plugin目录, 默认为plugins/
	showlog:	显示日志信息, 日志文件位于plugins/log.txt
	load_srcfile:	加载索引信息文件
	set_srcfile:	设置索引信息路径
	getinfo:	获取收集的项目信息的索引信息
	staticchk:	静态检测
	itersn:		输出所有的索引信息

	SRCINV> help
	========= USAGE INFO =========
	help:
				Show this help message
	exit:
				Exit this main process
	quit:
				Exit this main process
	load_plugin:
		(plugin_path) [plugin_args...]
	unload_plugin:
		(id|path)	Unload specific plugin
	reload_plugin:
		(id|path) [plugin_args]	Reload target plugin
	list_plugin:
				Show current loaded plugins
	do_make:
		(c|cpp|...) (sodir) (projectdir) (outfile) [extras]
				Build target project, make sure Makefile has EXTRA_CFLAGS
	do_sh:
				Execute bash command
	set_plugin_dir:
		(plugin_dir)	Set the plugin directory, the original still count
	showlog:
		Show current log messages
	load_srcfile:
		[srcfile]
	set_srcfile:
		(srcfile_name)
	getinfo:
		(res_path) (is_builtin) (linux_kernel?) (step)
			Get information of resfile, for step
				0 Get all information
				1 Get base information
				2 Get detail information
				3 Get xrefs information
				4 Get indirect call information
				5 Check if all GIMPLE_CALL are set
	staticchk:
		Run registered static check methods
	itersn:
		[output_path]	Traversal all sinodes to stderr/file
	sn_load:
		[id]		most for test cases
	========= USAGE END =========

	SRCINV> list_plugin
	0	0	loaded		fuzz
	>>>> /home/zerons/workspace/todo/srcinv/plugins/fuzz.so
	1	0	loaded		sibuf
	>>>> /home/zerons/workspace/todo/srcinv/plugins/sibuf.so
	2	1	loaded		staticchk
	>>>> /home/zerons/workspace/todo/srcinv/plugins/staticchk.so
	3	0	unload		(null)
	>>>> /home/zerons/workspace/todo/srcinv/plugins/test.so
	4	1	loaded		sinode
	>>>> /home/zerons/workspace/todo/srcinv/plugins/sinode.so
	5	7	loaded		getinfo
	>>>> /home/zerons/workspace/todo/srcinv/plugins/getinfo.so
	6	1	loaded		debuild
	>>>> /home/zerons/workspace/todo/srcinv/plugins/debuild.so
	7	0	loaded		sn_load
	>>>> /home/zerons/workspace/todo/srcinv/plugins/sn_load.so
	8	0	loaded		uninit
	>>>> /home/zerons/workspace/todo/srcinv/plugins/uninit.so
	9	1	loaded		resfile
	>>>> /home/zerons/workspace/todo/srcinv/plugins/resfile.so
	10	4	loaded		src
	>>>> /home/zerons/workspace/todo/srcinv/plugins/src.so
	11	1	loaded		gen_sample
	>>>> /home/zerons/workspace/todo/srcinv/plugins/gensample.so
	12	0	loaded		c
	>>>> /home/zerons/workspace/todo/srcinv/plugins/c.so
	13	0	loaded		itersn
	>>>> /home/zerons/workspace/todo/srcinv/plugins/itersn.so
	14	1	loaded		utils
	>>>> /home/zerons/workspace/todo/srcinv/plugins/utils.so

	list_plugin显示当前可供使用的plugin. 显示的信息包含:
	序号	引用计数	是否加载	plugin名称	plugin路径

更多的指令详细用法, 请参考项目仓库doc/commands.md文件.

框架的使用分为三个阶段, 信息收集, 信息解析, 信息使用. 下面将依次介绍.

3 - 信息收集

注意: 信息收集之前, 需要删除之前的结果文件.

对单个文件, 可以使用gcc的-fplugin, -fplugin-arg参数编译, 对使用make进行编译的项目, 确保Makefile提供了类似EXTRA_CFLAGS的编译参数, 此时可使用 make EXTRA_CFLAGS+='-fplugin=/.../x.so -fplugin-arg-x-output=/.../...' 编译

信息收集, 是使用编译器提供的接口, 提取源码编译时的中间信息, 进而对整个项目的信息 进行汇总. 此功能实现在collect目录下, 独立于主程序, 需要在项目编译时使用. 本质为 编译器插件.

关于GCC插件的使用, 该框架处理的是low-level GIMPLE形式的语句, 它的形成过程是:

	前端语言分析源码文件, 生成前端语言的AST表示
	将前端语言的AST表示转换成GIMPLE中间表示

然后GCC对GIMPLE中间表示进行处理, 这些过程叫pass. 包括从高级GIMPLE转换成低级GIMPLE(框 架使用的), IPA处理, GIMPLE优化, 最终由GIMPLE转换成RTL等.

Pass根据处理的对象及功能的不同, 分为四大类: GIMPLE_PASS, RTL_PASS, SIMPLE_IPA_PASS, IPA_PASS. 其中GIMPLE_PASS以GIMPLE中间表示为处理对象, RTL_PASS的处理对象为RTL中间表示, SIMPLE_IPA_PASS和IPA_PASS处理对象也是GIMPLE中间表示, 但功能主要是过程间分析(IPA, Inter-Procedural Analysis). 例如如下几个PASS(新版本GCC有些pass可能不存在了):

	all_lowering_passes
	|--->	useless		[GIMPLE_PASS]
	|--->	mudflap1	[GIMPLE_PASS]
	|--->	omplower	[GIMPLE_PASS]
	|--->	lower		[GIMPLE_PASS]
	|--->	ehopt		[GIMPLE_PASS]
	|--->	eh		[GIMPLE_PASS]
	|--->	cfg		[GIMPLE_PASS]
	...
	all_ipa_passes
	|--->	visibility	[SIMPLE_IPA_PASS]
	...

对源码中每个定义的函数, 会先执行一遍all_lowering_passes中的处理过程, 也就是说, 在处理 到cfg pass的时候, 有些函数并未进行lower处理.

更多的GCC插件信息, 可以参考refs[1] refs[3], 或者查询GCC源码.

当前实现的针对c源码文件的信息提取, 是gcc插件, 在加载时会检测当前编译的文件名称, 匹 配文件路径(主要针对内核源码结构进行的检测), 注册回调函数, 在cfg pass执行之前调用回 调函数. 在PLUGIN_ALL_IPA_PASSES_START执行时将数据写入文件. 文件大小使用PAGE_SIZE对齐.

这里我们不能使用PLUGIN_PRE_GENERICIZE来处理每个tree_function_decl, 原因在于, gcc在 这个处理阶段, 是流式的, 可能一个函数调用的函数还并未定义. 比如

	static void test_func0(void);
	static void test_func1(void)
	{
		test_func0();
	}

	static void test_func0(void)
	{
		/* test_func0 body */
	}

这种情况, 处理test_func1时, 会跟入test_func0, 而此时的test_func0的tree_function_decl 结构体并未进行完整的初始化.

当gcc完成整个源码文件的扫描工作, 所有的数据会被串起来(TREE_CHAIN), 我们需要尽可能的 避免数据的重复, 并保证数据的完整性.

当调用该pass的处理程序时, 处理对象为struct function, 其成员decl指向包含这个对象的 tree_function_decl. 这个过程会将tree_function_decl->saved_tree成员(为函数体)保留的 信息转换成GIMPLE语句保存到tree_function_decl->f->gimple_body中. 此时, 由于所有的函数 已被串起来, 我们在跟踪tree_function_decl的时候, 需要判断这个结构是否已经完成AST-> GIMPLE的转换, 如果还存在saved_tree, 不处理这个函数, 后续转换这个函数时会完成该函数 的信息收集.

对于tree_var_decl(变量), 我们着重需要记录的是非局部变量的信息. 在gcc的实现中, 函数 is_global_var的实现(gcc/gcc/tree.h)如下:

	static inline bool is_global_var (const_tree t)
	{
		return (TREE_STATIC(t) || DECL_EXTERNAL(t));
	}

TREE_STATIC对于变量来说, 表示这个函数是否使用静态存储区, 如果使用, 表明这个变量is global variable. DECL_EXTERNAL表示该变量是否是外部引用. 当前实现的c.cc在此基础上添 加了一个检测:

	if (is_global_var(node) &&
			((!DECL_CONTEXT(node)) ||
			 (TREE_CODE(DECL_CONTEXT(node)) == TRANSLATION_UNIT_DECL))) {
			objs[start].is_global_var = 1;
		}

DECL_CONTEXT为空或者为TRANSLATION_UNIT_DECL时, 表示该变量为函数外变量.

另外, 需要记录每个对象的指针, 每个位置信息等.

该collect/c.cc在linux kernel 4.14.x上的测试显示, make vmlinux -j9耗时大致为20分钟, 提取出的信息文件为19.4G.

4 - 信息解析

解析是框架需要实现的主要功能之一, 也是最复杂的. 我们需要完成提取的项目信息的整理和 分类, 构建索引信息, 方便后续的使用.

该实现主要在plugin/getinfo.c中, 其对每个编译文件的信息进行检测, 查看该文件编译类 型(enum si_lang_type), 查找是否有对应的注册函数(struct lang_ops), 并依次调用.

针对大型项目的解析, 为了有更好的体验, 框架使用ncurses来提供进度条提示(框架编译时使 用make ver=release).

信息解析过程, 需要将提取信息文件(称为resfile)加载到内存. 而在linux kernel 4.14.x中 的测试, 生成的resfile达到19G之多, 无法一次加载到内存中, 且后续的信息使用亦需要这些 信息, 解决方案如下:

	自定义进程的内存布局. 在RESFILE_BUF_START位置开始加载resfile文件, 对每个编
	译文件生成一个sibuf结构体, 记录文件的加载位置, 加载大小, 以及加载的数据在
	文件中的偏移等信息.

	当加载到内存的文件大小超过RESFILE_BUF_SIZE时, 调用munmap取消之前的加载, 继
	续加载后续的数据.

	当需要读取之前已经munmap的数据时, 只需要调用resfile__resfile_load函数, 将
	sibuf对应的文件数据加载到适当的内存位置, 即可直接进行读写.

	内存布局如下图:
	0x0			--- 0x400000		NULL pages
	0x400000		--- 0x403000		si_core指令
	0x602000		--- 0x603000		si_core数据
	0x603000		--- 0x605000		si_core数据
	0x605000		--- 0x647000		heap
	SRC_BUF_START		--- RESFILE_BUF_START	索引信息区域
	RESFILE_BUF_START	--- 0x????????		resfile加载区域
	0x700000000000		--- 0x7fffffffffff	线程 动态库 plugins 进程栈

	当SRC_BUF_START为0x100000000, RESFILE_BUF_START为0x1000000000时, 索引信息最
	高可达到64G, resfile可处理高达1024G(末端到达0x100 0000 0000)的数据文件.
4.1 - 信息调整

信息收集阶段生成的文件, 因为包含了很多指针, si_core进程并不能直接读写其中的信息, 需 要先完成适当的指针转换. 此过程需要与collect/x.cc对应. 名为PHASE1前半部分.

读取函数信息, 对其中包含的指针数据修改之后, 依次对每个对象进行访问并调整指针数据. 同 时, 对location结构, 由于location_t只有4字节大小, 设置其为sibuf->payload的偏移位置, 于是*(expanded_location *)(sibuf->payload + loc)即可获取需要的位置信息.

当所有对象调整完毕, 即完成PHASE1, 设置完文件状态信息, 返回.

4.2 - 基础信息获取

PHASE1后半部分, 提取每个文件的基础信息: 有哪些定义了的函数, 非局部变量, 类型.

函数分TYPE_FUNC_GLOBAL和TYPE_FUNC_STATIC,

非局部变量分TYPE_VAR_GLOBAL和TYPE_VAR_STATIC,

类型分TYPE_TYPE_LOC和TYPE_TYPE_NAME.

如果类型为TYPE_FUNC_GLOBAL/TYPE_VAR_GLOBAL/TYPE_TYPE_NAME, 为名称索引

如果类型为TYPE_FUNC_STATIC/TYPE_VAR_STATIC/TYPE_TYPE_LOC, 为位置索引

如果类型为TYPE_NONE, 该节点为tree_type_non_common, 放入sibuf->type_nodes中.

首先查询当前sinodes中, 是否有重复的节点, 如果存在, 为位置索引时则进行下次循环, 为名 称索引时需要检测名称冲突, 比如对于TYPE_*_GLOBAL, 存在weak symbol.

生成新的sinode, 根据当前得到的位置 名称等信息, 完成初始化.

4.3 - 详细信息获取

PHASE2, 获取每个sinode节点的详细信息.

对于类型, 需要获取该类型指向的类型, 或者类型的大小, 类型的成员等信息.

对于变量, 当前只获取了变量是什么类型.

对于函数, 首先获取返回值类型, 然后处理参数列表(以var_node_list表示), 然后获取函数体( 以code_path表示).

关于函数体的表示, 以label为分隔点, label之后的语句为一个code_path, 直到另一个label或者到GIMPLE_SWITCH/GIMPLE_GOTO/GIMPLE_COND/含nl的GIMPLE_ASM语句. 同时会检测该函数中是否有不可到达的语句, 比如:

	static int test_func(void)
	{
		int err = 0;
		if (err)
			return 1;
		else
			return 0;
		return 0;		/* not reachable */
	}
4.4 - 交叉引用信息整理

PHASE3, 处理非局部变量的初始化值, 设置每个变量的可能值(possible_value_list).

对每个函数(除了直接调用)和变量的使用位置(use_at_list)进行标记. 并获取直接调用信息.

每个函数的调用均由GIMPLE_CALL语句表示, 其第一个操作数为返回值, 第二个操作数可能为函 数地址, 也可能为VAR_DECL/PARM_DECL等, 后面的操作数为函数的实际参数. 直接调用即为第二 个操作数为函数地址的情形. 记录的函数使用位置是除第二个操作数外的所有引用位置.

PHASE3暂时还未通过linux kernel 4.14.x的vmlinux的resfile检测, 存在一些未考虑到的情形.

4.5 - 间接调用1

PHASE4, 处理被标记了的函数, 即表示该函数存在除直接调用之外的引用情形.

如果引用语句为GIMPLE_ASSIGN(赋值语句), 获取左值的var_node, 添加possible_value.

4.6 - 间接调用2

PHASE5, 处理GIMPLE_CALL语句的第二个参数为VAR_DECL/PARM_DECL的情形.

如果为VAR_DECL情形, 获取变量的var_node, 查看possible_value_list, 添加调用关系;

然后查看该变量的use_at_list, 检测对其的赋值, 然后递归跟踪.

例如:

	static void test_func0(void);
	static void test_func1(void)
	{
		void (*testf)(void);
		testf = test_func0;
		testf();
	}

或:

	static void test_func0(void);
	struct test_a {
		int a;
		void (*b)(void);
	};
	static struct test_a static_a = {
		.a = 1,
		.b = test_func0,
	};
	static void test_func1(void)
	{
		static_a.b();
	}

可以得到test_func1调用了test_func0的结果.

暂时未提供处理PARM_DECL调用情形的功能.

5 - 信息使用

对索引信息的使用, 可以有多种可能. 由于我们收集的信息是lower-level形式的GIMPLE语句, 所以编写的框架plugin处理的对象也是lower GIMPLE语句.

这里以未初始化变量引用的某种情形进行简单说明

例如如下代码:

	static void test_func(int flag)
	{
		int need_free;
		char *buf;
		if (flag) {
			buf = (char *)malloc(0x10);
			need_free = 1;
		}

		/* do something here */

		if (need_free)
			free(buf);
	}

plugins/uninit.cc显示了如何检测这种形式的问题.

该plugin会对所有的函数依次进行检测, 首先, 调用utils__gen_code_path获取该函数所有可能的执行流, 然后:

	获取该函数使用的一个局部变量(不包含函数内的static变量)
	遍历所有执行流, 查看该局部变量的第一个引用位置
	如果第一个引用位置的操作是读取该变量, 则存在未初始化变量引用的情形.

查看检测视频

6 - 后续

  • 对linux kernel生成的resfile的完整支持
  • 完全通过所有解析步骤
  • .s/.S文件中包含的符号提取, 完善调用链.
  • 对添加了内核防护的版本的辨识
  • 变量等数据的跨函数的追踪
  • 标记数据引用位置的操作, 比如读还是写, 或者取地址等
  • 对用户层应用的解析的完善, 比如很多符号都是外部库的
  • 跨函数的所有执行流的生成
  • 执行流之间的依赖关系, 比如sys_read函数的调用需要sys_open的输出
  • 验证样本的自动生成
  • 某些形式的代码问题的补丁自动生成
  • 其他编程语言的支持

欢迎各种建议以及新的思路, 当前, 提交请求(Push Request)也是极好的.

7 - 引用

[0] 项目地址, 仓库会在后续创建

[1] GNU Compiler Collection Internals

[2] GCC source code

[3] 深入分析GCC

[4] gcc plugins for linux kernel