查看原文
其他

Mac OS 内核扩展【漏洞挖掘】指导流程

360 Nirvan Team 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:16004488


杂谈与概述


首先表达一下我对漏洞挖掘的一些理解,这些理解可以让大家了解目前我是怎么看漏洞挖掘的,进而了解为什么会形成文章中提到的流程与方法。

漏洞挖掘是没有办法保证结果的:没有人敢说随便给他一个程序,他就可以从中找到漏洞。也有人说漏洞挖掘靠运气,我理解的运气是指:漏洞是由开发人员生产的,漏洞挖掘人员只是去发现开发人员生产的漏洞,对漏洞挖掘人员来说,审计的模块中是否存在漏洞是未知的。

既没法保证结果,又存在运气成分,难道漏洞挖掘是玄学不成?我认为不是。我们没法保证结果,但是我们可以保证过程。何为保证过程?保证过程是指我们可以在领域内总结出一套流程,总结出一些需要关注的、常规的审计点。流程与审计点代表的是我们团队的经验与当前水平,如果严格执行了流程与审计点后,还是没法在目标中找到漏洞,那说明以我们团队目前的水平就是无法在目标中找到漏洞。如何对待运气?我认为有两个方法,一是凭借审计人员的经验,以及对系统功能的整体了解,他可以“感觉到”系统的薄弱点在哪里,然后去寻找漏洞。另一个是强调覆盖面,我们既然无法确定哪个模块存在漏洞,那我们就一个模块一个模块的去做覆盖。

人工审计与模糊测试的关系,这也是无法回避的问题。其实人工审计与模糊测试之间的关系就是如何利用资源的问题。人工审计花费的是人力资源,而模糊测试花费的机器资源。有些问题适合用人力资源来解决,另一些问题适合使用机器来解决,二者没有任何矛盾,因此这不是一个二选一的问题,而是一个如何充分利用资源的问题。现在大家都有一个共识:人力成本要比电费贵,最终我们还是希望用机器来解决大部分问题。但是对于一个具体的团队来说,囿于团队的知识背景、知识储备、对问题理解的深度、对问题抽象的高度,无法对现有的所有漏洞进行建模,也就无法实现自动化的漏洞挖掘。虽然如此,我们的长远目标仍然是最大限度的实现自动化,提高生产效率,好像历届工业革命都不是靠砸人完成的。

这篇文章介绍了从拿到一个内核扩展的二进制文件开始,到完成漏洞挖掘的整个过程:每一步需要做什么,每一步需要做到什么程度,以及在漏洞挖掘的过程中我们需要审计的点。最重要的是,大家要明确我们的 Target:寻找漏洞,凡是与寻找漏洞无关的事情我们都不感兴趣,比如:逆向。但是逆向又是我们必须要做的,因此还会提供一系列的工具,来自动化部分逆向工作,尽量缩短从二进制文件到漏洞挖掘的路径。

从文章的结构上看,首先我们会回顾一些常见的攻击面,然后会介绍如何确定目标,并提供一些思路与工具帮助大家确定目标。在确定了目标后,会介绍如何对二进制文件做逆向,对于二进制审计来说这是非常重要的一个阶段。在完成逆向工作后,我们进入漏洞挖掘阶段,这也是发挥大家聪明才智的阶段,在这里会介绍一些常见的漏洞类型与审计方法。

常规攻击面


这里会介绍内核与内核扩展的攻击面,关于通用的内核攻击面并不是什么秘密,大家都知道,就是如下几个:


  • BSD 系统调用。

  • Mach 系统调用。

  • MIG 接口。

  • 设备的创建及 ioctl。创建设备的函数包括:cdevsw_add, cdevsw_add_with_bdev, bdevsw_add,大家只要在内核中或者内核扩展中查找相关的函数即可,找到相关结构后,重点关注 ioctl 的响应函数。

  • 网络协议。网络协议是通过调用 net_add_proto 添加的。需要关注:setsockopt、ioctl 的处理函数,以及对网络包的解析。

  • sysctl 变量。

  • 驱动暴露的接口。驱动主要会向用户空间暴露这几个功能:读取、设置属性;创建共享内存;通知对象的管理;externalMethod,getTargetAndMethodForIndex, 这部分接口。


上面列出的是粗粒度的攻击面,对于更具体的、更细粒度的攻击面,大家需要深入进去,比如:以 externalMethod 为例,大家需要深入分析每个实现。


确定目标


方法一:由方法主导。就是找到一种新的攻击方法,使原来不是攻击面的目标变成攻击目标。这个只能多多进行思维的发散,没人可以教这个能力。

方法二:通过大量的源码阅读与逆向分析,对系统非常熟悉,可以跨越方法、跨越类、跨越模块思考问题、寻找问题。软件设计的一种原则就高内聚、低耦合,低耦合不是没有耦合,存在耦合即存在互相影响,安全影响也是其中的一种影响。寻找这类安全问题,思维要在低层次与高层次之前随意切换,前提就是对功能的了解。没有什么捷径,多读源码、多逆向、多调试,如果可以做到比开发人员还了解相关的功能,自然能发现别人发现不了的问题。

方法三:一个一个的覆盖。没有太多技巧,参考前面的各种列表,进行地毯式覆盖。虽然没有技巧,但是闭源的程序、历史久远的程序中存在的漏洞数量会相对多些。闭源的程序主要指驱动,大家可以使用工具与内核类列表获得用户空间中可以打开的驱动列表,随便选一个进行逆向、审计。


逆向过程


逆向逆的是什么?通过逆向,我们主要想从程序中获得两方面的信息:逻辑;数据结构、类型系统。IDA Pro 的反编译插件极大的降低了分析程序逻辑的难度。这里我们会围绕如何还原内核扩展中的数据结构来讲解逆向过程,通过良好的数据结构,配合 F5 插件,可以极大的提交逆向出的程序的可读性。

插播广告,关于逆向的程度不知道大家是如何理解的,我对逆向的入门是通过这本书《Reversing:逆向工程揭密》。这么多年过去了,仍没忘记书中的观点,大致是:我们逆向出来的源码,通过使用合适的编译选项,可以生成逆向目标相同的二进制。这个目标太高了,我们在实际中确实没必要做到,但是想提醒一点:当你对目标做了审计,没有发现任何漏洞后,你应该反问自己对目标做了什么程度的逆向与调试?!

Note:下文会涉及到一些例子,例子使用的二进制是:macOS-10.12.4-16E195, AppleCameraInterface.kext, 5.59.0。

在 IDA Pro 中完成对目标驱动的自动分析后,我们还需要做如下几件事:

  • 创建虚函数表、类结构。

  • 导入依赖的类型。

  • 增加 Got 表的可读性。

  • 识别导入的虚函数表。

  • 识别 sMethods。

  • 识别 externalMethods 的参数。

  • 获取类的布局信息。

  • 识别 sysctl 结构。

识别虚函数表、类结构

为了提高 F5 伪代码的可读性,我们需要为 C++ 的虚函数表、类创建相应的结构体,大家可以使用工具来实现这个目标。在完成转换后,在 Structures 标签页下,大家可以看到如下的信息:

虚函数表会被转换成如下的结构:

如上图,“`vtable for’AppleCamIn”主要用于依赖导入,“`vtable for’AppleCamIn1”主要用来构造对象的 vptr,如下图:

大家会注意到如上的 2 个虚函数表中有大量的重复成员,我们目前并没有做去重,大家可以使用结构体嵌结构体的方式来去重,进而节省 IDA Pro Database 的存储空间与加载速度。另,大家可以使用工具所提供的功能来实现:在反编译窗口中,双击虚函数后,直接跳转到具体的实现。

为了辅助分析,大家使用 File -> Product File -> Create C Header File 来查看创建的结构及成员变量的类型:

导入依赖的类型

打开驱动的 Info.plist 文件,查看 OSBundleLibraries:

可以看到当前的驱动依赖 Kernel 和 IOPCIFamily。使用 IDA 加载 Kernel,使用脚本创建相应的虚表结构与类结构,然后导出类型 IDC:File -> Product File -> Dump Typeinfo to IDC File。最后在当前的 IDA 数据中导入 IDC,IDC 是 IDA 的原生脚本,导入的方式是:File -> Script file…,选择导出的 IDC 文件。需要将同样的操作流程应用到依赖库上,本例中是 IOPCIFamily。有的依赖库并不会在 OSBundleLibraries 中列出来,以 AppleCameraInterface 为例,它还依赖 IOACPIFamily 和 IOSurface,像这种隐含的依赖,遇到时再做处理。

增加 Got 表的可读性

在完成依赖库的导入后,我们现在需要处理 Got 表,下图是处理之前的 Got 表:

以 IOService 的虚表为例,命名 off_12038 没有意义,造成在逆向分析时还要多一步跳转才能知道这是什么类型:

使用工具处理 Got 表,处理后的效果如下:

在阅读汇编时可以直接明白这个符号的意义。同时这个脚本还可以处理一部分虚表的偏移,转换成函数,如下图:

识别导入的虚函数表

处理完 Got 表后,我们对上面提到的函数进行反编译,结果如下图:

可以看到具体的函数还是使用偏移值 +0x4B,原因是导入的虚函数表是在运行时处理的,静态解析不出来。大家可以使用工具做处理,原理是:单独创建一个 Segment 用来存在虚表,然后修改相关的引用,这样反编译插件就可以找到相应的函数。处理后的反编译效果如下图:

识别 sMethods

sMethods 是驱动的一个非常重要的攻击面,因此找到并识别出相关的结构是重要的一步。大家可以使用工具来识别、处理相关结构。

识别前:

识别后:

大家还可以使用工具来为结构添加索引,如下图:

识别 externalMethods 的参数

大家可以使用工具来处理,处理前:

处理前的反编译结果:

处理后的效果:

处理后的反编译效果:

该工具没有做 CFG 跟踪,无法处理所有情况,剩下的情况需要大家手工处理,手工处理的方式为,选中相关的指令行:

然后使用快捷键:T,调出菜单为寄存器指定类型:

确认无误后,点击 OK。这种方法也可以用来处理虚表、类等结构。

获取类的布局信息

在逆向部分的开头我们已经说明了,逆向主要逆的就是数据结构。这里会介绍一种逆向数据结构的可复用的流程与方法,这种逆向方法使用脚本来表达逆向的结果,具体的逆向结果。

内核扩展中的类对成员变量的设置主要在 init 与 start 两个函数中完成,下面我们以 AppleCamIn::start 为例来说明对类结构的还原。

处理的脚本如下图:

start 函数的初始反编译效果如下图:

将 v6 转成 IOPCIDevice *,方法是右键 v6,在弹出的菜单中选择:Convert to struct *:

转换后的效果图如下:

可以看到虚表已经被识别出来的。通过 this->gap[31] = (__int64)v6; 知道,有一个成员变量是 IOPCIDevice,我们在脚本中为类添加一个成员变量:

其中成员的偏移量的计算方法为:gap 的类型是 int64,gap 中每个成员的大小为 8 字节,成员相对于 gap 的偏移量为 31 * 8,因此总的偏移量为:8 + 31 * 8。也可以在反汇编窗口看直接的偏移量,然后在脚本中使用直接偏移量:

然后执行脚本,再次按 F5,刷新反编译结果,可以看到:

继续识别成员变量,IOPCIDevice::mapDeviceMemoryWithRegister 返回的类型是 IOMemoryMap *,我们将 v9 转换成对应的类型:

从反编译结果中我们我们知道 AppCamIn 类有个成员变量是 IOMemoryMap *,gap_0x8 的类型是 int64,起始偏移量为 this[15],相对偏移量为 9 * 0x8,总的偏移量为:this[15] + 9 * 0x8 = 0x8 * 15 + 9 * 0x8,将成员变量添加到类结构中:

再次执行脚本,F5 刷新反编译结果:

以此类推,可以完成类成员的识别与结构化。

识别 sysctl 结构

使用工具来查找、识别 sysctl 结构。

逆向部分总结

一句话:不要急躁,如果找不到漏洞,那就什么都不用想,按照如上的流程与方法,安安静静的做好逆向。

常见漏洞与审计


基本的错误类型大家可以通过阅读推荐资料(《The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities》,ISBN: 978-0321444424)来熟悉、掌握。对于 Mac 平台特有的错误类型,大家可以通过阅读 Google Project Zero 的漏洞报告来学习。这里多说一句,Google Project Zero 对行业的贡献不只只是提高了 IT 基础设施的安全性,同时也为我们提供了关于漏洞的、大量的、真实的漏洞与利用资料,大家一定要充分利用。

另外,就像这个世界上每天都会发生地震一样,漏洞领域每天都会有漏洞被公开,面对每天大量的信息我们该如何应对?这里给大家的建议是:

1、只关注当前领域的信息,对于其他的信息如果时间不够,可以略过。什么是当前领域?我们当前做二进制漏洞,二进制漏洞就是我们当前的领域,具体包括:漏洞披露、利用方法与思路、漏洞挖掘生产力工具。

2、对于具体的二进制漏洞,我们没有时间去复现每一个漏洞,实现每一个利用,大家可以锻炼下自己的抽象、总结能力:通读漏洞报告后,用一句话总结出错误类型或利用技巧方面的创新。

言归正传,完成逆向处理后,大家可以相对容易的找到如下几个常见的安全问题:

  • memcpy, bcopy 等溢出问题。

  • 整数的溢出、符号问题。

  • 竞态条件问题。

  • 二进制协议,即:从用户空间向内核中传递一个结构体。

  • 直接的信息泄露,即:从内核向用户空间拷贝数据,大小可控。

之所以说容易是指:这些错误涉及的功能比较小,我们通过在反编译结果中跟踪用户空间可控的数据在内核空间的传递,就可以相对直观的发现。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存