樂固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出來