运行时动态修复dex

Posted jltxgcy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了运行时动态修复dex相关的知识,希望对你有一定的参考价值。

    0x00

    本文的源代码已经上传至github,地址为https://github.com/jltxgcy/DynamicFixDex。在android2.3模拟器上可以运行。

    ForceApkObj:用于动态加载的apk。类似于Android中的Apk的加固(加壳)原理解析和实现一文中的ForceApkObj工程。

    FixDex:用于分离ForceApkObj里面的classes.dex,把他分离为classes_fix.dex和data.so,具体情况我们后来介绍。

    DynamicDex:用于动态加载ForceApkObj工程生成的ForceApkObj.apk,也就是脱壳程序,类似于Android中的Apk的加固(加壳)原理解析和实现一文中的ReforceApk工程。


    0x01

    使用步骤:

    1、将ForceApkObj工程生成的classes.dex拷贝到/sdcard/payload/目录下。

    2、Run FixDex工程,点击Button按钮,此时在/sdcard/payload/目录下生成了classes_fix.dex和data.so。

    3、把classes_fix.dex更名为classes.dex,然后替换ForceApkObj.apk里面的classes.dex,然后重新签名,生成新签名的ForceApkObj.apk。

    4、把ForceApkObj.apk和data.so放入/sdcard/payload/目录中,运行DynamicDex工程。

  

    0x02

    ForceApkObj工程很简单,主MainActivity界面点击屏幕会打开SubActivity,SubActivity的代码如下:

public class SubActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		handleException();
	}

	public void handleException() {
		Toast.makeText(this, "成功映射", Toast.LENGTH_LONG).show();
	}

}


    0x03

    FixDex用于用于分离ForceApkObj里面的classes.dex,把他分离为classes_fix.dex和data.so。代码如下:

			public void onClick(View arg0) {
				int codeoff = FindCode.findCode("Lcom/example/forceapkobj/SubActivity;", "handleException");
				Log.d("jltxgcy", "codeoff:" + codeoff);
				try {
					File file = new File("/sdcard/payload/classes.dex");
					byte[] dexByte = readFileBytes(file);
					String strso = "/sdcard/payload/data.so";
					writeFile(strso, dexByte, codeoff - 16, exceptionCode.length + 16);
					System.arraycopy(exceptionCode, 0, dexByte, codeoff, exceptionCode.length);
					//修改DEX file size文件头
					fixFileSizeHeader(dexByte);
					//修改DEX SHA1 文件头
					fixSHA1Header(dexByte);
					//修改DEX CheckSum文件头
					fixCheckSumHeader(dexByte);
					String str = "/sdcard/payload/classes_fix.dex";
					writeFile(str, dexByte, 0, dexByte.length);
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
    首先找到SubActivity类的handleException方法的偏移,这个方法是一个native方法,具体的请参考源代码,看源代码前,请先了解 apk自我保护的一种实现方式——运行时自篡改dalvik指令。核心的原理我解释下:

const DexCode  *code =
    	dexFindClassMethod(&gDexFile, className, methodName);
    if(code == NULL){
    	ALOGE("Error can not found setScoreHidden");
    	return 0;
    }
    position = (u4 *)code - (u4 *)dexBase;
    ALOGD("codeoff:%d", ((u4 *)code - (u4 *)dexBase));
    return position * 4 + 16;
    找到handleException在内存中的偏移,然后再减去dex头部的基地址,得到handleException在dex中本地偏移,那为什么要加上16呢?我们先看一个DexCode的结构。

struct DexCode {
    u2  registersSize;
    u2  insSize;
    u2  outsSize;
    u2  triesSize;
    u4  debugInfoOff;       /* file offset to debug info stream */
    u4  insnsSize;          /* size of the insns array, in u2 units */
    u2  insns[1];
    /* followed by optional u2 padding */
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};
    加上16其实就是insns[1]的偏移,也就是真正执行的指令的偏移。


    找到了真正执行的指令的偏移,然后我们把这个指令整个的DexCode拷贝到data.so中。

writeFile(strso, dexByte, codeoff - 16, exceptionCode.length + 16);
    codeoff-16其实就是DexCode的偏移。exceptionCode.length是insns[1]长度(指令的长度),16是registersSize+insSize+outsSize+triesSize+debugInfoOff+insnsSize长度和。


    然后把原来classes.dex中handleException的DexCode中insns[1],也就是真正执行的指令全部置为exceptionCode,也就是全0。这样如果不动态修复,当执行到这个方法时,就会报错。

System.arraycopy(exceptionCode, 0, dexByte, codeoff, exceptionCode.length);


    最后把修复后内容写入到classes_fix.dex中。

					String str = "/sdcard/payload/classes_fix.dex";
					writeFile(str, dexByte, 0, dexByte.length);


    0x03

    把classes_fix.dex更名为classes.dex,然后替换ForceApkObj.apk里面的classes.dex,然后重新签名,生成新签名的ForceApkObj.apk。

    然后把ForceApkObj.apk和data.so放入/sdcard/payload/目录中,运行DynamicDex工程。

    在Android加壳native实现一文中,我们讲述了如何在native加载ForceApkObj.apk,并替换原Application,执行ForceApkObj中的主MainActivity。在这里我们也可以用同样的方式加载ForceApkObj,执行ForceApkObj中的主MainActivity,但是执行到SubActivity时,由于在SubActivity类的onCreate方法中调用handleException方法,而这个方法指令在0x02步已经设置为全零。那么就会报错。

    如果想要执行正确,我们有一个思路,也就是代码中的实现,如下:

static void nativeParserDex(const char *className, const char *methodName)
{
	void *base = NULL;
	int module_size = 0;
	char filename[512];
	char path[512];
	int fd=-1;
	int r=-1;
	int len=0;
	struct stat st;
	u4 *addr;
	int newCodeOffPosition;
	u1 *store = (u1 *) malloc(2);


	// simple test code  here!
	for(int i=0; i<2; i++){
		sprintf(filename, "/mnt/sdcard/payload_odex/ForceApkObj.dex");

		base = get_module_base(-1, filename);
		if(base != NULL){
			break;
		}
	}

    if(base == NULL){
        ALOGE("Can not found module: %s", filename);
        return ;
    }

    module_size = get_module_size(-1, filename);

	// search dex from odex
	void *dexBase = searchDexStart(base);
	ALOGD("found dex start[%p]", dexBase);
	if(checkDexMagic(dexBase) == false){
		ALOGE("Error! invalid dex format at: %p", dexBase);
		return ;
	}

	DexHeader *dexHeader = (DexHeader *)dexBase;

    gDexFile.baseAddr   = (u1*)dexBase;
    gDexFile.pHeader    = dexHeader;
    gDexFile.pStringIds = (DexStringId*)((u4)dexBase+dexHeader->stringIdsOff);
    gDexFile.pTypeIds   = (DexTypeId*)((u4)dexBase+dexHeader->typeIdsOff);
    gDexFile.pMethodIds = (DexMethodId*)((u4)dexBase+dexHeader->methodIdsOff);
    gDexFile.pFieldIds  = (DexFieldId*)((u4)dexBase+dexHeader->fieldIdsOff);
    gDexFile.pClassDefs = (DexClassDef*)((u4)dexBase+dexHeader->classDefsOff);
    gDexFile.pProtoIds  = (DexProtoId*)((u4)dexBase+dexHeader->protoIdsOff);


    //dumpDexHeader(dexHeader);
    //dumpDexStrings(&gDexFile);
    //dumpDexTypeIds(&gDexFile);
    //dumpDexProtos(&gDexFile);
    //dumpFieldIds(&gDexFile);
    //dumpClassDefines(&gDexFile);


    // 2. found Dex Class!
    const DexCode  *code =
    	dexFindClassMethod(&gDexFile, className, methodName);
    if(code == NULL){
    	ALOGE("Error can not found setScoreHidden");
    	return ;
    }
    codeOffPosition = ((u4 *)code - (u4 *)dexBase) * 4;
    ALOGD("codeOffPosition:%d", codeOffPosition);
    dexFindClassData(&gDexFile, className);
    u1 *codeData = (u1 *)codeOffPoint;
    ALOGD("codeOffPoint:%p,codeOffPointByte:%d,codeOffPointData:%d,%d", codeOffPoint, codeOffPointByte, *codeData,*(codeData + 1));
    sprintf(path, "/mnt/sdcard/payload_odex/data.so");
    fd = open(path,O_RDONLY,0666);
	if (fd==-1) {
		return ;
	}

	r=fstat(fd,&st);
	if(r==-1){
		close(fd);
		return ;
	}

	len=st.st_size;
	addr=(u4 *)mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,0);
	ALOGD("addr:%p", addr);
	newCodeOffPosition = ((u4 *)addr - (u4 *)dexBase) * 4;
	writeLeb128(store, newCodeOffPosition);
	ALOGD("newCodeOffPosition:%d, store:%d,%d", newCodeOffPosition, *store,*(store + 1));
	ALOGD("mprotect in");
	*codeData = *store;
	codeData++;
	*codeData = *(store + 1);
	ALOGD("codeOffPointData:%d,%d", *(codeData-1),*(codeData));
}
    还记得我们在0x02步提取的data.so,实际上就是原来的handleException方法的DexCode结构体内容。我们只要找到目前指向DexCode结构体(当前方法指令置为全零)指针codeOff,我们再把data.so映射到内存,让codeOff指向这个地址,就完成了动态修复。因为这时候还没有loadClass,所以在此之前修复,loadClass就能形成正确的ClassObject,从而正确的执行指令。

    讲的是大概的原理,大家参考源码多看看就能理解,如果有不懂,请留言。我在Android 2.3模拟器试验成功!

以上是关于运行时动态修复dex的主要内容,如果未能解决你的问题,请参考以下文章

如何使用导航架构组件修复动态功能模块中片段的发布版本中的ClassNotFoundException?

360加固保dump出来的dex怎么修复

一步步手动实现热修复-dex文件的生成与加载

利用DexClassLoader动态加载dex文件

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Tinker 源码解析-代码修复和资源修复