android so文件攻防实战-libDexHelper.so反混淆
2022-8-8 18:0:26 Author: 看雪学苑(查看原文) 阅读量:20 收藏


本文为看雪论坛精华文章

看雪论坛作者ID:houjingyi

计划是写一个android中so文件反混淆的系列文章,目前这是第三篇。
第一篇:android so文件攻防实战-百度加固免费版libbaiduprotect.so反混淆(https://bbs.pediy.com/thread-271388.htm

第二篇:android so文件攻防实战-某团libmtguard.so反混淆(https://bbs.pediy.com/thread-271853.htm
今天分析的是企业版64位,我用LibChecker查了一下手机上的APP找到的,时间也还比较新。根据其他人的分析可知,libDexHelper.so是指令抽取的实现,libdexjni.so是VMP的实现。

去除混淆

首先因为加密过,肯定是不能直接反编译的,可以在libart.so下断点,进入JNI_onLoad以后就可以dump下来。

不过此时也不能直接F5,还存在以下混淆方式:

1.垃圾指令

这些垃圾指令是在switch的一个永远不会被执行到的分支里面,可以直接将IDA不能MakeCode的地方patch成NOP再MakeCode。

2.字符串加密
有好几个解密字符串的函数,0x186C4,0x7783C,0x95B9C。在android so文件攻防实战-百度加固免费版libbaiduprotect.so反混淆(https://bbs.pediy.com/thread-271388.htm)中我们是交叉引用拿到加密后的字符串和它对应的解密函数的表然后frida主动调用得到的解密后的字符串,但是在这里这个方法就不太好用了。因为这里加密后的字符串是在栈上一个byte一个byte拼起来的,和最后调用解密函数之间可能隔了很多条指令,甚至都不在一个block。

我最后用的是下面这种方案:以0x40110处调用0x186C4处的解密函数为例,这里面字符串解密的逻辑比较简单,需要三个参数。我们可以自己实现也可以用unicorn,我就用unicorn了。

import sysimport unicornimport binasciiimport threadingimport subprocess from capstone import *from capstone.arm64 import * with open("C:\\Users\\hjy\\Downloads\\out1.fix.so","rb") as f:    sodata = f.read() uc = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM)code_addr = 0x0code_size = 8*0x1000*0x1000uc.mem_map(code_addr, code_size)stack_addr = code_addr + code_sizestack_size = 0x1000000stack_top = stack_addr + stack_size - 0x8uc.mem_map(stack_addr, stack_size)uc.mem_write(code_addr, sodata)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X29, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X28, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X27, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X26, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X25, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X24, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X23, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X22, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X21, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X20, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X19, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X18, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X17, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X16, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X15, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X14, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X13, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X12, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X11, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X10, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X9, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X8, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X7, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X6, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X5, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X4, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, stack_addr)uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP, stack_top)X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0) uc.mem_write(X0, bytes.fromhex(sys.argv[1]))uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, int(sys.argv[2], 16))uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, int(sys.argv[3], 16)) uc.emu_start(0x1777C, 0x17780) X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0)decstr = uc.mem_read(X0, 80) print("decstr:", decstr)uc.mem_unmap(stack_addr, stack_size)uc.mem_unmap(code_addr, code_size)
总共有几百处调用,不可能全部人工去这样解出来,我写了另外以一个脚本去调用decstr.py。首先通过交叉引用找到所有调用解密函数的地方,然后把起始地址设为该block的起始地址,结束地址设为调用解密函数的地址,通过unicorn跑出decstr.py需要的三个参数之后调用decstr.py。
遇到unicorn.unicorn.UcError也有两个处理策略,一个是跳过该地址( loop_call_prepare_arg1),起始地址不变;一个是将起始地址设为下一条地址(loop_call_prepare_arg2)。当然这套方案还有优化的空间,比如生成调用解密函数需要的参数的代码和最后调用解密函数的代码不在一个block,就处理不了。
import unicornimport binasciiimport threadingimport subprocess from capstone import *from capstone.arm64 import * inscnt = 0start_addr = 0end_addr = 0stop_addr = 0stop_addr_list = [] def hook_code(uc, address, size, user_data):    global inscnt    global end_addr    global stop_addr    global stop_addr_list     md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)     for ins in md.disasm(sodata[address:address + size], address):        #rint(">>> 0x%x:\t%s\t%s" % (ins.address, ins.mnemonic, ins.op_str))        stop_addr = ins.address         if ins.address in stop_addr_list:            #print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))            uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)            return         inscnt = inscnt + 1        if (inscnt > 500):            uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, 0xffffffff)            return         if ins.mnemonic.find("b.") != -1:            print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))            uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)            return         if ins.mnemonic.find("bl") != -1:            print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))            uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)            return             if ins.op_str in ["x0","x1","x2","x3"]:                    X1 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X1)                    if X1 > 0x105A88:                        print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))                        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)                        return            if ins.op_str.startswith("#0x"):                addr = int(ins.op_str[3:],16)                if (addr > 0x14E50 and addr < 0x15820) \                or addr == 0x186C4 \                or addr > 0x105A88:                    print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))                    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)                    return def call_prepare_arg():    global inscnt    global start_addr    global end_addr    global stop_addr    global stop_addr_list     inscnt = 0     uc = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM)    code_addr = 0x0    code_size = 8*0x1000*0x1000    uc.mem_map(code_addr, code_size)    stack_addr = code_addr + code_size    stack_size = 0x1000000    stack_top = stack_addr + stack_size - 0x8    uc.mem_map(stack_addr, stack_size)    uc.hook_add(unicorn.UC_HOOK_CODE, hook_code)     uc.mem_write(code_addr, sodata)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X29, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X28, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X27, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X26, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X25, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X24, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X23, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X22, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X21, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X20, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X19, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X18, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X17, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X16, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X15, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X14, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X13, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X12, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X11, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X10, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X9, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X8, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X7, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X6, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X5, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X4, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, stack_addr)    uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, stack_addr)     uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP, stack_top)    uc.emu_start(start_addr, end_addr)     X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0)    decstr = uc.mem_read(X0, 80)    end_index = decstr.find(bytearray(b'\x00'), 1)    decstr = decstr[:end_index]     decstr = binascii.b2a_hex(decstr)    decstr = decstr.decode('utf-8')     X1 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X1)    X2 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X2)     pi = subprocess.Popen(['C:\\Python38\\python.exe', 'decstr.py', decstr, hex(X1), hex(X2)], stdout=subprocess.PIPE)    output = pi.stdout.read()    print(output) def loop_call_prepare_arg1():    global inscnt    global end_addr    global stop_addr    global stop_addr_list     loopcnt = 0    stop_addr_list = []     while True:        try:            loopcnt = loopcnt + 1            if(loopcnt > 200):                break            call_prepare_arg()        except unicorn.unicorn.UcError:            print("adding....")            print(hex(stop_addr))            stop_addr_list.append(stop_addr)        else:            break def loop_call_prepare_arg2():    global inscnt    global end_addr    global stop_addr    global stop_addr_list     global start_addr     loopcnt = 0    stop_addr_list = []     while True:        try:            loopcnt = loopcnt + 1            if(loopcnt > 200):                break            call_prepare_arg()        except unicorn.unicorn.UcError:            start_addr = stop_addr + 4        else:            break with open("C:\\Users\\hjy\\Downloads\\out1.fix.so","rb") as f:    sodata = f.read() all_addr = []with open('xref_decstr.txt', 'r', encoding='utf-8') as f:    for line in f:        addr = "0x" + line[2:]        addr = int(addr, 16)        all_addr.append(addr) for i in all_addr:     print("i:")    print(hex(i))     end_addr = i    CODE = sodata[i - 4:i]    md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)    for x in md.disasm(CODE, i - 4):        mnemonic = x.mnemonic     while mnemonic != "ret" \        and mnemonic != "b" \        and mnemonic != "br" \        and mnemonic != "cbz" \        and mnemonic != "cbnz":        i = i - 4        CODE = sodata[i - 4:i]        for x in md.disasm(CODE, i - 4):            mnemonic = x.mnemonic     start_addr = i     print("start_addr:")    print(hex(start_addr))    print("end_addr:")    print(hex(end_addr))     loop_call_prepare_arg1()    loop_call_prepare_arg2()
更恶心的是还有很多字符串是自己在函数内解密的,这种情况我也没想到有什么好的方法。

3.控制流混淆
第一种是把正常顺序执行的指令打乱成switch的形式,这个影响倒不是太大:

第二种是动态计算跳转地址,基本上类似于在android so文件攻防实战-某团libmtguard.so反混淆(https://bbs.pediy.com/thread-271853.htm)见过的那种,但是要更复杂。

比如这里的指令,在0x1DA0C处给X2赋值,X2此时为.data段中的一个地址,W0为偏移,取出值后在0x1DA18处乘4加上0x1DA20,最后的值就是0x1DA1C处X0的值。那么需要解决这么几个问题:

如何确定0x1DA0C处给X2赋的值;
将0x1DA00处的指令改成跳转指令,0x1DA00这个地址又该如何确定;

找到所有会跳转到0x1DA1C的指令,将跳转地址改成计算出来的X0的值。

第一个问题,其实和字符串解密面临的情况是类似的,比如这里需要找到和"LDR X2, [X29,#0x190+var_118]"对应的"STR XX, [X29,#0x190+var_118]"这条指令,然后再找给XX寄存器赋值的指令,然而这两条指令很可能和BR X0隔了好几个block。我的解决方法是通过IDA提供的idaapi.FlowChar功能,递归前面的block查找。不足之处在于前提条件是IDA正确识别了函数的起始地址,否则会出现我们需要的指令和BR X0不在同一个函数的情况,这样就处理不了。

第二个问题,在递归前面的block的时候就先找到0x1D9D4处这条给W0赋值的指令,然后从0x1D9D4处开始直到0x1DA1C,找到第一个存在交叉引用的地址,也就是0x1DA04。它的前一条指令0x1DA00就是需要改成跳转指令的地方。

第三个问题,确定了0x1DA00之后,那么从0x1DA00到0x1DA1C所有存在交叉引用的地址都要去交叉引用的地方修改跳转地址。不过这里有很多细节。
(1)如果W0是由CSEL,CSET,CSINC这些指令赋值的,像下面这种情况,那么需要把0x1DE80和0x1DE84修改成 B.GE和B.LT。

patch前:

patch后:

(2)0x1DE80处的CSEL W0, WZR, W8, LT,这里W8的值是在0x1D9DC MOV W8, #5赋值的,所以我的代码中有一个register_value_dict,在改掉0x1DA00处的指令之后会读取0x1DA00所在的block到0x1DA1C所在的block的所有指令,找到给寄存器赋值的指令然后把值存起来。


(3)有些地方还会有一条sub指令,这个也要考虑进去,比如下面这种情况0x33394处跳转的地址就应该按照W8为4计算。


最后的脚本放附件了。当然还有一些脚本处理不了的地方,不过问题已经不算太大了,需要的话可以动态调试确定。

4.函数地址动态计算



这个在IDA里面是能看清楚的,v35其实就是off_12EB80[0],即调用0x80FE0处的p329AAB59961F6410ABA963EF972FE303。

接下来我们就来分析libDexHelper.so,来看看它都干了些什么。精力有限,很多地方没能很详细去分析。有些地方分析的可能也不一定对,将就看吧。

功能分析

JNI_OnLoad(0x3EA68)的分析在最后。

0x15960

读/proc/self/maps,特征字符串:
libDexHelper.solibDexHelper-x86.solibDexHelper-x86_64.so/system/lib64/libart.so/system/lib64/libLLVM.so/system/framework/arm64/boot-framework.oat/system/lib64/libskia.so/system/lib64/libhwui.so.oatff c3 01 d1 f3 03 04 aa f4 03 02 aa f5 03 01 aa e8 03 00 aaGumInvocationListenerGSocketListenerEvent

0x16A30

获取系统属性,读/proc/%d/cmdline,特征字符串:
ro.yunos.versionro.yunos.version.releasepersist.sys.dalvik.vm.libpersist.sys.dalvik.vm.lib.2/system/bin/dex2oatLD_OPT_PACKAGENAMELD_OPT_ENFORCE_V1
off_12EF10:为2表示yunos,art模式;为1表示yunos,dalvik模式;为0表示非yunos。

0x17A70

md5。

0x186C4

字符串解密函数。

0x19674

返回字符串rw。

0x19778

返回字符串su。

0x1987C

返回字符串mount。

0x19998

写classes.dve文件。

0x19b48

读取目录中的文件。

0x19E08

创建String类型的数组,第一个参数是String列表,第二个参数是数组长度。

0x1A058

调用0x19E08创建数组:
/etc/sbin/system/system/bin/vendor/bin/system/sbin/system/xbin

0x1A740

调用0x19E08创建数组:
com.yellowes.sueu.chainfire.supersucom.noshufou.android.sucom.thirdparty.superusercom.koushikdutta.superusercom.noshufou.android.su.elite

0x1AF1C

调用0x19E08创建数组:
com.chelpus.lackypatchcom.ramdroid.appquarantinecom.koushikdutta.rommanagercom.dimonvideo.luckypatchercom.ramdroid.appquarantineprocom.koushikdutta.rommanager.license

0x1B7D0

调用0x19E08创建数组:
com.saurik.substratecom.formyhm.hiderootcom.amphoras.hidemyrootcom.devadvance.rootcloakcom.formyhm.hiderootPremiumcom.devadvance.rootcloakpluscom.amphoras.hidemyrootadfreecom.zachspong.temprootremovejbde.robv.android.xposed.installer
0x1C40C
system_property_get ro.product.cpu.abi和读/system/lib/libc.so判断是不是x86架构。

0x1C61C

查看classes.dve是否存在。

0x1C8D8

调用0x19E08创建数组:
/sbin//su/bin//data/local//system/bin//system/xbin//data/local/bin//system/sd/xbin//data/local/xbin//system/bin/.ext//system/bin/failsafe//system/usr/we-need-root/

0x1D518

初始化一些路径,特征字符串:
.cacheoat.payloadv1filter.jarclasses.odexclasses.vdexclasses.dexassets/classes.jar.cache/classes.jar.cache/classes.dex.cache/classes.odex.cache/classes.vdex

0x1E520

将libc中的一些函数的地址放到.DATA。
0x137BB0 fopen0x137BB8 fclose0x137BC0 fgets0x137BC8 fwrite0x137BD0 fread0x137BD8 sprintf0x137BE0 pthread_create

0x1F250

读/proc/self/cmdline,判断是否含有com.miui.packageinstaller从而判断是否由小米应用包管理组件启动。

0x1F710

先system_property_get ro.product.manufacturer和system_property_get ro.product.model判断是否是samsung,然后system_property_get ro.build.characteristics是否为emulator。

0x1FDC8

注册如下native函数:
RegisterNative(com/secneo/apkwrapper/H, attach(Landroid/app/Application;Landroid/content/Context;)V, [email protected][libDexHelper.so]0x2f6e4)RegisterNative(com/secneo/apkwrapper/H, b(Landroid/content/Context;Landroid/app/Application;)V, [email protected][libDexHelper.so]0x247c0)RegisterNative(com/secneo/apkwrapper/H, c()V, [email protected][libDexHelper.so]0x24c08)RegisterNative(com/secneo/apkwrapper/H, d(Ljava/lang/String;)Ljava/lang/String;, [email protected][libDexHelper.so]0x23d04)RegisterNative(com/secneo/apkwrapper/H, e(Ljava/lang/Object;Ljava/util/List;Ljava/lang/String;)[Ljava/lang/Object;, [email protected][libDexHelper.so]0x35ab0)RegisterNative(com/secneo/apkwrapper/H, f()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1a740)RegisterNative(com/secneo/apkwrapper/H, g()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1af1c)RegisterNative(com/secneo/apkwrapper/H, h()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1b7d0)RegisterNative(com/secneo/apkwrapper/H, n()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1c8d8)RegisterNative(com/secneo/apkwrapper/H, j()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1a058)RegisterNative(com/secneo/apkwrapper/H, k()Ljava/lang/String;, [email protected][libDexHelper.so]0x19778)RegisterNative(com/secneo/apkwrapper/H, l()Ljava/lang/String;, [email protected][libDexHelper.so]0x1987c)RegisterNative(com/secneo/apkwrapper/H, m()Ljava/lang/String;, [email protected][libDexHelper.so]0x19674)RegisterNative(com/secneo/apkwrapper/H, bb(Landroid/content/Context;Landroid/app/Application;Landroid/app/Application;)V, [email protected][libDexHelper.so]0x2921c)RegisterNative(com/secneo/apkwrapper/H, o(Landroid/content/Context;)I, [email protected][libDexHelper.so]0x2f158)RegisterNative(com/secneo/apkwrapper/H, p()V, [email protected][libDexHelper.so]0x1875c)RegisterNative(com/secneo/apkwrapper/H, q()I, [email protected][libDexHelper.so]0x23568)RegisterNative(com/secneo/apkwrapper/H, mu()I, [email protected][libDexHelper.so]0x1f250)

0x218A8

system_property_get ro.build.version.release/ro.build.version.sdk/ro.build.version.codename,最终返回sdkversion。

0x22068

创建一些目录:
/data/usr/0/包名/.cache/oat/data/usr/0/包名/.cache/oat/arm64/data/usr/0/包名/.payload

0x22a90

模拟器检测,特征字符串:
vboxsf/mnt/shared/install_apknemusf/mnt/shell/emulated/0/Music sharefolder/sdcard/windows/BstSharedFolder

0x23568

读proc/pid/cmdline找字符串":bbs",没搞懂这是什么意思。这个函数名是is_magisk_check_process。

0x247C0

调用setOuterContext。

0x24C08

system_property_get ro.product.brand,针对华为/荣耀机型,调用startLoadFromDisk。

0x26278

getDeclaredFields获取field对象数组之后调用equals,返回查找的指定的field对象。

0x27290

修改mInitialApplication和mClassLoader。

0x2921C

修改mAllApplications(remove和add)。

0x29CE8

模拟器检测,特征字符串:
com.bignox.app.store.hdcom.bluestacks.appguidancecom.bluestacks.settingscom.bluestacks.homecom.bluestack.BstCommandProcessorcom.bluestacks.appmart

0x2B670

通过FLAG_DEBUGGABLE判断是debug还是release。

0x2CAE0

通过android.content.pm.Signature获取签名的md5。

0x2F158

通过access以下文件判断是否被root:
/sbin/.magisk//sbin/.core/img/sbin/.core/mirror/sbin/.core/db-0/magisk.db

0x2F6E4

读/proc/self/cmdline,调用java层的com.secneo.apkwrapper.H.j,调用bindService,获取android_id,调用android.app.Application.attach,如果包名是com.huawei.irportalapp.uat调用setOuterContext。

0x31474

调用java层的com.secneo.apkwrapper.H.f(ff)加载v1filter.jar和原始dex。

查看/proc/self/maps:
anon:dalvik-classes.dex extracted in memory from v1filter.jar
anon:dalvik-DEX data
把多出来这样的段dump下来。

原始dex:

指令虚拟化是调用JniLib.cV解析执行的,最后一个参数是一个函数code索引,用来查找被虚拟化后的指令,其它是方法参数:

v1filter.jar:

0x3371C

hook libcutils.so/liblog.so中的android_log_write和android_log_buf_write,使其返回0。

0x339FC

currentActivityThread-mPackages-LoadedApk-mResources-getAssets。

0x34A00

调用android.content.res.Resources.getAssets,失败再调用0x339FC。

0x351DC

读取assets文件。

0x35AB0

对传入参数调用makeInMemoryDexElements,修改dalvik.system.DexFile.mFileName。

0x3766C

初始化下列字符串:
/data/user/0/cn.missfresh.application/.cache/classes.jar/data/user/0/cn.missfresh.application/.cache/classes.dex/data/user/0/cn.missfresh.application/.cache/v1filter.jar
调用0x80458计算包名hash,调用0x75AA8。
调用AAssetManager_open读取assets/resthird.data写入v1filter.jar,调用0x31474。

(看别人的分析应该读assets下面两个文件:classes0.jar是被加密的dex,classes.dgc是被加密的抽取后的指令。不过我分析的这个样本中没有classes0.jar和classes.dgc,可能是名字变了)

0x398F8/0x3A08C

检测dexhunter,dumpclass好像是dexhunter里面的吧。特征字符串:
_Z16hprofDumpClassesP15hprof_context_t_Z12dvmDumpClassPK11ClassObjecti_Z9dumpClassP7DexFileidumpclassdump_class

0x3BF10

参数是文件名,返回文件是否存在。

0x3BF7C

模拟器检测,特征字符串:
ueventd.ttVM_x86.rcinit.ttVM_x86.rcfstab.ttVM_x86bluestacksBlueStacks
0x3CE14
system_property_get ro.debuggable,调用检测模拟器的函数。

0x3D814

通过android.hardware.usb.action.USB_STATE监听USB状态。

0x42378

md5。

0x44708

hook下列函数(反调试):
vmDebug::notifyDebuggerActivityStart(hook后:0x446C0)art::Dbg::GoActive(hook后:0x446E4)art::Runtime::AttachAgent(hook后:0x45CF8)

0x46194

system_property_get ro.yunos.version。

0x4C2F0

hook下列函数(指令抽取还原):
art::ClassLinker::DefineClass(hook后:0x46BB8)art::ClassLinker::LoadMethod(hook后:0x46ED4/0x47BB8/0x488C0/0x491F8/0x49B0C)art::OatFile::OatMethod::LinkMethod(hook后:0x46BD8/0x46DB0)

0x4DB80

md5。

0x50280

读/proc/self/maps找到含有包名的段。

0x5074C

调用java层的com.secneo.apkwrapper.H1.find_dexfile。

0x50B60

调用java.lang.StackTraceElement.getMethodName和java.lang.StackTraceElement.getClassName。

0x57424

加载assets中的classes.dgg。

0x598FC

读/proc/self/maps找到libDexHelper.so。

0x59CE8

设置dex2oat的参数,--zip-fd/--oat-fd/--zip-location/--oat-location/--oat-file/--instruction-set。

0x5C600

hook libdvm.so中的函数(类似于0x67544),具体没仔细看,0x5BAA8-0x5BEF8都是被hook后的实现。

0x61E3C

hook libc中的下列函数:
fstatat64(hook后:0x5E778)stat(hook后:0x5E858)close(hook后:0x5EA20)openat(hook后:0x5ED20)open(hook后:0x5ED9C)pread(hook后:0x5FAB8)read(hook后:0x5FC14)mmap64(hook后:0x5FDDC)__openat_2(hook后:0x5FEF4)__open_2(hook后:0x5FF74)

0x64AE8

根据不同SDK版本返回Name Mangling之后的art::DexFileLoader::open。

0x65FE4

根据不同SDK版本返回Name Mangling之后的art::OatFileManager::OpenDexFilesFromOat。

0x67544

hook下列函数:
art::DexFileLoader::open(hook后:0x6D39C/0x6D3E8)art::OatFileManager::OpenDexFilesFromOat(hook后:0x6A2C0/0x6AF14/0x6B9B0/0x6C188/0x6CB5C)

0x6D4A0

patch掉art::Runtime::IsVerificationEnabled。

0x6DAD8

hook art::DexFileVerifier::Verify(hook后:0x6D38C/0x6D394,直接返回1)。

0x6E40C

hook art::DexFileLoader::open(hook后:0x6D39C/0x6D3E8)。

0x70410

hook下列函数:
art::DexFileVerifier::Verify(hook后:0x6EB04/0x6EB0C/0x6EB14,直接返回1)art::DexFile::OpenMemory(hook后:0x74EE8/0x74E90/0x74F38)Art::DexFile(hook后:0x74E30/0x74F88)

0x75054

hook libdvm.so中的函数,具体没仔细看,0x6EB1C/0x74DEC/0x6FFBC都是被hook后的实现。

0x75AA8

读java.lang.DexCache.dexfile(这个dexfile就是解压apk之后根目录的那个classes.dex)。

0x767F8

参数是so文件路径,打开该so文件。

0x76C8C

参数是libart.so中的一个函数,返回该函数地址。

0x76CCC

第一个参数是so中的函数名,第二个参数是so的相对路径,返回该函数在so中的地址。

0x76D90

参数是libdexfile.so中的一个函数,返回该函数地址。

0x76DD4

参数是libjdwp.so中的一个函数,返回该函数地址。

0x76E18

md5。

0x7783C

字符串解密函数。

0x79270

计算传入字符串的hash(不完全是md5)。

0x7A240

热补丁检测,特征字符串:
nuwaandfixhotfix.RiskStutinker

0x80458

调用0x79270。

0x804A8

hook libc中的下列函数:
msync(hook后:0x78470)close(hook后:0x7AF50)munmap(hook后:0x7A568)openat64(hook后:0x7DC48)__open_2(hook后:0x7DC80)_open64(hook后:0x7DCB8)_openat_2(hook后:0x7DCF0)ftruncate64(hook后:0x7DD30)mmap64(hook后:0x7EF60)pread64(hook后:0x7F5D0)read(hook后:0x7F7DC)write(hook后:0x8022C)

0x87F98

hook libdvm.so中的函数(类似于0x44708),具体没仔细看,0x856C0/0x87F00/0x87F4C都是被hook后的实现。

0x889B0

patch掉art::Runtime::UseJitCompilation。

0x8A794/0x8B71C/0x8B890

hook函数实现。

0x917E8

读/proc/sys/fs/inotify/max_queued_watches。

0x91848

读/proc/sys/fs/inotify/max_user_instances。

0x918A8

读/proc/sys/fs/inotify/max_user_watches。

0x95778

看起来好像是通过判断时间实现的反调试。

0x95A28

字符串查找函数。

0x95B9C

字符串解密函数。

0x95D60

socket连接。

0x96398

frida检测,读/proc/self/task,特征字符串:gum-js-loop;读/proc/self/fd,特征字符串linjector。

0x995D0

xposed检测,特征字符串:
.xposed.xposedbridgexposed_art

0x99D28

hook框架检测,特征字符串:
fridaddi_hookdexposedsubstrateadbi_hookMSFindSymbolhook_precallhook_postcallMSHookFunctionDexposedBridgeMSCloseFunctiondexstuff_loaddexdexposedIsHookedALLINONEs_arthookdexstuff_resolv_dvmdexposedCallHandlerart_java_method_hookartQuickToDispatcherdexstuff_defineclassdalvik_java_method_hookart_quick_call_entrypointfrida_agent_main
0x9C0BC
调用0x96398检测frida,system_property_get ro.product.model,调用0x9FD88检测xposed和自动脱壳机,hook dlopen(hook后:0x9B89C)和ptrace(hook后:0x95BF8)。

0x9CFCC

通过读取/proc/%d/status判断TracerPid等实现反调试。

0x9D878

通过读取/proc/%d/wchan判断是不是ptrace_stop实现反调试。

0x9DCF4

通过读取/proc/%ld/task/%ld/status判断TracerPid等实现反调试。

0x9ED44

通过java.lang.StackTraceElement.getClassName打印函数调用栈进行xposed检测。

0x9F770

通过java.lang.ClassLoader.getSystemClassLoader.loadClass打印类加载器进行xposed检测。

0x9FD88

调用0x9ED44和0x9F770,通过判断ServiceManager里是否有user.xposed.system进行xposed检测,然后检测自动脱壳机:

fart(
https://github.com/hanbinglengyue/FART)
FUPK3(
https://github.com/F8LEFT/FUPK3)
Youpk(
https://github.com/Youlor/Youpk)
检测方法是判断下列类或者方法是否存在:
dumpMethodCodefartthreadfartandroid/app/fupk3/Fupkandroid/app/fupk3/Globalandroid/app/fupk3/UpkConfigandroid/app/fupk3/FRefInvokecn/youlor/Unpacker

0xA18D4

getInstalledApplications获取系统中安装的APP信息。

0xA7D3C

解密出字符串Java和JNI_OnLoad,hook了几个函数,被hook的原地址未知,新地址:0xA43A0/0xA485C/0xA48F4/0xA54B0;hook dlsym(hook后:0xA4554)和dlopen(hook后:0xA4D30)。

0xB4B94

hook libc中的下列函数:
write(hook后:0xAA2CC)pwrite64(hook后:0xAA51C)close(hook后:0xAA774)read64(hook后:0xAAA9C)openat64(hook后:0xAACB8)__openat_2(hook后:0xAB6D4)__open_2(hook后:0xAC0F4)open64(hook后:0xACB10)read(hook后:0xAFE18)mmap64(hook后:0xB1C54)
system_property_get debug.atrace.tags.enableflags,hook bionic_trace_begin和bionic_trace_end(hook后:0xA8EF4和0xA8EF8,直接返回),没有找到则hook g_trace_marker_fd(hook后:0xA8EFC,返回-1)。

这些hook是为了透明加密,具体没仔细看,之前论坛也有人分析过,估计应该没有太大的变化:
梆梆加固之透明加密分析

0xB9BEC

sha1。

0xBAE64

md5init。

0xC3378

base64。

0xC5DDC

base64。

0xC7B18

APK签名相关。

0xD024C

sha1。

0xD1C04

sha1init。

0xD2E98

md5。

0xD6484

调用0xD75A0。

0xD5CDC

读/proc/self/cmdline。

0xD6578

hook libdvm.so中的函数(hook后:0xD6988)。

0xD68A8

根据off_12EF10处的值判断调用0xD6484还是0xD6578。

0xD75A0

hook libaoc.so中的函数(hook后:0xD69BC)。

0x3EA68

JNI_OnLoad。分析环境pixel4 android10,动态分析过程中一些没有被调用的函数不再分析。

1.初始化cpuabi字符串(arm64)于0x12E7C8

2.初始化so名字符串(libDexHelper)于0x12EC38

3.初始化字符串com/secneo/apkwrapper/H于0x137B10

4.调用0x1E520

5.
JNIEnv->FindClass(com/secneo/apkwrapper/H)JNIEnv->GetStaticFieldID(com/secneo/apkwrapper/H.PKGNAMELjava/lang/String;)JNIEnv->GetStaticObjectField(class com/secneo/apkwrapper/H, PKGNAME Ljava/lang/String; => "cn.missfresh.application")JNIEnv->GetStringUtfChars("cn.missfresh.application")
6.将包名存于0x138040

7.
JNIEnv->FindClass(android/app/ActivityThread)JNIEnv->GetStaticMethodID(android/app/ActivityThread.currentActivityThread()Landroid/app/ActivityThread;)JNIEnv->CallStaticObjectMethodV(class android/app/ActivityThread, currentActivityThread())JNIEnv->GetMethodID(android/app/ActivityThread.getSystemContext()Landroid/app/ContextImpl;)JNIEnv->CallObjectMethodV(android.app.ActivityThread, getSystemContext())JNIEnv->FindClass(android/app/ContextImpl)JNIEnv->GetMethodID(android/app/ContextImpl.getPackageManager()Landroid/content/pm/PackageManager;)JNIEnv->CallObjectMethodV(android.app.ContextImpl, getPackageManager())JNIEnv->GetMethodID(android/content/pm/PackageManager.getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;)JNIEnv->NewStringUTF("cn.missfresh.application")JNIEnv->CallObjectMethodV(android.content.pm.PackageManager, getPackageInfo("cn.missfresh.application", 0x0))JNIEnv->GetFieldID(android/content/pm/PackageInfo.applicationInfo Landroid/content/pm/ApplicationInfo;)JNIEnv->GetObjectField(android.content.pm.PackageInfo, applicationInfo Landroid/content/pm/ApplicationInfo;)JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.sourceDir Ljava/lang/String;)JNIEnv->GetObjectField(android.content.pm.ApplicationInfo, sourceDir Ljava/lang/String; => "/data/app/cn.missfresh.application-1")JNIEnv->GetStringUtfChars("/data/app/cn.missfresh.application-1")JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.dataDir Ljava/lang/String;)JNIEnv->GetObjectField(android.content.pm.ApplicationInfo, dataDir Ljava/lang/String; => "/data/data/cn.missfresh.application")JNIEnv->GetStringUtfChars("/data/data/cn.missfresh.application")
8.调用0x218A8
9.
JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.nativeLibraryDir Ljava/lang/String;)JNIEnv->GetObjectField([email protected]36d64342, nativeLibraryDir Ljava/lang/String; => "/data/app/cn.missfresh.application-1/lib/arm64")JNIEnv->GetStringUtfChars("/data/app/cn.missfresh.application-1/lib/arm64")
10.读/proc/pid/fd,匹配包名+base.apk,0x12EA38存放指向base.apk完整路径的指针的指针

11.
JNIEnv->FindClass(com/secneo/apkwrapper/H)JNIEnv->GetStaticFieldID(com/secneo/apkwrapper/H.ISMPAASLjava/lang/String;)JNIEnv->GetStaticObjectField(class com/secneo/apkwrapper/H, ISMPAAS Ljava/lang/String; => "###MPAAS###")JNIEnv->GetStringUtfChars("###MPAAS###")
12.将得到的结果和###MPAAS###比较,0x12E7F8指向0x137D9C,0x137D9C存放比较结果

13.调用0x22068

14.调用0x23568

15.调用0x1F250

16.将字符串lib/libart.so存放于0x1378A8

17.读/proc/self/maps,找权限为"r-xp"的lib/libart.so

18.初始化下列字符串:
/data/user/0/cn.missfresh.application/.cache/data/user/0/cn.missfresh.application/.cache/oat/arm64/data/user/0/cn.missfresh.application/.cache/classes.dve/data/app/cn.missfresh.application-xxx/oat/arm64/base.odex
19.fstat /data/app/cn.missfresh.application-xxx/oat/arm64/base.odex

20.计算md5(不太清楚具体算的什么),0x12EC98指向0x130080,0x130080存放计算结果

21.access /data/user/0/cn.missfresh.application/.cache/classes.dve,不存在则把之前算的md5写入该文件;存在则读取其中的值和之前算的比较,不相等则写入新计算的值

22.调用0x3371C(根据标记位决定是否调用)

23.调用0x1FDC8

24.初始化下列字符串:
/data/user/0/cn.missfresh.application/.cache/libDexHelper32/lib/armeabi-v7a/libDexHelper.so/lib/armeabi/libDexHelper.soassets/libDexHelper32
25.调用0x3766C

26.调用0xB4B94

27.调用0x9C0BC

28.调用0xD5CDC

29.调用0x24C08

30.调用0x1C40C

31.调用0xA7D3C

32.结束

参考

梆梆APP加固产品方案浅析:https://www.cnblogs.com/2014asm/p/14547218.html
某加固详细分析总结,另附该加固脱壳机:
https://bbs.pediy.com/thread-252828.htm

总结

样本混淆强度还是比较大的,比前两篇文章中的样本要复杂很多。不过分析过程中也是有一些技巧:比如位置相邻的函数之前其实是有联系的,和另外某些壳的代码有类似的地方,可以网上搜一下旧版本的分析博客,有一些函数名和字符串没有被抹去。

看雪ID:houjingyi

https://bbs.pediy.com/user-home-734571.htm

*本文由看雪论坛 houjingyi 原创,转载请注明来自看雪社区

# 往期推荐

1.AFL速通——流程及afl-fuzz.c源码简析

2.sql注入学习笔记

3.文件上传和文件包含的各种姿势

4.React Native Hermes 逆向实践

5.2022CISCN初赛 ez_usb WriteUp

6.APT Turla样本分析

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458461061&idx=1&sn=ae34ebab6a4902028eaf6347672c2ced&chksm=b18e130f86f99a195003cc9c92e0baeee7278515909c2eeb20bc7451c3a788419809d409f408#rd
如有侵权请联系:admin#unsafe.sh