炫技来了!使用SDR设备成功抓到蓝牙air packet, 并且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

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

另外我还开设了蓝牙Host的视频教程,也欢迎大家按需采购,做蓝牙教程咱是专业的!!

点击我购买蓝牙视频教程

点击我购买蓝牙开发板

一. 硬件环境

我是用的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)
{// 创建ZMPcontextvoid *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 0x55case 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 phyringbuffer_get(&bt_gfsk_ring_buf,&ll_byte_1,1);switch(ll_byte_1){case LL_UNCODED_PREAMBLE_1:// 前导码是0xaa 0xaa,所以暂判定是2M phyLL_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 phyLL_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 phyringbuffer_get(&bt_gfsk_ring_buf,&ll_byte_1,1);switch(ll_byte_1){case LL_UNCODED_PREAMBLE_2:// 前导码是0x55 0x55,所以暂判定是2M phyLL_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 phyLL_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 phyLL_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 codeif(ll_parse_packet_cb.access_code_pos == 0)ringbuffer_get(&bt_gfsk_ring_buf,ll_parse_packet_cb.access_code,LL_ACCESS_CODE_SIZE);elseringbuffer_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 phybreak;default:break;}if(valid_access_code){// 前导码跟access code匹配,状态机切换到读取pdu headerll_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 typell_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 checkll_set_parse_packet_state(W4_LL_CRC_CHECK);}else if(packet_type == LL_PACKET_TYPE_DATA){// TODO : parse data packet typell_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的CRCringbuffer_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];// 计算CRCbt_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 wiresharkif(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 decryptedll_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 headeruint32_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 headerfwrite(&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 headerfwrite(&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 packetfwrite(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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/24632.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言scanf( ) 函数、fprintf( ) 函数与 scanf( ) 函数和printf( ) 函数有什么不同?

一、问题 fscanf( ) 函数、fprintf( ) 函数与 printf( ) 函数、scanf( ) 函数的作⽤相似&#xff0c;都是格式化读写函 数&#xff0c;那么这两个读写函数有什么不同呢&#xff1f; 二、解答 两者的区别就在于前⾯的字符“f”&#xff0c;即 fscanfQ函数和 fprintfD函数的读写…

【Java基础】OkHttp 超时设置详解

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

AddressSanitizer理论及实践:heap-use-after-free、free on not malloc()-ed address

AddressSanity&#xff1a;A Fast Address Sanity Checker 摘要 对于C和C 等编程语言&#xff0c;包括缓冲区溢出和堆内存的释放后重用等内存访问错误仍然是一个严重的问题。存在许多内存错误检测器&#xff0c;但大多数检测器要么运行缓慢&#xff0c;要么检测到的错误类型有…

Java基础——数组Array

系列文章目录 文章目录 系列文章目录前言一、数组基本概念二、一维数组三、数组的模型四、数组对象的创建五、元素为引用数据类型的数组 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网…

leetcode 所有可能的路径(图的遍历)

leetcode 链接&#xff1a; 所有可能的路径 1 图的基本概念 1.1 有向图和无向图 左边是有向图&#xff0c;右边是无向图。对于无向图来说&#xff0c;图中的边没有方向&#xff0c;两个节点之间只可能存在一条边&#xff0c;比如 0 和 1 之间的边&#xff0c;因为是无向图&am…

【Vue】——组件的注册与引用

&#x1f4bb;博主现有专栏&#xff1a; C51单片机&#xff08;STC89C516&#xff09;&#xff0c;c语言&#xff0c;c&#xff0c;离散数学&#xff0c;算法设计与分析&#xff0c;数据结构&#xff0c;Python&#xff0c;Java基础&#xff0c;MySQL&#xff0c;linux&#xf…

Go微服务: 关于消息队列的选择和分类以及使用场景

消息队列概述 在分布式系统和微服务架构中&#xff0c;消息队列&#xff08;Message Queue&#xff09;是一个核心组件&#xff0c;用于在不同的应用程序或服务之间异步传递消息在 Go 语言中&#xff0c;有多种实现消息队列的方式&#xff0c;包括使用开源的消息队列服务&…

redis学习路线

待更新… 一、nosql讲解 1. 为什么要用nosql&#xff1f; 用户的个人信息&#xff0c;社交网络&#xff0c;地理位置&#xff0c;自己产生的数据&#xff0c;日志等等爆发式增长&#xff01;传统的关系型数据库已无法满足这些数据处理的要求&#xff0c;这时我们就需要使用N…

零基础入门学用Arduino 第一部分(三)

重要的内容写在前面&#xff1a; 该系列是以up主太极创客的零基础入门学用Arduino教程为基础制作的学习笔记。个人把这个教程学完之后&#xff0c;整体感觉是很好的&#xff0c;如果有条件的可以先学习一些相关课程&#xff0c;学起来会更加轻松&#xff0c;相关课程有数字电路…

KT1404A语音芯片USB连电脑,win7正常识别WIN10无法识别USB设备

一、简介 KT1404A语音芯片画的板子&#xff0c;USB连接电脑&#xff0c;win7可以正常识别到U盘&#xff0c;WIN10提示无法识别USB设备&#xff08;获取设备描述符失败&#xff09;&#xff0c;这是什么问题 问题 首先&#xff0c;这款芯片已经出货非常非常多了&#xff0c;所…

【Java】Java18的新特性

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

【Docker】上海交通大学开源镜像站服务变更:Docker 用户需迅速行动

近日&#xff0c;上海交通大学开源镜像站宣布了一个重大变更&#xff0c;对国内Docker用户来说&#xff0c;这一消息无疑具有紧迫性。 镜像站服务的变更 上海交通大学开源镜像站一直是国内Docker用户的重要资源&#xff0c;它提供了快速下载DockerHub仓库镜像的服务。然而&a…

react学习-高阶组件

1.简介 react高阶组件是一个函数&#xff0c;接收一个组件作为参数&#xff0c;返回一个新的组件&#xff0c;可以用来进行组件封装&#xff0c;将一些公共逻辑提取到高阶组件内部。 2.基本实现 以下案例为利用高阶组件来增强props import React, { Component } from "re…

浙江大学蒋明凯研究员《Nature》正刊最新成果!揭示生态系统磷循环响应大气二氧化碳浓度升高关键机制

随着大气二氧化碳浓度的升高&#xff0c;陆地生态系统固存额外碳汇的能力取决于土壤养分的可利用性。前期的研究证据表明&#xff0c;在土壤低磷环境下&#xff0c;大气二氧化碳浓度的升高可以提升成熟森林的光合速率&#xff0c;但是没有产生额外生物量固碳。热带和亚热带森林…

国产Sora免费体验-快手旗下可灵大模型发布

自从OpenAI公布了Sora后&#xff0c;震爆了全世界&#xff0c;但由于其技术的不成熟和应用的局限性&#xff0c;未能大规模推广&#xff0c;只有零零散散的几个公布出来的一些视频。昨日&#xff0c;快手成立13周年&#xff0c;可灵&#xff08;Kling&#xff09;大模型发布&am…

11-Linux文件系统与日志分析

11.1深入理解Linux文件系统 在处理Liunx系统出现故障时&#xff0c;故障的症状是最易发现。数学LInux系统中常见的日志文件&#xff0c;可以帮助管理员快速定位故障点&#xff0c;并及时解决各种系统问题。 11.1.1 inode与block详解 文件系统通常会将这两部分内容分别存放在…

常见八大排序(纯C语言版)

目录 基本排序 一.冒泡排序 二.选择排序 三.插入排序 进阶排序&#xff08;递归实现&#xff09; 一.快排hoare排序 1.单趟排序 快排步凑 快排的优化 &#xff08;1&#xff09;三数取中 &#xff08;2&#xff09;小区间优化 二.前后指针法(递归实现) 三.快排的非…

机器学习与数据挖掘知识点总结(一)

简介&#xff1a;随着人工智能&#xff08;AI&#xff09;蓬勃发展&#xff0c;也有越来越多的人涌入到这一行业。下面简单介绍一下机器学习的各大领域&#xff0c;机器学习包含深度学习以及强化学习&#xff0c;在本节的机器学习中主要阐述一下机器学习的线性回归逻辑回归&…

Python | Leetcode Python题解之第138题随机链表的复制

题目&#xff1a; 题解&#xff1a; class Solution:def copyRandomList(self, head: Optional[Node]) -> Optional[Node]:allNode[] # 用一个数组存储所有结点cur1headwhile cur1:allNode.append(cur1)cur1cur1.nextnlen(allNode)allRandom[-1]*n # 用一个数组存储所有节点…

超详解——识别None——小白篇

目录 1. 内建类型的布尔值 2. 对象身份的比较 3. 对象类型比较 4. 类型工厂函数 5. Python不支持的类型 总结&#xff1a; 1. 内建类型的布尔值 在Python中&#xff0c;布尔值的计算遵循如下规则&#xff1a; None、False、空序列&#xff08;如空列表 []&#xff0c;空…