可以看到,加固之后多了两个 .so 文件、一个 com.edog 的 smali 文件夹,My.XuanAo.LiuYao 这个目录是本身 apk 的代码结构,所以猜测加固全是在 com.edog 和 .so 文件里做的。
看下 AndroidManifest.xml。
可以看到原 apk 的 activity 没变,还是 .main,但是 application 变成新增的 com.edog.AppWrapper,因为 application 会在 activity 之前加载,所以判断加固正是在这里做的。
看下 smali,这里比较简单,不做过多的展开,直接跳到关键的内容。
可以看到这里在 libedog.so 里定义了一个 native 函数 d1,然后在 Utils.smali 调用了这个函数,IDA 动态调试跟进看一下。
在加载 libedog.so 的时候下断电,可以看到右侧的的 Java_com_edog_Elibrary_d1 函数,并且这个 .so 加载之后没有 .init(_array) 和 JNI_Onload 段,所以该函数就是之前定义的 native d1。
F5。
可以看到 d1 的大体结构,还算比较简单,具体的每个函数的功能不详细分析了,有反调试的,有解密的,等等。比较关键的是 restore 这个函数。
“dvmResolveClass”,看到这个可以明确使用的是 Dex 的类抽取做加固,通过动态地替换 DexCode 里的内容(见 Android 逆向学习笔记 (八)- Classes.dex 结构简单介绍)打到加固的目的。
如果这种加固在还原正确的 DexCode 之后没有擦出 insns 的话,可以使用 Qever 提供的一个小脚本来脱壳。
__author__ = 'QEver' DUMP_FILE_PREFIX = r'd:/' USER_DEX_FILES_OFFSET = 0x334 LOADED_CLASSES_OFFSET = 0xAC JAR_NAME_OFFSET = 0x24 SIZE_OF_DEY_HEADER = 0x28 DESC_OFFSET_OF_CLASS_OBJECT = 0x18 METHOD_OFFSET_OF_CLASS_OBJECT = 0x60 SIZE_OF_STRUCT_METHOD = 0x38 import os import idaapi import binascii def read_data(ea, size): return idaapi.dbg_read_memory(ea, size) def read_dword(ea): val = int(binascii.hexlify(idaapi.dbg_read_memory(ea, 4)), 16) r = (val & 0xff) << 24 | (val & 0xff00) << 8 | (val & 0xff0000) >> 8 | (val & 0xff000000) >> 24 return r def read_bool(ea): val = int(binascii.hexlify(idaapi.dbg_read_memory(ea, 1)), 16) return val def read_str(ea, max=256): c = '' while True: x = idaapi.dbg_read_memory(ea, 1) ea = ea + 1 max = max - 1 if x == '\0' or max < 0: break c += x return c class DvmDex: def __init__(self, ea): self.ea = ea self.pDexFile = read_dword(ea) self.baseAddr = read_dword(self.pDexFile + 0x2c) def p(self): print 'pDexFile = %x' % self.pDexFile print 'baseAddr = %x' % self.baseAddr class ClassObject: def __init__(self, ea): self.ea = ea self.desc_addr = read_dword(ea + DESC_OFFSET_OF_CLASS_OBJECT) def get_descriptor(self): return read_str(self.desc_addr) class DexOrJar: def __init__(self, ea): self.ea = ea self.filename = read_dword(ea) self.isDex = read_bool(ea + 4) self.pRawDexFile = read_dword(ea + 8) self.pJarFile = read_dword(ea + 12) def get_filename(self): if self.isDex == 0: return read_str(read_dword(self.pJarFile + JAR_NAME_OFFSET)) return read_str(self.filename) def get_dvmdex(self): if self.isDex == 0: return read_dword(self.pJarFile + JAR_NAME_OFFSET + 4) return read_dword(self.pRawDexFile + 4) def p(self): print 'filename = %s' % read_str(self.filename) print 'isDex = %d' % self.isDex print 'pRawDexFile = %x' % self.pRawDexFile print 'pJarFile = %x' % self.pJarFile class HashTable: def __init__(self, ea): self.ea = ea self.tableSize = -1 self.numEntries = -1 self.pEntries = None self.do_init() def do_init(self): self.tableSize = read_dword(self.ea) self.numEntries = read_dword(self.ea + 4) self.pEntries = read_dword(self.ea + 12) def get_table_size(self): return self.tableSize def get_num_entries(self): return self.numEntries def get_pentries(self): return self.pEntries def p(self): print 'tableSize = %d' % self.tableSize print 'numEntries = %d' % self.numEntries print 'pEntries = %x' % self.pEntries class Method: def __init__(self, ea): self.ea = ea def get_name(self): addr = self.ea + 0x10 return read_str(read_dword(addr)) def get_insns(self): addr = self.ea + 0x20 return read_dword(addr) def get_address(self): return self.ea def get_gdvm_address(): return idaapi.get_debug_name_ea("gDvm") def dump_all_dex(prefix=DUMP_FILE_PREFIX): gdvm = get_gdvm_address() print '[*] gDvm = 0x%x' % gdvm user_dex_files = read_dword(gdvm + USER_DEX_FILES_OFFSET) print '[*] gDvm.user_dex_files = 0x%x' % user_dex_files ht = HashTable(user_dex_files) max_size = ht.get_table_size() size = ht.get_num_entries() p = ht.get_pentries() print '[*] Found %s items in Dex Table' % size for i in range(max_size): x = read_dword(p) p += 8 if x == 0: continue doj = DexOrJar(x) print '[*] Dex in Address 0x%x, isDex = %d' % (x, doj.isDex) addr = doj.get_dvmdex() name = doj.get_filename() print '[*] found file : %s , dvmdex = 0x%x' % (name, addr) dd = DvmDex(addr) addr = dd.baseAddr base = addr - SIZE_OF_DEY_HEADER size = read_dword(addr + 0x20) + SIZE_OF_DEY_HEADER flag = read_str(base, 3) if flag != 'dey': base = addr size = size = read_dword(addr + 0x20) print '[*] found odex file = 0x%x <%s>, size = 0x%x' % (base, read_str(base, 7).replace('\n', '.'), size) name = os.path.basename(name) path = os.path.join(prefix, name) print '[*] Write to %s' % path data = read_data(base, size) f = open(path, 'wb') f.write(data) f.close() print '[*] Finish Write' def find_class(name): gDvm = get_gdvm_address() loaded_classes = read_dword(gDvm + LOADED_CLASSES_OFFSET) ht = HashTable(loaded_classes) max_size = ht.get_table_size() size = ht.get_num_entries() p = ht.get_pentries() + 4 print 'Finding for %d items, may take a long time...' % max_size for i in range(max_size): x = read_dword(p) p = p + 8 if x == 0: continue c = ClassObject(x) s = c.get_descriptor() if s == name: print '[*] Found Class <%s> : 0x%x' % (name, x) return x if s.find(name) != -1: print '[*] Found Class <%s> : 0x%x' % (s, x) def list_method(class_addr): x = ClassObject(class_addr) print '[*] List All Method of %s' % x.get_descriptor() m = class_addr + METHOD_OFFSET_OF_CLASS_OBJECT directMethodCount = read_dword(m) directMethodTable = read_dword(m + 4) virtualMethodCount = read_dword(m + 8) virtualMethodTable = read_dword(m + 12) for i in range(directMethodCount): method = Method(directMethodTable) directMethodTable = directMethodTable + SIZE_OF_STRUCT_METHOD print method.get_name(), hex(method.get_address()) for i in range(virtualMethodCount): method = Method(virtualMethodTable) virtualMethodTable = virtualMethodTable + SIZE_OF_STRUCT_METHOD print method.get_name() def list_all_class(): gDvm = get_gdvm_address() loaded_classes = read_dword(gDvm + LOADED_CLASSES_OFFSET) ht = HashTable(loaded_classes) max_size = ht.get_table_size() size = ht.get_num_entries() p = ht.get_pentries() + 4 for i in range(max_size): x = read_dword(p) p = p + 8 if x == 0: continue c = ClassObject(x) print c.get_descriptor() if __name__ == '__main__': #list_all_class() #find_class('alibaba') #list_method(class_addr) dump_all_dex('c:/test/')
使用这个脚本有一个需要注意的问题,这里是通过对应的 gDvm.userDexFile 来找到不同方法的 DexCode,所以使用不同型号、版本的测试机时 USER_DEX_FILES_OFFSET 需要做对应修改。
另外,也可以使用 find_class 来实现查找对应 Method 的 DexCode。
使用 QEver 的脚本脱壳具有一定的局限性,如果 Method 调用之后擦除 insns 那么就没法使用该脚本,不过可以通过 DexHunter 脱壳,他们的原理类似,只不过 DexHunter 增加了动态脱壳时机的判断。
其实,每一种脱壳都有 easy way and hard way,上面说的都是 easy way,hard way 则是通过理解 dvmResolveClass 的实现方式,通过对应的解密方式来还原。这种方式不在此讨论,详细脱壳方法可以在下面参考链接中看到。
Reference
* QEver: 一个dex脱壳脚本* APK加固之类抽取分析与修复