1. 程式人生 > >樂固libshella 2.10.1分析筆記

樂固libshella 2.10.1分析筆記

這篇文章是記錄本人在學習Legu脫殼的心得,分析的樣本是Legu libshella-2.10.so的版本。

本文分為幾個部分:

修復So檔案

第一次解密

第二次解密

解密Dex

Dalvik下載入Dex原理分析

Art下載入Dex原理分析

Dalvik下脫殼機編寫

Art下脫殼機編寫

修復So檔案

Legu的核心加固程式碼都是在libshella-x.so裡面,

我們用IDA開啟libshella-2.10.1.so,意料之中的是,IDA開啟什麼也看不到。

在這裡我用ThomasKing的ELF修復工具試著修復一下,修復之後已經可以看到很多函數了

但是修復後的So檔案在IDA還是看不到init_array段,JNI_OnLoad函式也是加密狀態。

由於So載入完之後會呼叫init_array函式,我們從Android原始碼入手來獲取init_array的地址,在Android原始碼 linker.cpp的程式碼中有這麼一段程式碼就是來呼叫init和init_aray函式的

CallFunction("DT_INIT", init_func);

CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);

將手機中的linker拖入ida,找到DT_INIT_ARRAY字串,可以獲取呼叫CallArray的地址,在我的linker中,0x295E處就是呼叫init段的地址

我用修復後的So檔案替換掉原始的Libshela-2.10.1.so檔案後,App還可以成功執行,在這裡就直接用修復後的So來動態除錯

第一次解密

在init_array的函式中,首先會解密出JNI_OnLoad和反除錯程式碼等等

解密是一個while迴圈,會從libshella.so 0x2000處開始解密,到0x3954處結束解密,解密完成之後,就只是建立了一個反除錯執行緒,檢測到反除錯就執行raise(9),最簡單的方法可以將call raise函式的程式碼nop掉

我沒有關注init_array的解密演算法是什麼樣的,當它解密完成之後,我將0x2000-0x3954偏移處的程式碼dump出來了,然後替換掉上面修復後的libshella_fix.so對應的位元組,這樣生成的So就包含解密後的Jni_OnLoad函數了

第二次解密

咋一看,很奇怪的是在JNI_OnLoad函式中,還會自身再呼叫JNI_OnLoad函式,關鍵地方在於sub_1968()函式,

這個函式比較龐大,我分析了半天,沒分析處到底是怎麼解密的,但是這裡並不影響後面的分析,

當這個函式執行完之後,通過dlsym 獲取JNI_OnLoad函式的地址已經不是開始的JNI_OnLoad函式地址,我們這裡成為new_JNI_Onload,而是處於動態分配的debug記憶體區域,其實這些debug記憶體區域的程式碼就是前面sub_1968()函式解密出來的

為了方便分析,我將libshella.so debug區域以及libc.so libdvm.so進行了記憶體快照拍攝

我們用上面記憶體快照拍攝的生成的idb來分析new_JNI_OnLoad函式,在new_OnLoad函式中,看到了久違的registerNative註冊操作,註冊了Java層的load,runCreate等native函式

解密Dex

在Java層的attachBaseContext函式中,首先執行的是native層的load函式,

在load函式中,首先會判斷當前是dalvik還是art虛擬機器,然後執行對應的載入Dex方法

在解密真正的dex之前,首先是獲取解密前dex的儲存位置,很簡單的是,解密前的Dex儲存地址=記憶體odex地址+DexHeade->dataOff+DexHeade->dataSize

獲取到解密前dex的記憶體地址,就會執行decrypt函式對dex進行解密,手動脫殼的話,在這裡就可以dump處真正的dex了

Dalvik下載入Dex

在我的那篇阿里早期加固程式碼還原的帖子中,在Dalvik下的Dex載入方式是通過Dalvik_dalvik_system_DexFile_openDexFile_bytearray這個方式進行記憶體載入的,但是Legu在這裡使用了一種更加高階的方法

首先Legu會通過loadDex函式載入mix.dex,從而得到一個表示mix.dex的mCookie物件

然後Legu會構造一個0x34位元組的結構體DexHeaderBak,用來儲存真正Dex的DexHeader資訊

然後用mmap分配一段記憶體mmap_buffer,在其中填充一些真正Dex的資訊,我畫了一張圖描述mmap_bufffer的記憶體結構

在Android4.4中,mCookie物件是指向DexOrJar結構的指標,

struct DexOrJar {

    char*       fileName;

    bool        isDex;

    bool        okayToFree;

    RawDexFile* pRawDexFile;

    JarFile*    pJarFile;

    u1*         pDexMemory; // malloc()ed memory, if any

};

struct RawDexFile {

    char*       cacheFileName;

    DvmDex*     pDvmDex;

};

Legu會通過mixdex_cookie獲取pRawDexFile指標,再來獲取pDvmDex指標,

最後將pDvmDex指向的內容全部替換為mmap_buffer結構體中的內容,這樣mix.dex的mCookie物件已經表示為真正的Dex,而不是原本的mix.dex了,關於Legu為什麼知道這麼做,可能要分析Dalvik_dalvik_system_DexFile_openDexFile_bytearray這個函式的原理了。

Dalvik下Dex的載入大部分都完成了,後面Legu載入的方法其實也就是MultiDex的多Dex載入方法,這裡不做分析。

Art(Android 6.0)下載入Dex分析

Android4.4下有Dalvik_dalvik_system_DexFile_openDexFile_bytearray這個函式載入Dex,但是Android N以上就沒有了這個函式,

Legu在Art下會hook幾個系統函式,並且獲取libart.so中的art::dexFile::OpenFile函式的地址

讓我開始很疑惑的是,Legu用OpenFile函式開啟記憶體中的oat檔案,並沒有任何載入dex的操作

我分析了下Android 6.0下的OpenFile函式

OpenFile首先會call fstat函式獲取location的大小,但是此時執行的卻是hook後的fstat,Legu會替換真正的location的大小,而是返回真正的Dex大小,

然後根據前面的大小呼叫MapFile函式,MapFile最終呼叫了mmap函式,此時執行的還是hook後的mmap,

fake_mmap程式碼如下,首先會解密處真正的dex內容,然後用真正的Dex地址替換mmap返回值

OpenFile進行了mmap操作後,會進行OpenMemory載入Dex,經過上面的hook,表面上是開啟base.odex oat檔案,實際上OpenMemory真正的Dex檔案,因此我們可以Hook OpenMemory達到dump dex的目的,所以可以看出Android Art下使用OpenMemory函式來載入dex檔案。

Dalvik下脫殼機編寫

根據上面的分析,在Dalivk下可以很容易地獲取到真正Dex的記憶體位置:真正Dex儲存地址=記憶體odex地址+DexHeade->dataOff+DexHeade->dataSize

App通過執行之後,注入後雖然可以Dump Dex,但是dump 的Dex跟原始的位元組有幾個位元組不同,導致重打包執行出錯,Legu在載入的時候是真正的Dex檔案,但是脫離Legu程式碼執行起來後發現有幾個位元組變了,剛開始老以為是Legu對方法位元組碼做了處理,後來除錯發現是Dalik自己改變的,位元組碼的變化不知道是不是Dalvik對位元組碼進行優化導致的。

Art下Dump 得到的Dex是完成正確的,推薦大家在Art下進行Dump Dex

void DumpDex_kitkat(char* pkgName)

{

    int pid=getpid();

    printf("pid:%d\n",pid);

    char filename[100]={0};

    char dumpfilepath[256]={0};

    char* s;

    unsigned int startAddr=NULL;

    unsigned int endAddr=NULL;

    unsigned int mainDexAddr;

    char* oatPath[256]={0};

    errno=0;

    sprintf(dumpfilepath,"/data/data/%s/dump.dex",pkgName);

    sprintf(filename,"/proc/%d/maps",pid);

    FILE *fp;

    fp = fopen(filename, "r");

    if(fp!=NULL)

    {

        char line [2048];

        while (fgets(line, sizeof(line), fp ) != NULL ) /* read a line */

        {

            if (strstr(line, pkgName) != NULL)

            {

                if (strstr(line, "classes.dex") != NULL)

                {

                    LOGI("dvm-found odex address");

                    s = strchr(line, '-');

                    if (s == NULL)

                        LOGI(" Error: string NULL");

                    *s++ = '\0';

                    //strtoul:將字串轉化成無符號整型

                    startAddr = (void *)strtoul(line, NULL, 16);

                    endAddr = (void *)strtoul(s, NULL, 16);

                    LOGI(" dvm classes.odex addr %x-%x", startAddr,endAddr);

                    break;

                }

            }

        }

        fclose ( fp);

    }

    else

    {

        LOGI("fopen maps failed");

        return;

    }

    if(startAddr==NULL || endAddr==NULL)

    {

        LOGI("found odex or oat file failed");

        return;

    }

    mainDexAddr=startAddr+0x28;

    LOGI("dexAddr:%s",(unsigned char*)mainDexAddr);

    int magic=*(unsigned int*)mainDexAddr;

    if(magic!=0x0A786564)

    {

        LOGI("not find main Dex");

        return ;

    }

    unsigned int OrgDexOffset=getOrgDexOffset(mainDexAddr);

    LOGI("OrgDexOffset:%d",OrgDexOffset);

    unsigned int realDexAddr=mainDexAddr+OrgDexOffset;

    magic=*(unsigned int*)realDexAddr;

    if(magic!=0x0A786564)

    {

        LOGI("not find real Dex");

        return ;

    }

    unsigned int dexSize=*(unsigned int*)(realDexAddr+0x20);

    LOGI("dexSize:%d",dexSize);

    void* buffer=malloc(dexSize);

    if(buffer==0)

    {

        LOGI("malloc dexsize buffer failed");

        return;

    }

    memcpy(buffer,(void*)realDexAddr,dexSize);

    FILE* fd_dump=fopen(dumpfilepath,"wb+");

    if(fd_dump==NULL)

    {

        LOGI("fopen dumpfile error:%s",strerror(errno));

        return;

    }

    fwrite(buffer,dexSize,1,fd_dump);

    free(buffer);

    fflush(fd_dump);

    fclsoe(fd_dump);

}

Art下脫殼機編寫

我這裡是針對Android 6.0下hook OpenMemory函式,在5.1下OpenMemory函式可能引數有所改變要另外做處理

注入zygote Hook OpenMemory函式dump dex

uint32_t new_art_dexFile_openMemory(void* DexFile_thiz,char* base,int size,void* location,

                                    void* location_checksum,void* mem_map,void* oat_dex_file,void* error_meessage )

{

    if(*((uint32_t*)base)==0x0A786564)

    {

        int pid=getpid();

        const char* proc_name=get_process_name(pid);

        if(strstr(proc_name,g_szTargetName))

        {

            LOGI("openMemory found target dex");

            char szDumpPath[256]={0};

            sprintf(szDumpPath,"/data/data/%s/dump_marsha_%x.dex",g_szTargetName,size);

            FILE* fd_dump=fopen(szDumpPath,"wb+");

            if(fd_dump==NULL)

            {

                LOGI("fopen dumpfile error:%s",strerror(errno));

                return;

            }

            fwrite(base,size,1,fd_dump);

            fflush(fd_dump);

            fclose(fd_dump);

            LOGI("dex file save at %s size:%x",szDumpPath,size);

        }

        return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);

    }

    else

    {

        return old_openMemory(DexFile_thiz,base,size,location,location_checksum,mem_map,oat_dex_file,error_meessage);

    }

}

最後測試發現libshella 2.7-2.10版本的Dex都可以成功Dump出來