转载

如何有效绕过Android kptr_restrict

本文中,我会介绍一些可以绕过Android kptr_restrict的方法,绕过Android kptr_restrict之后,我们可以更加容易地利用安卓的漏洞。

首先,让我们先从快速了解一下kptr_restrict这方面的东西。

kptr_restrict是什么?

从 前文 中我们知道,利用漏洞的时候往往需要知道内核内部指针的信息:不论是为了劫持他们,还是以一种受控的方式来打断它们

众所周知:Linux内核有一个功能可以让它过滤掉一些地址,以此来避免将内核地址泄漏给潜在的攻击者。这个可配置的特性被称为“kptr_restrict”,这一功能在Android 内核的源码树中至少已经出现了2年了。

对于所有可配置的内核参数,有一个特殊的文件能设置该特性如何过滤这些内核地址。对于kptr_restrict而言,这个特殊的文件在“proc/sys/kernel/kptr_restrict”下,但有一些很高的权限设置。

如何有效绕过Android kptr_restrict

事实上:只有root用户才能对它进行读写,其它的用户只能对它进行读取操作。

那么kptr_restrict是如何工作的了?首先,在输出的时候内核开发者需要一种方式来标记内核指针。这通过使用一种特殊的格式标识“%pk”来实现,它表示写到该处的值是内核指针,因此需要进行这些指针进行保护。

一共有三种值可以控制kptr_restrict提供的保护:

0.-该特性被完全禁止; 1-那些使用“%pk”打印出来的内核指针被隐藏(会以一长串0替换掉),除非用户有CAP_SYSLOG权限,并且没有改变他们的UID/GID(防止在撤销权限之前打开的文件泄露指针信息); 2.所有内核指使用“%pk”打印的都被隐藏。

该配置的默认值是通过 CONFIG_SECURITY_KPTR_RESTRICT 创建内核时提供的,但是所有我遇到的现代的Android设备,该值都被设置为了2。

然而,有多少内核开发者知道需要用”%pK”来保护内核指针了?通过grep正则在内核中查找这种格式的字符串就能容易的知道了。不言而喻,结果和预期的一样遭。

如何有效绕过Android kptr_restrict

在内核源代码中,在23个文件中一共发现了35次。不用说,内核指针常常使用普通指针格式标识”%p”来打印-简单的搜寻就能发现成百上千处用“%p”显示指针的地方。

既然都已经说到这了,那我们就来讨论下为什么kptr_restrict本身提供的保护就不够。

Method #1 – 通过shell获得dmesg

所有通过内核打印的log消息都被写到一个环形缓冲区中,它位于内核内存中。用户可以通过调用””dmesg”(display message)命令来读取该缓冲区。该命令实际上通过调用 sysylog 系统调用,来访问该缓冲区,我们可以从如下strace输出中看到。

如何有效绕过Android kptr_restrict

然而,syslog系统调用并不是所有用户访问到的-实际上,调用者要么有CAP_SYS_ADMIN权限,要么有比前者弱一些的CAP_SYSLOG权限。

不管哪种方式,大多数Android进程都不具备这样的能力,因此也就不能访问到内核日志了。事实上真是这样吗?

在Android上,“init”进程维护了一个“service”列表,它们可以按照需要进行启动或终止。这些服务在启动的时候由“init”进程从一个硬编码的列表配置文件加载,该文件通常被保存在root分区(只读),因此他们是只读的。

这些配置文件以Android特有的语言进行编写,即”Android Init Language”.该语言简单易用,它允许对权限进行完全的控制,能限定哪个服务能够被启动(UID/GID),以及他们的参数以及其类型。

Android的另一个特性是”系统属性”—他们是一些由“属性服务”维护的键值对,它也是init进程内的一个线程。该服务允许在各种敏感的系统属性上的基本的访问控制,这能阻止用户随意的更改他们想改的属性。

大多数属性的访问权限通常都是在属性服务中 硬编码 的(Android 4.4以前的都是,Android 5是由通过使用SELinux标签处理的)。

如何有效绕过Android kptr_restrict

然而,也有一些属性例外— “ctl.stat”,”ctl.stop”系统属性,前者用于启动系统服务,后者用于终止系统服务(使用“Android Init Language”进行定义)。

这些属性用SELinux标签进行了严格的检查,以确保只有一些特定的用户能够有权限修改系统服务的状态。

让人意外的是,当使用“adb”(android Debug Bridge)本地连接设备时,我们获得的是“shell”用户执行权限。该用户通常被允许启动和终止 “dumpstate”服务。实际上,这是adb命令行工具提供的一个特性,它允许开发者建立包含设备完整信息的出错报告。

如何有效绕过Android kptr_restrict

以上图所示方式运行adb(或者从adb shell执行”bugreport”),实际上设置了“ctl.start”系统属性启动了dumpstate服务。

如何有效绕过Android kptr_restrict

让我们看一下dumpstate服务的配置。

如何有效绕过Android kptr_restrict

由于该服务没有用户配置和组配置信息,因此实际上它是以root user-ID和root group-ID执行的,而这是相当危险的。

幸运的是,dumpstate服务的开发者意识到了以高权限运行它的安全风险,因此在启动该服务后,立即修改了它的user-ID,group-ID,更改其权限,类似下面这样:

如何有效绕过Android kptr_restrict

简言之,该服务将user-ID和group-ID设置为了shell user,但又显式的使其拥有CAP_SYSLOG权限。

深挖下去会发现dumpstate使用了syslog系统调(它有CAP_SYSLOG权限因此能执行)用来读取内核日志,并将读取的信息写回给调用者。这意味着,在adb shell上下文中,我们可以通过执行bugreport自由的读取内核日志。

然而,这并没有解决exp需要获取需要的符号-如前所述,这些符号通常应该以”%pK”格式打印出来。

但是,大多数内核内的指针并没有使用特殊的格式标识打印,而使用了通常的”%p”格式,因此也就没有收到监控。这也就意味着,内核日志是寻找内核指针的绝佳之地。

例如,当内核启动的时候,内核不同段的内存映射类似下图:

如何有效绕过Android kptr_restrict

现在,假设有一个我们需要发现的符号,我们可以通过国使用包含所有符号的虚文件(/proc/kalisyms)dump出所有的内核符号的列表。当kptr_restrict被启用的时候,kallsyms返回的符号列表是受监控的(因为它使用了”%pK”),因此不会显示任何内核指针。

如何有效绕过Android kptr_restrict

然而,kallsyms返回的符号是按照地址大小排列的,虽然这些地址并没有显示出来。此外,每个段都有特殊的标记符号做前缀和后缀,也使得事情变得简单。

如何有效绕过Android kptr_restrict

仅仅通过计算从结束标记或者开始标记处到我们的期望的符号之间的符号的个数,并加上遇到的每个符号的大小,我们可以使用这个列表来推断不同符号的位置。

另一个技术是导致期望的内核指针写到内核日志中去。例如,在基于Qualcomm的设备(这种设备基于”msm”内核),当视频频设备被打开的时候,视频设备的内核虚拟地址被写到了内核日志中。

如何有效绕过Android kptr_restrict

Method #2 – Retrieving the kernel symbols statically

为什么使用这个方法?

在许多情形下,虽然设备本身是可访问的,但它可能被严格锁定—例如,在极端情形下,adb访问可能被禁止,这将使第一种方法的利用变得复杂(除非我们能努力获取到shell访问权限)。在这种情形下,我们可能会寄希望于静态的从内核镜像建立内核符号的完整列表,而不用与设备进行交互 。

同样,由于KASLR(内核地址随机化)目前并没有在Android中采用,因此也就不用考虑映像中符号的位置在运行时会有所更改。这也就意味着,内核镜像必须包含所有建立完整符号列表需要的信息,包括他们的地址,这将和他们在真实设备上的地址一样。

如何获得内核镜像?

假设我们对一个真实设备有完全的访问权限,通过/dev/块我们可以直接从MMC读取内核映像。然而,在大多数情况下,直接读取MMC块需要root权限,这也就使得这种方法几乎不会被采用,毕竟有root访问权限了,我们可以禁用掉kptr_restrict.

更合理的方式是从特定设备的固件文件中获取内核镜像,并提取出来。对于不同的设备有很多工具能做到这一点(我写了一个脚本来unpack Nexus 5的 bootloader),已经有很多现成的工具供使用,google一下就能找到。

提醒一下,一定要确信下载的内核映像的版本与你设备的内核的版本相匹配。可以通过使用”uname -a”命令获取内核版本。

如何有效绕过Android kptr_restrict

获得镜像之后- 接下来怎么做?

为了弄清楚如何从内核映像中提取完整的符号列表,我们首先必须看看内核映像是如何建立的。阅读代码后发现,作为创建过程的一部分,使用了一个 特定的程序 将那些特殊格式的符号传递给内核映像。程序接收的符号map中包含了每个内核符号在内核虚拟地址空间中的位置,并输出包含压缩的符号表的汇编文件,而这个表又被编译进了最终的内核映像。

这也就意味着,只需理解了写符号表使用的格式就能从原始的二进制内核重建这个表。然而,对于那些正常编译的没有额外的符号内核,问题就有些棘手了。

由于这个脚本写的标签在最终的二进制内核中是不可见的,我们首先要做的就是找到二进制文件中的符号表的开始位置。幸运的是,办法很简单-还记得之前看的fromkallsyms符号表吧?最前面两个符号是标记符号,它指向内核text段的开始位置。由于内核加载的位置已知(通常为0xC0008000),我们可以在二进制中搜索首次连续出现两次这个值的地方,并从那个位置开始解析符号表结构。

如何有效绕过Android kptr_restrict

从整个符号表可以看出,它最终以NULL地址结束。紧跟符号表之后就是真正的符号,因此我们可以很容易的验证表的结构良好。

如何有效绕过Android kptr_restrict

接着,”markers”,”symbols”这两个表被写到文件中去。这是为了压缩表中的符号的大小,进而减小了二进制内核的大小。这个压缩的map将最常用的256个子串(称之为tokens)映射到一个字节值。接着,每个符号的名字被压缩为pascal风格字符串字节(也就是,字符串的前面一个字节表示字符串的长度,接着才是字符串真正的内容)。压缩名字的map中的每个字节对应一个token,进而对应一个最常用的子串。综合起来,类似下面这样:

如何有效绕过Android kptr_restrict

据内核开者的信息,这通常会有50%的压缩率。

我编写了一个python脚本,它可以在给定一个原始的二进制内核下提取出一个完整的符号表,其格式与kallsyms中使用的格式相同。 脚本地址

如何有效绕过Android kptr_restrict

Method #3 – 在内核中寻找公开信息

这是典型的绕过kptr_restrict带来的限制的一种方法。对于希望以各种设备为对象的远程攻击者而言,通常它是最好的方法,有如下几点原因:

1. 为了运行“bugreport”服务,第一种方式通常需要设备的shell访问权限: 2.第二种方法需要获取内核映像,对于各种各样的设备而言也是一件麻烦的事情。

不幸的是,相比其他种类的漏洞(如内存崩溃),内核开发者似乎更少意识到泄露内核指针带来的风险。

因此,找到一个内核内存泄露点通常是蛮容易的。没用多久,我就遇到了这样一个泄露点,它在任何上下文中都能访问得到。

不论何时Android中打开了一个socket,,它都用netfilter驱动的“qtaguid”进行标记。该驱动负责所有socket接收和发送的数据(以及标记),并允许基于分配给他们的tag在socket上做一些限制。这个特性是为了说明设备使用的数据( 可参考次链接 )。实际上每个进程的划分都是通过分配一个特定的标签完成的,并基于该标签对进程使用的数据进行监控。

如何有效绕过Android kptr_restrict

这个驱动也暴露了控制接口,通过它用户可以查询当前socket以及他们的标签,也可以从打开socket的地方获得user-id,group-ID.实现该接口的文件在/proc/net/xt_qtaguid/ctrl下,该文件的访问没有什么限制。

然而,读取该文件显示,它实际上包含每个socket的内核虚拟地址,并且没有受到监控。

如何有效绕过Android kptr_restrict

查看虚拟文件读实现的源代码,结果表明写该地址的时候并没有使用”%pk”格式标识别。

如何有效绕过Android kptr_restrict

实际上指针写到了sock结构(它是包含真正的socket结构的内核结构体)

它反过来又包含了所有在socket内进行操作的函数指针。

这也就意味着,例如我们有一个漏洞允许我们覆盖一个特定的内核地址,我们可以简单的这样做:

1.打开socket,并为其使用gtaguid标记 2.在/proc/net/xt_gtaguid/ctrl中查看socket的地址 3.覆盖指向socket 结构的指针,使其指向我们的地址空间。 4.在该地址处用假的含有完全可控函数指针的socket结构覆盖该地址 5.执行那些能导致内核执行我们的代码的操作(如关闭socket)

如何有效绕过Android kptr_restrict

正文到此结束
Loading...