360 老版本加固分析 Android4.4 - libprotectClass.so

前言

样本包名:com.nfbazi.xuankong 

使用手机:红米 HM1s , Android 4.4 ,root 

工具:ida , 010Editor 

概要:该保护类型使用的方式是,把 dex 中的方法体抽取加密保护了,运行时释放抽取的代码,并把 dex 中的 class_data_off  的偏移修复到释放的代码内存地址上

分析流程

0x01 - 静态分析 (dex)

apk 拖入jeb 中,可以看到壳入口:com.qihoo.util.StubApplication

 进入壳 代码中,看到如下代码:

 0x02 - 静态分析 (so)

把 libprotectClass.so 拖入 ida 中,找不到 jni_onLoad 方法

 

 Program Segmentation 中也看不到 .init 和 .init_array 段信息 

0x03 - 动态调试 

从静态 so 分析中,我们知道 360 对 libprotectClass.so 做了保护,真正的 Jni_onLoad 方法会在 so 运行时进行修复。

老规矩,我们还是使用 linker + init_array 下断的方式,不知道的同学可以看我之前的文章:乐固 libshella-2.9.0.2.so 逆向记录_xmanstyle的博客-CSDN博客

 0x03-01 so 代码混淆

通过linker + init_array 下断后,进入libprotectClass.so 的第一条指令如下:

 如图,so 中加入了很多混淆指令,每个块中其实只有第一条是真实指令,其他都是无意义的干扰指令,为此我写了个 idapython 脚本,试图过掉该混淆,但该脚本似乎有问题,运行脚本之后,so 指令有些乱了。单步跟踪的话,很快还是可以进入真实代码函数块中。

尝试 patch 过混淆脚本如下:(仍有问题,仅作思路参考)

import idc 
import idautils
import ida_bytes

def patch_code():
    # PUSH  {R0} 
    pattern = '01 00 2D E9'
    addr = idc.get_inf_attr(idc.INF_MIN_EA)
    end = idc.get_inf_attr(idc.INF_MAX_EA)
    while addr < end:
        # 二进制查找 
        addr = idc.find_binary(addr, idc.SEARCH_DOWN|idc.SEARCH_NEXT, pattern)
        # 下面的 api 更新
        #addr = idc.ida_bytes.bin_search(addr, idc.SEARCH_DOWN, pattern)
        if addr != idc.BADADDR:
            addr_n1 = idc.next_head(addr)
            addr_n2 = idc.next_head(addr_n1)
            addr_n3 = idc.next_head(addr_n2)
            addr_n4 = idc.next_head(addr_n3)
            
            # 0x13134 PUSH            {R0}
            # 0x13138 ADRL            R0, loc_12F50
            # 0x13140 SUB             R0, R0, #4
            # 0x13144 BX              R0; loc_12F4C
            # 0x13148 DCD 0x6F7F8379
            # -------
            # 0x13154 PUSH            {R0}
            # 0x13158 ADR             R0, loc_1300C
            # 0x1315c ADD             R0, R0, #4
            # 0x13160 SUB             R0, R0, #4
            # 0x13164 BX              R0; loc_1300C
            print("%s %s" % (hex(addr), idc.generate_disasm_line(addr, 0)))
            print("%s %s" % (hex(addr_n1), idc.generate_disasm_line(addr_n1, 0)))
            print("%s %s" % (hex(addr_n2), idc.generate_disasm_line(addr_n2, 0)))
            print("%s %s" % (hex(addr_n3), idc.generate_disasm_line(addr_n3, 0)))
            print("%s %s" % (hex(addr_n4), idc.generate_disasm_line(addr_n4, 0)))
            
            # 判断是否应该patch 
            pop_r0_addr, bx_addr = should_patch(addr, addr_n1, addr_n2, addr_n3, addr_n4)
            if pop_r0_addr != -1:
                print("%s" % idc.generate_disasm_line(pop_r0_addr, 0))
                print("[should-patch]")
                
                # idc.patch_dword(0x00014230, 0xe320f000)
                patch_addr = bx_addr
                while(patch_addr >= addr):
                    idc.patch_dword(patch_addr, 0xe320f000)
                    patch_addr = patch_addr - 4
                    if (patch_addr < addr):
                        break

                # patch pop r0 
                idc.patch_dword(pop_r0_addr, 0xe320f000)
            print("---------")

def nop(addr, endaddr):
    while addr <= endaddr:
        idc.patch_byte(addr, 0x90)
        addr += 1

def should_patch(addr, n1, n2, n3, n4):
    r0_addr = -1
    bx_addr = 0
    # n1 - ADRL 
    n1_mnem = idc.print_insn_mnem(n1)
    if (n1_mnem.lower() == "adrl") or (n1_mnem.lower() == "adr"):
        n3_mnem = idc.print_insn_mnem(n3)
        n3_ope = idc.print_operand(n3, 0)
        if (n3_mnem.lower() == 'bx') and (n3_ope.lower() == 'r0'):
            try:
                # 'BX              R0; loc_3418'
                line = idc.generate_disasm_line(n3, 0)
                jum_addr = line.split(';')[-1].strip()
                r0_addr = int(jum_addr.split('_')[-1], 16)
                bx_addr = n3
            except BaseException as e:
                pass 
            print("n3_mnem:[%s], ope:[%s], [%s]" % (n3_mnem, n3_ope, hex(r0_addr)))

        if r0_addr == -1:
            n4_mnem = idc.print_insn_mnem(n4)
            n4_ope = idc.print_operand(n4, 0)
            if (n4_mnem.lower() == 'bx') and (n4_ope.lower() == 'r0'):
                try:
                    # 'BX              R0; loc_3418'
                    line = idc.generate_disasm_line(n4, 0)
                    jum_addr = line.split(';')[-1].strip()
                    r0_addr = int(jum_addr.split('_')[-1], 16)
                    bx_addr = n4
                except BaseException as e:
                    pass 
                print("n4_mnem:[%s], ope:[%s], [%s]" % (n4_mnem, n4_ope, hex(r0_addr)))
    return (r0_addr, bx_addr)

patch_code()

 0x03-02 过反调试

以下是动态调试时发现的反调试的地方:

1. 检查 ida 的 android_server 进程 (libprotectClass.so 偏移地址:0x984c)
        判断/proc/pid/cmdline

        对抗方法:把 ida 手机上的 android_server 改个名字 ,启动时使用一个新的端口:./and_serv -p 44444 

2. bsdsignal  -  libprotectClass.so 偏移 0x9518

/data/app-lib/com.nfbazi.xuankong-1/libprotectClass.so	5FC99000	00031000
libprotectClass.so:5FCA2518 ; 0x9518 ---------------------------------------------------------------------------
libprotectClass.so:5FCA2518
libprotectClass.so:5FCA2518 loc_5FCA2518                            ; CODE XREF: libprotectClass.so:loc_5FC9FC9C↑p
libprotectClass.so:5FCA2518 PUSH            {R3,LR}
libprotectClass.so:5FCA251C LDR             R3, =(dword_5FCC9AD0 - 0x5FCA2528)
libprotectClass.so:5FCA2520 ADD             R3, PC, R3              ; dword_5FCC9AD0
libprotectClass.so:5FCA2524 LDR             R2, [R3]
libprotectClass.so:5FCA2528 CMN             R2, #1
libprotectClass.so:5FCA252C BEQ             loc_5FCA2534    ; 调整处理 bsdsignal
libprotectClass.so:5FCA2530 POP             {R3,PC}
libprotectClass.so:5FCA2534 ; ---------------------------------------------------------------------------
libprotectClass.so:5FCA2534
libprotectClass.so:5FCA2534 loc_5FCA2534                            ; CODE XREF: libprotectClass.so:5FCA252C↑j
libprotectClass.so:5FCA2534 LDR             R1, =(loc_5FCA22E8 - 0x5FCA2548)
libprotectClass.so:5FCA2538 MOV             R2, #1
libprotectClass.so:5FCA253C STR             R2, [R3]
libprotectClass.so:5FCA2540 ADD             R1, PC, R1              ; loc_5FCA22E8
libprotectClass.so:5FCA2544 MOV             R0, #5
libprotectClass.so:5FCA2548 BL              sub_5FC9B598 ; 
libprotectClass.so:5FCA254C MOV             R0, #5
libprotectClass.so:5FCA2550 POP             {R3,LR}
libprotectClass.so:5FCA2554 B               sub_5FC9B5C8

        对抗方法:使用 keyPatcher 把上面的指令 

        libprotectClass.so:5FCA252C BEQ             loc_5FCA2534 

        NOP 掉即可

经过上面两步操作,F9 让应用启动,如果没有发现报错,则表示已经过掉了应用的反调试保护,可以继续进行下面的步骤。

 0x03-03 so 解密,dump 静态分析 

上面的反调试过了之后,我们就可以愉快的单步调试了。

然后我们会遇到 so 修复的代码:

下面的 BL 指令会进入偏移 0x32dc 的地方:(libprotectClass.so    base 地址:0x5FC21000)
libprotectClass.so:5FC2459C loc_5FC2459C   ; DATA XREF: libprotectClass.so:5FC24604↓o
libprotectClass.so:5FC2459C BL              loc_5FC242DC

偏移 0x32dc 代码如下所示:
void *__fastcall _gnu_armfini_29(int a1, int a2)
{
  char v5[12]; // [sp+4h] [bp-124h] BYREF
  char v6[260]; // [sp+10h] [bp-118h] BYREF
  int v7; // [sp+114h] [bp-14h]

  v7 = *(_DWORD *)off_16EAC;
  memcpy(v5, byte_157E8, 0xAu);
  _arm_aeabi_6(v5, 10, v6);           ; 0xA 字节密钥  ,算密钥
  _gnu_arm_message(a1, a2, v6);       ; 解密 so ? 
  return memset(v6, 0, 0x102u);
}

在上图中的地方下个断点,执行到断点的时候,so 就解密了。这时候可以把内存中的 libprotectClass.so 用 idc 脚本 dump 一下,拖到 ida 中静态分析,就能识别到 Jni_Onload 函数了。看此时的偏移,Jni_Onload 函数在 0x00015084 这个位置。

 0x03-04 Jni_OnLoad ,Native 函数注册

经过上面的步骤,我们知道了 Jni_OnLoad 函数的偏移,目前我们还是继续动态调试, so-base + 0x00015084,在 Jni_Onload 函数下断。

 跟踪 Jni_OnLoad 函数,能得到如下结果:

获取到注册的 native 函数地址: 偏移: 0x1531c (so-base: 0x5FC21000)
libprotectClass.so:5FC3631C BLX             R12

registerNative 的 , 共注册了 6个 
JNINativemethod 指针:0x5FC518CC

// 前面的地址是 native 函数在 so 中的偏移 
0xfa64 - interface7(Landroid/app/Application;Landroid/content/Context;)
0x10b90 - interface6(Ljava/lang/String;)[B

// 应该是判断解密状态 - 进入 _cxa_fini_12
0x11cb0 - interface5()Z

0x12dc8 - interface4()Z
0x13ee8 - interface2()Z

// interface2 前置检查 - 进入 __cxa_fini_12 函数
0x14ffc - interface1()Z, 5FC35ffc -

0x03-04 liblog.so 动态释放

根据 gitroy 巨佬的博客 - [原创]脱壳成长之路(1)一款很老的2代壳脱壳经历-Android安全-看雪-安全社区|安全招聘|kanxue.com

知道该 so 会在 interface4() 中动态释放一个 liblog.so , 因此在 base + 0x12dc8 的地方下段,跟踪 interface4 函数。这里省略跟踪过程,有兴趣的同学可以跟踪下。

0x11f10:blx r4 这个偏移地方就是进入释放的代码中的 ,可以在这个地方下断,跟进释放后的代码段 

F7 跟进上面的断点,就会进入下面的图中: 

 

 此时 Shift + F7 看看 segments 

 然后把 debug117 前后两个 segment 一起 dump 一下,把 dump 的 bin 文件拖到 ida 中,以binary 的方式打开,选择一下 arm-little-endian 方式,就可以愉快的静态分析一波了。

0x03-05 dex 修复,突破口

0x03-04 中 dump 的内存,拖到ida 中之后,我们搜索 "dex" 相关的字符串,

找到 dex 字符串之后,查看交叉引用,发现在相对于 debug116 段 0xe170 偏移的地方,进行dex header 比较和相关操作,在这个地方下断之后,就能窥探到 dex 修复的相关流程。

经过一系列跟踪得到如下结果:

相对于 debug117 偏移 0xe5f8 的地方, 把 class_def_data 写到目标地址

---------------------------

 然后在相对于 debug117 偏移 0xdfac 的地方将 class_def_data 写到目标地址 

debug117:5FDC7FAC loc_5FDC7FAC                            ; CODE XREF: debug117:5FDC7F88↑j ; 0xdfac 
debug117:5FDC7FAC LDR             R4, [R3,#8]
debug117:5FDC7FB0 ADD             R2, R2, #1
debug117:5FDC7FB4 CMP             R4, #0
debug117:5FDC7FB8 LDRNE           R4, [R3,#4]
debug117:5FDC7FBC STRNE           R4, [R12,#0x18]
debug117:5FDC7FC0 STRNE           R8, [R3,#8]
debug117:5FDC7FC4 CMP             R2, R7                ; R7 就是待修复的 cls 个数,797 = 0x31d , r2 是已修复个数
debug117:5FDC7FC8 BNE             loc_5FDC7F8C
debug117:5FDC7FCC
debug117:5FDC7FCC loc_5FDC7FCC                            ; CODE XREF: debug117:5FDC7F64↑j
debug117:5FDC7FCC LDR             R3, =(unk_5FDD4524 - 0x5FDC7FD8)
debug117:5FDC7FD0 ADD             R3, PC, R3              ; unk_5FDD4524
debug117:5FDC7FD4 LDR             R0, [R3,#(dword_5FDD45C8 - 0x5FDD4524)]
debug117:5FDC7FD8 LDR             R1, [R3,#(dword_5FDD45D4 - 0x5FDD4524)]
debug117:5FDC7FDC BL              _5FDFA5DC
debug117:5FDC7FE0 B               loc_5FDC7F24

在上面 5FDC7FCC 的地址下断点,执行到断点时说明 class_def_data 已经写完,这时执行 idapython dump 脚本 (这里参考的是 gitroy 的 dump脚本,但不是以 hook 方式),脚本中传入 dex_header 地址即可。

dex dump 脚本如下:

import idc 
import idautils
import idaapi


def main(dex_start_addr):
    # dex_start_addr: 0x5f6ac028
    # 64 65 78 0A 30 33 35 00  ........dex.035.
    # idc.readstr(dex_start_addr) # 报错

    # print(idc.get_wide_byte(dex_start_addr)) # 0x64 = 100 
    # print(idc.get_wide_word(dex_start_addr)) # 0x6564 = 25956 
    # print(idc.get_wide_dword(dex_start_addr)) # 0x0a786564 = 175662436
    # print(idc.get_bytes(dex_start_addr, 4, False)) # b'dex\n'
    
    my_dex = dex_start_addr 

    class_def_size_off = my_dex + 0x60
    class_def_size_val = idc.get_wide_dword(class_def_size_off)
    print("class_def_size_val:%d" % class_def_size_val)

    class_defs_off = my_dex + 0x64
    class_defs_off_value = idc.get_wide_dword(class_defs_off)
    print("class_def_off:%d" % class_defs_off_value)

    classdef_item1 = my_dex + class_defs_off_value
    classdef_item1_data_off = classdef_item1 + 0x18 

    values = idc.get_wide_dword(classdef_item1_data_off)
    class_data_off_start = my_dex + values
    next_values = 0 

    for i in range(0, class_def_size_val):
        classdef_item1_next = classdef_item1 + 0x18
        values = idc.get_wide_dword(classdef_item1_next)
        print("[%d] values:%d" % (i, values))
        # 继续走下一个 classdef
        next_values = values
        classdef_item1 += 0x20;     # 每个 class_def_item 大小是 0x20 

    begin = my_dex;     # 需对应修改
    size = next_values  # 需对应修改
    list = []
    for i in range(size):
        byte_tmp = idc.get_wide_byte(begin + i)
        list.append(byte_tmp)
    
    wrote_dex_end = my_dex + size
    print("wrote_dex_end:%x" % wrote_dex_end)
    print("class_data_off_start:%x" % class_data_off_start)
    
    while wrote_dex_end < class_data_off_start:
        list.append(0)
        wrote_dex_end += 1

    begin = class_data_off_start;       # 需对应修改
    size = 1 * 1024 * 1024              # 需对应修改
    for i in range(size):
        byte_tmp = idc.get_wide_byte(begin + i)
        list.append(byte_tmp)

    file = "E:\\z-free-reverse\\1-LinGe\\2-rev\\ida-dump.dex" #需对应修改
    buf = bytearray(list)
    with open(file, 'wb') as fw:
        fw.write(buf)
    print('dump over at %s' % file)

# 传入dex 内存地址
main(0x5f6ac028)

收获

通过该样本还是学到了很多东西:
1. 学会 ida python 脚本使用 
    > 使用脚本过 libprotectClass.so 中的混淆指令
    > 使用脚本批量下断点,并用脚本自动 patch code 
    > 动态 dex 修复及 dump 
    > 使用脚本搜索二进制 xx xx xx 

2. 加强了 ida 工具使用 
    > ida 条件断点 
    > 动态释放的 so dump 并使用 ida 以 bin 文件方式加载分析 
    > 堆栈,寄存器的查看
    > keypatcher 的使用,动态调试时 patch 过反调试 

3. 加强了 dex/odex 文件格式的理解 
    > ida 动态调试时 header + 偏移,对照 010 中的解析理解

4. 注入工具的使用
    > bhook 
    > dobby 
    > shadowhook 

5. 其他   
    > 查找 so 中是否存在某条指令 
        >> 先用 keypatcher,得到指令的 二进制字节
        >> https://hexed.it/ , 选择本地 so 文件 
        >> 搜索二进制字节,记录出现位置的偏移
        >> 把 so 拖入 ida 中,G -> 去找到的偏移地方 c 键,即可看到指令

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值