DA14531芯片固件逆向系列(4)- L2CAP及ATT层收包再分析
文章首发地址
https://xz.aliyun.com/t/9199
前言
上一篇文件分析了DA14531从收包中断开始一直到L2CAP层的数据包处理过程,最近又抽了一点时间将L2CAP层和ATT层收包的过程梳理了一遍,本文将结合BLE的协议规范和代码介绍ATT层的报文解析流程,并介绍一下分析过程中发现的一些漏洞。
回顾
从上文可知 hci_acl_data_rx_handler 用于处理L2CAP的报文,该函数首先会调用 l2cc_pdu_unpack 对L2CAP的头部字段进行简单的检查,其实就是检查Length和Channel ID是否合法

然后将L2CAP的报文拷贝到新分配的内存,最后根据数据包的类型通过消息机制将数据交给sub_7F135F6 或者 l2cc_pdu_recv_ind_handler处理,其中sub_7F135F6用于处理ATT报文。
ATT层报文解析
首先我们看一下BLE协议规范中对 ATT 层的报文格式的定义,下图是一个L2CAP的蓝牙包的示意图

蓝牙协议的数据包分了好几个部分,其中 ATT 层的报文就位于图中的Information payload部分,sub_7F135F6就是在解析Information payload。
我们看看函数定义
int sub_7F135F6(int id, l2cc_pdu_recv_ind *l2cc_pdu_recv, unsigned int dest_task)
其中l2cc_pdu_recv_ind是一个比较复杂的结构体,其使用了union来统一表示 L2CAP 里面的各种不同类型的Payload.
struct l2cc_pdu_recv_ind
{
uint8_t status;
// 表示数据的大小
uint16_t rem_len;
/// Offset
uint16_t offset;
/// 指向具体的数据
struct l2cc_pdu pdu;
};
l2cc_pdu 用于表示L2CAP层的报文格式
struct l2cc_pdu
{
/// L2Cap Payload Length
uint16_t payld_len;
/// L2Cap Channel ID
uint16_t chan_id;
/// Data PDU definition
union l2cc_pdu_data
{
/// L2Cap packet code.
uint8_t code;
struct l2cc_lecb_send_data_req send_lecb_data_req;
struct l2cc_reject reject;
struct l2cc_update_param_req update_req;
.............................
.............................
}
}
可以看到和L2CAP的报文格式完全匹配,开头4字节是 Length 和 Channel ID,其中 Length 在hci_acl_data_rx_handler函数中经过检查,表示后面Information payload部分的长度。
Information payload采用联合体的方式定义,根据不同的code来选择不同的结构体定义进行表示。
ATT PDU格式如下

由图可知,ATT PDU由一个字节的opcode和变长的数据组成。
下面以 l2cc_att_err_rsp 为例
/// Error response
struct l2cc_att_err_rsp
{
/// Error Response - 0x01
uint8_t code;
/// The request that generated this error response
uint8_t op_code;
/// The attribute handle that generated this error response
uint16_t handle;
///The reason why the request has generated an error response
uint8_t reason;
};
可以看到Error response 的 ATT PDU的组成为
- 1字节的code
- 1字节的op_code
- 2字节的handle
- 1字节的reason
然后去翻看BLE规范中的定义发现完全符合

经过简单的浏览发现SDK中对于每种PDU的格式定义要比BLE规范中的更详细。
下面从代码角度开始分析,关键代码如下
int sub_7F135F6(int id, l2cc_pdu_recv_ind *l2cc_pdu_recv, unsigned int dest_task)
{
ret = atts_l2cc_pdu_recv_handler(dest_task >> 8, l2cc_pdu_recv);
if ( ret == 255 )
{
ret = attc_l2cc_pdu_recv_handler(dest_task >> 8, l2cc_pdu_recv);
函数主要逻辑就是首先调用atts_l2cc_pdu_recv_handler尝试对数据进行解析,如果函数返回值为255就尝试调用attc_l2cc_pdu_recv_handler进行解析。
atts_l2cc_pdu_recv_handler 和 attc_l2cc_pdu_recv_handler 实际就是根据ATT PDU 的 opcode 字段在atts_handlers 和 attc_handlers两个结构体数组中搜索到opcode对应的回调函数,其结构体定义如下
struct att_handler_item
{
unsigned __int8 code; // 表示 ATT PDU 的 opcode
unsigned __int8 d[3];
dummy_func func; // 函数指针
};
其中func的第二个参数就指向了 ATT PDU 的开头,搜索 opcode 对应处理函数的代码如下
for ( i = 0; i < 0xE; i = (i + 1) )
{
if ( atts_handlers_0_0[i].code == code )
{
func = atts_handlers_0_0[i].func;
}
}
if ( func )
{
result = (func)(dest_id, &l2cc_pdu_recv->pdu.data);
atts_handlers 和 attc_handlers的定义如下

我们以attc_handlers的第一项为例介绍分析ATT PDU解析代码的流程,可以看的第一项的定义为
att_handler_item <3, 0, sub_7F0FC0C+1>
即这里处理的code为3,回调函数为sub_7F0FC0C,去BLE手册中查找发现code为3表示的是Exchange MTU Response类型的PDU,其定义如下

然后可以去SDK中找到该PDU的结构体定义
/// Exchange MTU Response
struct l2cc_att_mtu_rsp
{
/// Exchange MTU Response - 0x03
uint8_t code;
/// Server Rx MTU size
uint16_t mtu_size;
};
然后我们去看 sub_7F0FC0C 的实现
int sub_7F0FC0C(int dest_id, l2cc_att_mtu_rsp *payload)
{
mtu_size = gattm_get_max_mtu();
if ( mtu_size >= payload->mtu_size )
{
mtu_size = payload->mtu_size;
}
gattc_set_mtu(dest_id, mtu_size);
if ( gattc_get_operation(dest_id, 1) == 1 )
{
v5 = 0;
}
else
{
v5 = 65;
}
gattc_send_complete_evt(dest_id, 1, v5);
return 0;
}
可以看的主要就是从payload里面取出mtu_size,然后和当前的mtu进行比较,最后会设置新的mtu。我们可以采用这种方式去分析其他ATT PDU的处理逻辑。
漏洞挖掘
梳理清楚ATT PDU的处理流程后,我们就可以逐个地分析固件中对每个ATT PDU的代码实现,然后从中发现漏洞,为了效率我们可以去分析每种PDU的定义,然后可以优先去查看结构体定义中带有变长成员、以及Length、offset这类敏感词的PDU处理逻辑,因为漏洞主要就处在处理变长数据的地方,比如
/// Find Information Response
struct l2cc_att_find_info_rsp
{
/// Find Information Response - 0x05
uint8_t code;
/// The format of the information data.
uint8_t format;
/// Data length
uint16_t data_len;
///The information data whose format is determined by the Format field
uint8_t data[__ARRAY_EMPTY];
};
l2cc_att_rd_by_type_rsp 整数溢出导致堆溢出
sub_7F0FDB4函数用于处理 Read By Type Response 类型的PDU,关键代码如下
int __fastcall sub_7F0FDB4(int id, l2cc_att_rd_by_type_rsp *payload)
{
operation = gattc_get_operation(id, 1);
if ( operation != 10 )
{
gattc_send_complete_evt(id, 1, v5);
return v30;
}
if ( gattc_get_mtu(id) - 2 > payload->each_len )
{
msg = ke_msg_alloc(dword_7F102F0 + 4, v23, src_id, payload->each_len + 4);
*msg = (payload->data[1] << 8) | payload->data[0];
sub_len = (payload->each_len - 2);
*(msg + 4) = sub_len;
*(msg + 2) = 0;
qmemcpy((msg + 6), &payload->data[2], sub_len); // 整数溢出
ke_msg_send(msg);
}
}
如果operation为0,就会最后一个if分支,然后会判断 payload->each_len 小于 gattc_get_mtu(id) - 2 就会去根据 each_len 分配内存,然后对 each_len - 2 保存到sub_len,最后拷贝sub_len个字节的数据到新分配的内存msg里面。
如果 sub_len 为 1,msg可用内存空间就是 5, 然后 sub_len 会由于整数溢出导致值为 0xFFFF ,然后进行memcpy时就会导致堆溢出。
ATT PDU处理函数多处越界读
ATT PDU处理函数中,很多都没有考虑输入数据的长度,直接去访问,如果输入数据长度小于访问的大小的话就会导致越界读,本节就介绍几个典型的例子
比如sub_7F1015C
int __fastcall sub_7F1015C(int a1, l2cc_att_rd_rsp *payload)
{
v4 = gattc_get_operation(a1, 1);
if ( v4 == 4 )
{
v6 = gattc_get_operation_ptr(a1, 1);
v7 = co_list_pop_front((off_7F102F4[a1] + 12));
qmemcpy(&v7[5].next + 3, payload->value, 0x10u); // 直接拷贝, 越界读
这里直接从 payload->value部分拷贝0x10字节,没有检查payload的实际长度,当payload剩余长度比较小(小于0x10)就会导致越界读。
这个例子是没有校验的,下面看一个校验失败的
void sub_7F11E24(int id, l2cc_att_rd_mult_req *payload)
{
v2 = 0;
v4 = co_list_size((_DWORD **)off_7F11FAC[id] + 24);
p_payload_length = (int)(payload - 1); 获取 &l2cc_pdu->payld_len
if ( !*p_payload_length )
{
*p_payload_length = gattc_get_mtu(v19) - 1;
}
while ( payload->nb_handles > v4 && *p_payload_length )
{
if ( v2 )
{
goto LABEL_13;
}
data = (int)payload + 2 * v4;
v2 = sub_7F1126C(v19, 0, *(unsigned __int16 *)(data + 4), (int)v16);
}
函数首先根据 payload - 1 可以获取到 l2cc_pdu 的开头,然后去访问 l2cc_pdu 里面的 payld_len 就可以得到payload的长度(p_payload_length)。
struct l2cc_pdu
{
/// L2Cap Payload Length
uint16_t payld_len;
/// L2Cap Channel ID
uint16_t chan_id;
但是后续代码只检查 p_payload_length 的值是否为0,假设 p_payload_length 为 1,就会进入循环,然后根据 v4 计算 data 的地址,然后去访问,这时就会导致越界读。
其他
没事干在SDK里面瞎浏览的时候发现gattc_write_req_ind_handler这个函数,然后在google上浏览发现该函数应该是用于处理GATT协议栈的某种报文的,然后在SDK中搜索gattc_write_req_ind_handler的引用
const struct ke_msg_handler pasps_default_state[] =
{
{GATTC_WRITE_REQ_IND, (ke_msg_func_t) gattc_write_req_ind_handler},
};
可以看到gattc_write_req_ind_handler对应的消息ID为 GATTC_WRITE_REQ_IND(值为 0xC15),然后继续搜索GATTC_WRITE_REQ_IND看看是谁会发送该消息,不过在SDK源码中没有找到,应该是在ROM中的固件会发送该消息,我们可以使用 之前的脚本 把所有的msg id 使用情况导出到文件,然后在文件中搜索该消息ID, 0xC15,最后找到位于0x7f114dc的函数会调用发送该消息
void __fastcall caller_gattc_write_req_ind(int a1, const void *value, size_t length, uint16_t offset, uint16_t handle, char a6)
{
msg = ke_msg_alloc(0xc15, v8, (a1 << 8) + 8, length + 6);
msg->handle = handle;
v10 = msg;
msg->length = length;
msg->offset = offset;
qmemcpy(msg->value, value, length);
ke_msg_send(v10);
主要就是把入参的value、length和offset通过消息发送出去,其中 length表示 value的长度,跟踪该函数的交叉引用,可以发现其中一条路径是 0x52 类型的 ATT PDU的会调函数调用
sub_7F11F42
caller_gattc_write_req_ind
0x52的结构体定义如下
/// Write Command
struct l2cc_att_wr_cmd
{
/// Write Command - 0x52
uint8_t code;
/// The handle of the attribute to be written
uint16_t handle;
/// Value length
uint16_t value_len;
/// The value to be written to the attribute
uint8_t value[__ARRAY_EMPTY];
};
sub_7F11F42 函数关键代码
void __fastcall sub_7F11F42(int a1, l2cc_att_wr_cmd *payload)
{
if ( !get_elem_by_handle(a1, 2, payload->handle, elem)
&& !verify_value_len(elem, payload->value_len, 0, v4) )
{
caller_gattc_write_req_ind(a1, payload->value, payload->value_len, 0, payload->handle, 82);
}
首先根据handle找到对应的元素,然后检查value_len不能超过elem的最大长度,最后就会通过caller_gattc_write_req_ind把数据发送到gattc_write_req_ind_handler进行后续处理。
在SDK中搜索 gattc_write_req_ind_handler 可以发现该函数有多个实现,猜测是针对不同的场景,用户可以重写该函数实现特定的功能,使用 search_msg_handler.py 搜索 0xC15 消息回调函数,也可以在固件中找到一些实现,其中部分可以和SDK的源码对应。
通过分析这个流程,我们可以从 gattc_write_req_ind_handler 开始一些漏洞挖掘,不过由于这部分在SDK中有源码,所以审计起来会轻松不少。
参考链接
https://www.cnblogs.com/iini/p/8977806.html 详解BLE空口包格式—兼BLE Link layer协议解析
https://tw511.com/a/01/14340.html
DA14531芯片固件逆向系列(4)- L2CAP及ATT层收包再分析的更多相关文章
- [工控安全]西门子S7-400 PLC固件逆向分析(一)
不算前言的前言:拖了这么久,才发现这个专题没有想象中的简单,学习的路径大致是Step7->S7comm->MC7 code->firmware,我会用尽量简短的语言把前两部分讲清楚, ...
- iOS逆向系列-脱壳
概述 通过iOS逆向系列-逆向App中使用class-dump工具导出App的Mach-O文件所有头文件.Hopper工具分析App的Mach-O文件代码大概实现.但是这些前体是App的Mach-O没 ...
- iOS逆向系列-逆向APP思路
界面分析 通过Cycript.Reveal. 对于Reveal安装配置可参考配置iOS逆向系列-Reveal 通过Reveal找到内存中的UI对象 静态分析 开发者编写的所有代码最终编译链接到Mach ...
- java基础解析系列(十)---ArrayList和LinkedList源码及使用分析
java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...
- 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇
一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...
- 【MM系列】SAP MM模块-MIGO收货后自动打印收货单
公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[MM系列]SAP MM模块-MIGO收货后自动 ...
- Android逆向系列文章— Android基础逆向(6)
本文作者:HAI_ 0×00 前言 不知所以然,请看 Android逆向-Android基础逆向(1) Android逆向-Android基础逆向(2) Android逆向-Android基础逆向(2 ...
- 烧写uboot和openwrt固件ARxx系列
以AR9331为例. 1.用烧录器将uboot烧写到flash中 (AR9331_U-Boot_Oolite-v1-v20170713.bin) 2.登录:192.168.1.1网页烧写uboot ...
- iOS逆向系列-tweak补充
tweak加载资源 开发自己的deb插件需要加载自己的资源,比如图片资源.iOS中常用的两种加载图片资源的方式: + (nullable UIImage *)imageNamed:(NSString ...
- iOS逆向系列-动态调试
Xcode调试App原理 Mac安装了Xcode Xcode的安装包中包含了debugserver 可执行类型的Mach-O文件,iPhone第一次连接Xcode调试会将Xcode中的debugser ...
随机推荐
- manim边学边做--空心多边形
空心的多边形Cutout是一种比较特殊的多边形,主要用于解决与形状.大小.位置等相关的数学问题. Cutout多边形可以定义物体表面的空洞或凹陷部分,从而更准确地模拟现实世界中的复杂形状. 比如,在P ...
- 心得小结,关于注重加强MCU下调试能力的意识
这两个月没有怎么更新博文,最近换工作了,根据新工作安排,大半年内都做MCU开发(就不要叫单片机了,太老土了). 入职新工作了,需重构拳头产品的软件,所以每天加班加点. 单片机与linux应用开发,开发 ...
- 【ARMv8】异常级别的定义EL0、EL1、EL2、EL3
Exception levels ARMv8-A系列定义了一系列的异常等级,从EL0到EL3,下面具体说明其含义: ELn中,随着n的增加,软件的执行权限也相应的增加: EL0被称为无特权执行: EL ...
- iOS关于屏蔽暗黑模式小结
不想适配暗黑模式可以关闭暗黑模式:在xcode12之前的版本Info.plist文件中添加Key:User Interface Style,值类型设置为String,值为Light,就可以不管在什么模 ...
- js中数据的基本类型
有5种基本数据类型分类 : 1. 数字型 number 2. 字符型 string 3. 布尔型 boolean 4. undefined 未定义 就是声明了但是没有赋值 5. null 空指针 ...
- 40. diff 的新旧节点数组如何比较
根据唯一标识符key值,把新旧的节点比较,不同就更新到新节点,相同就复用就节点,然后生成新的 Vnode :
- [MySQL] MySQL数据库中唯一标识符(ID)的梳理总结
0 引言 mysql 数据库中有各类id,在近期初步研究 flink cdc .debezium.mysql server id.server uuid.gtid.sesion id/connecti ...
- onethink自带编辑器内容无法修改
楼主小白一个,之前有大神带着进入onethink框架 在一个编辑页里面放两个编辑框的时候,悲催了--- 一个用作文本编辑,一个用于多图上传 发现前面的文本编辑的内容无法实现,后来楼主想调试一下 之前提 ...
- .NET周刊【10月第4期 2024-10-27】
国内文章 C#实现信创国产Linux麦克风摄像头推流(源码,银河麒麟.统信UOS) https://www.cnblogs.com/shawshank/p/18494362 随着国际形势变化,软件信创 ...
- 怎样在Windows 环境下安装Git附详细步骤图
Git下载路径:https://git-scm.com/ [步骤] 在非C盘创建一个git_install作为git的安装目录,双击安装包,按下一步默认安装即可 (后面有时间来补充各个参数意思,目前按 ...