亲历漏洞研究最让人难受的地方:看到打补丁版本,才知漏洞一直近在眼前(详述)...

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士团队

本文叙述的是作者还在谷歌 Project Zero (GPZ) 团队工作时发生的事情。说明了做漏洞研究更令人沮丧的一面:当你看到打补丁版本时,才意识到自己错过了就在眼前的一个 bug!如下为正文:

可疑代码

写好 oob_timestamp exploit (https://bugs.chromium.org/p/project-zero/issues/detail?id=1986)后,我花时间试图找到另外一个可 exploit 的漏洞。一般而言,如果你已经有了研究平台(另一个 exploit)帮助你分析,那么再开发一个 exploit 就会容易得多。比如,转储内核内存,确保堆喷射将对象放在预期位置。盲目地开发一个 exploit,就像我开发 voucher_swap 那样会难得多。(oob_timestamp:我依靠 checkra1n 在A11 上引导该 exploit,之后将其扩展到 A13)。因此我以为将下一个 exploit 偏离 oob_timestamp 应该会避免后续必须重新引导的问题。

由于我已经花了很长的时间为 oob_timestamp 逆向 iOS 13.3 (17C54),因此我决定在新的用户客户端上继续投入。我写了一个小程序,枚举可从 app 沙箱触及的 IOUserClient 类(在这个过程中发现了另外一个 bug)并查找之前并未研究的类。

先来快速了解下 Apple 内核:Apple 的内核被称作 XNU,而 IOKit 是 XNU 执行驱动的C++框架。虽然用户空间中的 app 可调用 IOServiceGetMatchingServices() 将句柄放到驱动中,但是这款 app 实际上无法对原始的驱动句柄做太多的工作。相反,该 app 需要指令驱动,调用 IOServiceOpen() 创建一个“用户客户端”,传递它想要的用户客户端类型。由于该用户客户端向用户空间提供多数功能,因此它会受限于沙箱检查,确保该 app 能够开放用户客户端的请求类型。一旦该 app 拥有为驱动所用的用户客户端句柄,那么该 app 就能通过调用用户客户端句柄上的函数如 IOConnectCallMethod() 等,指定该 app 想要调用的方法的 “选择器(selector)” (索引)和用户客户端进行交互。在内核中,IOConnectCallMethod() 将使用该选择器来索引用户客户端提供的方法表,调用所需方法。

当扫描可以打开的客户端时,我发现了一个可触及的类:H11ANEInDirectPathClient,它是 H11ANEIn 驱动的用户客户端。之前未见过该类,但搜索后得知它并不开源,也就是说代码的安全审计可能很少,因此很可能会存在一些比该内核开源部分更容易发现的 bug。

在逆向的过程中,我发现了一些有意思的事。首先,H11ANEIn 似乎实际上拥有2个用户客户端:H11ANEInDirectPathClient (我已经打开的)和 H11ANEInUserClient (我无法在沙箱中打开的)。读取方法 H11ANEIn::newUserClient()中的字符串后发现 H11ANEInDirectPathClient 是 H11ANEInUserClient 的低权限版本,因此我能打开前一个但打不开后一个就合理了。

if ( type == 1 ) // H11ANEInDirectPathClient


{


    _os_log_internal(...,


        "%s : ... : Creating direct evaluate client\n",


        "virtual IOReturn H11ANEIn::newUserClient(...)");


    ...


}


else // H11ANEInUserClient


{


    _os_log_internal(...,


        "%s : ... : Creating default full-entitlement client\n",


        "virtual IOReturn H11ANEIn::newUserClient(...)");


    ...


}

从 IOKit 用户客户端中查找漏洞的传统起始点是从所提供的外部方法中查找。它们通常是内核缓存镜像中函数指针靠近用户客户端 vtable 的可识别表。如下是我为两个用户客户端识别出的外部方法表,奇怪的是它们在内核缓存中背靠背而不是位于各自的 vtable 附近:

另外,在查看这两个表的交叉引用时我还发现一个情况:由于这些类基本相似,只不过其中一个是另外一个的低权限版本,苹果公司做出了异于寻常的决策:共享了与这两种用户客户端类型之间共享功能相对应的外部方法表的部分!

从每个用户客户端访问外部方法表之间重叠部分的 ::externalMethod() 方法中就可明显看出这一点。H11ANEInDirectPathClient 版本如下:

int H11ANEInDirectPathClient::externalMethod(H11ANEInDirectPathClient *this, u32 selector, IOExternalMethodArguments *args, IOExternalMethodDispatch *method, void *target)


{


    if ( !target )


        target = this;


    if ( selector <= 33 )


        method = &H11ANEInDirectPathClient_ExternalMethods_34[selector];


    return IOUserClient::externalMethod(this, selector, args, method, target);


}

H11ANEInUserClient 版本如下:

int H11ANEInUserClient::externalMethod(H11ANEInUserClient *this, u32 selector, IOExternalMethodArguments *args, IOExternalMethodDispatch *method, void *target)


{


    if ( !target )


        target = this;


    if ( selector <= 33 )


        method = &H11ANEInUserClient_ExternalMethods_34[selector];


    return IOUserClient::externalMethod(this, selector, args, method, target);


}

由于每个版本均可访问34种方法,而数组中的前3个方法为 H11ANEInDirectPathClient 保留,这就意味着最后3个将为 H11ANEInUserClient 保留,似乎共有37种方法。

于是,我开始深入挖掘可被 H11ANEInDirectPathClient 访问的方法,很快就认为该驱动钟的代码质量并不是很高。例如,我发现3500行的方法 H11ANEIn::ANE_ProgramSendRequest_gated()可通过选择器2和33触及,在该函数的顶部出现一些非常容易实现的界外读取漏洞。

由于args 的内容完全受控,因此  args->totInputBuffers 计数可能会随意变高,超过数组 inputBufferSymbolIndex 和 inputBufferSurfaceId的末端。

由于代码质量看似较低,而我并不太愿意遍历长度为数千行代码的函数,于是尝试一些非常容易的模糊测试。虽然模糊测试的经验有限,但我之前曾写过一个模糊测试工具,会从传递随机生成值得用户空间盲调 IOConnectCallMethod();令人惊讶的是,在找到真正得内核漏洞之前,这样做就足够了。于是,我决定使用这个模糊测试器并将其指向 H11ANEInDirectPathClient。

就在启动该模糊测试器 app 的一秒内,设备就有反应了。

我当然对这样的进展很兴奋,但实际上该 bug 是一个非常不起眼的空指针解引用;而且在 iOS 不可利用。进一步模糊测试后似乎也没有触发别的 bug,于是我向苹果公司发送了一个简略的非安全报告,称这部分代码可能会有问题,之后转身离开了 H11ANEInDirectPathClient。

再次邂逅符号

时间快进到8月底。

和之前的 iOS 12 测试版一样,某些 iOS 14 测试版中包含了一个符号化的内核缓存。我还没机会深挖这些问题,但我认为增加符号(尤其是可从错误的 C++ 方法名称中引用的有限的类型信息)将更快地逆转数千行 H11ANEIn 函数,因此更有价值。于是,我打开 IDA 并再次跳到外部方法表,查看是否有一些明显的变化。

我发现了外部方法表的一些情况:

很奇怪,H11ANEInDirectPathClient 和 H11ANEInUserClient的外部方法表都定义了符号。这就诡异了:我以为代码会由IOExternalMethodDispatch 结构的单一数组组成,因此 H11ANEInDirectPathClient 能够从0开始索引34种方法,而 H11ANEInUserClient 可以从3开始索引这34种方法。在这样的安排下,应该只有一个符号,而且是整个数组只有一个符号。

这时,我才发现:我关于外部方法数组重叠的认识纯粹是无稽之谈,而外部方法的“共享”是 H11ANEInDirectPathClient 造成的界外访问!较低权限的客户端应该只有3个方法,但不巧的是边界检查时存在一个错字,使得 H11ANEInDirectPathClient 能够访问并从更高权限的客户端调用外部方法。如此,H11ANEInDirectPathClient 向 H11ANEInUserClient 做出的每个调用都是间接地在 this 指针上触发了类型混淆!

事后诸葛亮,我意识到“共享外部方法数组”的安排并不合理:这样的安排不惜慎之又慎,避免多个用户客户端的两个类之间出现类型混淆问题,而且并不存在这种预防措施。当我反编译新的内核缓存中的 H11ANEInDirectPathClient::externalMethod() 时,证实了这一点,并且发现选择器上的边界检查从33减少到2,也就是说该 bug 已修复。

所以,我竟然错过了一直在眼前飘着的问题,我通过创造一种重叠方法表的概念证明了它存在的合理性。当然,雪上加霜的是,我之前报告的空指针解引用非安全问题只有在调用其中的两个界外方法才能实现。

另外一个复制粘贴问题

为什么会出现这个 bug?由于有 bug 的版本中包含了对 ::externalMethod() 实现的边界检查,因此我认为它是另外一个复制粘贴问题。我猜测,在苹果的源代码中, H11ANEInUserClient::externalMethod() 实际是这样的:

IOReturn H11ANEInUserClient::externalMethod(


    u32 selector, IOExternalMethodArguments *args,


    IOExternalMethodDispatch *method, void *target)


{


    if ( !target )


        target = this;


    if ( selector < H11ANEInUserClient::sMethodCount )


        method = &H11ANEInUserClient::sMethods[selector];


    return super::externalMethod(this, selector, args, method, target);


}

我猜测该代码被复制粘贴,用于创建 H11ANEInDirectPathClient 版本,但作者不小心忘记在选择器检查中更改类型名称:

IOReturn H11ANEInDirectPathClient::externalMethod(


    u32 selector, IOExternalMethodArguments *args,


    IOExternalMethodDispatch *method, void *target)


{


    if ( !target )


        target = this;


    if ( selector < H11ANEInUserClient::sMethodCount )


        method = &H11ANEInDirectPathClient::sMethods[selector];


    return super::externalMethod(this, selector, args, method, target);


}

除此之外,编译器背靠背放置外部方法表通常是很容易犯的错误,使该 bug 有可能被利用(与我所知道的以往的界外外部方法相反)。话虽如此,但我尚未检查该问题实际的可利用性。

结论

那么,我们能从这件事中学到什么?

首先,真的很容易错过 bug,即使是你认为应该是很明显就能看出来的 bug。我真想为自己错过这个 bug踢自己一脚,关键自己还像戏精一样证明这种代码模式为何会出现的合理性。如果说我必须一而再再而三地吸取某个教训,那么一定是对代码的固有怀疑,永远不要认为它现在做的事情是故意这样做的。

其次,虽然复制粘贴真的能够快速创建代码,但它也能快速制造 bug,而扫一眼源代码难以发现它的本质。通过查看反汇编程序可以很容易地看出2个数组是“重叠的”,但很难发现复制粘贴的代码中使用了两个非常类似的类名称中其中一个错误的名称。虽然这并不能百分百地解决问题,但它有助于将复制粘贴的代码分解为可复用的辅助函数。

最后,即使是我在查看符号化的内核缓存时发现了该 bug,但我并不想让苹果公司认为发布符号会带来安全风险。当苹果公司不慎发布符号化的内核缓存或开发二进制时,安全研究员喜上眉梢,但这只是因为这样做会节约逆向的时间,而不是因为它可使事情逆转。不管是否存在符号,任何有能力的攻击者最终会找到 bug;缺乏 bug 符号所起的作用只是让别人(比如我)看不到而最终没有报告该 bug。因此,保留符号是一种极其薄弱的保护措施,它只会阻止低阶攻击者且使已被发现的 bug 存在更久的时间。

你是否也有这种捶胸顿足的情况?来呀,在留言区互相伤害呀(ㄒoㄒ)~

推荐阅读

苹果修复三个已遭利用的 iOS 0day

从 CVE-2020-1048 到 CVE-2020-17001:Windows打印机模块中多个提权漏洞分析

原文链接

https://googleprojectzero.blogspot.com/2020/11/oops-i-missed-it-again.html

题图:Pixabay License

本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    觉得不错,就点个 “在看” 或 "赞” 吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值