在前一篇文章中,我们介绍了QSEE的漏洞及利用,接下来让我们将重点转移到QSEE shellcode。
之前讨论过,QSEE可以被提权——这里的提权不仅包含直接与TrustZone内核交互并访问硬件——安全的TrustZone文件系统(SFS),也包括一些系统内存的直接访问形式。
本文我们要讨论在不需要内核漏洞的情况下,如何利用“安全世界”的内存访问权限劫持“普通世界”中运行的Linux内核。
在上一篇文章中,当用户控件的Android应用与QSEE中运行的trustlet进行交互时,必须通过一个特殊的Linux 内核设备“qseecom”,该设备发送由QSEOS处理的SMC调用,并传递到请求的trustlet中,以便被处理:
每个发送到trustlet的命令都有一对对应的输入和输出缓冲区,用于传递“普通世界”和trustlet之间的通信信息。
但是,有一些更快的通信模式所必需的特殊用例——例如,当解密较大的DRM保护的媒体文件时,为了保证顺利播放,需要使用尽量少的通信消耗。
另外,有一些设备中包含trustlet是为了确保设备的完整性。例如,三星提供了一个“TrustZone-based Integrity Measurement Architecture (TIMA)”框架来保证设备完整性,TIMA会对“普通世界”内核定期检查,验证是否与原厂内核相匹配。
因此,Trustlet需要与“普通世界”进行快速通信,同时需要具备一定的检验系统内存的能力——听起来有些危险!下面让我们来深入分析。
继续对“widevine”trustlet的研究,以下代码为用于DRM加密内存块的命令:
该函数接收表示输入和输出缓冲区的指针,这两个缓冲区可以是用户提供的任意缓冲区。因此,如果想要访问他们需要一些准备。该函数通过调用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。其工作原理如下:
经过完整性检测,该函数会验证给定的内存区域是否位于“安全世界”。如果这就是问题所在,那是因为trustlet正在试图通过映射和修改TZBSP或QSEOS使用的内存区域攻击TrustZone内核。由于这一行为十分危险,“安全世界”中只有少数特定的区域可以映射到QSEE。如果给定的地址范围没有在特定的区域中,该操作就会被拒绝。
然而,对于“普通世界”中的任意地址,系统不会做任何额外的检查。这就意味着QSEOS允许使用qsee_register_shared_buffer将物理地址映射到“普通世界”。
由于 QSEE 拥有所有“普通世界”内存的读写权限,理论上我们可以直接在物理内存中定位“普通世界”运行的 Linux 内核 并注入代码。
让我们来创建一个不需要内核符号的 QSEE shellcode ——该方法可以用在所有的 QSEE 环境中,定位并劫持运行的 Linux 内核。
启动设备后,引导程序使用 Android 引导镜像中指定的数据,将 Linux 内核提取到给定的物理地址并执行:
Linux 内核的物理加载地址就可以通过全局可读的 /proc/iomem 文件用于任意进程:
然而,简单地获取内核加载地址并不是全部——系统中存在大量的内核镜像和内核符号。因此,我们需要找到所有动态使用运行时内核内存的符号。要知道, Linux 内核在内部维护着一个内核符号列表,允许内核函数使用特殊的搜索函数 kallsyms_lookup_name 查找这些符号。
内核符号列表中的名称使用 build 时生成的 256 为霍夫曼编码进行压缩,霍夫曼表存储在内核镜像中,在相同的位置还有代表索引的相应的描述符,用于解压名称,当然还包含符号的实际地址。
为了访问符号表中的所有信息,我们首先需要在内核镜像中找到它。
如果幸运的话,符号表的第一个区域——SymbolAddress Table,通常由两个指向内核虚拟加载地址(由于没有内核地址空间随机分配KASLR机制,可通过对物理加载地址计算得出)的指针开始。另外,该符号地址为内核虚拟地址范围内的单调非递减地址——以此来确定指向内核虚拟加载地址的连个连续指针。符号地址表如下:
既然已经找到了内核镜像中的符号表,接下来需要做的就是解压该表,来遍历并查找任何符号。
使用上述方法找到内核中的符号表后,我们就可以定位并从QSEE中劫持内核函数。根据以往的内核利用经验,我们可以从一个很少用到的网络协议PPPOLAC中劫持一个函数指针。
该函数指针存储在以下内核结构体中:
当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内核中的代码执行权限。
*原文地址: bits-please ,安小白编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)