1. 程式人生 > >Android通用脫殼機FUPK3

Android通用脫殼機FUPK3

轉自:https://bbs.pediy.com/thread-246117-2.htm

大家好,我是F8LEFT,潛水了這麼久,抽空來發個技術帖子。這次要發的作品是我以前寫的一個脫殼機FUPK3,這個脫殼機的思路應該是以前沒人放過的,這裡我不私藏了,放出來給大家來評評。
Android程式碼是開源的,那麼通過直接修改Android原始碼,把執行時的所有dex資料dump出來,不就可以實現一個通用的脫殼機了嗎?FUPk3就是基於上面的思路來實現的。FUPK3需要修改Android原始碼,匯出資料介面.不過,最為核心的脫殼操作是在一個so中執行的,脫殼時應用會自動載入該so,這樣好處在於可以通過替換這個so來達到動態修bug的效果。

  1. 程式碼注入
    程式啟動後讀取配置檔案/data/local/tmp/FUpk3.txt,如果命中脫殼的配置就會直接載入so /data/local/tmp/libFupk3.so,進行脫殼。修改Android原始碼

    1

    2

    3

    4

    5

    6

    7

    8

    try {

     UpkConfig config = new UpkConfig();

     if (config.load() && config.mTargetPackage.equals(data.info.getPackageName())) {

         Fupk upk = new Fupk(config.mTargetPackage);

         upk.unpackAfter(10000);

     }

    } catch (Throwable t) {

    }

  2. cookie
    脫殼需要知道app當前載入的所有dex檔案。每一個被載入到記憶體中的dex檔案都會被記錄為一個cookie值,存放在程式的loader中,所有類的載入都需要通過該loader進行。Java層的loader可能會比較複雜,比如會用到雙親委派機制等,不好遍歷。但是,到了Jni層,一切都變得簡單起來,在native中,會通過一個Hash表來儲存所有dex檔案。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    // 每個開啟的dex檔案都會儲存到gDvm.userDexFiles中

    static void addToDexFileTable(DexOrJar* pDexOrJar) {

     u4 hash = (u4) pDexOrJar;

     void* result;

     

     dvmHashTableLock(gDvm.userDexFiles);

     result = dvmHashTableLookup(gDvm.userDexFiles, hash, pDexOrJar,

             hashcmpDexOrJar, true);

     dvmHashTableUnlock(gDvm.userDexFiles);

     

     if (result != pDexOrJar) {

         ALOGE("Pointer has already been added?");

         dvmAbort();

     }

     

     pDexOrJar->okayToFree = true;

    }

    因此,想要知道當前apk載入了那些dex就簡單多了,只需要遍歷這個userDexFile這個Hashtable就好了。直接在Android原始碼中匯出介面。

    1

    2

    3

    4

    // @F8LEFT exported function

    HashTable* dvmGetUserDexFiles() {

     return gDvm.userDexFiles;

    }

    然後,直接在so裡面呼叫介面,就可以取出所有dex檔案的記憶體佈局。

    1

    2

    3

    4

    5

    auto fn = (HashTable* (*)())dlsym(libdvm, "dvmGetUserDexFiles");

    if (fn == nullptr) {

     goto bail;

    }

    gDvmUserDexFiles = fn();

  3. dex重建
    dex檔案被載入起來後,一些值就不會再被用到,如檔案頭sig,資料偏移等,部分殼會把這些資料抹除。這些資料需要直接從應用的執行時資料 DvmDex, ClassObject, MethodObject等取出來,並進行重建。還有一些常量資料,StringPool,TypePool等,這些是不會加密的,直接dump出來。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    bool DexDumper::fixDexHeader() {

     DexFile *pDexFile = mDvmDex->pDexFile;

     

     mDexHeader.stringIdsOff = (u4) ((u1 *) pDexFile->pStringIds - (u1 *) pDexFile->pHeader);

     mDexHeader.typeIdsOff = (u4) ((u1 *) pDexFile->pTypeIds - (u1 *) pDexFile->pHeader);

     mDexHeader.fieldIdsOff = (u4) ((u1 *) pDexFile->pFieldIds - (u1 *) pDexFile->pHeader);

     mDexHeader.methodIdsOff = (u4) ((u1 *) pDexFile->pMethodIds - (u1 *) pDexFile->pHeader);

     mDexHeader.protoIdsOff = (u4) ((u1 *) pDexFile->pProtoIds - (u1 *) pDexFile->pHeader);

     mDexHeader.classDefsOff = (u4) ((u1 *) pDexFile->pClassDefs - (u1 *) pDexFile->pHeader);

     return true;

    }

    對於Method的重構要複雜得多,每一代加固的發展重點都是增強對Method的CodeItem的加密。從整體加密到函式抽取到最新的函式VMP,還原難度也越來越高。VMP暫且不說,函式抽取本身還是可以攻破的。有些殼做得非常複雜,會在函式執行時還原始碼,執行完後又加密回去。針對這種情況,可以看到,在程式執行時的某一個時刻,dex的部分資料肯定是還原的,那麼我們可以通過不斷地手動構造程式碼,來遍歷觸發這些還原點,同時進行資料提取,最終完成修復。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

// 從Android原始碼中匯出方法

/* @F8LEFT

 * This method is used to export some data for fupk3 to dump dex file.

 * Fupk3 will hook this method and get data from it.

 */

void fupkInvokeMethod(Method* meth) {

    // it is no need to init or link class, the code of the method will

    // not exec actually, so just ignore it

    // anyway, I should make sure this method has code to execute

    if (dvmIsMirandaMethod(meth) || dvmIsAbstractMethod(meth)) {

        return;

    }

    dvmInvokeMethod((Object*)0xF88FF88F, meth, NULL, NULL, NULL, true);

}

 

bool fupkExportMethod(Thread* self, const Method* method) {

    return false;

}

 

FupkInterface gFupk = {

    NULL, NULL, NULL, NULL,

    fupkExportMethod

};

// @F8LEFT add end

// 對解析器入口插樁

 void dvmInterpret(Thread* self, const Method* method, JValue* pResult)

 {

    // @F8LEFT insert point for Fupk

    if ((u4)pResult->i == 0xF88FF88F) {

        gFupk.ExportMethod(self, method);

        return;

    }

    // @F8LEFT add end

    ...

 }

在上面,直接在Android原始碼中構造了脫殼用到的介面 gFupk,它提供了4個void*的空間來存放臨時資料,與一個可以替換的介面方法fupkExportMethod。這樣,只需要手動呼叫fupkInvokeMethod方法,程式就會走函式正常的執行流程,從dvmInvokeMethod開始,進入dvmInterpret中,並最終呼叫完gFupk.ExportMethod後直接退出Interpret。這樣當我們手動呼叫函式時,既可以讓殼還原出原始程式碼,又不會影響到程式本身的穩定性。

1

2

3

4

5

6

7

8

// 在so中直接替換ExportMethod

auto interface = FupkImpl::gUpkInterface;

if (interface == nullptr) {

    FLOGE("Unable to found fupk interface");

    return;

}

// Hook all

interface->ExportMethod = fupk_ExportMethod;

1

2

3

4

5

// 傳遞引數,然後直接呼叫

gUpkInterface->reserved0 = &shared;

shared.mCurMethod = dexMethod;

FupkImpl::fupkInvokeMethod(m);

shared.mCurMethod = nullptr;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

// 在ExportMethod中直接提取CodeItem資料

bool fupk_ExportMethod(void *thread, Method *method) {

    DexSharedData* shared = (DexSharedData*)gUpkInterface->reserved0;

    DexMethod* dexMethod = shared->mCurMethod;

    u4 ac = (method->accessFlags) & mask;

    if (method->insns == nullptr || ac & ACC_NATIVE) {

        if (ac & ACC_ABSTRACT) {

            ac = ac & ~ACC_NATIVE;

        }

        dexMethod->accessFlags = ac;

        dexMethod->codeOff = 0;

        return false;

    }

 

    if (ac != dexMethod->accessFlags) {

        dexMethod->accessFlags = ac;

    }

    dexMethod->codeOff = shared->total_point;

    DexCode *code = (DexCode*)((const u1*) method->insns - 16);

 

    u1 *item = (u1*) code;

    int code_item_len = 0;

    if (code->triesSize) {

        const u1*handler_data = dexGetCatchHandlerData(code);

        const u1 **phandler = (const u1**) &handler_data;

        u1 *tail = codeitem_end(phandler);

        code_item_len = (int)(tail - item);

    else {

        code_item_len = 16 + code->insnsSize * 2;

    }

    shared->extra.append((char*)item, code_item_len);

    shared->total_point += code_item_len;

    while(shared->total_point & 3) {

        shared->extra.push_back(shared->padding);

        shared->total_point++;

    }

    return true;

}

這樣,所有加密的資料都提取出來了,直接進行組合,以加密的方式dump出來。

1

2

3

4

5

6

7

8

9

10

size_t myfwrite(const void* buffer, size_t size, size_t count, FILE* stream) {

    char *tmp = new char[size * count];

    mymemcpy(tmp, buffer, size * count);

    for (size_t i = 0; i < size * count; ++i) {

        tmp[i] ^= encryptKey;

    }

    size_t rel = fwrite(tmp, size, count, stream);

    delete []tmp;

    return rel;

}

  1. dex修復
    上面dump下來的dex檔案是非標準的,可能存在部分的class資料不合法,並且一些軟體不認。所以需要一個寫一個修復的server來跳過非法的資料。server主要修改了baksmali與smali的程式碼,自動跳過無法反編譯的類。
  2. 其他
    Android加固發展到現在,經歷了好幾個大版本改動。最初的是dex整體加固,現在的是VMP加固,中間出了不少非常不錯的脫殼機,其中最為經典的有2個:ZjDroid與dexHunter,這兩個都是開源的,並且寫得非常好,即使是放到今天來看,也具有相當的參考價值,想要學習脫殼的同學們可以拜讀一下。另外,FUpk3是執行在dalvik上的,那麼要在art下脫殼怎麼辦呢?道理還是一樣的,只是art下會複雜很多, 跑解析的跑編譯的都有,修復起來需要記錄很多資料,這裡由於某些原因就不公開art下的脫殼機了。

  3. 寫在最後
    新手想要入門,需要什麼?1. 一臺谷歌親兒子(nexus) 2. 一個Ubuntu系統 3. 一套完整的Android原始碼。平時有事沒事可以多看看原始碼,刷刷機之類的。Android平臺與Windows上的不同,到目前為止,Android上系統的優秀的教材實在是少,如果沒人手把手地帶入門的話,基本上就是在白做功,幸好大部分難點的解決方案答案都可以在原始碼裡面找到,多熟悉一下總會有好處的。

7.https://github.com/F8LEFT/FUPK3
看雪上的md外掛怪怪的,大家去我的github上看吧,歡迎各種star與follow,演示視訊在這裡 https://pan.baidu.com/s/1HH_-TQGca1NLoSqzvOPB3Q 密碼:izm3