這是我通讀FART的筆記,將FART的原理用自己的理解記錄下來的一篇文章。
問題一:
爲了解決後續應用在加載執行解密後的dex文件中的Class和Method的問題,接下來就是通過利用java的反射修復一系列的變量。其中最爲重要的一個變量就是應用運行中的Classloader,只有Classloader被修正後,應用才能夠正常的加載並調用dex中的類和方法,否則的話由於Classloader的雙親委派機制,最終會報ClassNotFound異常,應用崩潰退出,這是加固廠商不願意看到的。
爲什麼會有解密後的dex文件中的Class和Method問題。會有什麼問題?爲什麼會有這樣的問題?
我的理解是:
因爲dex文件被加密了,而後需要進行解密。使用我們自定義的classloader進行讀取解密後的dex文件,但是本來沒有加殼的dex文件中的classloader被殼加密後再解密需要進行修復,只有這個應用中運行中classloader被修復後,才能正確運行原本dex裏的Method和類,否則的話由於Classloader的雙親委派機制,最終會爆出ClassNotFound異常,應用崩潰退出。
由此可見Classloader是一個至關重要的變量,所有的應用中加載的dex文件最終都在應用的Classloader中。
因此,只要獲取到加固應用最終通過反射設置後的Classloader,我們就可以通過一系列反射最終獲取到當前應用所加載的解密後的內存中的Dex文件。
總結就是:
首先要知道一個流程,就是加殼在什麼時候什麼地方加,這裏貼一個圖。
attachBaseContext、onCreate位置加殼。
加固後要對dex文件進行解密,解密至關重要的是修正Classloader,然後通過獲取classloader利用反射獲取到當前應用所加載的解密後的在內存裏的dex文件。
知識點 dex加密和hook
隨着加殼技術的發展,爲了對抗dex整體加固更易於內存dump來得到原始dex的問題,各加固廠商又結合hook技術,通過hook dex文件中類和方法加載執行過程中的關鍵流程,來實現在函數執行前才進行解密操作的指令抽取的解決方案。此時,就算是對內存中的dex整體進行了dump,但是由於其方法的最爲重要的函數體中的指令被加密,導致無法對相關的函數進行脫殼。
就是說,不再僅僅把整體的dex整體加固,因爲會被dump出內存中整塊的dex文件。然後利用hook技術,把內存中的重要函數體進行加密,然後函數被調用的時候,再進行解密。這樣,即使把整塊dex文件dump出來,重要函數還是不可見。
知識點二 Fupk3
Fupk3可以針對知識點一的問題。
由此,Fupk3誕生了,該脫殼工具通過欺騙殼而主動調用dex中的各個函數,完成調用流程,讓殼主動解密對應method的指令區域,從而完成對指令抽取型殼的脫殼。
原理很明顯,我不再是等你運行到重要函數的時候纔要你解密,而是在未運行之前,就騙殼主動調用dex中的各個函數,這樣殼就幫加密內容解密出來了。
詳細原理:
大概是說,Android是開源的,通過直接修改Android源碼,把運行時的所有dex數據dump出來,不就可以實現一個通用的脫殼機了嗎。
https://bbs.pediy.com/thread-246117.htm,真得好好拜讀一下!
現有ART環境下自動化脫殼工具及優缺點
我幾乎都沒用過,但是提前學習下,瞭解它們的適用範圍、原理以及優缺點,爲以後使用打下基礎吧。
dexhunter包含了dalvik和art環境,這兩種是說Andriod的虛擬機版本。art環境較dalvik做了較大改變。
但是dexhunter樹大招風。有對抗dexhunter的加固,比如:
- 添加無效類,這些類的初始化函數加入強制退出的代碼,被調用就會強制退出。
- 檢測dexhunter配置文件。
ART還有基於dex2oat編譯生成oat過程的內存dex的dump技術。
也是整體dump,如果是hook+抽取加密也是沒有辦法。有的還會對dex的流程進行hook,這些dex就不會走dex2oat流程。
基於dex加載過程中內存中DexFile結構體的dump,比如,ART通過hook OpenMem函數來實現在殼進行加載DexFile時對內存中的dex的dump的脫殼技術,早前在17年在DEF CON25黑客大會中,有人提議修改DexFile:DexFile(),以及OpenAndReadMagic()函數來實現對加殼應用的內存中的dex的dump脫殼技術。
這些都是整體dump,無法實現對指令抽取型殼的完全脫殼。
但是隨着Dalvik逐漸淘汰,Android4.4以下的系統,現在的APP漸漸不支持了,這就讓基於Dalvik的fupk3走向末路,但是ART方案也已經被實現和開發出來,由於某些原因並未開源。
指令抽取和vmp加固有什麼區別?
一個是抽取關鍵代碼,另一個是附加了自己的虛擬機和指令進行執行。
FART脫殼原理以及實現
- 內存中的DexFile結構體完整dex的dump
- 主動調用類中的每一個方法,並實現對應Codeltem的dump
- 通過主動調用dump下來的方法的CodeItem進行dex中被抽取方法的修復
內存中的DexFile結構體完整dex的dump
不是通過修改DexFile:DexFile(),這種做法還是有辦法對抗的,比如殼自己實現一套Dex文件的內存加載機制從而繞過。
這裏是通過在合適的時機點獲取到應用解密後的dex文件最終依附的Classloader,進行通過java的反射機制最終獲取到對應的DexFile結構體,並完成dex的dump。
思路:
1、獲取Classloader時機點的選擇。
APP中Application類中的attachBaseContext和onCreate函數是app中最先執行的方法,殼也是通過替換APP的Application類並自己實現這兩個函數,並在這兩個函數中實現dex的解密加載,hook系統中的Class和method加載執行流程中的關鍵函數,最後通過反射完成關鍵變量如最終的Classloader,Application等的替換從而完成執行權的交付。
我們選擇在任意一個在Application的onCreate函數執行之後的函數,一個應用最終都要由一個個的Activity來展示應用的界面並和用戶完成交互,所以可以選擇ActivityThread中的performLaunchActivity函數作爲時機,來獲取最終的應用的Classloader。選擇該函數還有一個好處在於該函數和應用最終的application同在ActivityThread類中,可以很方便獲取到該類的成員。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent){
......
Activity activity = null;
try{
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//下面通過application的getClassLoader()獲取最終的Classloader,並開啓線程,在新線程中完成內存中的dex的dump以及主動調用過程,由於該過程相對耗時,爲了防止應用出現ANR,從而開啓新線程,在新線程中進行,主要的工作都在getDexFilesByClassLoader_23
//addstart
packagename = r.packageInfo.getPackageName();
if(mInitalApplication != null){
fina java.lang.ClassLoader finalcl = mInitialApplication.getClassLoader();
new Thread(new Runnable(){
@Override
public void run() {
getDexFilesByClassLoader_23(finalcl);
}
}).start()
}
}
}
getDexFilesByClassLoader_23()函數的主要流程就是通過一系列的反射,最終獲取到當前Classloader中的mCookie,即Native層中的DexFile。
Classloader中的mCookie和Native層的DexFile是什麼關係?
我感覺Fupk3也提到了這個變量。
爲了在C/C++中在文件完成對dex的dump操作,這裏我們在framework層的DexFile類中添加兩個Native函數供調用:
libcore/dalvik/src/main/java/dalvik/system/DexFile.java中
private static native void dumpDexFile(String dexfilepath, Object cookie);
private static native void dumpMethodCode(String eachclassname, String methodname, Object cookie, Object method);
這兩個函數分別用於完成內存中dex的dump以及構造主動調用鏈,完成方法體的dump,在對應的C++文件中添加這兩個Native函數的實現並完成註冊:
Art/runtime/native/dalvik_system_DexFile.cc文件中
static void DexFile_dumpDexFile(JNIEnv* env, jclass, jstring filepath jobject cookie){
std::unique_ptr<std:vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
if (dex_files.get() == nullptr){
DCHECK(env->ExceptionCheck());
return;
}
int dexnum = 0;
char dexfilepath[1000];
for (auto&dex_file : *dex_files){
const unit8_t* begin_ = dex_file->getbegin(); //Start of data.
size_t size = dex_file->getsize(); //length of data;
int dexfilesize = (int)size_;
const char *filepathcstr = env->GetStringUTFChars(filepath, nullptr);
memset(dexfilepath,0,1000);
sprintf(dexfilepath, "%s_%d_%d", filepathcstr, dexfilesize, dexnum);
dexnum++;
//對抗抽取指令型加固殼,由於這類殼會通過hook libc中的關鍵文件讀寫函數來防止dump,這裏直接使用系統調用完成dex文件的dump
int fp = open(dexfilepath, O_CREATE|O_APPEND|O_RDWR, 0666);
write(fp,(void*)begin_,size_);
fsync(fp);
close(fp);
}
}
上面實現了對Classloader中加載的dex的dump,那麼主動調用類中函數,如果這個函數被加密了,如何dump呢?
類函數的主動調用設計實現
可以從JNI提供的相關函數源碼來對類函數主動調用鏈的構造。
JNI提供了一系列java層函數與Native層函數交互的接口。當需要在Native層中的c/c++函數中調用位於java層的函數時,需要先獲取到該函數的jmethodid然後再通過注入jni中提供的call開頭的一系列函數來完成對java層中函數的調用。我們以jni的CallObjectMethod函數爲例,進行分析。下面開始源碼分析:
static jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID mid, ...){
va_list ap;
va_start(ap, mid);
CHECK_NON_NULL_ARGUMENT(obj);
CHECK_NON_NULL_ARGUMENT(mid);
ScopeObjectAccess soa(env);
JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap));
va_end(ap);
return soa.AddLocalReference<jobject>(result.GetL());
}
該函數中通過InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap)函數來完成調用,下面看該函數內容:
該函數首先對jmethodid進行了轉換,轉換成ArtMethod對象指針,進而通過函數InvokeWithArgArray完成調用,下面再看InvokeWithArgArray函數內容
JValue InvokeVirtualOrInterfaceWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,jobject obj, jmethodID mid, va_list args) {
......
ArtMethod* method = FindVirtualMethod(receiver, soa.DecodeMethod(mid));
......
InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
.....
}
InvokeWithArgArray函數:
static void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,ArtMethod* method, ArgArray* arg_array, JValue* result,
const char* shorty)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
uint32_t* args = arg_array->GetArray();
if (UNLIKELY(soa.Env()->check_jni)) {
CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(sizeof(void*)), args);
}
method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}
最終是Invoke完成調用Java層函數的調用。於是,我們可以構造出自己的invoke函數,在該函數中再調用ArtMethod的Invoke方法從而完成主動調用,我們在傳參的時候,設定一個標誌位,在ArtMethod的Invoke函數中進行判斷髮現是主動調用時就進行方法體的dump並直接返回,從而完成對殼的欺騙。
閱讀下面這塊代碼
libcore/dalvik/src/main/java/dalvik/system/DexFile.java
static void DexFile_dumpMethodCode(JNIEnv* env, jclass, jstring eachclassname, jstring methodname,jobject cookie,jobject method) {
ScopedFastNativeObjectAccess soa(env);
ArtMethod* called_method = ArtMethod::FromReflectedMethod(soa, method);
method ->myfartInvoke(method );
return;
}
對Java層傳來的參數直接轉成Native層的ArtMethod對象,接下來就是myfartInvoke
void ArtMethod::myfartInvoke(ArtMethod* artmethod)
{ 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");
}
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,const char* shorty) {
// unsigned int tempresult=(unsigned int)self;
//這裏的self=nullptr是表示由我們主動調用的標誌位
if (self== nullptr) {
LOG(INFO) <<"art_method.cc::Invoke is invoked by myfartinvoke";
dumpArtMethod(this);
return;
}
......
接下來的dump函數,和fupk3一樣直接採用dexhunter裏的。
到這裏,就完成了內存中DexFile結構體中的dex的整體dump以及對抗抽取型指令的dump,下面就是修復被抽取的函數部分。
抽取類函數的修復
爲什麼要進行修復?
就比如一個單身女孩子,談了戀愛有了男朋友,但是後來因爲種種原因分手了,她又恢復了單身的狀態,那麼她在沒談和分手後肯定會有些方面會發生改變了,對吧。
所以要修復。
殼在完成對內存中加載的dex解密後,該dex索引去即stringid, typeid, methodid, classdef和對應的data區中string列表並沒有加密。而對於classdef中類函數的CodeItem部分可能被加密存儲或者直接指向內存中另一塊區域。前面我們知道了,這裏我們只需要使用dump下來的method的代碼塊來解析對應的被抽取方法即可。
實驗驗證
等我的pixel的手機到,然後整上一整。
後續…