本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78003184
前段時間在看雪論壇發現了《發現一個安卓萬能脫殼方法》這篇文章,文章說的很簡略,其實原理很簡單也很有意思,說白了還是dalvik虛擬機模式下基於Android運行時的內存dex文件的dump,對一些免費版本的加固殼還是有效果的,dalvik模式下二代之後的加固殼就不行了。文章脫殼的原理涉及到dalvik模式下dex文件的類查找和加載的過程,下圖是dalvik模式下dex文件的類查找和加載的流程示意圖(native層的實現):
Dalvik虛擬機模式下Android普通apk應用的類方法的調用過程:
先通過類方法所在類的類簽名字符串查找到指定的目標類的 ClassObject描述對象,然後通過查找到的目標類對象 ClassObject查找獲取到該類方法的描述結構體Method,再通過類方法描述結構體Method進行來方法的調用。dalvik模式下基於 dexFindClass 函數脫殼的原理就是在指定類簽名字符串目標類的查找和加載過程中尋找脫殼點, 從 Dalvik_dalvik_system_DexFile_defineClassNative函數-->dvmDefineClass函數-->findClassNoInit函數-->dexFindClass函數 這整個流程是Android普通應用類查找和加載的流程,dexFindClass函數用於查找類的 DexClassDef 結構,Android普通應用目標類的查找和加載實現主要是在函數findClassNoInit裏實現,http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Class.cpp#1473。
Android普通Apk應用類方法的查找流程梳理:
1.Android普通Apk應用類方法的查找和加載實現主要是從 函數findClassNoInit 開始的,很明顯函數findClassNoInit調用完成之後返回是ClassObject類型的指針,ClassObject對象就是dex文件中類加載到內存之後的表現形式,類方法的調用也是先獲取到類方法所在類的描述結構體ClassObject。
2 .dalvik模式下Android類的查找和加載過程中,先調用 函數dvmLookupClass 進行類查找,如果查找不到指定類簽名字符串的目標類,則進行目標類加載處理,意思就是說Android普通apk應用的在進行目標類的第一次查找時,目標類的ClassObject描述對象肯定是不存在的,需要先進行目標類的加載纔會生成目標類的ClassObject描述對象,下次再查找該目標類就不用再進行類加載了。在進行目標類的加載時先調用 函數dexFindClass 獲取到目標類的描述結構體 DexClassDef。
3. 將指定目標類的 DexClassDef傳給函數loadClassFromDex進行目標類的加載,函數loadClassFromDex返回之後得到就是內存加載後的類 ClassObject。
函數loadClassFromDex先通過目標類的DexClassDef描述結構體,獲取目標類的DexClassData信息結構體,接着根據DexClassData信息結構體獲取到目標類的DexClassDataHeader描述結構體,然後將目標類的DexClassDef、DexClassData、DexClassDataHeader等類的描述結構體信息傳給函數loadClassFromDex0,最終由函數loadClassFromDex0進行目標類的內存加載。
/*
* Try to load the indicated class from the specified DEX file.
*
* This is effectively loadClass()+defineClass() for a DexClassDef. The
* loading was largely done when we crunched through the DEX.
*
* Returns NULL on failure. If we locate the class but encounter an error
* while processing it, an appropriate exception is thrown.
*/
static ClassObject* loadClassFromDex(DvmDex* pDvmDex,
const DexClassDef* pClassDef, Object* classLoader)
{
ClassObject* result;
DexClassDataHeader header;
const u1* pEncodedData;
const DexFile* pDexFile;
assert((pDvmDex != NULL) && (pClassDef != NULL));
pDexFile = pDvmDex->pDexFile;
if (gDvm.verboseClass) {
ALOGV("CLASS: loading '%s'...",
dexGetClassDescriptor(pDexFile, pClassDef));
}
// 通過目標類的DexClassDef獲取到目標類的DexClassData
pEncodedData = dexGetClassData(pDexFile, pClassDef);
if (pEncodedData != NULL) {
// 獲取到目標類的DexClassDataHeader
dexReadClassDataHeader(&pEncodedData, &header);
} else {
// Provide an all-zeroes header for the rest of the loading.
memset(&header, 0, sizeof(header));
}
// 根據傳入的目標類的DexClassDef、DexClassData以及DexClassDataHeader
// 對目標類進行內存加載得到目標類內存加載之後的類描述結構體ClassObject
result = loadClassFromDex0(pDvmDex, pClassDef, &header, pEncodedData,
classLoader);
if (gDvm.verboseClass && (result != NULL)) {
ALOGI("[Loaded %s from DEX %p (cl=%p)]",
result->descriptor, pDvmDex, classLoader);
}
return result;
}
4. 指定類簽名字符串的目標類加載到內存以後,將其添加到普通apk應用進程的類Hash表中,方便下次該類的查找,以後查找該類的時候就不用再加載了,直接查找就能查找到。
/*
* Add a new class to the hash table.
*
* The class is considered "new" if it doesn't match on both the class
* descriptor and the defining class loader.
*
* TODO: we should probably have separate hash tables for each
* ClassLoader. This could speed up dvmLookupClass and
* other common operations. It does imply a VM-visible data structure
* for each ClassLoader object with loaded classes, which we don't
* have yet.
*/
bool dvmAddClassToHash(ClassObject* clazz)
{
void* found;
u4 hash;
// 對指定簽名字符串的類做Hash處理
hash = dvmComputeUtf8Hash(clazz->descriptor);
// 鎖
dvmHashTableLock(gDvm.loadedClasses);
// 指定類的簽名字符串hash和類加載後描述結構體ClassObject
// 添加的進程的loadedClasses的哈希表中
found = dvmHashTableLookup(gDvm.loadedClasses, hash, clazz,
hashcmpClassByClass, true);
dvmHashTableUnlock(gDvm.loadedClasses);
ALOGV("+++ dvmAddClassToHash '%s' %p (isnew=%d) --> %p",
clazz->descriptor, clazz->classLoader,
(found == (void*) clazz), clazz);
//dvmCheckClassTablePerf();
/* can happen if two threads load the same class simultaneously */
return (found == (void*) clazz);
}
5.指定簽名字符串的目標類的ClassObject查找函數dvmLookupClass的實現。
/*
* Search through the hash table to find an entry with a matching descriptor
* and an initiating class loader that matches "loader".
*
* The table entries are hashed on descriptor only, because they're unique
* on *defining* class loader, not *initiating* class loader. This isn't
* great, because it guarantees we will have to probe when multiple
* class loaders are used.
*
* Note this does NOT try to load a class; it just finds a class that
* has already been loaded.
*
* If "unprepOkay" is set, this will return classes that have been added
* to the hash table but are not yet fully loaded and linked. Otherwise,
* such classes are ignored. (The only place that should set "unprepOkay"
* is findClassNoInit(), which will wait for the prep to finish.)
*
* Returns NULL if not found.
*/
ClassObject* dvmLookupClass(const char* descriptor, Object* loader,
bool unprepOkay)
{
ClassMatchCriteria crit;
void* found;
u4 hash;
crit.descriptor = descriptor;
crit.loader = loader;
// 對指定類的簽名字符串做hash處理
hash = dvmComputeUtf8Hash(descriptor);
LOGVV("threadid=%d: dvmLookupClass searching for '%s' %p",
dvmThreadSelf()->threadId, descriptor, loader);
dvmHashTableLock(gDvm.loadedClasses);
// 根據指定類的簽名hash值查找該目標類的ClassObject
found = dvmHashTableLookup(gDvm.loadedClasses, hash, &crit,
hashcmpClassByCrit, false);
dvmHashTableUnlock(gDvm.loadedClasses);
/*
* The class has been added to the hash table but isn't ready for use.
* We're going to act like we didn't see it, so that the caller will
* go through the full "find class" path, which includes locking the
* object and waiting until it's ready. We could do that lock/wait
* here, but this is an extremely rare case, and it's simpler to have
* the wait-for-class code centralized.
*/
if (found && !unprepOkay && !dvmIsClassLinked((ClassObject*)found)) {
ALOGV("Ignoring not-yet-ready %s, using slow path",
((ClassObject*)found)->descriptor);
found = NULL;
}
return (ClassObject*) found;
}
6. Dalvik模式下基於Android運行時類加載的函數dexFindClass脫殼的要點: 由於一些免費版的apk加固基本都是dex文件的整體加載,粒度比較粗還沒有細分到dex文件類方法的加固處理,在dalvik模式下Android運行時的類加載需要使用到內存加載的dex文件(此時一代的加固apk一般都已經在內存裏解密完成,因此在解密後dex文件的類加載時,我們可以獲取到解密後的dex文件的內存地址,這裏選擇在類加載的相關函數dexFindClass中進行內存dex文件的獲取和dump處理),很多的普通apk應用都會調用dexFindClass函數,那麼該怎麼設置dex文件內存dump的過濾條件呢?被加固的dex雖然dex文件整體被加固了,類被隱藏了但是礙於加固的處理方法被加固dex文件的 主Activity 還是暴露給我們了,故將被加固dex文件的主Activity類的簽名字符串作爲過濾條件。
7. 以Android 4.4.4版本的系統爲例,在dalvik虛擬機動態庫文件libdvm.so 中,dvmFindClassNoInit、dvmFindDirectMethodByDescriptor、dvmFindVirtualMethodByDescriptor、dvmFindLoadedClass、dvmFindLoadedClass、dvmFindClass、dexFindClass 等函數是導出函數,可以被Hook掉。
8. Dalvik模式下基於Android運行時類加載的函數dexFindClass脫殼,對Android源碼的修改(以Android 4.4.4 r1的源碼爲例),主要代碼修改在源碼文件 /dalvik/libdex/DexFile.cpp 中,修改如下:
// *******添加脫殼的代碼********************************************************
// http://androidxref.com/4.4.4_r1/xref/dalvik/libdex/DexFile.cpp
//#include <asm/siginfo.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
// 需要脫殼的apk的主activity的名稱字符串
static char mainActivityName[128] = {0};
// 內存dex文件dump後文件夾
// /data/data/apk應用的包名/
static char dexDumpName[128] = {0};
// 記錄脫殼配置文件是否讀取的標記
static bool readable = true;
// 信號互斥量
static pthread_mutex_t read_mutex;
// 線程回調函數
void* ReadThread(void *arg)
{
FILE *fp = NULL;
while (mainActivityName[0] == 0 || dexDumpName[0] == 0)
{
// 讀取脫殼的配置文件/data/local/tmp/dex_dump_ok的信息
fp = fopen("/data/local/tmp/dex_dump_ok", "r");
if (fp==NULL)
{
sleep(1);
continue;
}
// 讀取需要脫殼的apk的主activity的名稱字符串(第1行)
fgets(mainActivityName, 128, fp);
mainActivityName[strlen(mainActivityName)-1] = 0;
// 讀取脫殼apk的內存dex文件dump後的文件名稱字符串(第2行)
fgets(dexDumpName, 128, fp);
dexDumpName[strlen(dexDumpName)-1] = 0;
fclose(fp);
fp = NULL;
}
// 釋放信號量
pthread_mutex_lock(&read_mutex);
return NULL;
}
// *******添加脫殼的代碼********************************************************
/*
* Look up a class definition entry by descriptor.
*
* "descriptor" should look like "Landroid/debug/Stuff;".
*/
const DexClassDef* dexFindClass(const DexFile* pDexFile,
const char* descriptor)
{
const DexClassLookup* pLookup = pDexFile->pClassLookup;
u4 hash;
int idx, mask;
// *******添加脫殼的代碼****************************************************
// 1.讀取配置文件(獲取需要脫殼的apk的主activity類字符串)
int uid = getuid();
// 過濾掉系統進程
if (uid)
{
// 打印當前被查找的類名稱
ALOGI("dexFindClass--DexFile addr: 0x%08x, Class descriptor: %s", (int)pDexFile, descriptor);
if (readable)
{
// 創建互斥信號通量
pthread_mutex_lock(&read_mutex);
if (readable)
{
readable = false;
// 釋放互斥信號通量
pthread_mutex_unlock(&read_mutex);
pthread_t read_thread;
// 創建線程,讀取脫殼配置文件/data/local/tmp/dex_dump_ok的信息
pthread_create(&read_thread, NULL, ReadThread, NULL);
}
else
{
// 釋放互斥信號通量
pthread_mutex_unlock(&read_mutex);
}
}
}
// 2,進行需要脫殼的apk的主activity類字符串的匹配
// 格式:"Landroid/debug/Stuff;"
if (strcmp(mainActivityName, descriptor) == 0) {
// 3.匹配成功進行內存dex文件的dump處理
char szBuffer[128] = {0};
// 字符串拼接得到內存dex文件的dump路徑
// /data/data/com.example.seventyfour.tencenttest/
strcat(szBuffer, dexDumpName);
strcat(szBuffer, "dump_dex_over");
// 打印dex文件的dump文件路徑
ALOGI("DEX_DUMP_PATH: %s", szBuffer);
// 創建新文件保存dump的內存dex文件
FILE* file = fopen(szBuffer, "wb+");
if (file == NULL) {
ALOGI("DEX_DUMP_PATH--fopen: %s error !", szBuffer);
} else {
// 保存三倍dex文件長度(比較暴力)
// 暫時不考慮Hook系統函數write或者read反內存dump的情況
fwrite(pDexFile->baseAddr, (pDexFile->pHeader->fileSize)*3, 1, file);
// 關閉文件
fclose(file);
}
// 打印內存dex文件的信息
ALOGI("DEX_DUMP_PATH--addr: 0x%08x, lenth: %d", pDexFile->baseAddr, pDexFile->pHeader->fileSize);
}
// *******添加脫殼的代碼****************************************************
hash = classDescriptorHash(descriptor);
mask = pLookup->numEntries - 1;
idx = hash & mask;
/*
* Search until we find a matching entry or an empty slot.
*/
while (true) {
int offset;
offset = pLookup->table[idx].classDescriptorOffset;
if (offset == 0)
return NULL;
if (pLookup->table[idx].classDescriptorHash == hash) {
const char* str;
str = (const char*) (pDexFile->baseAddr + offset);
if (strcmp(str, descriptor) == 0) {
return (const DexClassDef*)
(pDexFile->baseAddr + pLookup->table[idx].classDefOffset);
}
}
idx = (idx + 1) & mask;
}
}
9. 將Android 4.4.4 r1的源碼文件 /dalvik/libdex/DexFile.cpp 按上面的修改以後,重新編譯dalvik虛擬機的源碼生成新的動態庫文件libdvm.so,make snod 重新生成新的Android系統鏡像文件system.img,重啓Nexus 5手機進入刷機模式,使用新的Android系統鏡像文件system.img進行刷機。加固apk脫殼的時候使用比較簡單,先安裝需要脫殼的apk應用到手機設備上,按下面的格式構建脫殼配置文件 dex_dump_ok,adb push 脫殼配置文件 dex_dump_ok 到手機設備 /data/local/tmp 文件夾下,運行需要脫殼的加固apk,過一會兒在脫殼apk應用的包名路徑下就會生成內存dump的dex文件 dump_dex_over。
adb push dex_dump_ok /data/local/tmp
10. Dalvik模式下基於Android運行時類加載的函數dexFindClass脫殼,只針對第一代加殼的apk脫殼比較有效,我這裏是通過修改Android源碼的方式來進行內存dex文件dump脫殼操作,比較麻煩,由於dexFindClass函數 在動態庫文件 libdvm.so 中是導出函數,因此也可以使用Hook dexFindClass函數的方式來進行dex文件內存dump的脫殼。整體來說,Dalvik模式下基於Android運行時類加載的函數dexFindClass脫殼的原理比較簡單,主要是爲了熟悉一下dalvik模式下dex文件類查找和加載的流程。
參考資料: