详解iOS零点击无线电近程漏洞 (三)

匿名者  1041天前

在这篇文章中,我们将为读者详细介绍近期在iOS中发现的一个零点击无线电近程漏洞,由于篇幅过大,我们将分多篇发表。更多精彩内容,敬请期待!

接上文

Radiotap标准

在前文中,我们已经考察了802.11 AWDL帧中数据的组织结构:最前面是一个ieee80211头部,然后是Apple OUI、AWDL Action帧的头部,等等。如果我们的WiFi适配器连接到一个WiFi网络,我们就能利用这些信息来传输这样的帧。但问题是,我们并没有连接到任何网络。这意味着,我们需要给自己的帧附加一些元数据,以告诉WiFi适配器以无线方式传输这个帧时,所需要注意的事项。例如,它应该使用什么频道、什么带宽及调制方案来注入该帧?它是否应该尝试重传,直到收到ACK为止?注入帧时,它应该使用什么样的信号强度?

Radiotap是一个通信标准,用于详细描述这种类型的帧元数据;无论是在注入帧还是接收帧时,都需要用到这些元数据。实际上,它就是一个可变大小的头部,我们可以将其放到要注入的帧的前面(或者从嗅探到的帧的开头部分读出该头部)。

不过,我们指定的Radiotap字段到底是否被遵守和应用,具体取决于我们使用的驱动程序:有的驱动程序可能不允许从用户空间向帧中注入内容。下面是一个使用MacBook Pro上的内置MacOS数据包嗅探器,从AWDL帧中捕获的radiotap头部的示例。需要注意的是,这里Wireshark已经对二进制的Radiotap格式进行了相应的解析处理:

1.png      

Wireshark已经对pcaps中的radiotap头部进行了解析,并以人类可读的形式展示出来

从这个radiotap头部中,我们可以看到时间戳、数据传输速率,频道(5.220 GHz,即频道44)和调制方案(OFDM)。此外,我们还可以看到接收信号的强度和噪声指标。

接下来,让我们详细介绍radiotap头部:

static uint8_t u8aRadiotapHeader[] = {

  0x00, 0x00, //version

  0x18, 0x00, //size

  0x0f, 0x80,0x00, 0x00, // included fields

  0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //timestamp

  0x10, // addFCS

  0x00,// rate

  0x00, 0x00,0x00, 0x00, // channel

  0x08, 0x00, //NOACK; don't retry

};

在了解radiotap标准和基本的头部之后,我们就可以使用cap_inject接口和监控模式下的无线适配器,以无线方式发送AWDL帧了:

int pcap_inject(pcap_t *p, const void *buf, size_tsize)

当然,上述代码还不能立即正常工作,而且经过一些反复试验,发现似乎它没有严格遵守规定的速率和频道。另外,我们发现使用该适配器进行注入操作时,似乎只能在1Mbps下工作,并且radiotap头部中指定的频道并非用于注入的频道。不过,这个问题不难解决——我们可以手动设置wifi适配器的频道:

iw dev wlan0 set channel 6

尽管1Mbps的注入速度的确很慢,但这足以让测试AWDL帧以无线方式进行传输,我们可以通过运行在另一个监听模式下的设备上的Wireshark来检测这个帧。但在目标设备上,似乎什么也没有检测到。因此,现在是时候进行一些调试工作了!

用DTrace进行调试

SEEMOO实验室的论文已经给出了MacOS的启动参数建议,以便让AWDL内核驱动程序记录更为细致的日志信息。尽管这些日志信息是非常有帮助的,但其中的信息通常仍然无法满足我们的要求。

在最初提交的PoC中,我展示了如何使用MacOS内核调试器来修改一个待传输的AWDL帧。通常情况下,根据我的经验,MacOS的内核调试器不仅难用,并且很不可靠。尽管从技术上讲可以使用ldb的python绑定来编写相关的脚本,但我并不推荐这样做。

然而,苹果公司还提供了另一种解决方案,那就是DTrace!在我看来,MacOS的内核调试器很糟糕,但DTrace却很好用。DTrace是一个动态跟踪框架,最初是由Sun Microsystems为Solaris系统开发的。它已经被移植到包括MacOS在内的许多平台上,并且是默认安装的。它是Instruments等工具背后的魔力源泉。DTrace允许我们将跟踪代码的小片段hook到几乎任何我们想要的地方,无论是在用户空间程序中,还是在内核中,都是如此。当然,Dtrace有它的怪癖:hook是用D语言写的,而D语言没有循环语句,同时,变量的作用域也需要花点时间才能理解,但并不妨碍它成为终极调试和逆向工具。

例如,每当分配一个新的IO80211AWDLPeer对象时,我可以在MacOS上通过下面的trace脚本进行跟踪,并输出其堆地址和MAC地址:  

self char* mac;

 

fbt:com.apple.iokit.IO80211Family:_ZN15IO80211AWDLPeer21withAddressAndManagerEPKhP22IO80211AWDLPeerManager:entry{

  self->mac =(char*)arg0;

}

 

fbt:com.apple.iokit.IO80211Family:_ZN15IO80211AWDLPeer21withAddressAndManagerEPKhP22IO80211AWDLPeerManager:return

 printf("new AWDL peer: %02x:%02x:%02x:%02x:%02x:%02xallocation:%p", self->mac[0], self->mac[1], self->mac[2],self->mac[3], self->mac[4], self->mac[5], arg1);

}

这里,我们共创建了两个hook,一个在函数入口处运行,另一个在同一函数返回前运行。我们可以使用self->语法在入口点和返回点之间传递变量,而DTrace能够确保入口和返回点的正确匹配。

我们必须在dtrace脚本中使用重整后的(mangled)C++符号;借助于c++filt命令,我们可以得到去重整后的(demangled)版本:

$ c++filt -n_ZN15IO80211AWDLPeer21withAddressAndManagerEPKhP22IO80211AWDLPeerManager

IO80211AWDLPeer::withAddressAndManager(unsigned charconst*, IO80211AWDLPeerManager*)

实际上,入口点处的hook用于“保存”指向MAC地址的指针,该指针被作为第一个参数传递;并将其与当前线程和堆栈帧相关联。然后,返回点处的hook负责打印该MAC地址和函数的返回值(返回点处的hook中的arg1是函数的返回值),在本例中,就是新分配的IO80211AWDLPeer对象的地址。

借助于DTrace,我们可以很轻松地建立自定义堆日志工具的原型。例如,如果您的跟踪目标是一个特定的分配长度,并希望了解哪些对象在那里结束,则可以使用类似下面的DTrace脚本:

/* some globals with values */

BEGIN {

 target_size_min = 97;

 target_size_max = 128;

}

 

fbt:mach_kernel:kalloc_canblock:entry {

  self->size= *(uint64_t*)arg0;

}

 

fbt:mach_kernel:kalloc_canblock:return

/self->size >= target_size_min ||

 self->size<= target_size_max   /

{

 printf("target allocation %x = %x", self->size, arg1);

  stack();

}

其中,两个“/”之间的表达式使得hook代码只有符合特定条件时才会执行。在这个例子中,将条件限制为调用了kalloc_canblock,其大小介于target_size_min和target_size_max之间。同时,内置的stack()函数会打印堆栈跟踪信息,便于我们了解特定长度范围内的内存分配情况。即使是系统调用而触发内核空间的分配,我们仍可以使用ustack()在用户空间继续跟踪该堆栈。

DTrace还可以安全地对无效地址解除引用,而不会让内核崩溃,这使得它在原型设计和调试堆的过程中非常有用。通过一些巧妙的方法,它还可以处理其他一些事情,比如转储链接列表和监视特定对象的销毁。

我强烈建议大家花一点时间来学习DTrace;一旦您掌握了其编程模型,就会发现它的确是一个非常强大的工具。

抵达入口点

通过使用DTrace来记录栈帧,我能够追踪合法的AWDL帧在代码中的路径,并确定伪造的AWDL帧走了多远。通过这个过程,我发现,至少对于MacOS系统来说,内核中有两个AWDL解析器:一个是我们已经在IO80211Family kext中看到的解析器,另一个解析器则要简单得多,它主要用于特定芯片组的驱动程序中。在这个更简单的解析器中,有三项检查没有通过,从而使得伪造的AWDL帧根本无法抵达IO80211Family代码:

首先,对源MAC地址进行验证。 MAC地址实际上包含多个字段:

1.png      

MAC地址的前半部分是一个OUI。T第一个字节的最低有效位定义了该地址是多播还是单播。 T第二位定义了该地址是本地管理的还是全球唯一的。

 

另外,libpcap例子中的源MAC地址01:23:45:67:89:ab其实是一个不合时宜的选择,因为它设置了多播位。但是,AWDL只想处理单播地址,并拒绝来自多播地址的帧。为了解决这个问题,我们可以选择一个没有设置多播位的新MAC地址。

接下来的检查是,帧的变长有效载荷部分的前两个TLV的类型,它们必须分别为类型4(同步参数),和类型6(服务参数)。

最后,同步参数中的频道号码必须与实际接收该帧的频道相匹配。

随着这三个问题的解决,我终于能够让任意控制的字节出现在远程设备的actionfr ameReport方法上,这就意味着,我们的项目需要进入下一个阶段了。

AWDL客户端的框架

我们已经看到,AWDL使用时分复用技术,在用于AWDL的频道(通常是6和44)和设备所连接的接入点使用的频道之间快速切换。通过解析AWDL对等体发送的PSF和MIF帧中的AWDL同步参数TLV,就可以计算出它们将来何时会被监听。OWL项目使用linux libev库,以试图只在其他对等体的监听时间段内进行传输。

就我们的目的而言,这种方法还面临几个问题。

首先,非常重要的一点是,这使目标定位变得非常困难。AWDL的Action帧(通常)被发送到一个广播目标MAC地址(即ff:ff:ff:ff:ff:ff:ff:ff)。这是一个网状网络,这些帧旨在供所有对等方用于构建网状网络。

虽然在同一时间利用附近的每个监听的AWDL设备将是一个有趣的研究问题,但这也带来了许多挑战,它们已经远远超出了最初的范围。因此,我非常需要一种方法来确保只有我控制的设备才能处理我发送的AWDL帧。

通过一些实验发现,所有的AWDL帧也可以被发送到单播地址,这时设备仍然可以解析它们。这就带来了另一个挑战,因为AWDL虚拟接口的MAC地址是在每次激活接口时随机生成的。在MacOS上测试时,只需运行下列命令即可:

ifconfig awdl0

通过以上命令,我们就可以确定当前的MAC地址。对于iOS系统来说,情况却有点复杂;我选择的技术是在AWDL的社交频道上进行嗅探,并将信号强度与设备的移动联系起来,以确定其当前的AWDL MAC。

当我们向一个单播地址发送AWDL的Action帧时,还有一个重要的区别:如果该设备目前正在该频道上监听并接收帧,它将发送一个ACK。这被证明是非常有用的。我们最终将使用AWDL的Action帧来建立一些相当复杂的原语,并通过滥用协议来建立一个怪物机器。能够知道目标设备是否真的收到了一个帧,意味着我们可以把AWDL帧当作一个可靠的传输媒介。对于AWDL的典型用法来说,这是没有必要的;但我们对AWDL的用法并不是典型的。

这个ACK嗅探模型将是我们的AWDL帧注入API的构建块。

接收ACK

现在,我们已经能够以无线方式传播ACK了,但是,这并不意味着我们真的能看到它们。尽管我们用于注入的WiFi适配器在技术上必须能够接收ACK(因为它们是一个基本的协议构件),但能够在监控界面上看到它们,当前还是无法保证的。

1.png     

wireshark的截图显示了一个伪造的AWDL帧,然后是目标设备的确认

由于libpcap接口是相当通用的,因此,没有任何方法可以表明一个帧是否被ACK。内核驱动程序甚至可能不知道是否收到了ACK。我并不想深入研究注入接口的内核驱动程序或固件,因为这本身就是一项重大投资,所以我尝试了一些其他的想法。

由于802.11g和802.11a协议的ACK帧是基于计时的,因此,在每个传输的帧之后有一个短暂的窗口,如果接收者收到了该帧,他们可以进行ACK。正是由于这个原因,ACK帧不包含源MAC地址。这是没有必要的,因为由于计时技术的关系,ACK已经与源设备完美地关联在一起了。

如果我们也在监听模式下监听我们的注入接口,我们也许能够自己接收ACK帧并将其关联起来。如前所述,并不是所有的芯片组和驱动程序都会提供所需的全部管理帧。

对于我的早期原型,我设法在我的WiFi适配器中找到一对频道,其中一个可以成功地在2.4GHz频道上以1Mbps的速度注入,另一个可以成功地在该频道上以1Mbps的速度嗅探ACK。

虽然1Mbps的速度特别慢;一个相对较大的AWDL帧在这个速度下最终会在空中停留10ms或更长时间,所以,如果您的可用窗口只有几ms,每秒也传不了多少帧。不过,作为一个起点来说,这已经足够了。

我为这个exploit建立的注入框架使用了两个线程:一个用于帧注入,一个用于ACK嗅探。帧的注入可以使用try_inject函数,该函数可以提取伪造的源MAC地址,并向第二个嗅探线程发出信号,开始寻找发送到该MAC的ACK帧。

使用pthread条件变量,注入线程可以等待一段有限的时间,在此期间,嗅探线程可能会或可能不会收到ACK。如果嗅探线程确实收到了ACK,它将记录这一事实,然后向条件变量发出信号。注入线程将停止等待,并检查是否收到了ACK。

建议大家浏览一下exploit中的try_inject_internal,以及mutex和条件变量的设置代码。

在try_inject周围有一个叫做inject的封装器,它将不断调用try_inject,直到成功为止。通过这两个方法,我们不仅可以完成时间敏感的帧注入,也可以完成时间不敏感的帧注入。

这两个方法需要用到可变数量的pkt_buf_t指针,以及一个简单的自定义的、可变长度的缓冲区封装对象。这种方法的优点是,它允许我们快速建立新的AWDL框架结构的原型,而不需要编写模板代码。例如,下面就是注入一个基本的AWDL帧并重新传输它直到目标接收它所需的全部代码:

inject(RT(),

       WIFI(dst,src),

       AWDL(),

      SYNC_PARAMS(),

      SERV_PARAM(),

      PKT_END());

虽然建立这个API花了一些时间,不过从长远来看,却能节省大量的时间,同时,还能给让我们轻松尝试新的想法。

随着注入框架的就绪,我们接下来就可以开始考虑如何真正利用这个漏洞了。

小结

在这篇文章中,我们将为读者详细介绍近期在iOS中发现的一个零点击无线电近程漏洞,由于篇幅过大,我们将分多篇发表。更多精彩内容,敬请期待!

(未完待续)

最新评论

昵称
邮箱
提交评论