首发于先知论坛

https://xz.aliyun.com/t/9185

前言

本文介绍逆向DA14531芯片的固件,并介绍一些辅助自动化分析的脚本的实现。DA14531是Dialog公司研制的蓝牙芯片,用户可以在官网下载SDK来开发蓝牙的上层业务。

相关代码

https://github.com/hac425xxx/BLE-DA145XX/
ps: 文件全部发出后公开github项目

SDK环境搭建和IDA加载固件

下载SDK后,进入其中一个示例项目

DA145xx_SDK\6.0.14.1114\projects\target_apps\ble_examples\ble_app_profile\Keil_5

使用Keil打开,然后选择 Project->Manage->Component,设置目标为DA14531.

然后编译之后就会下面的目录生成编译好的固件

DA145xx_SDK\6.0.14.1114\projects\target_apps\ble_examples\ble_app_profile\Keil_5\out_DA14531\Objects

其中比较关键的几个文件有

ble_app_profile_531.axf: 工程文件编译为ELF文件
ble_app_profile_531.bin: 可以直接刷入系统RAM的二进制
ble_app_profile_531.hex: hex文件格式,相比.bin文件该文件中带有刷入的地址

其中 .axf 其实就是ELF文件,里面有符号信息和加载基地址等,.bin文件可以直接写入到芯片的固定地址,查看芯片手册中的Memory Map部分

可以猜测.bin文件的基地址为0x7fc0000,这点也可以通过用IDA分析.axf 文件得到证明,下面为了简化分析流程,直接用IDA分析ble_app_profile_531.axf.

随便打开一些函数分析,发现有的函数会直接将某些没有加载到内存的地址作为函数进行调用,比如

int __fastcall gattc_write_req_ind_handler()
{
v6 = MEMORY[0x7F22448](param->handle, att_idx); // 将 0x7F22448 作为函数进行调用

查看芯片手册可以知道这块地址的描述为

Boot/BLE ROM
Contains Boot ROM code and BLE protocol related code.

可以知道这块内存里面应该有一些协议栈底层的代码,而且翻看SDK的代码发现很多系统的函数都没有源码,在编译出来后的二进制里面也没有对应函数的实现,猜测这部分函数的实现也在这个区域中。

最后让群友帮忙在开发板里面用jtag把这块内存dump了下来,然后咱们加载到IDA

File -> Load file -> Additional binary file

选择dump出来的二进制文件,然后设置加载地址为 0x7fc0000

加载完后,这些函数调用就可以正常识别了。

加载函数符号

对部分函数分析一段时间后,在搜索SDK的时候,忽然发现了一个比较神奇的文件

da14531_symbols.txt

文件的部分内容如下

0x07f2270b T custs1_set_ccc_value
0x07f22823 T gattc_cmp_evt_handler
0x07f22837 T custs1_val_set_req_handler
0x07f22857 T custs1_val_ntf_req_handler
0x07f228b3 T custs1_val_ind_req_handler
0x07f2290f T custs1_att_info_rsp_handler
0x07f2294b T gattc_read_req_ind_handler
0x07f22b57 T gattc_att_info_req_ind_handler
0x07f22b99 T custs1_value_req_rsp_handler

看起来好像是一个列是符号内存地址,第三列是符号名,后面去网上搜索这个文件的作用,发现是keil支持的一种输入文件,用于在链接的时候把符号引用替换为对应的内存地址地址,这样在固件运行时就可以正常调用函数,这样也可以让一部分代码不开源。

最后写了个idapython脚本加载这个符号文件

import idaapi
import idc fpath = "da14531_symbols.txt" def define_func(addr, name):
if addr & 1:
addr -= 1
idaapi.split_sreg_range(addr, idaapi.str2reg("T"), 1, idaapi.SR_user)
else:
idaapi.split_sreg_range(addr, idaapi.str2reg("T"), 0, idaapi.SR_user) if idaapi.create_insn(addr):
idc.add_func(addr)
idaapi.set_name(addr, name,idaapi.SN_FORCE) def define_data(addr, name):
idaapi.set_name(addr, name,idaapi.SN_FORCE) with open(fpath, "r") as fp:
for l in fp:
try:
addr, type, name = l.strip().split(" ")
if addr.startswith(";"):
continue
addr = int(addr, 16)
if type == "T":
define_func(addr, name)
else:
define_data(addr, name)
except:
pass
  1. 主要逻辑就是一行一行的处理文件,丢弃 ; 开头的行
  2. 然后根据第二列的值来进行对应的处理
  3. 如果是T表示这个符号是一个函数地址调用define_func处理,否则就当做变量符号调用define_data处理

主要提一下的就是在处理函数的时候的代码

def define_func(addr, name):
if addr & 1:
addr -= 1
idaapi.split_sreg_range(addr, idaapi.str2reg("T"), 1, idaapi.SR_user)
else:
idaapi.split_sreg_range(addr, idaapi.str2reg("T"), 0, idaapi.SR_user) if idaapi.create_insn(addr):
idc.add_func(addr)
idaapi.set_name(addr, name,idaapi.SN_FORCE)
  1. 首先需要根据地址的最低位是否为1来判断是否为thumb指令,然后根据情况设置idaapi.str2reg("T") 寄存器的值,IDA会根据这个寄存器的值来判断后面反汇编指令时采用的是thumb指令还是arm指令
  2. 然后调用idaapi.create_insnida从函数地址处开始进行反汇编并创建指令
  3. 指令创建成功之后就调用idc.add_func创建一个函数并使用idaapi.set_name设置函数的名称

执行脚本后很多的系统函数都识别出来了。

操作系统任务识别

创建任务API分析

分析嵌入式系统,首先需要将系统中存在的task/进程识别出来,经过一番的资料查找和SDK学习,可以知道DA145x芯片中的操作系统为Riviera Waves实时系统,该系统使用用ke_task_create来创建一个任务,从SDK中可以获取函数的定义如下

/**
****************************************************************************************
* @brief Create a task.
*
* @param[in] task_type Task type.
* @param[in] p_task_desc Pointer to task descriptor.
*
* @return Status
****************************************************************************************
*/
uint8_t ke_task_create(uint8_t task_type, struct ke_task_desc const * p_task_desc);

可以看到函数两个参数,第一个表示任务的类型,第二个参数为ke_task_desc结构体指针,表示任务的描述信息。

如果需要识别所有的任务,就可以通过查找 ke_task_create 的交叉引用,然后获取函数调用的两个参数即可拿到任务的类型和对应的任务描述符地址,然后再解析ke_task_desc就可以获取到每个任务的具体信息。

为了自动化的实现该目标,需要一个能在IDA中获取函数调用参数值的脚本,下面首先分析此脚本的实现

函数调用参数识别脚本

脚本地址

https://github.com/hac425xxx/BLE-DA145XX/blob/main/argument_tracker.py

参数追踪主要在ArgumentTracker类中实现,脚本实现了两种参数识别的方式分别为**基于汇编指令和模拟执行的函数调用参数识别 **, 基于IDA伪代码的函数调用参数识别

下面分别对其实现进行介绍

基于汇编指令和模拟执行的函数调用参数识别

这种方法由 reobjc 脚本演变而来

https://github.com/duo-labs/idapython/blob/master/reobjc.py

功能实现于track_register函数,主要思路是:

  1. 追踪存储函数参数的寄存器/内存地址的使用,做一个类似污点分析的功能,直到找到最初赋值的位置(比如ldr, mov)
  2. 然后从赋值点开始使用unicorn模拟执行,一直执行到函数调用的位置
  3. 然后从unicorn获取此时的对应寄存器和内存的值就可以得到具体的函数参数值

示例:

rom_ble:07F09CC0                 LDR             R1, =0x7F1F550
rom_ble:07F09CC2 MOVS R0, #0 ; task_type
rom_ble:07F09CC4 ADDS R1, #0x28 ; '(' ; p_task_desc
rom_ble:07F09CC6 BL ke_task_create

假设现在需要追踪参数二(即R1)的值,步骤如下:

  1. 首先从07F09CC6往前搜索R1的赋值点,发现 07F09CC4 这里是一条ADD指令不是最初赋值点,继续往上搜索
  2. 最后找到07F09CC0这里是LDR指令
  3. 然后使用unicron07F09CC0开始执行,一直执行到07F09CC6即可获取到在调用ke_task_create参数二(即R1)的值

下面看看关键的代码

        while curr_ea != idc.BADADDR:
mnem = idc.print_insn_mnem(curr_ea).lower()
dst = idc.print_operand(curr_ea, 0).lower()
src = idc.print_operand(curr_ea, 1).lower() if dst == target and self.is_set_argument_instr(mnem):
target = src
target_value = src
target_ea = curr_ea
if target.startswith("="):
break if dst == target == "r0" and self.is_call_instr(mnem):
previous_call = curr_ea
break
curr_ea = idc.prev_head(curr_ea-1, f_start)

主要就是不断调用idc.prev_head往前解析指令,然后对每条指令进行分析,实现一个反向的污点跟踪,直到找到目标的赋值点为止,找到赋值点后就使用Unicorn去模拟执行

基于IDA伪代码的函数调用参数识别

有的时候基于汇编指令向后做跟踪会丢失部分信息,示例:

if(cond1)
{
v4 = 0x101
} if(cod2)
{
v4 = 0x303;
} if(cod4)
{
v4 = 0x202;
} some_func(v4 - 1, 0)

对于这样的代码如果直接使用第一种方式实际只会得到 v4 = 0x201,会漏掉两种可能值。

为了缓解这种情况,实现了一个基于IDA伪代码的参数值识别脚本,功能实现于decompile_tracer函数。

其主要思路也是类似,首先定位需要获取的参数,然后提取参数字符串,分别跟踪参数的每个组成部分,找到赋值点,然后求出每个部分的值,从而得到参数的所有取值.

还是以上面的为例,假设需要获取参数1的值,处理流程如下

  1. 首先提取得到参数1的组成部分为 v4 和 1,1为常量,只需要追踪v4
  2. 然后往上追踪,找到v4的可能值为0x2020x3030x101
  3. 最后得到v4 - 1的所有可能值为0x2010x3020x100

任务自动化识别

首先找到ke_task_create的交叉引用,然后利用ArgumentTracker中基于汇编的参数获取模式来提取参数的值

def dump_ke_task_create():
retsult = {}
logger = CustomLogger()
m = CodeEmulator()
at = ArgumentTracker() ke_task_create_addr = idaapi.get_name_ea(idaapi.BADADDR, "ke_task_create")
for xref in XrefsTo(ke_task_create_addr, 0):
frm_func = idc.get_func_name(xref.frm)
ret = at.track_register(xref.frm, "r1")
if ret.has_key("target_ea"):
if m.emulate(ret['target_ea'], xref.frm):
reg = m.mu.reg_read(UC_ARM_REG_R1)
retsult[xref.frm] = reg

首先获取ke_task_create的地址,然后查找其交叉引用

  1. 对于每个交叉引用使用track_register来追踪r1寄存器(即参数二)
  2. ret['target_ea']表示赋值点,然后使用CodeEmulator从赋值点执行到函数调用的位置(xref.frm
  3. 执行成功后读取r1的值,即可得到任务描述符的地址

拿到任务描述符的地址后下面需要定义描述符的类型,首先看看ke_task_desc的定义

/// Task descriptor grouping all information required by the kernel for the scheduling.
struct ke_task_desc
{
/// Pointer to the state handler table (one element for each state).
const struct ke_state_handler* state_handler;
/// Pointer to the default state handler (element parsed after the current state).
const struct ke_state_handler* default_handler;
/// Pointer to the state table (one element for each instance).
ke_state_t* state;
/// Maximum number of states in the task.
uint16_t state_max;
/// Maximum index of supported instances of the task.
uint16_t idx_max;
};

这里主要关注ke_state_handler,该结构中有一个msg_table,里面是一些函数指针和其对应的消息id

/// Element of a message handler table.
struct ke_msg_handler
{
/// Id of the handled message.
ke_msg_id_t id;
/// Pointer to the handler function for the msgid above.
ke_msg_func_t func;
}; /// Element of a state handler table.
struct ke_state_handler
{
/// Pointer to the message handler table of this state.
const struct ke_msg_handler *msg_table;
/// Number of messages handled in this state.
uint16_t msg_cnt;
};

我们也就按照结构体定义使用相应的IDApython的接口即可(注意:使用idapython设置结构体前要确保对应的结构体已经导入到IDB中)


for k, v in retsult.items():
frm_func = idc.get_func_name(k)
task_desc_ea = v
task_desc_name = "{}_task_desc".format(frm_func.split("_init")[0])
define_ke_task_desc(task_desc_ea, task_desc_name) handler = idaapi.get_dword(task_desc_ea + 4)
define_ke_state_handler(handler)

识别消息和回调函数的交叉引用

Riviera Waves系统中任务之间使用消息来传递消息,中断处理程序做了简单处理后就会通过发送消息交给对应的消息处理函数进行处理,常用方式是使用ke_msg_alloc分配消息,然后使用ke_msg_send将消息发送出去。

ke_msg_alloc的定义如下

/**
****************************************************************************************
* @brief Allocate memory for a message
*
* @param[in] id Message identifier
* @param[in] dest_id Destination Task Identifier
* @param[in] src_id Source Task Identifier
* @param[in] param_len Size of the message parameters to be allocated
*
*/
void *ke_msg_alloc(ke_msg_id_t const id, ke_task_id_t const dest_id,
ke_task_id_t const src_id, uint16_t const param_len);

其中第一个参数为消息ID,在系统中有很多消息处理回调函数表,回调函数表大体结构都是由消息ID和函数指针组成,在处理消息发送出去后,系统会根据消息中的其他参数(比如dest_id)找到相应的回调函数表,然后根据消息ID去表中找到对应的回调函数,最后调用回调函数处理消息数据。

那我们就可以找到所有ke_msg_alloc的调用点,然后提取出id,就可以知道每个函数使用了哪些消息id,然后根据消息id去二进制里面搜索,找到消息处理函数,最后将两者建立交叉引用,这样在逆向分析的时候就很舒服了。

示例

rom_ble:07F17C4E                 LDR             R0, =0x805 ; id
rom_ble:07F17C50
rom_ble:07F17C50 loc_7F17C50 ; DATA XREF: sub_7F06B94↑r
rom_ble:07F17C50 ; sub_7F0CE30↑r ...
rom_ble:07F17C50 BL ke_msg_alloc

建立完交叉引用后在调用ke_msg_alloc的位置,可以看的其事件消息的处理函数可能为sub_7F06B94sub_7F0CE30

下面介绍根据消息ID搜索消息处理函数的实现

def search_msg_handler(msg_id):

    ret = []

    data = " ".join(re.findall(".{2}", struct.pack("H", msg_id).encode("hex")))
addr = 0x07F00000
find_addr = idc.find_binary(addr, SEARCH_DOWN, data) while find_addr != idaapi.BADADDR:
func_addr = idaapi.get_dword(find_addr + 4)
if is_func_ea(func_addr):
print " msg_id 0x{:X} @ 0x{:X}, handler: 0x{:X}".format(msg_id, find_addr, func_addr)
ret.append(func_addr) # custom_msg_handler
func_addr = idaapi.get_dword(find_addr + 2)
if is_func_ea(func_addr):
print " [custom_msg_handler] msg_id 0x{:X} @ 0x{:X}, handler: 0x{:X}".format(msg_id, find_addr, func_addr)
ret.append(func_addr) find_addr = idc.find_binary(find_addr + 1, SEARCH_DOWN, data) return ret

经过逆向分析,发现消息处理函数和消息id的关系主要有两种情况

  1. 消息id起始地址 + 2的位置是函数地址
  2. 消息id起始地址 + 4的位置是函数地址

两种情况分别对应custom_msg_handlerke_msg_handler两种定义消息回调函数的结构体


/// Custom message handlers
struct custom_msg_handler
{
ke_task_id_t task_id;
ke_msg_id_t id;
ke_msg_func_t func;
}; /// Element of a message handler table.
struct ke_msg_handler
{
ke_msg_id_t id;
ke_msg_func_t func;
};

脚本也是这样的逻辑,分别尝试这两个位置,如果是函数的话就认为是对应的回调函数,这样处理的坏处是没有考虑消息的其他参数,可能导致有的消息处理函数对于某些场景实际是调用不了的,但是还是会被我们的脚本建立交叉引用,所以只能说是可能的消息处理函数,不过这样也可以简化很多分析流程了。

最后使用IDA函数设置交叉引用即可

def add_ref(frm, to):
idaapi.add_dref(frm, to, idaapi.dr_R)
idaapi.add_dref(to, frm, idaapi.dr_R)

脚本使用方式

首先使用argument_tracker.py获取固件中每个函数的msg id的使用情况,然后将结果导出到文件中

https://github.com/hac425xxx/BLE-DA145XX/blob/main/argument_tracker.py#L610

然后使用search_msg_handler.py导入之前获取到的结果,并搜索消息ID对应的回调函数,最后为两者建立交叉引用。

https://github.com/hac425xxx/BLE-DA145XX/blob/main/search_msg_handler.py#L70

总结

本文介绍开始分析一个芯片的一些流程,介绍一些辅助人工的脚本的实现原理。

DA14531芯片固件逆向系列(1)-固件加载和逆向分析的更多相关文章

  1. 老调重弹:JDBC系列之<驱动加载原理全面解析) ----转

      最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读者 ...

  2. RX系列四 | RxAndroid | 加载图片 | 提交表单

    RX系列四 | RxAndroid | 加载图片 | 提交表单 说实话,学RxJava就是为了我们在Android中运用的更加顺手一点,也就是RxAndroid,我们还是先一步步来,学会怎么去用的比较 ...

  3. 【iOS系列】-UIWebView加载网页禁止左右滑动

    [iOS系列]-UIWebView加载网页禁止左右滑动 问题: 做项目时候,用UIWebView加载网页的时候,要求是和微信网页中打开的网页的效果一样,也即是只能上下滑动,不能左右滑动,也不能缩放. ...

  4. SpringBoot系列之配置文件加载位置

    SpringBoot系列之配置文件加载位置 SpringBoot启动会自动扫描如下位置的application.properties或者application.yml文件作为Springboot的默认 ...

  5. 动态加载框架DL分析

    动态加载框架DL分析 插件化开发,主要解决三个问题1.动态加载未安装的apk,dex,jar等文件2.activity生命周期的问题,还有service3.Android的资源调用的问题 简单说一下怎 ...

  6. insmod模块加载过程代码分析1【转】

    转自:http://blog.chinaunix.net/uid-27717694-id-3966290.html 一.概述模块是作为ELF对象文件存放在文件系统中的,并通过执行insmod程序链接到 ...

  7. Unity5系列资源管理AssetBundle——加载

    上次我们进行了AssetBundle打包,现在我们还把打包的资源加载到我们的游戏中.在加载之前,我们需要把打包好的Bundle包裹放到服务器上,如果没有,也可以使用XAMPP搭建本地服务器. 加载的A ...

  8. QT自定义控件系列(二) --- Loading加载动画控件

    本系列主要使用Qt painter来实现一些基础控件.主要是对平时自行编写的一些自定义控件的总结. 为了简洁.低耦合,我们尽量不使用图片,qrc,ui等文件,而只使用c++的.h和.cpp文件. 由于 ...

  9. 【光速使用开源框架系列】图片加载框架ImageLoader

    [关于本系列] 最近看了不少开源框架,网上的资料也非常多,但是我认为了解一个框架最好的方法就是实际使用.本系列博文就是带领大家快速的上手一些常用的开源框架,体会到其作用. 由于作者水平有限,本系列只会 ...

  10. cesium 入门开发系列矢量瓦片加载展示(附源码下载)

    前言 cesium 入门开发系列环境知识点了解:cesium api文档介绍,详细介绍 cesium 每个类的函数以及属性等等cesium 在线例子 内容概览 cesium 实现矢量瓦片加载效果 源代 ...

随机推荐

  1. 音视频入门-4-ffmpeg命令快速体验音视频开发/ ffmpeg编译过程经历的99八十一难

    <1>我的实验所使用的视频文件告知读者 1. 这是我在ubuntu环境上实验使用的视频文件, 我在windows上查看了详细信息,然后拖进ubuntu内,重命名为video-test.mp ...

  2. seaborn.lmplot详解

    官方文档 首先我们要知道,lmplot是用来绘制回归图的. 让我们来看看他的API: seaborn.lmplot(x, y, data, hue=None, col=None, row=None,  ...

  3. 干货必收藏!墨天轮最受DBA欢迎的250份学习文档合集

    作为一个DBA,必须要精通SQL命令.各种数据库架构.数据库管理和维护.数据库调优,必要的时候,还需要为开发人员搭建一个健壮.结构良好.性能稳定的数据库环境. 技术也是不断进步的,社会的发展要求DBA ...

  4. netCore 封装一个检验邮箱的类

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  5. 复用对评论和对文章回复的弹层 popup- vant2

    基本样式: ps:当message 即输入的内容的长度为 0 的时候,按钮禁止使用 : <template> <div class="comment-post"& ...

  6. 77.const声明对象修改对象里面的值会触发报错吗

    不会,因为对象是复杂类型数据 :对象的地址保存在栈内存中,对象的数据保存在堆内存中 : 只要对象的地址不发生改变,无论堆内存的对象数据如何改变,对象的值就不会改变 :

  7. MYSQL存储过程-练习2 while 循环

    MYSQL存储过程-练习2 while 循环 1 #WHILE循环 2 DELIMITER $$ 3 4 CREATE PROCEDURE `sp_while`() 5 BEGIN 6 DECLARE ...

  8. python中的内置函数zip函数

    关于zip()函数,有几点要讲的. 首先,官方文档中,它是这样描述的: Make an iterator that aggregates elements from each of the itera ...

  9. (超全)Python汇总篇,200+Python标准库介绍

    关于Python标准库 众所周知,Python是一个依赖强大的组件库完成对应功能的语言,为了便捷实现各项功能,前辈大牛们打造了多种多样的工具库公开提供给大众使用,而越来越多的库已经因为使用的广泛和普遍 ...

  10. Protues中51单片机按键无法复位(已解决)

    前言 昨晚用 Protues 搭建了 51 的最小系统电路,在实物中好用的复位电路,到仿真里不能正常复位了. 51 单片机是高电平复位,所以在运行时 RST 引脚应该是低电平,但在仿真中 RST 引脚 ...