首页
论坛
课程
招聘
[原创]android so文件攻防实战-libDexHelper.so反混淆
2022-7-10 21:45 8979

[原创]android so文件攻防实战-libDexHelper.so反混淆

2022-7-10 21:45
8979

计划是写一个android中so文件反混淆的系列文章,目前这是第三篇。
第一篇:android so文件攻防实战-百度加固免费版libbaiduprotect.so反混淆
第二篇:android so文件攻防实战-某团libmtguard.so反混淆
今天分析的是企业版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反混淆中我们是交叉引用拿到加密后的字符串和它对应的解密函数的表然后frida主动调用得到的解密后的字符串,但是在这里这个方法就不太好用了。因为这里加密后的字符串是在栈上一个byte一个byte拼起来的,和最后调用解密函数之间可能隔了很多条指令,甚至都不在一个block。
我最后用的是下面这种方案:以0x40110处调用0x186C4处的解密函数为例,这里面字符串解密的逻辑比较简单,需要三个参数。我们可以自己实现也可以用unicorn,我就用unicorn了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import sys
import unicorn
import binascii
import threading
import 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 = 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.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,就处理不了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import unicorn
import binascii
import threading
import subprocess
 
from capstone import *
from capstone.arm64 import *
 
inscnt = 0
start_addr = 0
end_addr = 0
stop_addr = 0
stop_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反混淆见过的那种,但是要更复杂。

比如这里的指令,在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,特征字符串:

1
2
3
4
5
6
7
8
9
10
11
12
libDexHelper.so
libDexHelper-x86.so
libDexHelper-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
.oat
ff c3 01 d1 f3 03 04 aa f4 03 02 aa f5 03 01 aa e8 03 00 aa
GumInvocationListener
GSocketListenerEvent

0x16A30

获取系统属性,读/proc/%d/cmdline,特征字符串:

1
2
3
4
5
6
7
ro.yunos.version
ro.yunos.version.release
persist.sys.dalvik.vm.lib
persist.sys.dalvik.vm.lib.2
/system/bin/dex2oat
LD_OPT_PACKAGENAME
LD_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创建数组:

1
2
3
4
5
6
7
/etc
/sbin
/system
/system/bin
/vendor/bin
/system/sbin
/system/xbin

0x1A740

调用0x19E08创建数组:

1
2
3
4
5
6
com.yellowes.su
eu.chainfire.supersu
com.noshufou.android.su
com.thirdparty.superuser
com.koushikdutta.superuser
com.noshufou.android.su.elite

0x1AF1C

调用0x19E08创建数组:

1
2
3
4
5
6
com.chelpus.lackypatch
com.ramdroid.appquarantine
com.koushikdutta.rommanager
com.dimonvideo.luckypatcher
com.ramdroid.appquarantinepro
com.koushikdutta.rommanager.license

0x1B7D0

调用0x19E08创建数组:

1
2
3
4
5
6
7
8
9
com.saurik.substrate
com.formyhm.hideroot
com.amphoras.hidemyroot
com.devadvance.rootcloak
com.formyhm.hiderootPremium
com.devadvance.rootcloakplus
com.amphoras.hidemyrootadfree
com.zachspong.temprootremovejb
de.robv.android.xposed.installer

0x1C40C

system_property_get ro.product.cpu.abi和读/system/lib/libc.so判断是不是x86架构。

0x1C61C

查看classes.dve是否存在。

0x1C8D8

调用0x19E08创建数组:

1
2
3
4
5
6
7
8
9
10
11
/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

初始化一些路径,特征字符串:

1
2
3
4
5
6
7
8
9
10
11
12
.cache
oat
.payload
v1filter.jar
classes.odex
classes.vdex
classes.dex
assets/classes.jar
.cache/classes.jar
.cache/classes.dex
.cache/classes.odex
.cache/classes.vdex

0x1E520

将libc中的一些函数的地址放到.DATA。

1
2
3
4
5
6
7
0x137BB0 fopen
0x137BB8 fclose
0x137BC0 fgets
0x137BC8 fwrite
0x137BD0 fread
0x137BD8 sprintf
0x137BE0 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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RegisterNative(com/secneo/apkwrapper/H, attach(Landroid/app/Application;Landroid/content/Context;)V, RX@0x4002f6e4[libDexHelper.so]0x2f6e4)
RegisterNative(com/secneo/apkwrapper/H, b(Landroid/content/Context;Landroid/app/Application;)V, RX@0x400247c0[libDexHelper.so]0x247c0)
RegisterNative(com/secneo/apkwrapper/H, c()V, RX@0x40024c08[libDexHelper.so]0x24c08)
RegisterNative(com/secneo/apkwrapper/H, d(Ljava/lang/String;)Ljava/lang/String;, RX@0x40023d04[libDexHelper.so]0x23d04)
RegisterNative(com/secneo/apkwrapper/H, e(Ljava/lang/Object;Ljava/util/List;Ljava/lang/String;)[Ljava/lang/Object;, RX@0x40035ab0[libDexHelper.so]0x35ab0)
RegisterNative(com/secneo/apkwrapper/H, f()[Ljava/lang/String;, RX@0x4001a740[libDexHelper.so]0x1a740)
RegisterNative(com/secneo/apkwrapper/H, g()[Ljava/lang/String;, RX@0x4001af1c[libDexHelper.so]0x1af1c)
RegisterNative(com/secneo/apkwrapper/H, h()[Ljava/lang/String;, RX@0x4001b7d0[libDexHelper.so]0x1b7d0)
RegisterNative(com/secneo/apkwrapper/H, n()[Ljava/lang/String;, RX@0x4001c8d8[libDexHelper.so]0x1c8d8)
RegisterNative(com/secneo/apkwrapper/H, j()[Ljava/lang/String;, RX@0x4001a058[libDexHelper.so]0x1a058)
RegisterNative(com/secneo/apkwrapper/H, k()Ljava/lang/String;, RX@0x40019778[libDexHelper.so]0x19778)
RegisterNative(com/secneo/apkwrapper/H, l()Ljava/lang/String;, RX@0x4001987c[libDexHelper.so]0x1987c)
RegisterNative(com/secneo/apkwrapper/H, m()Ljava/lang/String;, RX@0x40019674[libDexHelper.so]0x19674)
RegisterNative(com/secneo/apkwrapper/H, bb(Landroid/content/Context;Landroid/app/Application;Landroid/app/Application;)V, RX@0x4002921c[libDexHelper.so]0x2921c)
RegisterNative(com/secneo/apkwrapper/H, o(Landroid/content/Context;)I, RX@0x4002f158[libDexHelper.so]0x2f158)
RegisterNative(com/secneo/apkwrapper/H, p()V, RX@0x4001875c[libDexHelper.so]0x1875c)
RegisterNative(com/secneo/apkwrapper/H, q()I, RX@0x40023568[libDexHelper.so]0x23568)
RegisterNative(com/secneo/apkwrapper/H, mu()I, RX@0x4001f250[libDexHelper.so]0x1f250)

0x218A8

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

0x22068

创建一些目录:

1
2
3
/data/usr/0/包名/.cache/oat
/data/usr/0/包名/.cache/oat/arm64
/data/usr/0/包名/.payload

0x22a90

模拟器检测,特征字符串:

1
2
3
4
5
vboxsf
/mnt/shared/install_apk
nemusf
/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

模拟器检测,特征字符串:

1
2
3
4
5
6
com.bignox.app.store.hd
com.bluestacks.appguidance
com.bluestacks.settings
com.bluestacks.home
com.bluestack.BstCommandProcessor
com.bluestacks.appmart

0x2B670

通过FLAG_DEBUGGABLE判断是debug还是release。

0x2CAE0

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

0x2F158

通过access以下文件判断是否被root:

1
2
3
4
/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

初始化下列字符串:

1
2
3
/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里面的吧。特征字符串:

1
2
3
4
5
_Z16hprofDumpClassesP15hprof_context_t
_Z12dvmDumpClassPK11ClassObjecti
_Z9dumpClassP7DexFilei
dumpclass
dump_class

0x3BF10

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

0x3BF7C

模拟器检测,特征字符串:

1
2
3
4
5
ueventd.ttVM_x86.rc
init.ttVM_x86.rc
fstab.ttVM_x86
bluestacks
BlueStacks

0x3CE14

system_property_get ro.debuggable,调用检测模拟器的函数。

0x3D814

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

0x42378

md5。

0x44708

hook下列函数(反调试):

1
2
3
vmDebug::notifyDebuggerActivityStart(hook后:0x446C0)
art::Dbg::GoActive(hook后:0x446E4)
art::Runtime::AttachAgent(hook后:0x45CF8)

0x46194

system_property_get ro.yunos.version。

0x4C2F0

hook下列函数(指令抽取还原):

1
2
3
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中的下列函数:

1
2
3
4
5
6
7
8
9
10
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下列函数:

1
2
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下列函数:

1
2
3
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

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

1
2
3
4
5
nuwa
andfix
hotfix
.RiskStu
tinker

0x80458

调用0x79270。

0x804A8

hook libc中的下列函数:

1
2
3
4
5
6
7
8
9
10
11
12
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检测,特征字符串:

1
2
3
.xposed.
xposedbridge
xposed_art

0x99D28

hook框架检测,特征字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
frida
ddi_hook
dexposed
substrate
adbi_hook
MSFindSymbol
hook_precall
hook_postcall
MSHookFunction
DexposedBridge
MSCloseFunction
dexstuff_loaddex
dexposedIsHooked
ALLINONEs_arthook
dexstuff_resolv_dvm
dexposedCallHandler
art_java_method_hook
artQuickToDispatcher
dexstuff_defineclass
dalvik_java_method_hook
art_quick_call_entrypoint
frida_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)
检测方法是判断下列类或者方法是否存在:

1
2
3
4
5
6
7
8
dumpMethodCode
fartthread
fart
android/app/fupk3/Fupk
android/app/fupk3/Global
android/app/fupk3/UpkConfig
android/app/fupk3/FRefInvoke
cn/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中的下列函数:

1
2
3
4
5
6
7
8
9
10
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.

1
2
3
4
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

1
2
3
JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.nativeLibraryDir Ljava/lang/String;)
JNIEnv->GetObjectField(android.content.pm.ApplicationInfo@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.

1
2
3
4
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.初始化下列字符串:

1
2
3
4
/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.初始化下列字符串:

1
2
3
4
/data/user/0/cn.missfresh.application/.cache/libDexHelper32
/lib/armeabi-v7a/libDexHelper.so
/lib/armeabi/libDexHelper.so
assets/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

总结

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


[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2022-7-20 14:14 被houjingyi编辑 ,原因:
上传的附件:
收藏
点赞11
打赏
分享
打赏 + 150.00雪花
打赏次数 1 雪花 + 150.00
 
赞赏  Editor   +150.00 2022/08/08 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (8)
雪    币: 1015
活跃值: 活跃值 (962)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huangjw 活跃值 2022-7-10 23:14
2
0
谢谢分享,学习中。 目测是精华贴
雪    币: 162
活跃值: 活跃值 (2083)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huaerxiela 活跃值 2022-7-11 12:01
3
0
666
雪    币: 431
活跃值: 活跃值 (1082)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
王麻子本人 活跃值 2022-7-11 12:54
4
0
雪    币: 3965
活跃值: 活跃值 (469)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
wpwpahj 活跃值 2022-7-14 10:14
5
0
大佬能来一期so反调试、hook的吗
雪    币: 205
活跃值: 活跃值 (751)
能力值: ( LV5,RANK:73 )
在线值:
发帖
回帖
粉丝
falconnnn 活跃值 2022-7-14 17:25
6
0
能否发一下原apk?,只有so无法dump分析
雪    币: 5909
活跃值: 活跃值 (4219)
能力值: ( LV15,RANK:531 )
在线值:
发帖
回帖
粉丝
houjingyi 活跃值 11 2022-7-17 10:57
7
0
falconnnn 能否发一下原apk?,只有so无法dump分析
文章里面有包名的
雪    币: 38
活跃值: 活跃值 (360)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
轻快笑着行 活跃值 2022-7-21 13:58
8
0
mark
雪    币: 248
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
@=llfly 活跃值 2022-7-23 16:55
9
0
mark
游客
登录 | 注册 方可回帖
返回