1. 程式人生 > >apk自我保護的一種實現方式——執行時自篡改dalvik指令

apk自我保護的一種實現方式——執行時自篡改dalvik指令

玩過Android開發的人應該都知道,Android apk的保護是非常差的,辛辛苦苦寫的程式碼,被別人翻個底朝天倒不說,被人改了程式碼移頭換面再拿出來害人就不能忍了。

除自帶的SDK外,Android的分析和修改工具還有很多,Android下的靜態分析工具,最常見的是利用ApkTool(見http://code.google.com/p/android-apktool/)反編譯apk,將dalvik位元組碼生成smali彙編,通過對smali彙編的閱讀分析,結合實際軟體執行時行為,搜尋定位關鍵資訊點,再針對性的修改彙編指令,再通過ApkTool重新編譯打包簽名生成apk來執行,以達到破解的目的。除ApkTool之外,還有一些工具,如IDA pro、dex2jar結合jd-gui以及androguard等等。

當然,應對反編譯apk來破解的方式有多種,比如加上混淆,使用簽名校驗等等,這裡說的一種方式,並非常規方式,但若實現好之後,對使用者想要通過常規的靜態分析程式碼的方式來破解apk,不僅白費力氣,還很容易被帶進溝裡去。

本文的方法並非獨創,在實際工作中的需要對dalvik指令分析統計時,意外搜尋到bluebox的這篇文章:http://bluebox.com/labs/android-security-challenge/ 。可惜的是,他們沒有提供原始碼,就只能按照思路,自己實現了。實現的原始碼在https://github.com/freshui/dexBytecodeTamper,有興趣的同學可以對照本文後,下載下來瞅瞅,這裡先說說原理吧(只簡單挑一些說,詳細請查閱android原始碼文件)。

1.Dalvik檔案格式分析

Dex檔案的格式,在Android原生程式碼中,由一個DexFile的結構體描述:

struct DexFile {
    const DexHeader*    pHeader;
    const DexStringId*  pStringIds;
    const DexTypeId*    pTypeIds;
    const DexFieldId*   pFieldIds;
    const DexMethodId*  pMethodIds;
    const DexProtoId*   pProtoIds;
    const DexClassDef*  pClassDefs;
    const DexLink*      pLinkData;
};

以上dexFile結構描述的,是編譯後直接生成的是dex檔案的格式,在apk檔案(就是一個zip檔案,可用winrar或其他解壓縮工具直接開啟)中,dex檔案是classes.dex。

Dalvik格式的詳細情況,可以參看2008年的Google IO: Dalvik VM Intenals,說的很清楚。整體結構相比ELF格式來說,要簡單一些,如下圖(拷貝自Dalvik VM Internals 的ppt),通常由7個部分組成,header記錄了dalvik檔案的一些資訊,並標記其餘幾個部分在檔案中的位置,而其他幾個部分的XXX_ids或XXX_defs,實際上都只是索引,索引的內容是存在data部分的。

dalvik與傳統的class檔案相比的一個優勢,就是將所有的常量字串集統一管理起來了,這樣就可以減少冗餘(實際我感覺,也壓榨不了多少,現在都是幾個G的記憶體了,沒多少意義了),最終的dex檔案size也能變小一些。在上圖的幾大部分中,string_ids和elf的string table有點類似,存的是每個字串的offset的位置,其他幾個部分若要引用字串,則直接使用string_ids的下標即可,方式同elf格式的索引。索引關係可見下圖:

詳細的dex檔案介紹就不說了,有興趣的可以直接翻看android的原始碼,雖然表面看起來蠻嚇人的,但其結構及複雜度不及elf,解析起來比ELF更是簡單。

odex檔案格式

apk安裝時,會通過dexopt來驗證並生產優化後的dalvik位元組碼odex檔案。過程是將apk中的classes.dex解壓後,用dexopt處理,並儲存到/data/dalvik-cache/[email protected]@<package-name>[email protected]。odex檔案會解析相關依賴,將載入所需的依賴庫列表附加在檔案中,同時會修改部分指令以加快解析和處理的速度。

odex檔案可以看做是dex檔案的一個超集,其結構如下:

dex檔案作為優化後的odex的一部分,在本文的分析中,只需要從odex中找出dexFile的部分即可。

2. Dex檔案解析

我們的目標是自修改位元組碼。要實現自修改位元組碼,就需要先定位到想要修改得程式碼的位置,這就需要先解析dex檔案。dex檔案我們放在native程式碼中解析處理,naitive程式碼的分析破解,要比dalvik複雜多了。這裡對dex檔案解析的實現,可以參考dalvik原始碼,其中函式可以直接拿過來使用,詳情可參考其dexDump模組,程式碼細節不想說,可參看dalvik原始碼或示例。

2.1 定位修改檔案

要修改自身dalvik指令之前,我們需要查詢到要修改的odex檔案的map地址,然後通過mprotect呼叫,增加可寫屬性以便修改。程式碼如下:

首先要找到odex檔案在本程序中map的位置,注意android對dalvik-cache目錄下的odex檔名的命名方式:
 

	void *base = NULL;
	int module_size = 0;
	char filename[512];

	// simple test code  here!
	for(int i=0; i<2; i++){
		sprintf(filename, "/data/dalvik-cache/[email protected]@%s-%[email protected]", "com.freshui.dextamper", i+1);

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

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

找到odex檔案後,還需要知道改odex檔案在其中的size,以便mprotect修改屬性:

    module_size = get_module_size(-1, filename);


odex檔案找到後,我們需要確定dex檔案在其中的偏移,以便分析dex檔案格式,查詢Dex檔案偏移位置:

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


找到dex所在的偏移,就可以解析dex檔案頭,定位dex檔案各部分所在的區域,以供下一步解析使用:

    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);

以上,已經基本確定了dex檔案的資訊,接下來要修改dalvik位元組碼了。

修改位元組碼之前,還需要定位到需要修改的byte Code的存放位置。目前能做到的修改僅針對dalvik指令部分,其他部分的修改未做嘗試。dalvik指令的資料結構為:

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] */
};


這裡的insns陣列存放的就是dalvik的位元組碼。我們只要定位到相關類方法的DexCode資料段,即可通過修改insns陣列,篡改指令。

定位位元組碼之前,需要定位到DexMethod的位置(因為只有method才有指令),在DexMehod結構中,有成員指向DexCode資料結構的偏移:

/* expanded form of encoded_method */
struct DexMethod {
    u4 methodIdx;    /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff;      /* file offset to a code_item */
};

這裡,codeOff即為要找的DexCode所在的偏移位置。

為找到DexMethod,我們需要找到此Method的確定資訊:其所屬的類,函式名,引數和返回值資訊等。知道這些資訊後,可以通過Dex檔案結構中的class_defs和method_ids表查詢到DexMethod結構,並最終返回DexCode。

相關程式碼封裝在一個函式中實現:

static const DexCode *dexFindClassMethod(DexFile *dexFile, const char *clazz, const char *method)
{
	ALOGD("found: %s->%s", clazz, method);
    DexClassData* classData = dexFindClassData(dexFile, clazz);
    if(classData == NULL) return NULL;

    const DexCode* code = dexFindMethodInsns(dexFile, classData, method);

    if(code != NULL) {
        dumpDexCode(code);
    }
    //dumpDexClassDataMethod(&gDexFile, classData);

    return code;
}

找到DexCode後,即可修改指令了:

    const DexCode  *code =
    	dexFindClassMethod(&gDexFile, "Lcom/freshui/dextemper/GameControl;", "setScoreHidden");

    const DexCode  *code2 = dexFindClassMethod(&gDexFile, "Lcom/freshui/dextemper/GameControl;", "setScore");

    // remap!!!!
    if(mprotect(base, module_size, PROT_READ | PROT_WRITE | PROT_EXEC) == 0){
    	ALOGD("Found the odex module at: %p [%x]", base, module_size);
    	DexCode *pCode = (DexCode *)code2;

    	// Modify!
    	pCode->registersSize = code->registersSize;
        for(u4 k=0; k<code->insnsSize; k++){
        	pCode->insns[k] = code->insns[k];
        }

        // cleanup write PROT
        mprotect(base, module_size, PROT_READ | PROT_EXEC);
    }


修改指令的方式:

1. 如上文描述,需要先查詢到DexCode的偏移位置

2. 需要將odex檔案map的記憶體位置,用mprotect設定記憶體訪問許可權,允許修改。

3. 修改DexCode中,insns所指向的dalvik位元組碼

4. 將odex所map的記憶體位置的訪問許可權改回,當然不做也沒關係。

細節不詳述,有興趣可以看看示例程式碼。

這裡需要注意一點:dalvik指令也不是可以任意修改,需要注意size、暫存器及索引不能有異常,一般只能針對每個dex檔案都要重新編輯。另外一個重要點就是索引問題,這個也不展開說了。