首页
社区
课程
招聘
[原创]乐固分析
2022-2-1 04:50 37090

[原创]乐固分析

2022-2-1 04:50
37090

样本

应用宝随手下载一个安装包,本文分析的是作业帮v13.28.0


工具

jadx、idapro、unicorn


分析

直接将apk拖入jadx,找到入口MyWrapperProxyApplication


继承了WrapperProxyApplication,在attachBaseContext中调用了initProxyApplication,而initProxyApplication中调用com.wrapper.proxyapplication.Util.PrepareSecurefiles,然后加载libshell-super.2019.so


PrepareSecurefiles这个方法反编译出来又长又臭,主要就是检查下/data/data/com.baidu.homework/files/prodexdir目录下的一些文件是否完整。

这些文件都是从assets释放的,主要有两个文件tosversion和0OO00l111l1l。

通过后续分析可知tosversion适用于判断应用升级的,0OO00l111l1l是保存原始dex的加密文件。


然后把libshell-super.2019.so拖入ida进行分析,首先看看.init_array,有一堆函数

把这些函数挨个大概看下,除了最后一个sub_29BC0都是在做字符串解密,这些字符加密的方式为每个字符串和一个固定的字符进行异或


虽然解密的方式十分简单,但是需要处理的字符串数量太多了,手动一个一个处理肯定是不行的,这就需要脚本了。

脚本处理有两种方式,

一种是分析汇编代码,找出需要解密的字符串起始地址、字符串长度、解密key;

另一种是unicorn直接运行.init_array中的函数,然后把运行后的内容直接加载到ida中;


首先来看第一种方法,通过分析可知,每次解密的指令格式都是固定的。

通过第三条指令LDR可以获取到字符串起始地址。

第四条有可能是跳转指令B,也可能没有。

通过倒数第四条指令SUBS可以获取到字符串长度。

通过EOR.W指令可以获取到用于解密的字符。


根据以上分析结果,写出idc脚本,执行一遍发现,存在解密失败的。

找到失败的地址,发现当字符串长度为1的时候,指令格式不一致。


把这个情况兼容一下,脚本内容如下:

import idc


def find_next_chunk(addr):
    if not idc.is_code(idc.get_full_flags(addr)):
        return False, None, None, None, None

    op = idc.print_insn_mnem(addr)
    if op != 'MOVS' and op != 'MOV.W':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'STRD.W':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'LDR':
        return False, None, None, None, None
    arr_start = idc.get_wide_dword(idc.get_operand_value(addr, 1)) + addr + 2 * 3

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'ADD':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op == 'B':
        addr = idc.get_operand_value(addr, 0)
        op = idc.print_insn_mnem(addr)
    if op != 'LDRD.W':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'LDRB':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'EOR.W':
        return False, None, None, None, None
    xor_ch = idc.get_operand_value(addr, 2)

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'STRB':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op != 'ADDS':
        return False, None, None, None, None

    addr = idc.next_head(addr)
    op = idc.print_insn_mnem(addr)
    if op == 'ADC.W':
        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'STR':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'SUBS':
            return False, None, None, None, None
        arr_len = idc.get_operand_value(addr, 1)

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'STR':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'SBCS.W':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'BCC':
            return False, None, None, None, None

        next_addr = idc.next_head(addr)
        return True, next_addr, xor_ch, arr_start, arr_len

    elif op == 'STR':
        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'ADCS.W':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'STR':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'ADCS.W':
            return False, None, None, None, None

        addr = idc.next_head(addr)
        op = idc.print_insn_mnem(addr)
        if op != 'BNE':
            return False, None, None, None, None

        next_addr = idc.next_head(addr)
        return True, next_addr, xor_ch, arr_start, 1

    else:
        return False, None, None, None, None


def decode_arr(arr_start, arr_len, xor_ch):
    for addr in range(arr_start, arr_start + arr_len):
        ch = idc.get_wide_byte(addr)
        # print hex(ch)
        idc.patch_byte(addr, ch ^ xor_ch)
    idc.create_strlit(arr_start, arr_start + arr_len + 1)


def proc_func(func_addr):
    if not func_addr:
        return
    # Thumb, func_start_addr+1==func_addr
    func_start_addr = idc.get_func_attr(func_addr, idc.FUNCATTR_START)
    func_end_addr = idc.get_func_attr(func_addr, idc.FUNCATTR_END)
    print hex(func_start_addr), '----->', hex(func_end_addr)
    addr = func_start_addr
    while addr < func_end_addr:
        succ, next_addr, xor_ch, arr_start, arr_len = find_next_chunk(addr)
        if succ:
            print hex(addr).ljust(10), hex(xor_ch).ljust(10), hex(arr_start).ljust(10), arr_len
            decode_arr(arr_start, arr_len, xor_ch)
            addr = next_addr
        else:
            print '*' * 10, hex(addr)
            addr = idc.next_head(addr)
    print '\n' * 3


def decode_str():
    idc.auto_wait()
    start_addr = idc.get_segm_by_sel(idc.selector_by_name('.init_array'))
    end_addr = idc.get_segm_end(start_addr)
    addr = start_addr
    while addr + 4 <= end_addr:
        func_addr = idc.get_wide_dword(addr)
        proc_func(func_addr)
        addr += 4

decode_str()


ida加载运行该脚本,得到解密后的内容。



然后是第二种方法,通过unicorn直接运行.init_array中的解密函数,然后把解密后的内容直接加载到ida中

import unicorn

import idc


def func_block_handle(uc, address, size, user_data):
    if address in (0, 0x29BC0):
        uc.emu_stop()


def decode_str():
    idc.auto_wait()

    dir_path = r'/Users/lll19/Downloads/legu/'
    bin_len = idc.prev_addr(idc.BADADDR)
    bin_len = (bin_len / 0x1000 + (1 if bin_len % 0x1000 else 0)) * 0x1000
    bin_path = dir_path + 'elf_bin'
    idc.savefile(bin_path, 0, 0, bin_len)
    f_bin = open(bin_path, 'rb')
    bin_bytes = bytes(f_bin.read())
    f_bin.close()

    stack_size = 0x100000
    stack_top = bin_len
    stack_bottom = stack_top + stack_size

    uc = unicorn.Uc(unicorn.UC_ARCH_ARM, unicorn.UC_MODE_THUMB)
    uc.hook_add(unicorn.UC_HOOK_BLOCK, func_block_handle)
    uc.mem_map(0, bin_len)
    uc.mem_write(0, bin_bytes)
    uc.mem_map(stack_top, stack_size)

    start_addr = idc.get_segm_by_sel(idc.selector_by_name('.init_array'))
    end_addr = idc.get_segm_end(start_addr)
    addr = start_addr
    while addr + 4 <= end_addr:
        func_addr = idc.get_wide_dword(addr)
        addr += 4
        if not func_addr:
            continue
        print hex(func_addr)

        func_end_addr = idc.find_func_end(func_addr)
        uc.reg_write(unicorn.arm_const.UC_ARM_REG_SP, stack_bottom)
        uc.reg_write(unicorn.arm_const.UC_ARM_REG_LR, 0)
        uc.emu_start(func_addr, func_end_addr)

    f_save = open(bin_path, 'wb')
    f_save.write(str(uc.mem_read(0, bin_len)))
    f_save.close()
    idc.loadfile(bin_path, 0, 0, bin_len)

decode_str()


然后开始分析JNI_OnLoad,这个函数被混淆了,于是继续unicorn走起。

根据输出可知,该函数主要执行了以下几个操作,

通过RegisterNatives注册了WrapperProxyApplication.Ooo0ooO0oO()

调用0x1f668处的函数进行上下文初始化,

调用0x2B604处的函数生成解密key,

调用0xcea8处的函数加载dex。

先分析下sub_1f668处的初始化过程,东西比较多就不贴图了

首先是一些常规操作,获取vm类型、PackageInfo、ActivityThread、ClassLoader等。

然后是通过GetMethodID获取之前注册的WrapperProxyApplication.Ooo0ooO0oO()MethodID,然后在MethodID指向的内存通过Android源码可知,MethodID实际是ArtMethod对象的指针查找之前注册的函数地址,找到后保存该偏移值,后面会通过这个偏移值对系统native方法进行hook。

再然后就是加载0OO00l111l1l,解析该文件,把该文件的各种数据指针缓存起来,用于后面数据解密

再然判断系统是否升级,读取prodexdir/.updateIV.dat中缓存的数据与libart.sodex2oat的大小进行比较,如果不相等则将.odex.vdex文件删除并更新.updateIV.dat


0OO00l111l1l数据结构如下,前4字节为dex的数量,后面分别为三种数据,通过后面分析可知

第一部分数据为压缩的dex,其中的指令被抽取了

第二部分为压缩且加密的索引数据

第三部分为压缩且加密的指令数据


看下sub_2B604的代码,比较简单,读取文件内容与byte_31391处的字符处理后存放在byte_36A8C

大概分析下sub_cea8过程,

调用sub_1CD90DexFile.defineClassNative()进行hook,原理是先获取defineClassNativeMethodID,然后通过先前获取到的偏移值,将该函数的本地函数指针保存起来,再通过RegisterNatives重新为该函数注册一个本地函数。

调用java方法com.wrapper.proxyapplication.MultiDex.preparetoinstallDexes(),获取dexElements

hook几个系统函数mmap、execve、execv,hook的方式是通过遍历重定位项实现的。这个几个hook在我分析的过程中没用上

多线程调用函数sub_CE14解密加载dex并opt

等待所有线程结束后,获取mCookie缓存起来

取消之前hook的几个系统函数,并设置几个环境变量

构造当前应用的原始application,并调用其attach()方法


剩下的流程都不重要了,

现在只关心解密函数sub_CE14,

首先调用sub_CC2C进行dex解压缩并写入/data/data/com.baidu.homework/files/prodexdir下的dex文件,再调用java方法com.wrapper.proxyapplication.MultiDex.installDexes()进行dex替换

解压缩函数为sub_2B2AC看看反编译代码,全是各种字符操作,具体算法就不看了,待会儿直接上unicorn

dex加载成功后调用sub_10EB4进行指令解密

首先对第二部分数据进行解密,解密函数为sub_2315C,解密完成后调用sub_2B2AC解压缩,再调用sub_115F4生成每个类结构的索引

然后对第三部分数据进行解密,解密函数为sub_2315C,解密完成后调用sub_2B2AC解压缩,得到所有抽取的指令


现在只剩最后一步了,那就是在class加载的时候,对抽取的指令进行填充,这里需要分析的是前面hook的DexFile.defineClassNative(),对应的函数为sub_1C900,其主要内容如下:

首先查找class所在的dex和对应的索引sub_22B90

然后进行指令填充sub_101B8

最后调用原来的函数地址

sub_22B90的代码和对应的数据如下,通过分析可知,

从偏移8开始是一个hash表,每个表项3个字段,分别为hash值、类名偏移、类结构索引偏移

偏移为4的字段为hash表的大小

  


函数sub_101B8对class指令进行填充,函数被混淆了,既然这样,也就不看了,直接unicorn


到此为止,所有的流程都分析完了。

开始准备脱壳脚本,需要模拟执行的函数流程分为以下几部分:

1、执行0x2B604处的代码。读取tosversion文件的内容,处理后作为解密的key(该处只读取了16个字节,但是解密的时候,复制了32字节的key,但是不影响,实际执行解密的时候只用了前16字节

2、循环执行0x2B2AC处的代码。解压出所有被抽取指令的dex

3、循环执行0x2315C、0x2B2AC、0x115F4这三处代码,分别对应class信息的解密、解压、建索引

4、循环执行0x2315C、0x2B2AC这两处代码,分别对应指令的解密、解压

5、遍历所有dex的hash表,循环执行0x101b8处的代码进行指令修复



完整的脱壳脚本就不贴了,提示超出字数限制了,我把它放在附件里面了。。。。。

样本打包后附件大小也超限制了。。。。。

样本我放网盘了,附件只保留了脚本文件。。。。。


贴个脱壳修复后的图


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
点赞23
打赏
分享
打赏 + 150.00雪花
打赏次数 1 雪花 + 150.00
 
赞赏  Editor   +150.00 2022/03/09 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (45)
雪    币: 2240
活跃值: (3335)
能力值: ( LV13,RANK:405 )
在线值:
发帖
回帖
粉丝
奔跑的阿狸 1 2022-2-1 09:08
2
0
雪    币: 2676
活跃值: (3451)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
luoyesiqiu 3 2022-2-1 14:13
3
0
很详细
雪    币: 2469
活跃值: (2706)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
院士 2022-2-1 19:20
4
0
厉害了。
雪    币: 5022
活跃值: (3766)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_rjdrqvpa 2022-2-2 12:24
5
0

.

最后于 2023-6-23 20:00 被mb_rjdrqvpa编辑 ,原因:
雪    币: 3313
活跃值: (2886)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 2022-2-2 13:33
6
0
mb_rjdrqvpa MT论坛某管理员Hasky分析过程抄袭你的文章, 只不过是换了个乐固样本apk, 而且他的文章分析过程极其简单, 似乎没看懂你的文章帖子还标记精华链接:&nbsp;https://bbs.bi ...
 说抄袭的这位 你多多少少是没多少脑子 你所谓的几乎不一样 只不过是过程差不多 麻烦你去百度搜搜乐固的帖子 看看是不是基本一样 再加上楼主用的脚本解密 而我只是hook去脱 外加上分析几个地方 我贴根此帖 样本版本完全不一样 何来抄袭 
雪    币: 6193
活跃值: (4457)
能力值: ( LV9,RANK:185 )
在线值:
发帖
回帖
粉丝
卧勒个槽 4 2022-2-2 13:43
7
0
mb_rjdrqvpa MT论坛某管理员Hasky分析过程抄袭你的文章, 只不过是换了个乐固样本apk, 而且他的文章分析过程极其简单, 似乎没看懂你的文章帖子还标记精华链接:&nbsp;https://bbs.bi ...
多谢提醒,我看了下,应该就只有这两张图上的描述是一样的,
雪    币: 3313
活跃值: (2886)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 2022-2-2 13:46
8
0
卧勒个槽 多谢提醒,我看了下,应该就只有这两张图上的描述是一样的,[em_84]
乐固基本都是一样的流程与分析过程 我这并不是抄袭您的 我们手法完全不一样了 我单纯的提到hook去脱 您这是分析过程操作 所以不存在抄袭 论坛很多乐固 都差不多 
雪    币: 6193
活跃值: (4457)
能力值: ( LV9,RANK:185 )
在线值:
发帖
回帖
粉丝
卧勒个槽 4 2022-2-2 13:49
9
0
D-t 乐固基本都是一样的流程与分析过程 我这并不是抄袭您的 我们手法完全不一样了 我单纯的提到hook去脱 您这是分析过程操作 所以不存在抄袭 论坛很多乐固 都差不多
雪    币: 5022
活跃值: (3766)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_rjdrqvpa 2022-2-2 15:41
10
1

.

最后于 2023-6-23 20:00 被mb_rjdrqvpa编辑 ,原因:
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_lasgffyh 2022-2-2 16:22
11
0
太厉害了,膜拜
雪    币: 3313
活跃值: (2886)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 2022-2-2 16:48
12
0
mb_rjdrqvpa 前面的, 你敢说没抄袭?你和本帖作者思路这么同步吗?连截图这几段文字都一模一样的, 没有半点差别?你说网上分析乐固帖子都大同小异, 你能找到这种几段文字一模一样的分析帖子出来?毋庸置疑, Hasky帖 ...
就像作者说的 前面几个文字一样 已经删除了  几个文字就抄袭了?  可真好笑
雪    币: 13430
活跃值: (4758)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 2022-2-2 17:47
13
0
楼上D-t脱壳实力有目共睹。。。
雪    币: 3313
活跃值: (2886)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 2022-2-2 18:30
14
0
tDasm 楼上D-t脱壳实力有目共睹。。。
没有 只是最简单的 各位有手就行 hook很基本
雪    币: 62
活跃值: (518)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2022-2-4 11:02
15
0
大佬太强啦 unicore炉火纯青
雪    币: 248
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
@=llfly 2022-2-8 11:12
16
0
学习unicore、idapython的好帖子!大佬nb
雪    币: 2415
活跃值: (1246)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_vjhvccto 2022-2-9 13:56
17
0
谢谢大佬感谢
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 11:15
18
0

这下终于知道了MT论坛哈士奇大佬在看雪上的账号哈~

最后于 2022-2-11 11:55 被JimmyJLNU编辑 ,原因:
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 11:19
19
0
    卧勒个槽 多谢提醒,我看了下,应该就只有这两张图上的描述是一样的,[em_84]

的确就是这么一回事,因为腾讯加固的基本流程是类似的,即使版本号不断更新。所以并不存在抄袭问题。

没想到哈士奇大佬也亲自过来认证了~

最后于 2022-2-11 11:42 被JimmyJLNU编辑 ,原因:
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 11:25
20
0
mb_rjdrqvpa MT论坛某管理员Hasky分析过程抄袭你的文章, 只不过是换了个乐固样本apk, 而且他的文章分析过程极其简单, 似乎没看懂你的文章帖子还标记精华链接:&nbsp;https://bbs.bi ...

实锤认证了

最后于 2022-2-11 11:28 被JimmyJLNU编辑 ,原因:
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 11:26
21
0
tDasm 楼上D-t脱壳实力有目共睹。。。

这个人就是在MT论坛上熟知的哈士奇管理员,大佬级别的人物。

最后于 2022-2-11 11:27 被JimmyJLNU编辑 ,原因:
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 11:41
22
0
D-t 没有 只是最简单的 各位有手就行 hook很基本
没想到你也过来认证了
雪    币: 77
活跃值: (315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JimmyJLNU 2022-2-11 12:04
23
0
卧勒个槽 [em_69]

我这里还有几个样本,你帮忙看一下是不是都是一样的?这两个是我在官网自行加固的样本。

下载地址:蓝奏网盘

密码:JimmyJLNU

最后于 2022-2-11 12:09 被JimmyJLNU编辑 ,原因:
雪    币: 6193
活跃值: (4457)
能力值: ( LV9,RANK:185 )
在线值:
发帖
回帖
粉丝
卧勒个槽 4 2022-2-11 12:40
24
0
JimmyJLNU 卧勒个槽 [em_69] 我这里还有几个样本,你帮忙看一下是不是都是一样的?这两个是我在官网上自行加固的样本。下载地址:蓝奏网盘 密码:Jimm ...
两个都能脱,你可以把附件里面的0OO00l111l1l和tosversion替换后跑脚本试下
雪    币: 13430
活跃值: (4758)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 2022-2-11 14:33
25
0
卧勒个槽 两个都能脱,你可以把附件里面的0OO00l111l1l和tosversion替换后跑脚本试下

你这个运行环境介绍一下?IDA pro什么版本?unicorn是ida插件?unicorn在哪下载?

最后于 2022-2-11 14:38 被tDasm编辑 ,原因:
游客
登录 | 注册 方可回帖
返回