文件格式的處理

010editor打開libDexHelper.so。

一眼假的section。

操作系統加載并不關心section,先直接刪除掉,后面修復dynamic后再重新生成一份。

一眼假的dynamic。

dynamic除了vaddr,其他成員都是可以偽造欺騙的,因為操作系統也只關心dynamic的vaddr。

通過分析PT_LOAD的信息,即RVAFA的轉換,對dynamic的file_offset進行簡單修復。

跑工具重新生成了一份section后,ida已經可以正常的解析so了。

so解密后dump


簡單看了一下init_arraryJNI_OnLoad方法,都是被加密的。

猜測解密方法在.init方法中。

暫時不去關心解密算法的實現,等.init方法運行結束后,直接dump內存,可以看到數據已經解密完成。

dump內存可以寫ida腳本,也可以直接使用dd來dump(注意先要使用ida把app掛起,殼對/proc/self/mem的讀寫做了監控)。

混淆的簡單處理

ida識別寄存器跳轉失敗。


類似于這樣:

實際是ida對switch-case的識別失敗。

解決辦法就是幫助ida正確識別switch-case,具體可以參考這篇文章

正確修復后,ida也就能正常反編譯了。

平坦化的處理


代碼的正常流程被switch-case打亂了。

簡單分析了一下匯編的代碼結構,可以發現直接跳轉到分發塊的case塊,會直接寫死下一個要執行的case塊。

于是可以通過計算獲取到目標case塊的地址,將case塊跳回分發塊,改成直接跳轉到對應的case塊的地址。

對于跳回def塊的case塊,則會通過條件執行來決定下一個要執行的case塊。

同樣可以計算出不同條件所要跳轉的case塊,將對應的跳轉改為條件跳轉。

腳本還有待完善,有一些跳轉的修改可能需要手動去修改。

import idautils
import idc 
import idaapi
from keystone import *
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
# 跳轉表的信息,根據switch-case表修改
jump_table    = 0x1154B4
element_sz    = 2
element_base  = 0x3B348
element_shift = 2
# def塊 和 分發塊,根據switch-case表修改
def_block  = 0x3B330
jump_block = 0x3B338
def_init_block = -1
# input reg switch,根據switch-case表修改
reg_base = 129
reg_switch = reg_base + 0
# 當前處理的函數,根據switch-case表修改
func_addr = 0x38330 
f_blocks = idaapi.FlowChart(idaapi.get_func(func_addr), flags=idaapi.FC_PREDS)
def get_code_refs_to_list(ea):
    result = list(idautils.CodeRefsTo(ea, True))
    return result
def get_block(addr, f_blocks):
    for block in f_blocks:
        if block.start_ea <= addr and addr <= block.end_ea - 4:
            return block 
    return None
def get_next_case(start_ea, end_ea):
    next_case = -1
    ea = start_ea
    while ea < end_ea:
        mnem = idc.ida_ua.ua_mnem(ea)
        if mnem == 'MOV':
            op1 = idc.get_operand_value(ea, 0)
            op2 = idc.get_operand_value(ea, 1)
            op2_type = idc.get_operand_type(ea, 1)
            if op1 == reg_switch and op2_type == idc.o_imm:
                next_case = op2
        ea = ea + 4
    return next_case
def get_cond(ea):
    cond = None
    disasm = idc.GetDisasm(ea)
    if disasm.find('LT') != -1:
        cond = 'blt'
    elif disasm.find('EQ') != -1:
        cond = 'beq'
    elif disasm.find('CC') != -1:
        cond = 'bcc'
    elif disasm.find('GT') != -1:
        cond = 'bgt'
    elif disasm.find('NE') != -1:
        cond = 'bne'  
    elif disasm.find('GE') != -1:
        cond = 'bge'  
    elif disasm.find('HI') != -1:
        cond = 'bhi'  
    elif disasm.find('LE') != -1:
        cond = 'ble'
    else:
        print('unknow cond:0x%x' % ea)
    return cond
def get_cond_next_case(start_ea, end_ea):
    cond_case = -1
    uncond_case = -1
    cond_reg = idc.get_operand_value(end_ea - 8, 1)
    uncond_reg = idc.get_operand_value(end_ea - 8, 2)
    ea = start_ea
    while ea < end_ea:
        mnem = idc.ida_ua.ua_mnem(ea)
        if mnem == 'MOV':
            op1 = idc.get_operand_value(ea, 0)
            op2 = idc.get_operand_value(ea, 1)
            op2_type = idc.get_operand_type(ea, 1)
            if op1 == cond_reg and op2_type == idc.o_imm:
                cond_case = op2
            if op1 == uncond_reg and op2_type == idc.o_imm:
                uncond_case = op2
        ea = ea + 4
    
    # 部分case值的初始化,在init塊中,不在對應的case塊
    if cond_case == -1 or uncond_case == -1:
        #print('def_init_block 0x%x' % def_init_block)
        block = get_block(def_init_block, f_blocks)
        ea = block.start_ea
        end_ea = block.end_ea
        cond_flag = False
        uncond_flag = False
        if cond_case == -1:
            cond_flag = True
        if uncond_case == -1:
            uncond_flag = True
        while ea < end_ea:
            mnem = idc.ida_ua.ua_mnem(ea)
            if mnem == 'MOV':
                op1 = idc.get_operand_value(ea, 0)
                op2 = idc.get_operand_value(ea, 1)
                op2_type = idc.get_operand_type(ea, 1)
                if cond_flag:
                    if op1 == cond_reg and op2_type == idc.o_imm:
                        cond_case = op2
                if uncond_flag:
                    if op1 == uncond_reg and op2_type == idc.o_imm:
                        uncond_case = op2
            ea = ea + 4          
    if cond_reg == 160:
        cond_case = 0
    elif uncond_reg == 160:
        uncond_case = 0
    return cond_case, uncond_case
def do_patch(ea, opcode, src, dst):
    jump_offset = " ({:d})".format(dst - src)
    repair_opcode = opcode + jump_offset
    #print(repair_opcode)
    encoding, count = ks.asm(repair_opcode)
    idaapi.patch_byte(ea, encoding[0])
    idaapi.patch_byte(ea + 1, encoding[1])
    idaapi.patch_byte(ea + 2, encoding[2])
    idaapi.patch_byte(ea + 3, encoding[3])
jump_block_list = get_code_refs_to_list(jump_block)
jump_def_list = get_code_refs_to_list(def_block)
def hex_to_dec(hex_str):
    #print(hex_str)
    #print(hex_str[0])
    # 把16進制字符串轉成帶符號10進制
    if hex_str[0] in '0123456789':
        dec_data = int(hex_str, 16)
    else:
        # 負數算法
        width = 32  # 16進制數所占位數
        d = 'FFFF' + hex_str
        dec_data = int(d, 16)
        if dec_data > 2 ** (width - 1) - 1:
            dec_data = 2 ** width - dec_data
            dec_data = 0 - dec_data
    return dec_data
def do_B_block(addr, cond):
    block = get_block(addr, f_blocks)
    if block is None:
        return
    next_case = get_next_case(block.start_ea, block.end_ea)
    if next_case == -1:
        return
    
    if element_sz == 1:
        case_data = idc.get_wide_byte(jump_table + next_case)
        if case_data > 0x7f:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        jmp_addr = jmp_off + element_base
    elif element_sz == 2:
        case_data = idc.get_wide_word(jump_table + next_case * 2)
        if case_data > 0x7fff:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        jmp_addr = jmp_off + element_base
    print('jump_block_list->addr: 0x%x, next_case: %d, jmp_addr: 0x%x' % (addr, next_case, jmp_addr))
    
    if cond == 'cbnz':
        reg_cmp = idc.get_operand_value(addr - 4, 0)
        cond = "cbnz x{:d}, ".format(reg_cmp - reg_base)
    elif cond == 'cbz':
        reg_cmp = idc.get_operand_value(addr - 4, 0)
        cond = "cbz x{:d}, ".format(reg_cmp - reg_base)
    do_patch(addr, cond, addr, jmp_addr)
# 處理 jump_block
for addr in jump_block_list:
    mnem = idc.ida_ua.ua_mnem(addr)
    if mnem == 'B':
        do_B_block(addr, 'b')
    elif mnem == 'TBZ':
        do_B_block(addr, 'b')
    elif mnem == 'CBNZ':
        do_B_block(addr, 'cbnz')
    elif mnem == 'CBZ':
        do_B_block(addr, 'cbz')
    else:
        print('unknow jump_block:0x%x' % addr)
def do_cond_block(addr, ins):
    cond = get_cond(addr - 4)
    if cond is None:
        print('unkown cond 0x%x' % addr)
        return
    block = get_block(addr, f_blocks)
    if block is None:
        return
    
    cond_case, uncond_case = get_cond_next_case(block.start_ea, block.end_ea)
    if cond_case == -1 or uncond_case == -1:
        return
    
    if mnem == 'CSINC':
        uncond_case = uncond_case + 1
    cond_jmp_addr = -1
    uncond_jmp_addr = -1
    if element_sz == 1:
        case_data = idc.get_wide_byte(jump_table + cond_case)
        if case_data > 0x7f:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        cond_jmp_addr = jmp_off + element_base
        case_data = idc.get_wide_byte(jump_table + uncond_case)
        if case_data > 0x7f:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        uncond_jmp_addr = jmp_off + element_base
    elif element_sz == 2:
        case_data = idc.get_wide_word(jump_table + cond_case * 2)
        if case_data > 0x7fff:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        cond_jmp_addr = jmp_off + element_base
        case_data = idc.get_wide_word(jump_table + uncond_case * 2)
        if case_data > 0x7fff:
            case_data = hex_to_dec(hex(case_data)[2:])
        jmp_off = case_data * (2 * element_shift) 
        uncond_jmp_addr = jmp_off + element_base
    print('jump_def_list->addr: 0x%x, cond_case: %d, cond_jmp_addr: 0x%x, uncond_case: %d, uncond_jmp_addr: 0x%x' % 
        (addr, cond_case, cond_jmp_addr, uncond_case, uncond_jmp_addr))
    
    do_patch(addr - 4, cond, addr - 4, cond_jmp_addr)
    do_patch(addr, 'b', addr, uncond_jmp_addr)
# 處理 def_block
for addr in jump_def_list:
    #print('jump_def_list->addr: 0x%x' % addr)
    
    if addr + 4 == def_block:
        def_init_block = addr
        # print('def_init_block 0x%x' % def_init_block)
        continue
    
    mnem = idc.ida_ua.ua_mnem(addr)
    if mnem != 'B':
        continue
    
    mnem = idc.ida_ua.ua_mnem(addr - 4)
    if mnem == 'CSEL':
        do_cond_block(addr, 'CSEL')
    elif mnem == 'CSINC':
        do_cond_block(addr, 'CSINC')


JNI_OnLoad做了簡單的測試。

修改前:

修改后:

frida檢測分析


準備工作做完了,開始上frida。

意料之中,存在frida檢測。

簡單測試了一下:

◆單獨啟動了frida- server,不注入app進程,程序正常運行。

◆注入app進程,但不進行任何hook,程序閃退。

看來是對frida的基本特征做了檢測。

對openat進行hook。

/**
 * gum js 引擎的線程名字:Name: gum-js-loop
 * vala   引擎的線程名字:Name: gmain
 * dbus   線程名字:Name: gdbus
 * */


定位到檢測代碼的位置:

用的去特征的frida-server,不知道為啥gdbus的特征仍然存在。

gdbus字符串的比較在sub_9DE68進行,簡單的處理一下。

function pass_frida(){
    var module = Process.findModuleByName("libDexHelper.so")
    var base = module.base
    var sub_9DE68 = base.add(0x9DE68)
    Interceptor.attach(sub_9DE68, {
        onEnter:function(args){
        },
        onLeave:function(ret){
            if(ret != 0){
                ret.replace(0)
            }
        }
    })
}


除了通過hook svc調用來定位,還可以通過hookpthread_create來獲取創建的所有線程來定位檢測。

>>> hex(0x000000764F830C5C - 0x000000764F792000)
'0x9ec5c'
>>> hex(0x000000764F83C97C - 0x000000764F792000)
'0xaa97c'
>>> hex(0x000000764F82F73C - 0x000000764F792000)
'0x9d73c'
>>> hex(0x000000764F875FD0 - 0x000000764F792000)
'0xe3fd0'
>>> hex(0x000000764F82F030 - 0x000000764F792000)
'0x9d030'


此時frida已經可以使用了。

繼續測試,會發現對部分libc庫函數(比如open)進行hook時,一樣會被檢測到。

猜測是存在代碼的hash值校驗,或比較了內存與文件中的代碼是否一致。

ida調試檢測分析


逆向分析時,肯定是少不了動態調試的,且遇到反調試肯定也是不可避免的。

首先也是通過對openat進行hook來定位檢測。

定位到線程0xaa97c。

也是很常規的檢測方式。

簡單修改一下,JNI_OnLoad就可以正常調試了。

此時此刻,想要徹底攻克殼的保護,需要的就只剩下時間和耐心了。