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檔案都要重新編輯。另外一個重要點就是索引問題,這個也不展開說了。