本文章主要介绍用ZYNQ7020+AD9361+Gnu radio搭建一个蓝牙抓包器的文章。用SDR(Software Defined Radio)设备研究了下BLE的抓包,通过BLE的抓包了解下蓝牙的controller的一点点内容,日拱一卒,拱了2个周终于可以抓到蓝牙37通道的广播数据,并且可以把数据传送到Wireshark中。

本文章主要介绍是用ZYNQ7020+AD9361+Gnu radio是搭建一个蓝牙抓包器的文章。

由于之前一直做蓝牙Host,对controller觉得是一个比较虚无缥缈的东西,得不到的总是在骚动,所以最近用我用吃灰了2年的SDR(Software Defined Radio)设备研究了下BLE的抓包,通过BLE的抓包了解下蓝牙的controller的一点点内容,日拱一卒,拱了2个周终于可以抓到蓝牙37通道的广播数据,并且可以把数据传送到Wireshark中。整份工程是C语言+gnu radio来完成,没有借用python等。为什么选用C语言而不用python呢,因为我是想着做完这个找到灵感,是不是可以直接用SDR做蓝牙芯片了(哈哈,愿景哈)

代码连接:https://github.com/sj15712795029/bt_sdr_air_sniffer

欢迎有大牛一起来实现这个伟大的愿景!!!

一. 硬件环境

 

 

我是用的SDR设备就是ZYNQ7020 + AD9361

其中 Zynq-7020 是赛灵思(Xilinx)推出的一款高度集成的片上系统(SoC),结合了可编程逻辑 (FPGA) 和基于 ARM Cortex-A9 的双核处理器。这个系统具有强大的灵活性和高性能,适用于广泛的应用领域。Zynq-7020 内部包括了85K可编程逻辑单元,560个DSP slices,220个I/O pins,以及多个高速接口如USB、Gigabit Ethernet 和SDIO。它还集成了多个硬件加速器,能够处理复杂的计算任务。Zynq-7020 提供了广泛的软件支持,包括Linux、FreeRTOS 等操作系统,并配有丰富的开发工具,如Vivado和SDK,帮助开发者快速进行硬件设计和软件开发。该芯片特别适合于嵌入式系统、工业自动化、通信、视频处理和医疗设备等需要高计算能力和实时处理的领域,通过其高效的硬件加速和灵活的配置选项,为用户提供了高度优化的解决方案。

 

其中AD9361 是美国模拟器件公司(Analog Devices, Inc.)推出的一款高性能、可编程的射频收发器,广泛应用于无线通信系统。以下是对 AD9361 的详细介绍:

AD9361 是一款集成的射频收发器,覆盖从 70 MHz 到 6.0 GHz 的频率范围,支持宽带调制解调功能。它具有双通道,支持从 200 kHz 到 56 MHz 的信号带宽,能够适应各种无线通信标准,如 LTE、BT、WiFi、WiMAX 和公共安全通信等。

主要特点包括:

  • 双通道设计:支持多输入多输出 (MIMO) 配置,增强了数据传输的可靠性和效率。
  • 高集成度:将发射器、接收器、PLL、混频器、ADC、DAC 和滤波器集成在一个芯片内,简化了系统设计,减小了物理尺寸。
  • 宽频带:覆盖 70 MHz 到 6.0 GHz 的宽频范围,适用于多种应用。
  • 可编程性:通过 SPI 接口进行控制和配置,支持动态调整频率和带宽。
  • 高线性度和低噪声:提供优异的接收灵敏度和发射质量,确保通信系统的性能和稳定性。
  • 低功耗:在提供高性能的同时,保持较低的功耗,适合于电池供电的便携设备。

AD9361 常用于软件定义无线电 (SDR) 平台、蜂窝基站、微波中继、军事通信和测试测量设备中。其灵活性和高性能使其成为开发多种无线通信系统的理想选择。通过结合高集成度和广泛的频率覆盖,AD9361 有效简化了设计流程,缩短了产品上市时间,同时提供了卓越的系统性能。

1. 抓包短期架构

 

这个架构主要有几点工作:

1)使用PC GNU radio来控制ad9361做特定频率,特定带宽的射频收发,以及GFSK的解调

2)使用PC GNU radio把GFSK解调( Demodulation )后的数据通过ZMQ发送到C语言程序

备注:中间可能不仅仅是蓝牙数据,可能有其他垃圾数据或者其他无线技术的数据,这点需要在C语言中做解析,剔除掉不是蓝牙的数据

3)C语言程序收到GFSK解调( Demodulation )后的数据就行解析(寻找前导码/读取Access code/读取PDU header/读取PDU payload/读取CRC/去白PDU跟CRC/CRC校验等步骤)

4)做完第三步后通过pcap的format格式把数据发送给wireshark

2. 抓包长期架构

 

1)使用ZYNQ 7020 ARM Core驱动AD9361进行特定频率收数据,使用ZYNQ 7020 FPGA core进行GFSK的解调(Demodulation)/寻找前导码/读取Access code/读取PDU header/读取PDU payload/读取CRC/去白PDU跟CRC/CRC校验等步骤

2)把得到的数据通过ethernet参照pcap格式发送给wireshark

二 . 使用

目前我做的过程使用起来比较简单:

1)设置ZYNQ7020跟PC在同一个网段,要保证能ping通

ifconfig eth0 10.88.110.66

 

备注:目前我在GNU Radio中的PlutoSDR Source的URI写死的是10.88.110.66,所以你想用这个工程,需要修改grc文件

2)启动gnuradio-companion

gnuradio-companion

需要注意的是我安装的gnu radio的版本是3.8.5

 

至于怎样安装gnu radio以及libiio libiio-ad9361自行百度,我就不具体介绍了,这些算基本的环节

3)点击运行ble_sniffer_no_gui.grc

 

4)在ubuntu创建跟wireshark通信的fifo

mkfifo /tmp/fifo1

备注:当然你也可以选择用其他通信手段跟wireshark通信,或者保存文件的形式

5)wireshark开启抓包

wireshark -S -k -i /tmp/fifo1

6)开启蓝牙抓包C语言程序

./bt_start_sniffer -o /tmp/fifo1

备注:一定要先启动wireshark

当你完成以上几步,就可以实现抓包啦

可以看到C语言程序在解析Link layer的数据

 

wireshark在实时解析蓝牙数据封包

 

 

局限性:当前只能抓取BLE的广播通道的数据封包,并且只能抓取Channel 37通道的数据

我有几个疑惑大家也可以给我解惑下:

1)虽然AD9361可以支持40M带宽,但是这也不能抓蓝牙全频段,所以理论上要2个AD9361来实时抓取蓝牙全信道封包,你可能会说:可以写个跳频算法啊,跟着跳频就行了,但是我还想做BR/EDR的sniffer,那个在开始之前你并不知道在哪个channel上去连接,所以你无法提前预知跟着跳频,所以你要全频道抓取(80M带宽)

2)如果是全频段抓取80M带宽(用两个AD9361),但是我怎么知道某一时间抓取的数据他是在哪个频段上呢?我自己的想法是40M带宽,我用100M sample rate去采样,比如50M时候收到的数据,那么是就认定他是20M带宽处的数据,也就是2420M的频率,不知道这种想法是否准确,哪怕准确,我觉得运算量也很大,所以我觉得这一定不是最优解!

三.GNU radio的剖析

在开始介绍GNU radio的各个组件之前,我们先来看下我做的ble sniffer的整个grc文件

 

下面我们就来一个介绍!

1. plutosdr souce设置

plutosdr soruce是antsdr的射频接收的组件!也就是收到的无线电数据首先会经过这里处理

 

蓝牙的广播通道是37、38、39

 

 

蓝牙低功耗(Bluetooth Low Energy, BLE)的信道37的频率中心点是2402 MHz。由于每个信道的带宽是2 MHz,因此信道37的频率范围可以计算如下:

  • 中心频率:2402 MHz
  • 信道带宽:2 MHz

因此,信道37的频率范围可以表示为中心频率的±1 MHz:

  • 下限频率:2402 MHz - 1 MHz = 2401 MHz
  • 上限频率:2402 MHz + 1 MHz = 2403 MHz

所以,信道37的频率范围是 2401 MHz 到 2403 MHz

这个是antsdr的射频接收部分配置,如下:

 

IIO context URI:Antsdr的ip地址, 这个是开发板的ip地址,通过libiio通信

LO Frequency: 如果想抓BLE 37通道的广播,中心频点是2.402g,这里我们虽然写的ble_channel是0,但是对于ble channel其实就是advertising channel 37

 

sample rate: 采样率为10M

RF bandwidth:2M

RX Gain: -90

2. Simple Squelch


在 GNU Radio 中,Simple Squelch 是一个用于信号门限控制的模块。它的主要作用是通过设置一个阈值来控制信号的通过或抑制,当输入信号的幅度低于设定的阈值时,输出将被强制为零,从而消除低于此阈值的噪声和不需要的信号。

 

3. Frequency Xlating FIR Filter

在 GNU Radio 中,Frequency Xlating FIR Filter(频率平移 FIR 滤波器)是一个功能强大的模块,用于对输入信号进行频率平移(变频)和滤波。这个模块结合了频率转换和有限脉冲响应(FIR)滤波的功能,是信号处理中的一个重要工具。

4. GFSK demod

在 GNU Radio 中,GFSK(Gaussian Frequency Shift Keying)解调器是用于解调通过 GFSK 调制的信号的模块。GFSK 是一种常用的频率调制技术,尤其在低功耗和短距离无线通信中,如蓝牙和一些工业、科学和医疗(ISM)频段应用。

 

5. unpakced to packed

在GNU Radio中,“Unpacked to Packed”模块用于将解包的比特流转换为打包的字节流。这对于处理比特级数据(如BPSK调制解调器输出)非常有用,可以将数据压缩成字节形式以便后续处理或传输。

“Unpacked to Packed”模块将输入的比特流(每个比特作为一个字节存储,通常值为0或1)转换为输出的字节流(每个字节包含多个比特,具体数量由你设置)。此过程的反向操作是“Packed to Unpacked”模块。

6. ZMQ

在GNU Radio中,ZeroMQ(zmq)模块用于在不同的GNU Radio流图(flowgraphs)之间,或者在GNU Radio与其他外部应用之间进行高效的消息传递和数据交换。ZeroMQ是一个高性能的异步消息库,适用于构建可扩展的分布式或并行计算应用。

特点

  • 高效的消息传递库,适用于高吞吐量和低延迟的应用。
  • 支持多种通信模式(如pub/sub、push/pull),非常灵活。
  • 可跨进程和跨网络工作。

适用场景

  • 实时数据处理和高吞吐量场景。
  • 分布式系统或需要在多台计算机之间传输数据。
  • 需要高性能和低延迟通信的应用,如软件定义无线电(SDR)系统中的信号处理。

 

可以看到address是tcp://127.0.0.1:55555,也就是他会通过local host的tcp port 55555把数据丢出去,然后正式由我们的C语言程序接管!而C语言就是订阅这个,来进行收数据流。

四.C语言程序分析

1. 接收ZMP数据流

我们把代码都加上注释,方便大家查看

bt_ret_t bt_zmq_subscriber(void)
{
    // 创建ZMPcontext
    void *zmq_ctx_context = zmq_ctx_new();
    if (zmq_ctx_context == NULL) {
        ZMP_TRACE_ERROR("Failed to create ZeroMQ context");
        return BT_RET_DISALLOWED;
    }

    // 创建ZMP socket,订阅类型
    zmq_subscriber = zmq_socket(zmq_ctx_context, ZMQ_SUB); 
    if (zmq_subscriber == NULL) {
        ZMP_TRACE_ERROR("Failed to create ZeroMQ subscriber socket");
        zmq_ctx_destroy(zmq_ctx_context);
        zmq_ctx_context = NULL;
        return BT_RET_DISALLOWED;
    }

    // 连接ZMP,可以看到地址就是我们的grc文件中plutosdr的地址
    int rc = zmq_connect(zmq_subscriber, "tcp://127.0.0.1:55555"); 
    if (rc != 0)
    {
        ZMP_TRACE_ERROR("zmq_connect fail :0x%x", rc);
        return BT_RET_DISALLOWED;
    }

    // 订阅所有主题
    /* subscriber all topic */
    zmq_setsockopt(zmq_subscriber, ZMQ_SUBSCRIBE, "", 0);

    return BT_RET_SUCCESS;
}
// zmp monitor的线程,通过polling的方式来监听是否有数据可读,如果有数据,
// 那么交于data_ready_handler的回调函数来读取数据
static void* zmq_data_monitor_thread(void* arg) 
{
    zmp_data_ready_handler_t data_ready_handler = (zmp_data_ready_handler_t)arg;
    while(1)
	{
		zmq_pollitem_t items[] = {
        	{ zmq_subscriber, 0, ZMQ_POLLIN, 0 }
    	};

		zmq_poll(items, 1, -1);

		if (items[0].revents & ZMQ_POLLIN)
		{
			data_ready_handler();
		}
	}
    return NULL;
}

// 创建一个thread,这个现成用来处理监听zmp是否有数据
bt_ret_t bt_zmq_start_monitor_data(zmp_data_ready_handler_t handler)
{
	pthread_t start_monitor_thread_id;
	pthread_create(&start_monitor_thread_id, NULL, zmq_data_monitor_thread, handler);
	return BT_RET_SUCCESS;
}
uint16_t bt_zmq_read_data(uint8_t *data, uint16_t read_len)
{
	return zmq_recv(zmq_subscriber, data, read_len-1, 0);
}

void zmp_data_ready_handler(void)
{
	uint8_t buffer[256] = {0};

    // 读取ZMP数据
	uint16_t read_len = bt_zmq_read_data(buffer,sizeof(buffer));
	//bt_hex_dump(buffer,read_len);

	// 把读取到的ZMP数据加到ringbuffer中
	if(ringbuffer_space_left(&bt_gfsk_ring_buf) > read_len)
		ringbuffer_put(&bt_gfsk_ring_buf,buffer,read_len);
}

2. 蓝牙封包解析的状态机

typedef enum
{
    W4_LL_PREAMBLE,
    W4_LL_ACCESS_CODE,
	W4_LL_READ_HEADER,
	W4_LL_READ_PAYLOAD,
    W4_LL_CRC_CHECK,
    W4_DECRYPTION,
} ll_parse_packet_state_t;

整个状态机会在所有收到的蓝牙以及非蓝牙来识别出来是蓝牙数据,我们来看下蓝牙BLE LL层数据封包的格式:

LE devices shall use the packets as defined in the following sections. There are two basic formats: one for the LE Uncoded PHY and one for the LE Coded PHY

2.1 PACKET FORMAT FOR THE LE UNCODED PHYS

 

ⅰ. Preamble

The preamble is 1 octet when transmitting or receiving on the LE 1M PHY and 2 octets when transmitting or receiving on the LE 2M PHY

所有链路层数据包都有一个 8 位/16位前导码。 在接收机中使用前导码来执行频率同步,符号定时估计和自动增益控制(AGC)训练。

  • 广播信道是1M phy, 所以前导码是10101010b(0xAA)
  • 广播信道是2M phy, 所以前导码是1010101010101010b(0xAAAA)
  • 如果数据信道是1M phy,分组前导码是 10101010b(0xAA)或 01010101b(0x55),具体取决于接入地址的 LSB。 如果接入地址的 LSB 是 1,则前导应为 01010101b,否则前导应为 10101010b。
  • 如果数据信道是2M phy,分组前导码是 1010101010101010b(0xAAAA)或 0101010101010101b(0x5555),具体取决于接入地址的 LSB。 如果接入地址的 LSB 是 1,则前导应为 0101010101010101b,否则前导应为 1010101010101010b。

 

ⅱ. Access Address

由发起者生成,用来在两个设备之间识别一个LL层连接

  • 所有广播数据包的访问地址都是 0b10001110_10001001_10111110_11010110 (0x8E89BED6)
  • 所有连接数据包的访问地址都是随机值,并遵循一定规则,每次连接重新生成。
ⅲ. PDU

Protocol Data Unit,协议数据单元

PDU 有两种,广播信道传输的是广播 PDU,连接信道传输的是连接 PDU。

广播信道PDU的格式

 

数据信道的PDU的格式

 

ⅳ. CRC

每个 Link Layer 数据包的结尾都有 24 位的 CRC 校验数据,它通过 PDU 计算得出。

 

2.2 PACKET FORMAT FOR THE LE CODED PHY

 

Each packet consists of the Preamble, FEC block 1, and FEC block 2.

 

ⅰ. Preamble

The Preamble is not coded, The Preamble is 80 symbols in length and consists of 10 repetitions of the symbol pattern '00111100' (in transmission order).

ⅱ. Coding Indicator(CI)

 

熟悉了这些数据格式才能更好的做后面的解析!

3. 代码处理逻辑

有了上面的格式,我们解析起来就清晰了,下面我来整个用流程图介绍下我们对于蓝牙数据格式解析的流程,如下

4. 寻找前导码

// 定义两种前导码格式,对于广播封包因为access code是固定的
// 所以他的前导码一定是0xaa,但是对于数据封包要看access code,所以还是不同
#define LL_UNCODED_PREAMBLE_1 0xAA
#define LL_UNCODED_PREAMBLE_2 0x55

case W4_LL_PREAMBLE:
{
    uint8_t ll_byte_0;
    uint8_t ll_byte_1;
    // 从ringbuffer中取一个byte来判断前导码
    ringbuffer_get(&bt_gfsk_ring_buf,&ll_byte_0,1);
    switch(ll_byte_0)
    {
        case LL_UNCODED_PREAMBLE_1:
            // 如果前导码是0xaa,要继续取一个byte来判断是否是2M phy
            ringbuffer_get(&bt_gfsk_ring_buf,&ll_byte_1,1);
            switch(ll_byte_1)
            {
                case LL_UNCODED_PREAMBLE_1:
                    // 前导码是0xaa 0xaa,所以暂判定是2M phy
                    LL_PREPARSE_TRACE_DEBUG("2M phy data, preamble type is LL_UNCODED_PREAMBLE_1");
                    ll_set_phy(LL_UNCODED_PHY_2M);
                    break;
                case LL_UNCODED_PREAMBLE_2:
                case LL_CODED_PREAMBLE:
                default:
                    // 如果前导码第一个字节是0xaa, 但是后面不是0xaa,暂判断为1M phy
                    LL_PREPARSE_TRACE_DEBUG("1M phy data, preamble type is LL_UNCODED_PREAMBLE_1");
                    ll_set_phy(LL_UNCODED_PHY_1M);
                    /* store this byte to parse access code */
                    ll_parse_packet_cb.access_code[0] = ll_byte_1;
                    ll_parse_packet_cb.access_code_pos = 1;
                    break;
            }

            ll_set_preamble_type(LL_UNCODED_PREAMBLE_1);
            ll_set_parse_packet_state(W4_LL_ACCESS_CODE);

            break;
        case LL_UNCODED_PREAMBLE_2:
            // 如果前导码是0x55,要继续取一个byte来判断是否是2M phy
            ringbuffer_get(&bt_gfsk_ring_buf,&ll_byte_1,1);
            switch(ll_byte_1)
            {
                case LL_UNCODED_PREAMBLE_2:
                    // 前导码是0x55 0x55,所以暂判定是2M phy
                    LL_PREPARSE_TRACE_DEBUG("2M phy data, preamble type is LL_UNCODED_PREAMBLE_2");
                    ll_set_phy(LL_UNCODED_PHY_2M);
                    break;
                case LL_UNCODED_PREAMBLE_1:
                case LL_CODED_PREAMBLE:
                default:
                    // 如果前导码第一个字节是0x55, 但是后面不是0x55,暂判断为1M phy
                    LL_PREPARSE_TRACE_DEBUG("1M phy data, preamble type is LL_UNCODED_PREAMBLE_2");
                    ll_set_phy(LL_UNCODED_PHY_1M);
                    /* store this byte to parse access code */
                    ll_parse_packet_cb.access_code[0] = ll_byte_1;
                    ll_parse_packet_cb.access_code_pos = 1;
                    break;
            }

            // 状态机切换为wait for access code解析的阶段
            ll_set_preamble_type(LL_UNCODED_PREAMBLE_2);
            ll_set_parse_packet_state(W4_LL_ACCESS_CODE);
            break;
        case LL_CODED_PREAMBLE:
            // TODO: support codec phy
            LL_PREPARSE_TRACE_WARNING("Unsupport preamble ,maybe is codec phy");
            break;
        default:
            break;
    }
    break;
}

5. 解析Access code

case W4_LL_ACCESS_CODE:
{
    // 因为access code是4个byte
    // 这里处理是因为上次判断前导码判断是1M/2M phy的时候有可能多读取了1个byte,所以如果多读取了
    // 那么认定是已经读取了一个byte的access code,继续再读取3个byte
    // 如果没有多读取那么读取4个byte的access code
    if(ll_parse_packet_cb.access_code_pos == 0)
        ringbuffer_get(&bt_gfsk_ring_buf,ll_parse_packet_cb.access_code,LL_ACCESS_CODE_SIZE);
    else
        ringbuffer_get(&bt_gfsk_ring_buf,ll_parse_packet_cb.access_code + ll_parse_packet_cb.access_code_pos,
                    LL_ACCESS_CODE_SIZE - ll_parse_packet_cb.access_code_pos);
    
    uint8_t lsb_data = ll_parse_packet_cb.access_code[LL_ACCESS_CODE_SIZE-1];
    bool valid_access_code = false;

    /* 判断access code高位跟前导码是否匹配 */
    switch(ll_get_preamble_type())
    {
        case LL_UNCODED_PREAMBLE_1:
            if((lsb_data & 0x80) != 0)
                valid_access_code = true;
            break;
        case LL_UNCODED_PREAMBLE_2:
            if((lsb_data & 0x80) == 0)
                valid_access_code = true;
            break;
        case LL_CODED_PREAMBLE:
            // TODO: support codec phy
            break;
        default:
            break;
    }

    if(valid_access_code)
    {
        // 前导码跟access code匹配,状态机切换到读取pdu header
        ll_set_parse_packet_state(W4_LL_READ_HEADER);

        // 广播包的数据前导码是固定的,所以这里是判断是正常的广播包还是数据包
        if(ll_parse_packet_cb.access_code[0] == 0xD6 && ll_parse_packet_cb.access_code[1] == 0xBE && 
                ll_parse_packet_cb.access_code[2] == 0x89 && ll_parse_packet_cb.access_code[3] == 0x8e)
        {
            LL_PREPARSE_TRACE_DEBUG("Advertising packet type");
            ll_set_packet_type(LL_PACKET_TYPE_ADV);
        }
        else
        {
            LL_PREPARSE_TRACE_DEBUG("data packet type");
            ll_set_packet_type(LL_PACKET_TYPE_DATA);
        }
    }
    else
    {
        // 前导码跟access code不匹配,说明这个并不是蓝牙数据,状态机reset,回归到等到前导码
        LL_PREPARSE_TRACE_ERROR("preamble and access code is not match");
        ll_parse_packet_cb_reset();
    }
    break;
}

6. 读取PDU header

case W4_LL_READ_HEADER:
{
    ll_packet_type_t packet_type = ll_get_packet_type();
    if(packet_type == LL_PACKET_TYPE_ADV)
    {
        // 广播包的PDU header是2个字节,所以读取2个字节
        uint8_t adv_header[LL_ADV_PACKET_HEADER_SIZE];
        ringbuffer_get(&bt_gfsk_ring_buf,adv_header,LL_ADV_PACKET_HEADER_SIZE);
        memcpy(ll_parse_packet_cb.packet_payload,adv_header,LL_ADV_PACKET_HEADER_SIZE);
        // 进行去白处理
        ll_packet_data_dewhitening(adv_header,LL_ADV_PACKET_HEADER_SIZE,ll_parse_packet_cb.channel);
        ll_parse_packet_cb.adv_payload_len = adv_header[1];
        LL_PREPARSE_TRACE_DEBUG("Advertising packet payload len:%d",ll_parse_packet_cb.adv_payload_len);
        ll_set_parse_packet_state(W4_LL_READ_PAYLOAD);

    }
    else if(packet_type == LL_PACKET_TYPE_DATA)
    {
        // TODO : parse data packet type
        ll_parse_packet_cb_reset();
    }
    break;
}

里面有几个知识点需要额外说明下:

  • 广播包的PDU header构成
  • 去白处理的流程

a. 广播包的header格式

 

  • PDU Type(4 bits)

描述了PDU的类型。例如,可能表示该PDU是一个广播包、扫描请求、扫描响应等。

 

  • RFU(1 bit)

预留供将来使用(Reserved for Future Use)。目前该位通常被设置为0。

  • ChSel(1 bit)

通道选择位(Channel Selection)。指示是否使用特定的通道选择算法来选择下一个广告信道。

  • TxAdd(1 bit)

发送地址类型位(Transmit Address Type)。指示发送方的地址类型是公有地址还是随机地址。如果是公有

地址,该位为0;如果是随机地址,该位为1。

  • RxAdd(1 bit)

接收地址类型位(Receive Address Type)。指示接收方的地址类型是公有地址还是随机地址。如果是公有

地址,该位为0;如果是随机地址,该位为1。

  • Length(8 bits)

PDU数据字段的长度。表示紧随PDU头部的有效载荷数据的长度,以字节为单位。

b. 去白处理

数据白化/去白防止重复位(00000000或11111111)的长序列。它被应用于发射机的 CRC 之后的链路层的 PDUCRC 字段,注意PDU以及CRC都要去白,我最开始做的时候CRC一直校验不过,就是因为CRC部分我没有做去白处理

在蓝牙通信中,“去白处理”是用于恢复原始数据的过程,因为发送数据之前,蓝牙协议会进行“白化”处理,以保证数据的随机性,减少连续的0或1出现的概率,从而降低误码率。去白处理的流程如下:

  • 白化处理介绍
    • 白化处理是通过一个伪随机序列(由LFSR生成)对数据进行异或运算,改变数据的比特分布,使得数据在传输过程中更加随机。
    • LFSR(线性反馈移位寄存器,Linear Feedback Shift Register)是一个用于生成伪随机序列的移位寄存器。
  • 伪随机序列生成
    • 蓝牙使用一个基于LFSR的生成多项式:1 + x^4 + x^7,生成长度为7位的伪随机序列。
    • 初始状态是由蓝牙信道索引决定的,这意味着不同的信道会产生不同的伪随机序列。
  • 去白处理步骤
    • 初始化LFSR:根据接收到的数据包中的信道索引初始化LFSR。
    • 生成伪随机序列:使用LFSR生成与数据包长度相同的伪随机序列。
    • 数据恢复:将接收到的白化数据与伪随机序列进行逐位异或运算(XOR),得到原始数据。

具体的去白处理步骤如下:

  • 初始化LFSR
    • 设定LFSR的初始状态(通常由信道索引决定)。
    • 例如,信道索引为37,LFSR初始值可能是0100101。
  • 生成伪随机序列
    • 使用LFSR生成伪随机序列。例如,多项式1 + x^4 + x^7表示LFSR的第7位是前4位和第1位的异或。
    • 对LFSR进行移位和异或操作,生成与数据包长度相同的伪随机序列。
  • 数据恢复
    • 将接收到的白化数据与生成的伪随机序列进行逐位异或运算。
    • 例如,接收到的白化数据是1010110,伪随机序列是1100101,那么去白处理结果是:
      • 第1位:1 XOR 1 = 0
      • 第2位:0 XOR 1 = 1
      • 第3位:1 XOR 0 = 1
      • 依此类推,最终恢复的原始数据为0110011。

 

去白算法如下:

uint8_t bt_swap_bits(uint8_t value) {
    return (value * 0x0202020202ULL & 0x010884422010ULL) % 1023;
}

void ll_packet_data_dewhitening(uint8_t *data, int length, uint8_t channel) {
    uint8_t lfsr = bt_swap_bits(channel) | 2;

    for (int i = 0; i < length; i++) {
        uint8_t d = bt_swap_bits(data[i]);
        for (int j = 128; j >= 1; j >>= 1) {
            if (lfsr & 0x80) {
                lfsr ^= 0x11;
                d ^= j;
            }
            lfsr <<= 1;
        }
        data[i] = bt_swap_bits(d);
    }

}

7. 读取PDU payload

这部分没有什么好说的,就是根据header去白后读出来payload的长度,去接收就可以了

case W4_LL_READ_PAYLOAD:
{
    ll_packet_type_t packet_type = ll_get_packet_type();
    if(packet_type == LL_PACKET_TYPE_ADV)
    {
        if(ringbuffer_len(&bt_gfsk_ring_buf) < ll_parse_packet_cb.adv_payload_len)
        {
            LL_PREPARSE_TRACE_WARNING("bt_gfsk_ring_buf len: %d<%d\n",ringbuffer_len(&bt_gfsk_ring_buf),ll_parse_packet_cb.adv_payload_len);
            return;
        }
        // 读取PDU payload长度
        ringbuffer_get(&bt_gfsk_ring_buf,ll_parse_packet_cb.packet_payload + LL_ADV_PACKET_HEADER_SIZE,ll_parse_packet_cb.adv_payload_len);
        ll_parse_packet_cb.payload_len = LL_ADV_PACKET_HEADER_SIZE + ll_parse_packet_cb.adv_payload_len;

        // 状态切换到CRC check
        ll_set_parse_packet_state(W4_LL_CRC_CHECK);

    }
    else if(packet_type == LL_PACKET_TYPE_DATA)
    {
        // TODO : parse data packet type
        ll_parse_packet_cb_reset();
    }
    break;
}

8. CRC check

case W4_LL_CRC_CHECK:
{

    if(ringbuffer_len(&bt_gfsk_ring_buf) < LL_PACKET_CRC_SIZE)
    {
        LL_PREPARSE_TRACE_WARNING("bt_gfsk_ring_buf len: %d<%d\n",ringbuffer_len(&bt_gfsk_ring_buf),LL_PACKET_CRC_SIZE);
        return;
    }
    // 读取3个byte的CRC
    ringbuffer_get(&bt_gfsk_ring_buf,ll_parse_packet_cb.packet_payload + ll_parse_packet_cb.payload_len,LL_PACKET_CRC_SIZE);
    ll_parse_packet_cb.payload_len += LL_PACKET_CRC_SIZE;
    // 把PDU跟CRC整个数据包做去白处理
    ll_packet_data_dewhitening(ll_parse_packet_cb.packet_payload,ll_parse_packet_cb.payload_len,ll_parse_packet_cb.channel);
    
    uint8_t calc_crc[LL_PACKET_CRC_SIZE];
    // 计算CRC
    bt_crc(ll_parse_packet_cb.packet_payload,ll_parse_packet_cb.payload_len - LL_PACKET_CRC_SIZE,PDU_AC_CRC_IV,calc_crc);

    // CRC比较,如果正确就是完整的蓝牙数据封包,然后叫给数据封包解析部分以及injection to wireshark
    if(memcmp(calc_crc,ll_parse_packet_cb.packet_payload + ll_parse_packet_cb.payload_len - LL_PACKET_CRC_SIZE,LL_PACKET_CRC_SIZE) == 0)
    {
        LL_PREPARSE_TRACE_DEBUG("Complete and correct packet,crc check is pass");
        ll_parse_adv_pdu(ll_parse_packet_cb.packet_payload,ll_parse_packet_cb.payload_len);
        bt_injection_write(ll_parse_packet_cb.channel,ll_parse_packet_cb.access_code,
                                ll_parse_packet_cb.packet_payload,ll_parse_packet_cb.payload_len);
    }
    else
    {
        LL_PREPARSE_TRACE_ERROR("Uncomplete or uncorrent packet,crc check is fail");		
    }

    // TODO: What situations need to be decrypted
    ll_parse_packet_cb_reset();
    break;
}

里面有一个知识点,就是CRC部分

在蓝牙低功耗(BLE)通信中,循环冗余校验(CRC)用于确保数据的完整性和准确性。CRC是一个重要的误码检测机制,它通过生成一个校验码并附加到数据包后面,在接收端进行校验以检测数据传输过程中是否出现错误。

a. CRC的基本原理

CRC是一种基于多项式除法的校验方法。发送端在数据后面附加CRC校验码,接收端接收到数据后,使用相同的多项式进行计算,如果结果为零,说明数据无误,否则说明数据在传输过程中发生了错误。

b. 蓝牙CRC的生成和校验过程

蓝牙协议使用24位的CRC码,并且有一个特定的多项式和初始化值。

ⅰ. CRC多项式

蓝牙使用的CRC多项式是:x^24+x^10+x^9+x^6+x^4+x^3+x^1+x^0

对应的二进制表示为:0x100065B

ⅱ. CRC初始化值

CRC的初始化值取决于PDU(协议数据单元)的类型。对于不同类型的PDU,BLE规范定义了不同的初始化值。广告信道和数据信道使用不同的初始化值。

广告信道的CRC初始化值是固定的:0x555555。

数据通道的CRC初始化是连接封包中的CRC init field

ⅲ. CRC计算步骤
  1. 初始化CRC寄存器:根据PDU类型设置初始值。
  2. 将数据与多项式逐位异或:从数据的最高位开始,与多项式逐位异或运算。
  3. 移位:将寄存器内容左移一位,最高位补0。
  4. 重复上述步骤:直到所有数据处理完毕。
  5. 生成CRC码:处理完所有数据后,寄存器中的值就是生成的CRC码。

9. 解析LL广播PDU

/*
 * Advertising physical channel PDU header
 * 
 *  +--------+-----+-------+--------+--------+--------+
 *  | PDU    | RFU | ChSel | TxAdd  | RxAdd  | Length |
 *  | Type   | (1  | (1    | (1     | (1     | (8     |
 *  | (4     | bit)| bit)  | bit)   | bit)   | bits)  |
 *  +--------+-----+-------+--------+--------+--------+
 * 
 * PDU Type: Indicates the type of PDU (4 bits)
 * RFU: Reserved for Future Use (1 bit)
 * ChSel: Channel Selection (1 bit)
 * TxAdd: Transmitter Address Type (1 bit)
 * RxAdd: Receiver Address Type (1 bit)
 * Length: Length of the payload in bytes (8 bits)
 */

typedef struct {
    // 这个就是PDU header各个field占用的bit数
	uint8_t type:4;
	uint8_t rfu:1;
	uint8_t chan_sel:1;
	uint8_t tx_addr:1;
	uint8_t rx_addr:1;

	uint8_t len;

    // 特定的根据type来做一个联合体
    union {
		uint8_t   payload[0];
		ll_pdu_adv_adv_ind_t adv_ind;
		ll_pdu_adv_direct_ind_t direct_ind;
		ll_pdu_adv_scan_req_t scan_req;
		ll_pdu_adv_scan_rsp_t scan_rsp;
		ll_pdu_adv_connect_ind_t connect_ind;
        ll_pdu_adv_com_ext_adv_t adv_ext_ind;
	} BT_PACK_END;

} BT_PACK_END ll_pdu_adv_t;

具体的PDU type要做不同格式的解析,比如我们来具一个普通的adv ind的例子,可以看到spec的格式是这样

 

AdvA: 蓝牙的广播地址,这个要看header的TxADD,如果是0,那么就代表这个是public地址,如果是1,那么就是random地址

AdvData:广播数据,格式如下:

 

其中type就是在assigned number文档中

 

 

 

我们来看下wireshark中的数据

 

其他数据包我就不具体解析了,我们截图一下各个数据包的格式

 

 

 

 

 

 

 

 

10. injection data to wireshark

在介绍这样注入之前,首先我们先了解下pcap的格式,因为往wireshark写我是选用的pcap的格式

10.1 pcap的格式

a. Global Header

一个pcap文件由一个文件头组成,后续跟着0填充或者多个数据包,每个数据包也有一个头。

读写pcap文件不需要进行大小端转换,效率更高。

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 0 |                          Magic Number                         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4 |          Major Version        |         Minor Version         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8 |                           thiszone                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12 |                           sigfigs                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16 |                           snapLen                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20 |                           network                             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    typedef struct pcap_hdr_s {
            guint32 magic_number;   /* magic number */
            guint16 version_major;  /* major version number */
            guint16 version_minor;  /* minor version number */
            gint32  thiszone;       /* GMT to local correction */
            guint32 sigfigs;        /* accuracy of timestamps */
            guint32 snaplen;        /* max length of captured packets, in octets */
            guint32 network;        /* data link type */
    } pcap_hdr_t;

文件头的长度是24个字节,每个字段的意思是:

Magic Number (32 bits)

用来检测自身文件格式和字节序。写文件程序写入0xa1b2c3d4时,以本地的字节序格式写入这个字段。读文件程序可以读出0xa1b2c3d4(同样的字节序)或0xd4c3b2a1(相反的字节序)。如果读文件程序读出相反的0xd4c3b2a1值,它就知道后面的字段也需要反过来。对于纳秒级分辨率文件,写文件程序写入0xa1b23c4d,与原来不同两个低位字节内部交换高低4位,而读文件程序将会读出0xa1b23c4d(同样的字节序)或0x4d3cb2a1(相反的字节序)。

Major Version (16 bits)

无符号整数,指定当前格式的主版本号。当前版本号是2。如果后续规则做了修改,可能会变化这个版本号。不同版本号之间理论上可能无法兼容,解析文件的时候需要检查版本号进行确认。

Minor Version (16 bits)

无符号整数,指定当前格式的副版本号。当前版本号是4。与主版本号一样,不同版本号之间理论上可能无法兼容,解析的时候需要确认。

Thiszone (32 bits)

后面包头的时间戳的GMT(UTC)和本地时区之间的修正,以秒为单位。例如如果时间戳在GMT(UTC),thiszone就是0。如果时间戳在中欧时区(阿姆斯特丹,柏林,……),时间是GMT+1:00,thiszone必须是-3600。实际上,时间戳一直在GMT,所以thiszone一直是0

sigfigs (32 bits)

理论上,时间戳在捕获中的精确度;实际上,所有的工具设置这一字段为0

SnapLen (32 bits)

无符号整数,指定了一个pcap文件中所有数据包的最大字节数。一个数据包,超过这个数的部分将会被丢掉。这个值不能为0,如果没有设定最大限制,那么应该大于或者等于最大的数据包长度。

Network (32 bits)

链路层头类型,指定包开始的头的类型这可以是各种各样的类型,例如802.11,带有多种无线电信息的802.11,PPP,令牌环网,光纤分布式数据接口等等。可以通过以下链接查看type

Link-layer header types | TCPDUMP & LIBPCAP

我们做蓝牙的可能用到是这样两个network type

 

一个是BR/EDR BB(baseband)的,一个事BLE LL(link layer)的, 所以我们用SDR做传统蓝牙抓包器也是有可能的

我们随便打开一个pcap的包,用hexdump工具来看下

 

当然这个不是蓝牙的network哈,供大家参考格式的,大家可以自行去对下对齐

b. data header format
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 0 |                      Timestamp (Seconds)                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4 |            Timestamp (Microseconds or nanoseconds)            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8 |                    Captured Packet Length                     |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12 |                    Original Packet Length                     |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16 /                                                               /
   /                          Packet Data                          /
   /                  variable length, not padded                  /
   /                                                               /

数据包由一个16位长度的包头组成,后续跟着包的数据。每一个字段的详细解释:

Timestamp (Seconds) and Timestamp (Microseconds or nanoseconds)

时间戳,记录了秒和秒的小数部分——可能是毫秒,也可能是纳秒。

Captured Packet Length (32 bits)

无符号整数。数据的长度(字节数)。SnapLen限制的就是这个值。

Original Packet Length (32 bits)

无符号整数。数据包原始长度。因为SnapLen限制,数据包记录的时候有可能被截断,Captured Packet Length表示记录的字节数,这个表示网络包实际的长度。

Packet Data

原始网络数据,包括链接层的头(link-layer headers)。Captured Packet Length记录的就是这个字段的长度。链接层的头的内容根据LinkType字段定义。

数据包没有4字节对齐。如果不是4字节的倍数,也不会进行填充,所以不能保证数据包保存的位置在4字节边界上。

c. data payload format

这个就要根据不同的network type来具体查格式了,因为我们当前做的是BLE的,netwrok type是256,所以我们应该是查询LINKTYPE_BLUETOOTH_LE_LL_WITH_PHDR的数据格式

LINKTYPE_BLUETOOTH_LE_LL_WITH_PHDR | TCPDUMP & LIBPCAP

格式如下(我就直接贴英文了):

 

All multi-octet fields are expressed in little-endian format. Fields with a corresponding Flags bit are only considered valid when the bit is set.

The RF Channel field ranges 0 to 39.

The Signal Power and Noise Power fields are signed integers expressing values in dBm.

The Access Address Offenses field is an unsigned integer indicating the number of deviations from the valid access address that led to the packet capture.

The Reference Access Address field corresponds to the Access Address configured into the capture tool that led to the capture of this packet.

The Flags field represents packed bits defined as follows:

  • 0x0001 indicates the LE Packet is de-whitened
  • 0x0002 indicates the Signal Power field is valid
  • 0x0004 indicates the Noise Power field is valid
  • 0x0008 indicates the LE Packet is decrypted
  • 0x0010 indicates the Reference Access Address is valid and led to this packet being captured
  • 0x0020 indicates the Access Address Offenses field contains valid data
  • 0x0040 indicates the RF Channel field is subject to aliasing
  • 0x0380 is an integer bit field indicating the LE Packet PDU type
  • 0x0400 indicates the CRC portion of the LE Packet was checked
  • 0x0800 indicates the CRC portion of the LE Packet passed its check
  • 0x3000 is a PDU type dependent field
  • 0xC000 is an integer bit field indicating the LE PHY mode

The PDU types indicated by flag bit field 0x0380 are defined as follows:

  1. Advertising or Data (Unspecified Direction)
  2. Auxiliary Advertising
  3. Data, Master to Slave
  4. Data, Slave to Master
  5. Connected Isochronous, Master to Slave
  6. Connected Isochronous, Slave to Master
  7. Broadcast Isochronous
  8. Reserved for Future Use

For PDU types other than type 1 (auxiliary advertising), the PDU type dependent field (using flag bits 0x3000) indicates the checked status of the MIC portion of the decrypted packet:

  • 0x1000 indicates the MIC portion of the decrypted LE Packet was checked
  • 0x2000 indicates the MIC portion of the decrypted LE Packet passed its check

For PDU type 1 (auxiliary advertising), the PDU type dependent field (using flag bits 0x3000) is an integer bit field indicating the auxiliary advertisement type:

  1. AUX_ADV_IND
  2. AUX_CHAIN_IND
  3. AUX_SYNC_IND
  4. AUX_SCAN_RSP

The LE PHY modes indicated by flag bit field 0xC000 are defined as follows:

  1. LE 1M
  2. LE 2M
  3. LE Coded
  4. Reserved for Future Use

有了这个基础我们来看下代码

打开文件写入Gloal Header

bt_ret_t bt_injection_open(uint8_t *file_name)
{
	injection_fd = fopen((const char *)file_name, "wb");
    if (!injection_fd) {
        return BT_RET_INVALID_ARG;
    }

    // Write PCAP file header
    uint32_t magic = PCAP_MAGIC;
    uint16_t version_major = PCAP_MAJOR;
    uint16_t version_minor = PCAP_MINOR;
    int32_t thiszone = PCAP_ZONE;
    uint32_t sigfigs = PCAP_SIG;
    uint32_t snaplen = PCAP_SNAPLEN;
    uint32_t network = PCAP_BLE_LL_WITH_PHDR;

    // 这里就是写global header部分
    fwrite(&magic, sizeof(magic), 1, injection_fd);
    fwrite(&version_major, sizeof(version_major), 1, injection_fd);
    fwrite(&version_minor, sizeof(version_minor), 1, injection_fd);
    fwrite(&thiszone, sizeof(thiszone), 1, injection_fd);
    fwrite(&sigfigs, sizeof(sigfigs), 1, injection_fd);
    fwrite(&snaplen, sizeof(snaplen), 1, injection_fd);
    fwrite(&network, sizeof(network), 1, injection_fd);

    return BT_RET_SUCCESS;
}
bt_ret_t bt_injection_write(uint8_t ble_channel, uint8_t *ble_access_address, uint8_t* ble_data, size_t ble_data_len) 
{
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    uint32_t sec = ts.tv_sec;
    uint32_t usec = ts.tv_nsec / 1000;

    uint32_t ble_len = ble_data_len + 14;
    uint32_t incl_len = ble_len;
    uint32_t orig_len = ble_len;

    // Write PCAP packet header
    fwrite(&sec, sizeof(sec), 1, injection_fd);
    fwrite(&usec, sizeof(usec), 1, injection_fd);
    fwrite(&incl_len, sizeof(incl_len), 1, injection_fd);
    fwrite(&orig_len, sizeof(orig_len), 1, injection_fd);

    uint8_t channel = BLE_CHAN_MAP(ble_channel);
    uint8_t ff = 0xff;
    uint8_t zero = 0x00;
    uint16_t ble_flags = BLE_FLAGS;

    // 写data header
    fwrite(&channel, sizeof(channel), 1, injection_fd);
    fwrite(&ff, sizeof(ff), 1, injection_fd);
    fwrite(&ff, sizeof(ff), 1, injection_fd);
    fwrite(&zero, sizeof(zero), 1, injection_fd);
    fwrite(ble_access_address, 4, 1, injection_fd);
    fwrite(&ble_flags, sizeof(ble_flags), 1, injection_fd);
    fwrite(ble_access_address, 4, 1, injection_fd);

    // 写data payload部分,也就是LL部分的数据格式
    // Write BLE packet
    fwrite(ble_data, sizeof(uint8_t), ble_data_len, injection_fd);

    fflush(injection_fd);

	return BT_RET_SUCCESS;
}

五. 下一步规划

说实话,目标有点大,而且暂时没有方向,说说我的目标吧:

  • 做完善BLE air sniffer(GUN Radio方向)
  • 做一个简易的BR/EDR air sniffer(GUN Radio方向)
  • 做完善BLE air sniffer(FPGA方向)
  • 做一个简易的BR/EDR air sniffer(FPGA方向)
  • 用SDR模拟一个BLE controller
  • 用SDR模拟一个BR/EDR controller

顺序不分先后,不排除一个做不下去,但是总要有个目标,共勉!!!


登陆