在Android系统下有一个DexClassLoader类,可以动态加载dex文件,但是,这个类有一个缺陷,就是第一次启动并加载一个dex文件时,会执行一次dex2oat或 dexopt操作,正常情况下不会感觉到它的不足,但是如果将它用于加固,就会出现第一次启动时间特别长的问题,为此可以使用下面的方法去提升第一次启动的速度:
首先是art模式下,art模式下支持解释执行dex文件,不需要编译,也不需要oat文件,所以直接hook execv函数,让调用dex2oat的进程直接退出就好,
hook回调函数代码如下:
int hook_execv(const char *name, char **argv) {
int ret = 0;
char tmp[512];
if (!IsDex2oat(name)) {
return ori_execv(name, argv);
}
exit(0);
}
hook的方式是got hook,代码如下:
void* GotHook(soinfo *si, char *Target_Name, void* Hook_Callback) {
Elf32_Rel *rel = (Elf32_Rel*)si->plt_rela;
int count = si->plt_rela_count;
void *Target_Addr = 0;
int idx = 0;
for (idx = 0; idx<count; ++idx, ++rel) {
unsigned type = ELF32_R_TYPE(rel->r_info);
unsigned sym = ELF32_R_SYM(rel->r_info);
Elf32_Addr reloc = (Elf32_Addr)(rel->r_offset + (u4)si->base);
Elf32_Addr sym_addr = 0;
const char* sym_name = 0;
if (type == 0) {
continue;
}
Elf32_Sym *s = 0;
struct soinfo_mine *lsi = 0;
if (sym != 0) {
sym_name = get_string(((Elf32_Sym*)si->symtab)[sym].st_name, si);
printf("%d sym_name=%s\n", idx, sym_name);
if (strcmp(sym_name, Target_Name))
continue;
printf("%d sym_name=%s\n", idx, sym_name);
Target_Addr = *(void**)reloc;
if (Hook_Callback != 0) {
Elf32_Addr seg_page_start = PAGE_START(reloc);
Elf32_Addr seg_page_end = PAGE_END(reloc + 4);
errno=0;
int ret = mprotect((void*)seg_page_start, seg_page_end-seg_page_start,
PROT_READ | PROT_WRITE);
*(void**)reloc = Hook_Callback;
}
}
}
return Target_Addr;
}
int JNI_onLoad() {
…
libart = (soinfo*)dlopen("libart.so", RTLD_NOW);
ori_execv = (fnexecv)GotHook((soinfo*)libart, "execv", (void*)hook_execv);
…
}
其中使用到的结构体,可以从android的linker的源码里拿取。
在dalvik虚拟机下,正常的情况下需要执行一个dexopt的过程,但同时,dalvik虚拟机里,也支持一种从内存里直接加载dex文件的字节码的办法:
4b3K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4F1k6s2u0G2K9h3c8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3y4q4)9J5k6e0c8Q4x3X3f1J5i4K6g2X3M7U0u0Q4x3V1k6^5M7X3g2X3i4K6u0r3k6r3q4D9N6X3W2C8i4K6u0r3N6X3#2Q4x3V1k6F1j5i4c8A6N6X3g2Q4x3V1k6V1j5h3I4$3K9h3E0Q4y4h3k6K6P5i4y4@1k6h3#2Q4y4h3k6p5k6i4S2r3K9h3I4W2i4K6u0W2j5%4m8H3i4K6t1K6x3U0b7&6i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1@1i4@1u0p5i4@1u0r3i4@1f1%4i4K6V1@1i4@1p5^5i4@1f1^5i4@1u0r3i4K6V1&6i4@1f1@1i4@1t1^5i4@1q4m8i4@1f1#2i4K6R3%4i4@1u0p5i4@1f1$3i4K6V1#2i4@1t1H3i4@1f1#2i4K6S2r3i4@1q4r3i4@1f1@1i4@1u0n7i4@1p5#2i4@1f1%4i4@1p5%4i4K6V1J5i4@1f1#2i4K6S2m8i4@1p5H3i4@1f1^5i4@1u0p5i4@1u0p5i4@1f1#2i4K6R3$3i4K6R3#2i4@1f1#2i4@1q4p5i4K6V1^5i4@1f1@1i4@1t1^5i4@1q4p5i4@1f1%4i4K6W2m8i4K6R3@1k6r3g2^5i4@1f1$3i4K6V1$3i4K6R3%4i4@1f1@1i4@1u0n7i4@1t1$3i4@1f1K6i4K6R3H3i4K6R3J5
但是可悲的是这个函数并没有导出,我们无法直接去调用。
为此有两种解决办法:
1、 使用函数dvmLookupInternalNativeMethod去查找Dalvik_dalvik_system_DexFile_openDexFile_bytearray的地址。
2、 用Dalvik_dalvik_system_DexFile_openDexFile_bytearray内部使用的已导出的函数去重写一个自己的my_Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数。
为了使这种快速加载技术支持2.x的版本,所以最好使用的是第2种方法。
void* LoadByte(JNIEnv *env, u1 *byDexFile, int len) {
jbyte *bytes = (jbyte*)byDexFile;
u1 *pnc = (u1*)bytes;
RawDexFile *pRawDexFile = (RawDexFile*)malloc(sizeof(RawDexFile));
DvmDex *pDvmDex = NULL;
void* pClassLookup = NULL;
LOGI2("dvmDexFileOpenPartial_ptr=%p %p\n", dvmDexFileOpenPartial_ptr, dexCreateClassLookup_ptr);
dvmDexFileOpenPartial_ptr(pnc, len, &pDvmDex);
pClassLookup = (void*)dexCreateClassLookup_ptr(pDvmDex->pDexFile);
LOGI2("pDvmDex=%p pDvmDex->pDexFile=%p\n", pDvmDex, pDvmDex->pDexFile);
pDvmDex->pDexFile->pClassLookup = pClassLookup;
pRawDexFile->pDvmDex = pDvmDex;
DexOrJar *pd = (DexOrJar*)malloc(sizeof(DexOrJar));
LOGI2("pnc=%p\n", pnc);
pd->isDex = true;
pd->pRawDexFile = pRawDexFile;
pd->pDexMemory = pnc;
pd->filename = strdup("<memory>");
void *libdvm = dlopen("libdvm.so", RTLD_NOW);
unsigned addrOfgDvm = (unsigned)dlsym(libdvm, "gDvm") + 0x330;
fndvmHashTableLookup addrOfdvmHashTableLookup = (fndvmHashTableLookup)dlsym(libdvm, "_Z18dvmHashTableLookupP9HashTablejPvPFiPKvS3_Eb");
//int result = dvmHashTableLookup_ptr(addrOfgDvm, (int)pd, (int)pd, hashcmpDexOrJar, true);
LOGI2("result=%p\n", pd);
if (old_DvmDex == NULL)
old_DvmDex = pDvmDex;
return pd;
}
成功调用上面的两种内存加载dex文件方法中的任意一种后,得到的返回值是一个DexOrJar结构体的指针。这个指针就是
068K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4F1k6s2u0G2K9h3c8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3y4q4)9J5k6e0c8Q4x3X3f1J5i4K6g2X3M7U0u0Q4x3V1k6^5M7X3g2X3i4K6u0r3L8r3W2T1j5$3!0J5k6g2)9J5c8X3c8S2L8s2k6A6K9#2)9J5c8Y4y4J5j5#2)9J5c8X3#2S2K9h3&6Q4x3V1k6B7j5i4k6S2i4K6u0r3k6r3q4D9N6X3W2C8i4K6u0r3M7%4W2K6N6r3g2E0i4K6u0r3c8r3g2^5c8X3W2D9k6g2)9J5k6h3A6S2N6X3q4Q4x3U0x3K6y4#2!0q4z5q4!0n7c8W2)9&6z5g2!0q4z5g2)9^5y4#2)9^5b7#2!0q4y4#2)9&6b7g2)9^5y4r3#2o6L8$3!0C8K9h3g2Q4c8e0y4Q4z5o6m8Q4z5o6t1`.
得到mCookie后,该怎么将它的值传给DexFile呢?
我使用的方法是hook openDexFileNative/openDexFile函数。
这两个函数没有导出,但它们是一个jni函数,hook的办法是替换这两个方法的Method结构体的里的回调函数。
这两个JNI方法与一般的JNI方法不同,它们是内部JNI,因此,它的回调函数保存在这两个位置之中的一个:
1、 ((u4**)Method->insns)[10];
2、 Method->nativeFunc(当Method->insns==0时)
Hook代码:
addr = (u4**)openDexFileNative_med->insns;
if (openDexFileNative_med->insns == 0) {
Dalvik_dalvik_system_DexFile_openDexFileNative_ptr = (Dalvik_dalvik_system_DexFile_openDexFileNative_func)openDexFileNative_med->nativeFunc;
openDexFileNative_med->nativeFunc = (u4)Dalvik_dalvik_system_DexFile_my_openDexFileNative;
}
else {
Dalvik_dalvik_system_DexFile_openDexFileNative_ptr = (Dalvik_dalvik_system_DexFile_openDexFileNative_func)addr[10];
addr[10] = (u4*)Dalvik_dalvik_system_DexFile_my_openDexFileNative;
}
至此,总结一下dalvik下加速启动的过程:
1、 调用DexClassLoader,加载一个payload.dex文件
2、 DexClassLoader调用openDexFileNative函数
3、 由于openDexFileNative被hook了,实际调用的是my_openDexFileNative
4、 my_openDexFileNative里调用my_Dalvik_dalvik_system_DexFile_openDexFile_bytearray加载内存中的dex文件,并将结果返回
5、 成功实现秒加载payload.dex文件。
不知道怎么插入图片,很抱歉,先写原理,过些时间,放出源码。
爱讨论的加个群456853837,补个群号282215163
[培训]科锐逆向工程师培训第53期2025年7月8日开班!