从0到TrustZone第三篇:从QSEE劫持Linux内核

转载:http://www.freebuf.com/vuls/104733.html

本系列文章

在前一篇文章中,我们介绍了QSEE的漏洞及利用,接下来让我们将重点转移到QSEE shellcode。

之前讨论过,QSEE可以被提权——这里的提权不仅包含直接与TrustZone内核交互并访问硬件——安全的TrustZone文件系统(SFS),也包括一些系统内存的直接访问形式。

本文我们要讨论在不需要内核漏洞的情况下,如何利用“安全世界”的内存访问权限劫持“普通世界”中运行的Linux内核。

war_of_worlds.png

与QSEE交互

在上一篇文章中,当用户控件的Android应用与QSEE中运行的trustlet进行交互时,必须通过一个特殊的Linux 内核设备“qseecom”,该设备发送由QSEOS处理的SMC调用,并传递到请求的trustlet中,以便被处理:

Screenshot from 2016-04-29 01-09-43.png

每个发送到trustlet的命令都有一对对应的输入和输出缓冲区,用于传递“普通世界”和trustlet之间的通信信息。

但是,有一些更快的通信模式所必需的特殊用例——例如,当解密较大的DRM保护的媒体文件时,为了保证顺利播放,需要使用尽量少的通信消耗。

另外,有一些设备中包含trustlet是为了确保设备的完整性。例如,三星提供了一个“TrustZone-based Integrity Measurement Architecture (TIMA)”框架来保证设备完整性,TIMA会对“普通世界”内核定期检查,验证是否与原厂内核相匹配。

因此,Trustlet需要与“普通世界”进行快速通信,同时需要具备一定的检验系统内存的能力——听起来有些危险!下面让我们来深入分析。

继续对“widevine”trustlet的研究,以下代码为用于DRM加密内存块的命令:

Screenshot from 2016-05-05 16-28-21.png

该函数接收表示输入和输出缓冲区的指针,这两个缓冲区可以是用户提供的任意缓冲区。因此,如果想要访问他们需要一些准备。该函数通过调用cacheflush_register完成准备,一旦加密进程完成,通过调用cacheflush_deregister释放缓冲区。

分析发现,cacheflush_register和cacheflush_deregister都是围绕QSEE系统调用的简单的封装程序:

cacheflush_register cacheflush_deregister
qsee_register_shared_buffer qsee_prepare_shared_buf_for_nosecure_read
qsee_prepare_shared_buf_for_secure_read qsee_deregister_shared_buffer

那么这些系统调用的作用是什么呢?

查看QSEOS相关代码发现这些调用的名字是有些误导性的——实际上,qsee_prepare_shared_buf_for_secure_read只能使数据缓存中的给定范围无效(QSEE会查看更新的数据),qsee_prepare_shared_buf_for_nosecure_read可以删除数据缓存中给定的范围(“普通世界”可以收到QSEE做出的更改)

至于qsee_register_shared_buffer——该系统调用主要用于将给定范围实际映射到QSEE。其工作原理如下:

Screenshot from 2016-05-05 19-44-43.png

经过完整性检测,该函数会验证给定的内存区域是否位于“安全世界”。如果这就是问题所在,那是因为trustlet正在试图通过映射和修改TZBSP或QSEOS使用的内存区域攻击TrustZone内核。由于这一行为十分危险,“安全世界”中只有少数特定的区域可以映射到QSEE。如果给定的地址范围没有在特定的区域中,该操作就会被拒绝。

然而,对于“普通世界”中的任意地址,系统不会做任何额外的检查。这就意味着QSEOS允许使用qsee_register_shared_buffer将物理地址映射到“普通世界”。

劫持Linux内核

由于QSEE拥有所有“普通世界”内存的读写权限,理论上我们可以直接在物理内存中定位“普通世界”运行的Linux内核并注入代码。

让我们来创建一个不需要内核符号的QSEE shellcode——该方法可以用在所有的QSEE环境中,定位并劫持运行的Linux内核。

启动设备后,引导程序使用Android引导镜像中指定的数据,将Linux内核提取到给定的物理地址并执行:

Screenshot from 2016-05-05 20-23-47.png

Linux内核的物理加载地址就可以通过全局可读的/proc/iomem文件用于任意进程:

Screenshot from 2016-05-05 20-51-08.png

然而,简单地获取内核加载地址并不是全部——系统中存在大量的内核镜像和内核符号。因此,我们需要找到所有动态使用运行时内核内存的符号。要知道,Linux内核在内部维护着一个内核符号列表,允许内核函数使用特殊的搜索函数kallsyms_lookup_name查找这些符号。

内核符号列表中的名称使用build时生成的256为霍夫曼编码进行压缩,霍夫曼表存储在内核镜像中,在相同的位置还有代表索引的相应的描述符,用于解压名称,当然还包含符号的实际地址。


为了访问符号表中的所有信息,我们首先需要在内核镜像中找到它。

如果幸运的话,符号表的第一个区域——SymbolAddress Table,通常由两个指向内核虚拟加载地址(由于没有内核地址空间随机分配KASLR机制,可通过对物理加载地址计算得出)的指针开始。另外,该符号地址为内核虚拟地址范围内的单调非递减地址——以此来确定指向内核虚拟加载地址的连个连续指针。符号地址表如下:

Screenshot from 2015-08-25 16-45-56 (1).png

既然已经找到了内核镜像中的符号表,接下来需要做的就是解压该表,来遍历并查找任何符号。

使用上述方法找到内核中的符号表后,我们就可以定位并从QSEE中劫持内核函数。根据以往的内核利用经验,我们可以从一个很少用到的网络协议PPPOLAC中劫持一个函数指针。

该函数指针存储在以下内核结构体中:

Screenshot from 2015-08-16 02-07-43 (1).png

当PPPOLAC套接字关闭时,覆盖其中的release指针会导致内核执行用户提供的函数指针。

总结

综上所述,获取Linux内核中的代码执行权限需要执行以下步骤:

1、获取QSEE代码执行权限

2、使用qsee_register_shared_buffer映射QSEE中的所有内核地址

3、找到内核符号表

4、在符号表中查找“pppolac_proto_ops”符号

5、覆盖指向用户提供的函数地址的指针

6、使用qsee_prepare_shared_buf_for_nosecure_read清除QSEE中的改变

7、使用PPPOLAC套接字使内核调用用户提供的函数

完整利用代码传送门。

注意,该代码目前只能一次读取一个DWORD,所以运行缓慢,欢迎提供改善意见(例如,同时读取较大的内存块会提速)。

下一篇文章将继续从0到TrustZone的旅程,尝试获取TrustZone内核中的代码执行权限。


你可能感兴趣的:(从0到TrustZone第三篇:从QSEE劫持Linux内核)