DA14531芯片固件逆向系列(1)-固件加載和逆向分析


首發於先知論壇

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.

1612267798354.png

然后編譯之后就會下面的目錄生成編譯好的固件

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部分

1611668769467.png

可以猜測.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

1612269972622.png

加載完后,這些函數調用就可以正常識別了。

加載函數符號

對部分函數分析一段時間后,在搜索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

總結

本文介紹開始分析一個芯片的一些流程,介紹一些輔助人工的腳本的實現原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM