1. 程式人生 > >Android FART脫殼機流程分析

Android FART脫殼機流程分析

> 本文首發於安全客 > > 連結:https://www.anquanke.com/post/id/219094 ## 0x1 前言 在Android平臺上,程式設計師編寫的Java程式碼最終將被編譯成位元組碼在Android虛擬機器上執行。自從Android進入大眾的視野後,apktool,jadx等反編譯工具也層出不窮,功能也越來越強大,由Java編譯成的位元組碼在這些反編譯工具面前變得不堪一擊,這相當於一個人裸奔在茫茫人海,身體的各個部位被眾人一覽無餘。一種事物的出現,也會有與之對立的事物出現。有反編譯工具的出現,當然也會有反反編譯工具的出現,這種技術一般我們加固技術。APP經過加固,就相當於給那個裸奔的人穿了衣服,“衣服”在一定程度上保護了APP,使APP沒那麼容易被反編譯。當然,有加固技術的出現,也會有反加固技術的出現,即本文要分析的脫殼技術。 Android經過多個版本的更迭,它無論在外觀還是內在都有許多改變,早期的Android使用的是dalvik虛擬機器,Android4.4開始加入ART虛擬機器,但不預設啟用。從Android5.0開始,ART取代dalvik,成為預設虛擬機器。由於dalvik和ART執行機制的不同,在它們內部脫殼原理也不太相同,本文分析的是ART下的脫殼方案:FART。它的整體思路是通過**主動呼叫**的方式來實現脫殼,專案地址:https://github.com/hanbinglengyue/FART 。FART的程式碼是通過修改少量Android原始碼檔案而成的,經過修改的Android原始碼編譯成系統映象,刷入手機,這樣的手機啟動後,就成為一臺可以用於脫殼的脫殼機。 ## 0x2 流程分析 FART的入口在`frameworks\base\core\java\android\app\ActivityThread.java`的performLaunchActivity函式中,即APP的Activity啟動的時候執行fartthread ```java private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { Log.e("ActivityThread","go into performLaunchActivity"); ActivityInfo aInfo = r.activityInfo; if (r.packageInfo == null) { r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); } ...... //開啟fart執行緒 fartthread(); ...... } ``` fartthread函式開啟一個執行緒,休眠一分鐘後呼叫fart函式 ```java public static void fartthread() { new Thread(new Runnable() { @Override public void run() { try { Log.e("ActivityThread", "start sleep,wait for fartthread start......"); Thread.sleep(1 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } Log.e("ActivityThread", "sleep over and start fartthread"); fart(); Log.e("ActivityThread", "fart run over"); } }).start(); } ``` fart函式中,獲取Classloader,反射獲取一些類。反射呼叫`dalvik.system.DexPathList`的dexElements欄位得到`dalvik.system.DexPathList$Element`類物件陣列,Element類儲存著dex的路徑等資訊。接下來通過遍歷`dexElements`,得到每一個Element物件中的DexFile物件,再獲取DexFile物件中的mCookie欄位值,呼叫DexFile類中的`String[] getClassNameList(Object cookie)`函式並傳入獲取到mCookie,以得到dex檔案中所有的類名。隨後,遍歷dex中的所有類名,傳入`loadClassAndInvoke`函式。 ```java public static void fart() { ClassLoader appClassloader = getClassloader(); List dexFilesArray = new ArrayList(); Field pathList_Field = (Field) getClassField(appClassloader, "dalvik.system.BaseDexClassLoader", "pathList"); Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList"); Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements"); Field dexFile_fileField = null; try { dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile"); } catch (Exception e) { e.printStackTrace(); } Class DexFileClazz = null; try { DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile"); } catch (Exception e) { e.printStackTrace(); } Method getClassNameList_method = null; Method defineClass_method = null; Method dumpDexFile_method = null; Method dumpMethodCode_method = null; for (Method field : DexFileClazz.getDeclaredMethods()) { if (field.getName().equals("getClassNameList")) { getClassNameList_method = field; getClassNameList_method.setAccessible(true); } if (field.getName().equals("defineClassNative")) { defineClass_method = field; defineClass_method.setAccessible(true); } if (field.getName().equals("dumpMethodCode")) { dumpMethodCode_method = field; dumpMethodCode_method.setAccessible(true); } } Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie"); for (int j = 0; j < ElementsArray.length; j++) { Object element = ElementsArray[j]; Object dexfile = null; try { dexfile = (Object) dexFile_fileField.get(element); } catch (Exception e) { e.printStackTrace(); } if (dexfile == null) { continue; } if (dexfile != null) { dexFilesArray.add(dexfile); Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie"); if (mcookie == null) { continue; } String[] classnames = null; try { classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie); } catch (Exception e) { e.printStackTrace(); continue; } catch (Error e) { e.printStackTrace(); continue; } if (classnames != null) { for (String eachclassname : classnames) { loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method); } } } } return; } ``` loadClassAndInvoke除了傳入上面提到的類名,還傳入ClassLoader物件和dumpMethodCode函式的Method物件,看上面的程式碼可以知道,dumpMethodCode函式來自DexFile,原本的DexFile類沒有這個函式,是FART加上去的。dumpMethodCode究竟做了什麼我們待會再來看,先把loadClassAndInvoke函式看完。loadClassAndInvoke工作也很簡單,根據傳入的類名來載入類,再從載入的類獲取它的所有的建構函式和函式,然後呼叫dumpMethodCode,傳入Constructor物件或者Method物件 ```java public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) { Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname); Class resultclass = null; try { resultclass = appClassloader.loadClass(eachclassname); } catch (Exception e) { e.printStackTrace(); return; } catch (Error e) { e.printStackTrace(); return; } if (resultclass != null) { try { Constructor cons[] = resultclass.getDeclaredConstructors(); for (Constructor constructor : cons) { if (dumpMethodCode_method != null) { try { dumpMethodCode_method.invoke(null, constructor); } catch (Exception e) { e.printStackTrace(); continue; } catch (Error e) { e.printStackTrace(); continue; } } else { Log.e("ActivityThread", "dumpMethodCode_method is null "); } } } catch (Exception e) { e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } try { Method[] methods = resultclass.getDeclaredMethods(); if (methods != null) { for (Method m : methods) { if (dumpMethodCode_method != null) { try { dumpMethodCode_method.invoke(null, m); } catch (Exception e) { e.printStackTrace(); continue; } catch (Error e) { e.printStackTrace(); continue; } } else { Log.e("ActivityThread", "dumpMethodCode_method is null "); } } } } catch (Exception e) { e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } } } ``` 上面提到dumpMethodCode函式在DexFile類中,DexFile的完整路徑為:`libcore\dalvik\src\main\java\dalvik\system\DexFile.java`,它是這麼定義的: ```java private static native void dumpMethodCode(Object m); ``` 可見,它是一個native方法,它的實際程式碼在:`art\runtime\native\dalvik_system_DexFile.cc`,程式碼為: ```cpp static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method) { ScopedFastNativeObjectAccess soa(env); if(method!=nullptr) { ArtMethod* artmethod = ArtMethod::FromReflectedMethod(soa, method); myfartInvoke(artmethod); } return; } ``` DexFile_dumpMethodCode函式中,method是loadClassAndInvoke函式傳過來的`java.lang.reflect.Method`物件,傳進來的Java層Method物件傳入FromReflectedMethod函式得到ArtMethod結構指標,再將ArtMethod結構指標傳入myfartInvoke函式。 myfartInvoke實際程式碼在`art/runtime/art_method.cc`檔案裡 ```cpp extern "C" void myfartInvoke(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) { JValue *result = nullptr; Thread *self = nullptr; uint32_t temp = 6; uint32_t *args = &temp; uint32_t args_size = 6; artmethod->Invoke(self, args, args_size, result, "fart"); } ``` 在myfartInvoke函式中,值得關注的是self被設定為空指標,並傳入ArtMethod的Invoke函式。 Invoke函式也是在`art/runtime/art_method.cc`檔案裡,在Invoke函式開頭,它對self引數做了個判斷,如果self為空,說明Invoke函式是被FART所呼叫的,反之則是系統本身的呼叫。self為空的時候,呼叫dumpArtMethod函式,並立即返回 ```cpp void ArtMethod::Invoke(Thread * self, uint32_t * args, uint32_t args_size, JValue * result, const char *shorty) { if (self == nullptr) { dumpArtMethod(this); return; } ...... } ``` dumpArtMethod函式這裡就到了dump dex的程式碼了。 ```cpp extern "C" void dumpArtMethod(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) { char *dexfilepath = (char *) malloc(sizeof(char) * 2000); if (dexfilepath == nullptr) { LOG(INFO) << "ArtMethod::dumpArtMethodinvoked,methodname:" << PrettyMethod(artmethod). c_str() << "malloc 2000 byte failed"; return; } int fcmdline = -1; char szCmdline[64] = { 0 }; char szProcName[256] = { 0 }; int procid = getpid(); sprintf(szCmdline, "/proc/%d/cmdline", procid); fcmdline = open(szCmdline, O_RDONLY, 0644); if (fcmdline >
0) { read(fcmdline, szProcName, 256); close(fcmdline); } if (szProcName[0]) { const DexFile *dex_file = artmethod->GetDexFile(); const char *methodname = PrettyMethod(artmethod).c_str(); const uint8_t *begin_ = dex_file->Begin(); size_t size_ = dex_file->Size(); memset(dexfilepath, 0, 2000); int size_int_ = (int) size_; memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "%s", "/sdcard/fart"); mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "/sdcard/fart/%s", szProcName); mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "/sdcard/fart/%s/%d_dexfile.dex", szProcName, size_int_); int dexfilefp = open(dexfilepath, O_RDONLY, 0666); if (dexfilefp >
0) { close(dexfilefp); dexfilefp = 0; } else { dexfilefp = open(dexfilepath, O_CREAT | O_RDWR, 0666); if (dexfilefp > 0) { write(dexfilefp, (void *) begin_, size_); fsync(dexfilefp); close(dexfilefp); } } //下半部分開始 const DexFile::CodeItem * code_item = artmethod->
GetCodeItem(); // (1) if (LIKELY(code_item != nullptr)) { int code_item_len = 0; uint8_t *item = (uint8_t *) code_item; if (code_item->tries_size_ > 0) { // (2) const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item,code_item->tries_size_)); uint8_t *tail = codeitem_end(&handler_data); code_item_len = (int)(tail - item); } else { code_item_len = 16 + code_item-> insns_size_in_code_units_ * 2; } memset(dexfilepath, 0, 2000); int size_int = (int) dex_file->Size(); // Length of data uint32_t method_idx = artmethod->get_method_idx(); sprintf(dexfilepath, "/sdcard/fart/%s/%d_%ld.bin", szProcName, size_int, gettidv1()); int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666); if (fp2 > 0) { lseek(fp2, 0, SEEK_END); memset(dexfilepath, 0, 2000); int offset = (int) (item - begin_); sprintf(dexfilepath, "{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:", methodname, method_idx, offset, code_item_len); int contentlength = 0; while (dexfilepath[contentlength] != 0) contentlength++; write(fp2, (void *) dexfilepath, contentlength); long outlen = 0; char *base64result = base64_encode((char *) item, (long) code_item_len, &outlen); write(fp2, base64result, outlen); write(fp2, "};", 2); fsync(fp2); close(fp2); if (base64result != nullptr) { free(base64result); base64result = nullptr; } } } } if (dexfilepath != nullptr) { free(dexfilepath); dexfilepath = nullptr; } } ``` dumpArtMethod函式開始先通過`/proc//cmdline`虛擬檔案讀取程序pid對應的程序名,根據得到的程序名在sdcard下建立目錄,所以在脫殼之前要給APP寫入外部儲存的許可權。之後通過ArtMethod的GetDexFile函式得到DexFile指標,即ArtMethod所在的dex的指標,再從DexFile的Begin函式和Size函式得到dex檔案在記憶體中起始的地址和dex檔案的大小,接著用write函式把記憶體中的dex寫到檔名以`_dexfile.dex`的檔案中。 但該函式還沒完,dumpArtMethod函式的下半部分,對函式的CodeItem進行dump。可能有些人就有疑問了,函式的上半部分不是把dex給dump了嗎,為什麼還需要取函式的CodeItem進行dump呢?對於某些殼,dumpArtMethod的上半部分已經能對dex進行整體dump,但是對於部分抽取殼,dex即使被dump下來,函式體還是以nop填充,即空函式體,FART還把函式的CodeItem給dump下來是讓使用者手動來修復這些dump下來的空函式。 我們來看dumpArtMethod函式的下半部分,這裡將會涉及dex檔案的結構,如果不瞭解請結合文件來看。註釋`(1)`處,從ArtMethod中得到一個CodeItem。註釋`(2)`處,根據CodeItem的`tries_size_`,即try_item的數量來計算CodeItem的大小: (1)如果tries_size_不為0,說明這個CodeItem有try_item,那麼去把CodeItem的結尾地址給算出來 ```cpp const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item,code_item->tries_size_)); uint8_t *tail = codeitem_end(&handler_data); code_item_len = (int)(tail - item); ``` codeitem_end函式怎麼算出CodeItem的結束地址呢? GetTryItems第二引數傳入`tries_size_`,即跳過所有的try_item,得到encoded_catch_handler_list的地址,然後傳入codeitem_end函式 ```cpp uint8_t *codeitem_end(const uint8_t ** pData) { uint32_t num_of_list = DecodeUnsignedLeb128(pData); for (; num_of_list > 0; num_of_list--) { int32_t num_of_handlers = DecodeSignedLeb128(pData); int num = num_of_handlers; if (num_of_handlers <= 0) { num = -num_of_handlers; } for (; num > 0; num--) { DecodeUnsignedLeb128(pData); DecodeUnsignedLeb128(pData); } if (num_of_handlers <= 0) { DecodeUnsignedLeb128(pData); } } return (uint8_t *) (*pData); } ``` codeitem_end函式的開頭讀取encoded_catch_handler_list結構中包含多少個encoded_catch_handler結構,如果不為0,遍歷所有encoded_catch_handler結構,讀取encoded_catch_handler結構中有多少encoded_type_addr_pair結構,有的話全部跳過,即跳過了整個encoded_catch_handler_list結構。最後函式返回的pData即為CodeItem的結尾地址。 得到了CodeItem結尾地址,用CodeItem結尾的地址減去CodeItem的起始地址得到CodeItem的真實大小。 (2)如果tries_size_為0,那麼就沒有try_item,直接就能把CodeItem的大小計算出來: ```cpp code_item_len = 16 + code_item->insns_size_in_code_units_ * 2; ``` CodeItem的大小計算出來之後,接下來可以看到,有幾個變數以格式化的方式列印到dexfilepath ```cpp sprintf(dexfilepath, "{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:", methodname, method_idx, offset, code_item_len ); ``` - name 函式的名稱 - method_idx 來源FART新增的函式:`uint32_t get_method_idx(){ return dex_method_index_; }`,函式返回dex_method_index_,dex_method_index_是函式在`method_ids`中的索引 - offset 是該函式的CodeItem相對於dex檔案開始的偏移 - code_item_len CodeItem的長度 資料組裝好之後,寫入到以`.bin`為字尾的檔案中: ```cpp write(fp2, (void *) dexfilepath, contentlength); long outlen = 0; char *base64result = base64_encode((char *) item, (long) code_item_len, &outlen); write(fp2, base64result, outlen); write(fp2, "};", 2); ``` 對於上面的dexfilepath,它們是明文字元,直接寫入即可。而對於CodeItem中的bytecode這種非明文字元,直接寫入不太好看,所以FART選擇對它們進行base64編碼後再寫入。 分析到這裡好像已經結束了,從主動呼叫,到dex整體dump,再到函式CodeItem的dump,都已經分析了。但是FART中確實還有一部分邏輯是沒有分析的。如果你使用過FART來脫過殼,會發現它dump下來的dex中還有以`_execute.dex`結尾的dex檔案。這種dex是怎麼生成的呢? 這一部分的程式碼也是在`art\runtime\art_method.cc`檔案中 ```cpp extern "C" void dumpDexFileByExecute(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) { char *dexfilepath = (char *) malloc(sizeof(char) * 2000); if (dexfilepath == nullptr) { LOG(INFO) << "ArtMethod::dumpDexFileByExecute,methodname:" << PrettyMethod(artmethod). c_str() << "malloc 2000 byte failed"; return; } int fcmdline = -1; char szCmdline[64] = { 0 }; char szProcName[256] = { 0 }; int procid = getpid(); sprintf(szCmdline, "/proc/%d/cmdline", procid); fcmdline = open(szCmdline, O_RDONLY, 0644); if (fcmdline > 0) { read(fcmdline, szProcName, 256); close(fcmdline); } if (szProcName[0]) { const DexFile *dex_file = artmethod->GetDexFile(); const uint8_t *begin_ = dex_file->Begin(); // Start of data. size_t size_ = dex_file->Size(); // Length of data. memset(dexfilepath, 0, 2000); int size_int_ = (int) size_; memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "%s", "/sdcard/fart"); mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "/sdcard/fart/%s", szProcName); mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000); sprintf(dexfilepath, "/sdcard/fart/%s/%d_dexfile_execute.dex", szProcName, size_int_); int dexfilefp = open(dexfilepath, O_RDONLY, 0666); if (dexfilefp > 0) { close(dexfilefp); dexfilefp = 0; } else { dexfilefp = open(dexfilepath, O_CREAT | O_RDWR, 0666); if (dexfilefp > 0) { write(dexfilefp, (void *) begin_, size_); fsync(dexfilefp); close(dexfilefp); } } } if (dexfilepath != nullptr) { free(dexfilepath); dexfilepath = nullptr; } } ``` 可以看到,dumpDexFileByExecute函式有點像dumpArtMethod函式的上半部分,即對dex檔案的整體dump。那麼,dumpDexFileByExecute在哪裡被呼叫呢? 通過搜尋,在`art\runtime\interpreter\interpreter.cc`檔案的開始,看到了FART在art名稱空間下定義了一個dumpDexFileByExecute函式 ```cpp namespace art { extern "C" void dumpDexFileByExecute(ArtMethod* artmethod); namespace interpreter { ...... } } ``` 同時在檔案其中找到了對dumpDexFileByExecute函式的呼叫: ```cpp static inline JValue Execute(Thread* self, const DexFile::CodeItem* code_item, ShadowFrame& shadow_frame, JValue result_register) { if(strstr(PrettyMethod(shadow_frame.GetMethod()).c_str(),"")!=nullptr) { dumpDexFileByExecute(shadow_frame.GetMethod()); } ...... } ``` 在Execute函式中,通過判斷函式名稱中是否為``決定要不要呼叫dumpDexFileByExecute,即判斷傳入的是否為靜態程式碼塊,對於加了殼的App來說靜態程式碼塊是肯定存在的。如果Execute傳入的是靜態程式碼塊則呼叫dumpDexFileByExecute函式,並傳入一個ArtMethod指標。 dumpDexFileByExecute中對dex進行了整體dump,可以把它看作是dumpArtMethod方式的互補,有時dumpArtMethod中得不到想得到的dex,用dumpDexFileByExecute或許能得到驚喜。 ## 0x3 結語 非常感謝FART作者能夠開源FART,這使得人們對抗ART環境下App殼得到了良好的思路。FART脫殼機理論上來講能脫大多數殼,但是仍有例外,需要自行摸索。 ## 0x4 參考 - https://bbs.pediy.com/thread-252630.htm - https://source.android.google.cn/devices/tech/dalvik/de