Deobfuscating libDexHelper.so: Inside an Enterprise Android Shell
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Deobfuscating libDexHelper.so: Inside an Enterprise Android Shell
This is the third in a series on deobfuscating .so files on Android. The first covered libbaiduprotect.so (a free shell); the second covered libmtguard.so (a food-delivery app). Today’s target is an enterprise 64-bit sample I found with LibChecker — recent enough to be interesting. Based on prior analysis by others, libDexHelper.so is the instruction-extraction implementation, and libdexjni.so is the VMP implementation.
Removing the obfuscation
Since it’s encrypted, it can’t be decompiled directly. Breakpoint in libart.so, enter JNI_OnLoad, and dump it:

Figure 1 — Dumping from JNI_OnLoad
But it still won’t F5 cleanly. The remaining obfuscation:
1. Junk instructions

Figure 2 — Junk instructions
These junk instructions live in a switch branch that is never executed. Wherever IDA can’t MakeCode, patch it to NOP and MakeCode again.
2. String encryption
There are several string-decryption functions: 0x186C4, 0x7783C, 0x95B9C. In the first post (libbaiduprotect.so), I took the cross-references to get the table of encrypted strings and their decrypt functions, then called them via Frida to get the plaintext. That approach doesn’t work well here, because the encrypted string is assembled byte-by-byte on the stack, often many instructions — or even a whole block — away from the final call to the decrypt function.
The approach I settled on: take the call to the decrypt function 0x186C4 at 0x40110 as an example. The decryption logic is simple and needs three arguments. I could reimplement it myself or use unicorn — I used unicorn.
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)
There are several hundred call sites, so doing this by hand is out of the question. I wrote a second script that calls decstr.py: it finds every call to a decrypt function via cross-references, sets the start address to the start of that block and the end address to the call site, runs unicorn to recover the three arguments decstr.py needs, then invokes decstr.py. On unicorn.unicorn.UcError there are two strategies — skip the address (loop_call_prepare_arg1, keeping the start address) or advance the start address to the next instruction (loop_call_prepare_arg2). The approach still has room to improve: if the code that prepares the arguments and the call to the decrypt function aren’t in the same block, it can’t handle them.
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()
Even worse, many strings are decrypted inside the function itself — I haven’t found a good method for those either.
3. Control-flow obfuscation
The first kind shuffles normally sequential instructions into a switch form; it doesn’t hurt too much:

Figure 3 — Switch-form control flow
The second kind computes the jump target dynamically — basically what I saw in libmtguard.so, but more complex:

Figure 4 — Dynamically computed jump target
Take the instructions here: at 0x1DA0C a value is assigned to X2 — an address in the .data segment — with W0 as the offset. After reading the value, at 0x1DA18 it’s multiplied by 4 and added to 0x1DA20; the result is X0’s value at 0x1DA1C. That raises a few questions:
- How to determine the value assigned to X2 at
0x1DA0C. - How to determine
0x1DA00, where the instruction should be changed into a jump. - How to find every instruction that jumps to
0x1DA1Cand rewrite the jump target to the computed X0.
The first problem is like the string decryption case: find the STR XX, [X29,#0x190+var_118] that corresponds to the LDR X2, [X29,#0x190+var_118], then find the instruction that assigns XX — but those two instructions may be several blocks away from the BR X0. My solution uses IDA’s idaapi.FlowChar to recurse backwards through blocks. The limitation: IDA must have correctly identified the function’s start, otherwise the needed instruction and the BR X0 may end up in different functions and it won’t work.
For the second problem, while recursing backwards I first find the instruction at 0x1D9D4 that assigns W0, then from 0x1D9D4 up to 0x1DA1C find the first address with a cross-reference — 0x1DA04. Its preceding instruction, 0x1DA00, is where the jump should go.
For the third problem, once 0x1DA00 is fixed, every cross-referenced address from 0x1DA00 to 0x1DA1C needs its jump target rewritten. There are details here:
(1) If W0 is assigned by CSEL, CSET, CSINC and the like, as below, then 0x1DE80 and 0x1DE84 must be changed to B.GE and B.LT.
(2) At 0x1DE80, CSEL W0, WZR, W8, LT — W8 is assigned at 0x1D9DC MOV W8, #5, so my code keeps a register_value_dict: after patching the instruction at 0x1DA00, it reads all instructions from the block at 0x1DA00 to the block at 0x1DA1C, finds the register assignments, and stores the values.

Figure 5 — Before the patch

Figure 6 — After the patch
(3) Some places also have a sub instruction to account for — e.g. below, the jump at 0x33394 should be computed with W8 equal to 4.

Figure 7 — A sub instruction to account for
The final script is attached. There are still cases it can’t handle, but they’re no longer a big deal — dynamic debugging can pin those down.
4. Dynamically computed function addresses

Figure 8 — Dynamically computed function address (1)

Figure 9 — Dynamically computed function address (2)
This one is clear in IDA: v35 is really off_12EB80[0] — i.e. it calls p329AAB59961F6410ABA963EF972FE303 at 0x80FE0.
Now let’s look at what libDexHelper.so actually does. Time is limited, so I couldn’t analyze every part in detail, and some of the analysis may be off — read with that caveat.
Functional analysis
The analysis of JNI_OnLoad (0x3EA68) is at the end.
0x15960
Reads /proc/self/maps. Signature strings:
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
Reads system properties and /proc/%d/cmdline. Signature strings:
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 means YunOS in ART mode; 1 means YunOS in dalvik mode; 0 means not YunOS.
0x17A70
md5.
0x186C4
The string-decryption function.
0x19674
Returns the string rw.
0x19778
Returns the string su.
0x1987C
Returns the string mount.
0x19998
Writes the classes.dve file.
0x19B48
Reads files in a directory.
0x19E08
Creates a String array; first argument is a string list, second is the array length.
0x1A058
Calls 0x19E08 to create an array:
/etc
/sbin
/system
/system/bin
/vendor/bin
/system/sbin
/system/xbin
0x1A740
Calls 0x19E08 to create an array:
com.yellowes.su
eu.chainfire.supersu
com.noshufou.android.su
com.thirdparty.superuser
com.koushikdutta.superuser
com.noshufou.android.su.elite
0x1AF1C
Calls 0x19E08 to create an array:
com.chelpus.lackypatch
com.ramdroid.appquarantine
com.koushikdutta.rommanager
com.dimonvideo.luckypatcher
com.ramdroid.appquarantinepro
com.koushikdutta.rommanager.license
0x1B7D0
Calls 0x19E08 to create an array:
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
Reads ro.product.cpu.abi via system_property_get and reads /system/lib/libc.so to check for x86.
0x1C61C
Checks whether classes.dve exists.
0x1C8D8
Calls 0x19E08 to create an array:
/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
Initializes some paths. Signature strings:
.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
Puts the addresses of some libc functions into .DATA:
0x137BB0 fopen
0x137BB8 fclose
0x137BC0 fgets
0x137BC8 fwrite
0x137BD0 fread
0x137BD8 sprintf
0x137BE0 pthread_create
0x1F250
Reads /proc/self/cmdline and checks for com.miui.packageinstaller to tell whether it was launched by Xiaomi’s package-installer component.
0x1F710
First reads ro.product.manufacturer and ro.product.model to check for Samsung, then reads ro.build.characteristics for emulator.
0x1FDC8
Registers the following native methods:
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
Reads ro.build.version.release, ro.build.version.sdk, and ro.build.version.codename via system_property_get, and returns the SDK version.
0x22068
Creates some directories:
/data/usr/0/<package>/.cache/oat
/data/usr/0/<package>/.cache/oat/arm64
/data/usr/0/<package>/.payload
0x22A90
Emulator detection. Signature strings:
vboxsf
/mnt/shared/install_apk
nemusf
/mnt/shell/emulated/0/Music sharefolder
/sdcard/windows/BstSharedFolder
0x23568
Reads /proc/pid/cmdline looking for the string :bbs — I never figured out what that’s for. The function is named is_magisk_check_process.
0x247C0
Calls setOuterContext.
0x24C08
Reads ro.product.brand via system_property_get; for Huawei/Honor devices it calls startLoadFromDisk.
0x26278
Calls getDeclaredFields to get the field-object array, then calls equals and returns the requested field object.
0x27290
Modifies mInitialApplication and mClassLoader.
0x2921C
Modifies mAllApplications (remove and add).
0x29CE8
Emulator detection. Signature strings:
com.bignox.app.store.hd
com.bluestacks.appguidance
com.bluestacks.settings
com.bluestacks.home
com.bluestack.BstCommandProcessor
com.bluestacks.appmart
0x2B670
Determines debug vs release via FLAG_DEBUGGABLE.
0x2CAE0
Gets the signature’s md5 via android.content.pm.Signature.
0x2F158
Checks for root by accessing these files:
/sbin/.magisk/
/sbin/.core/img
/sbin/.core/mirror
/sbin/.core/db-0/magisk.db
0x2F6E4
Reads /proc/self/cmdline, calls the Java-level com.secneo.apkwrapper.H.j, calls bindService, gets android_id, calls android.app.Application.attach, and — if the package name is com.huawei.irportalapp.uat — calls setOuterContext.
0x31474
Calls the Java-level com.secneo.apkwrapper.H.f(ff) to load v1filter.jar and the original dex. Looking at /proc/self/maps:
anon:dalvik-classes.dex extracted in memory from v1filter.jar
anon:dalvik-DEX data
I dump the extra segments. The original dex:

Figure 10 — The original dex
Instruction virtualization is executed by JniLib.cV; the last argument is a function-code index used to look up the virtualized instructions, the rest are method parameters:

Figure 11 — Instruction virtualization
v1filter.jar:

Figure 12 — v1filter.jar
0x3371C
Hooks android_log_write and android_log_buf_write in libcutils.so/liblog.so and makes them return 0.
0x339FC
currentActivityThread → mPackages → LoadedApk → mResources → getAssets.
0x34A00
Calls android.content.res.Resources.getAssets; falls back to 0x339FC on failure.
0x351DC
Reads an assets file.
0x35AB0
Calls makeInMemoryDexElements on the argument and modifies dalvik.system.DexFile.mFileName.
0x3766C
Initializes the following strings:
/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
Calls 0x80458 to compute a package-name hash, then 0x75AA8. Calls AAssetManager_open to read assets/resthird.data and write it to v1filter.jar, then calls 0x31474. (Per other write-ups, it should read two files under assets: classes0.jar, the encrypted dex, and classes.dgc, the encrypted extracted instructions. But the sample I analyzed had neither — maybe they were renamed.)
0x398F8 / 0x3A08C
Detects dexhunter (dumpclass is a dexhunter thing, I think). Signature strings:
_Z16hprofDumpClassesP15hprof_context_t
_Z12dvmDumpClassPK11ClassObjecti
_Z9dumpClassP7DexFilei
dumpclass
dump_class
0x3BF10
Takes a filename and returns whether the file exists.
0x3BF7C
Emulator detection. Signature strings:
ueventd.ttVM_x86.rc
init.ttVM_x86.rc
fstab.ttVM_x86
bluestacks
BlueStacks
0x3CE14
Reads ro.debuggable via system_property_get and calls the emulator-detection functions.
0x3D814
Listens for USB state via android.hardware.usb.action.USB_STATE.
0x42378
md5.
0x44708
Hooks the following functions (anti-debugging):
vmDebug::notifyDebuggerActivityStart (patched to 0x446C0)
art::Dbg::GoActive (patched to 0x446E4)
art::Runtime::AttachAgent (patched to 0x45CF8)
0x46194
Reads ro.yunos.version via system_property_get.
0x4C2F0
Hooks the following functions (instruction extraction / restoration):
art::ClassLinker::DefineClass (patched to 0x46BB8)
art::ClassLinker::LoadMethod (patched to 0x46ED4/0x47BB8/0x488C0/0x491F8/0x49B0C)
art::OatFile::OatMethod::LinkMethod (patched to 0x46BD8/0x46DB0)
0x4DB80
md5.
0x50280
Reads /proc/self/maps and finds the segment containing the package name.
0x5074C
Calls the Java-level com.secneo.apkwrapper.H1.find_dexfile.
0x50B60
Calls java.lang.StackTraceElement.getMethodName and getClassName.
0x57424
Loads classes.dgg from assets.
0x598FC
Reads /proc/self/maps and finds libDexHelper.so.
0x59CE8
Sets up dex2oat arguments: --zip-fd, --oat-fd, --zip-location, --oat-location, --oat-file, --instruction-set.
0x5C600
Hooks functions in libdvm.so (similar to 0x67544); didn’t look closely. 0x5BAA8–0x5BEF8 are the patched implementations.
0x61E3C
Hooks the following libc functions:
fstatat64 (patched to 0x5E778)
stat (patched to 0x5E858)
close (patched to 0x5EA20)
openat (patched to 0x5ED20)
open (patched to 0x5ED9C)
pread (patched to 0x5FAB8)
read (patched to 0x5FC14)
mmap64 (patched to 0x5FDDC)
__openat_2 (patched to 0x5FEF4)
__open_2 (patched to 0x5FF74)
0x64AE8
Returns the name-mangled art::DexFileLoader::open for the given SDK version.
0x65FE4
Returns the name-mangled art::OatFileManager::OpenDexFilesFromOat for the given SDK version.
0x67544
Hooks the following functions:
art::DexFileLoader::open (patched to 0x6D39C/0x6D3E8)
art::OatFileManager::OpenDexFilesFromOat (patched to 0x6A2C0/0x6AF14/0x6B9B0/0x6C188/0x6CB5C)
0x6D4A0
Patches art::Runtime::IsVerificationEnabled.
0x6DAD8
Hooks art::DexFileVerifier::Verify (patched to 0x6D38C/0x6D394, which just return 1).
0x6E40C
Hooks art::DexFileLoader::open (patched to 0x6D39C/0x6D3E8).
0x70410
Hooks the following functions:
art::DexFileVerifier::Verify (patched to 0x6EB04/0x6EB0C/0x6EB14, return 1)
art::DexFile::OpenMemory (patched to 0x74EE8/0x74E90/0x74F38)
Art::DexFile (patched to 0x74E30/0x74F88)
0x75054
Hooks functions in libdvm.so; didn’t look closely. 0x6EB1C/0x74DEC/0x6FFBC are the patched implementations.
0x75AA8
Reads java.lang.DexCache.dexfile (this dexfile is the classes.dex at the APK root after unpacking).
0x767F8
Takes an .so path and opens it.
0x76C8C
Takes a function in libart.so and returns its address.
0x76CCC
First argument is a function name in the .so, second is the .so’s relative path; returns the function’s address in the .so.
0x76D90
Takes a function in libdexfile.so and returns its address.
0x76DD4
Takes a function in libjdwp.so and returns its address.
0x76E18
md5.
0x7783C
String-decryption function.
0x79270
Computes a hash of the input string (not exactly md5).
0x7A240
Hot-patch detection. Signature strings:
nuwa
andfix
hotfix
.RiskStu
tinker
0x80458
Calls 0x79270.
0x804A8
Hooks the following libc functions:
msync (patched to 0x78470)
close (patched to 0x7AF50)
munmap (patched to 0x7A568)
openat64 (patched to 0x7DC48)
__open_2 (patched to 0x7DC80)
_open64 (patched to 0x7DCB8)
_openat_2 (patched to 0x7DCF0)
ftruncate64 (patched to 0x7DD30)
mmap64 (patched to 0x7EF60)
pread64 (patched to 0x7F5D0)
read (patched to 0x7F7DC)
write (patched to 0x8022C)
0x87F98
Hooks functions in libdvm.so (similar to 0x44708); didn’t look closely. 0x856C0/0x87F00/0x87F4C are the patched implementations.
0x889B0
Patches art::Runtime::UseJitCompilation.
0x8A794 / 0x8B71C / 0x8B890
Hook function implementations.
0x917E8
Reads /proc/sys/fs/inotify/max_queued_watches.
0x91848
Reads /proc/sys/fs/inotify/max_user_instances.
0x918A8
Reads /proc/sys/fs/inotify/max_user_watches.
0x95778
Looks like time-based anti-debugging.
0x95A28
String-search function.
0x95B9C
String-decryption function.
0x95D60
Socket connection.
0x96398
Frida detection: reads /proc/self/task looking for gum-js-loop, and /proc/self/fd looking for linjector.
0x995D0
Xposed detection. Signature strings:
.xposed.
xposedbridge
xposed_art
0x99D28
Hook-framework detection. Signature strings:
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
Calls 0x96398 to detect Frida, reads ro.product.model via system_property_get, calls 0x9FD88 to detect Xposed and automatic unpackers, and hooks dlopen (patched to 0x9B89C) and ptrace (patched to 0x95BF8).
0x9CFCC
Anti-debugging by reading /proc/%d/status and checking TracerPid and similar.
0x9D878
Anti-debugging by reading /proc/%d/wchan and checking for ptrace_stop.
0x9DCF4
Anti-debugging by reading /proc/%ld/task/%ld/status and checking TracerPid and similar.
0x9ED44
Xposed detection by printing the call stack via java.lang.StackTraceElement.getClassName.
0x9F770
Xposed detection by printing the class loader via java.lang.ClassLoader.getSystemClassLoader.loadClass.
0x9FD88
Calls 0x9ED44 and 0x9F770, detects Xposed by checking whether user.xposed.system exists in the ServiceManager, then detects automatic unpackers:
- FART (https://github.com/hanbinglengyue/FART)
- FUPK3 (https://github.com/F8LEFT/FUPK3)
- Youpk (https://github.com/Youlor/Youpk)
The check is whether these classes or methods exist:
dumpMethodCode
fartthread
fart
android/app/fupk3/Fupk
android/app/fupk3/Global
android/app/fupk3/UpkConfig
android/app/fupk3/FRefInvoke
cn/youlor/Unpacker
0xA18D4
getInstalledApplications — gets installed apps on the system.
0xA7D3C
Decrypts the strings Java and JNI_OnLoad, hooks a few functions (original addresses unknown; new addresses: 0xA43A0/0xA485C/0xA48F4/0xA54B0), and hooks dlsym (patched to 0xA4554) and dlopen (patched to 0xA4D30).
0xB4B94
Hooks the following libc functions:
write (patched to 0xAA2CC)
pwrite64 (patched to 0xAA51C)
close (patched to 0xAA774)
read64 (patched to 0xAAA9C)
openat64 (patched to 0xAACB8)
__openat_2 (patched to 0xAB6D4)
__open_2 (patched to 0xAC0F4)
open64 (patched to 0xACB10)
read (patched to 0xAFE18)
mmap64 (patched to 0xB1C54)
Reads debug.atrace.tags.enableflags via system_property_get, hooks bionic_trace_begin and bionic_trace_end (patched to 0xA8EF4 and 0xA8EF8, which return directly); if not found, hooks g_trace_marker_fd (patched to 0xA8EFC, returns -1). These hooks implement transparent encryption — I didn’t look closely, and it’s been analyzed before, so it’s probably unchanged.
0xB9BEC
sha1.
0xBAE64
md5init.
0xC3378
base64.
0xC5DDC
base64.
0xC7B18
APK-signature related.
0xD024C
sha1.
0xD1C04
sha1init.
0xD2E98
md5.
0xD6484
Calls 0xD75A0.
0xD5CDC
Reads /proc/self/cmdline.
0xD6578
Hooks a function in libdvm.so (patched to 0xD6988).
0xD68A8
Decides whether to call 0xD6484 or 0xD6578 based on the value at off_12EF10.
0xD75A0
Hooks a function in libaoc.so (patched to 0xD69BC).
0x3EA68
JNI_OnLoad. Environment: Pixel 4, Android 10. Functions that weren’t called during dynamic analysis are left unanalyzed.
- Initializes the cpuabi string (
arm64) at 0x12E7C8. - Initializes the .so-name string (
libDexHelper) at 0x12EC38. - Initializes the string
com/secneo/apkwrapper/Hat 0x137B10. - Calls 0x1E520.
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")
- Stores the package name at 0x138040.
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")
- Calls 0x218A8.
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")
- Reads
/proc/pid/fd, matches<package>+base.apk; 0x12EA38 holds a pointer-to-pointer to the fullbase.apkpath.
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###")
- Compares the result with
###MPAAS###; 0x12E7F8 points to 0x137D9C, which holds the comparison result. - Calls 0x22068.
- Calls 0x23568.
- Calls 0x1F250.
- Stores the string
lib/libart.soat 0x1378A8. - Reads
/proc/self/maps, findslib/libart.sowithr-xppermissions. - Initializes the following strings:
/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
fstaton/data/app/cn.missfresh.application-xxx/oat/arm64/base.odex.- Computes an md5 (not sure exactly what of); 0x12EC98 points to 0x130080, which holds the result.
accesson/data/user/0/cn.missfresh.application/.cache/classes.dve; if it doesn’t exist, write the computed md5 to it; if it exists, read it and compare — if different, write the new value.- Calls 0x3371C (based on a flag).
- Calls 0x1FDC8.
- Initializes the following strings:
/data/user/0/cn.missfresh.application/.cache/libDexHelper32
/lib/armeabi-v7a/libDexHelper.so
/lib/armeabi/libDexHelper.so
assets/libDexHelper32
- Calls 0x3766C.
- Calls 0xB4B94.
- Calls 0x9C0BC.
- Calls 0xD5CDC.
- Calls 0x24C08.
- Calls 0x1C40C.
- Calls 0xA7D3C.
- End.
References
- Bangcle APP protection overview (Chinese): https://www.cnblogs.com/2014asm/p/14547218.html
Conclusion
The sample is heavily obfuscated — much more complex than the samples in the previous two writeups. There are some tricks that helped: functions adjacent in address are often related; parts of the code resemble other packers (they’ve clearly been copied around); and searching for older analysis write-ups of the same packer surfaces function names and strings that weren’t wiped.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch