FART正餐前甜點:ART下幾個通用簡單高效的dump內存中dex方法

本篇是對FART后續的補充,以及在實現FART過程中偶然發現的幾個通用簡單高效的脫殼方法,在FART后續的實現中,對內存中整體dex的dump也已經換成該方法來實現。
該方法可以說簡單高效并且實現也較為簡單,可以很輕松通過xposed或者frida等hook框架通過很短的代碼便能夠實現對加固應用的脫殼。同時,該方法通用性較強。下面結合源碼對該方案的原理和實現做簡單的介紹。
ART類加載執行流程以及ArtMethod類
在上一篇《FART:ART環境下基于主動調用的自動化脫殼方案》文章中對當前ART環境下的通用脫殼方案進行了簡單的總結,比如dexhunter、hook OpenMem方案,以及hook DexFile類函數方案等。
最后,FART使用了通過classloader來實現對classloader中的dex的dump來脫殼的目的。該方法在獲取到最終應用dex運行的classloader后,通過調用在框架層DexFile類中添加的相關jni函數來達到獲取內存中整體dex的目的。
整個實現過程可以說非常的繁瑣,而且需要對Android系統有著非常清楚的認識。同時,該實現過程需要使用大量的反射進而帶來了效率較低的問題。(當然,對于逆向來說。效率往往不是最重要的,達成目的才是關鍵)。
在一次閱讀源碼的過程中,偶然發現了幾處通用高效的dump內存中dex的方法。該方法主要涉及到ART環境下的類加載執行流程以及相關的類。
ART環境下函數執行過程中最關鍵的類便是ArtMethod類,這里以一張前輩繪制的圖來說明ART環境下的類加載執行流程(詳細內容可以參見文末的參考鏈接)。
從該圖中的右下部分可以看到,當ART在調用函數前需要對函數所屬的類完成加載鏈接,并最終準備好類中的每一個函數對應的ArtMethod對象以供接下來類的初始化以及函數的調用。
整個流程可以簡單概括為LoadClass->LoadClassMembers->LinkCode。LoadClassMembers函數負責準備接下來類函數執行過程中所需要的變量和函數。
該函數首先是遍歷內存中dex的相關field并初始化為ArtField對象;遍歷類中所有的函數,并初始化函數對應的ArtMethod對象。我們主要看下LoadClassMembers函數:
void ClassLinker::LoadClassMembers(Thread* self, const?DexFile& dex_file,
const?uint8_t* class_data,
Handle<mirror::Class> klass,
const?OatFile::OatClass* oat_class) {
{
// Note:?We cannot have thread suspension until the field and method arrays are setup or else
// Class::VisitFieldRoots may miss some fields or methods.
ScopedAssertNoThreadSuspension nts(self, __FUNCTION__);
// Load static fields.
//遍歷dex中的相關field
ClassDataItemIterator it(dex_file, class_data);
const?size_t num_sfields = it.NumStaticFields();
ArtField* sfields = num_sfields != 0?? AllocArtFieldArray(self, num_sfields) : nullptr;
for?(size_t i = 0; it.HasNextStaticField(); i++, it.Next()) {
CHECK_LT(i, num_sfields);
LoadField(it, klass, &sfields[i]);
}
klass->SetSFields(sfields);
klass->SetNumStaticFields(num_sfields);
DCHECK_EQ(klass->NumStaticFields(), num_sfields);
// Load instance fields.
const?size_t num_ifields = it.NumInstanceFields();
ArtField* ifields = num_ifields != 0?? AllocArtFieldArray(self, num_ifields) : nullptr;
for?(size_t i = 0; it.HasNextInstanceField(); i++, it.Next()) {
CHECK_LT(i, num_ifields);
LoadField(it, klass, &ifields[i]);
}
klass->SetIFields(ifields);
klass->SetNumInstanceFields(num_ifields);
DCHECK_EQ(klass->NumInstanceFields(), num_ifields);
// Load methods.
//遍歷dex中的相關Method并初始化
if?(it.NumDirectMethods() != 0) {
klass->SetDirectMethodsPtr(AllocArtMethodArray(self, it.NumDirectMethods()));
}
klass->SetNumDirectMethods(it.NumDirectMethods());
if?(it.NumVirtualMethods() != 0) {
klass->SetVirtualMethodsPtr(AllocArtMethodArray(self, it.NumVirtualMethods()));
}
klass->SetNumVirtualMethods(it.NumVirtualMethods());
size_t class_def_method_index = 0;
uint32_t last_dex_method_index = DexFile::kDexNoIndex;
size_t last_class_def_method_index = 0;
//首先遍歷初始化DirectMethod
for?(size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
LoadMethod(self, dex_file, it, klass, method);
LinkCode(method, oat_class, class_def_method_index);
uint32_t it_method_index = it.GetMemberIndex();
if?(last_dex_method_index == it_method_index) {
// duplicate case
method->SetMethodIndex(last_class_def_method_index);
} else?{
method->SetMethodIndex(class_def_method_index);
last_dex_method_index = it_method_index;
last_class_def_method_index = class_def_method_index;
}
class_def_method_index++;
}
//然后遍歷初始化VirtualMethod
for?(size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
ArtMethod* method = klass->GetVirtualMethodUnchecked(i, image_pointer_size_);
LoadMethod(self, dex_file, it, klass, method);
DCHECK_EQ(class_def_method_index, it.NumDirectMethods() + i);
LinkCode(method, oat_class, class_def_method_index);
class_def_method_index++;
}
DCHECK(!it.HasNext());
}
self->AllowThreadSuspension();
}
有dump整體dex經驗比如dalvik下通過hook dexparse或者dvmDexFileOpenPartial來達成定位內存中dex起始地址并dump的方法的人或許在這里便一眼看出該函數是一個脫殼點。
該函數的第二個參數 const DexFile& dex_file包含了對當前處理的dex的DexFile對象的引用,通過該引用,我們便可以定位到該dex在內存中的起始地址并達成dump脫殼。
同時,也可以看到,在對類中的函數進行遍歷并初始化ArtMethod過程中的LoadMethod(self, dex_file, it, klass, method)函數也包含了對DexFile對象的引用,因此這也是一個脫殼點。接下來具體看LoadMethod函數:
void ClassLinker::LoadMethod(Thread* self, const?DexFile& dex_file, const?ClassDataItemIterator& it,
Handle<mirror::Class> klass, ArtMethod* dst) {
uint32_t dex_method_idx = it.GetMemberIndex();
const?DexFile::MethodId& method_id = dex_file.GetMethodId(dex_method_idx);
const?char* method_name = dex_file.StringDataByIdx(method_id.name_idx_);

ScopedAssertNoThreadSuspension ants(self, “LoadMethod”);
//初始化相關變量
dst->SetDexMethodIndex(dex_method_idx);
dst->SetDeclaringClass(klass.Get());
//初始化CodeItem指針
dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());

dst->SetDexCacheResolvedMethods(klass->GetDexCache()->GetResolvedMethods());
dst->SetDexCacheResolvedTypes(klass->GetDexCache()->GetResolvedTypes());

uint32_t access_flags = it.GetMethodAccessFlags();

if?(UNLIKELY(strcmp(“finalize”, method_name) == 0)) {
// Set finalizable flag on declaring class.
if?(strcmp(“V”, dex_file.GetShorty(method_id.proto_idx_)) == 0) {
// Void return type.
if?(klass->GetClassLoader() != nullptr) { // All non-boot finalizer methods are flagged.
klass->SetFinalizable();
} else?{
std::string temp;
const?char* klass_descriptor = klass->GetDescriptor(&temp);
// The Enum class declares a “final” finalize() method to prevent subclasses from
// introducing a finalizer. We don’t want to set the finalizable flag for Enum or its
// subclasses, so we exclude it here.
// We also want to avoid setting the flag on Object, where we know that finalize() is
// empty.
if?(strcmp(klass_descriptor, “Ljava/lang/Object;”) != 0?&&
strcmp(klass_descriptor, “Ljava/lang/Enum;”) != 0) {
klass->SetFinalizable();
}
}
}
} else?if?(method_name[0] == ‘<‘) {
// Fix broken access flags for initializers. Bug 11157540.
bool is_init = (strcmp(“<init>”, method_name) == 0);
bool is_clinit = !is_init && (strcmp(“<clinit>”, method_name) == 0);
if?(UNLIKELY(!is_init && !is_clinit)) {
LOG(WARNING) << “Unexpected ‘<‘ at start of method name “?<< method_name;
} else?{
if?(UNLIKELY((access_flags & kAccConstructor) == 0)) {
LOG(WARNING) << method_name << ” didn’t have expected constructor access flag in class “
<< PrettyDescriptor(klass.Get()) << ” in dex file “?<< dex_file.GetLocation();
access_flags |= kAccConstructor;
}
}
}
dst->SetAccessFlags(access_flags);
}

該函數主要是通過指針對內存中的dex文件進行訪問,獲取到ArtMethod所需的相關內容后完成對ArtMethod的初始化工作,如:
? dst->SetDexMethodIndex(dex_method_idx);
??dst->SetDeclaringClass(klass.Get());
??dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());
??dst->SetDexCacheResolvedMethods(klass->GetDexCache()->GetResolvedMethods());
??dst->SetDexCacheResolvedTypes(klass->GetDexCache()->GetResolvedTypes());
這幾個賦值語句。在FART的實現中如何來確定被修復的函數屬于哪一個類哪一個方法呢?
事實上區分函數的唯一性可以靠該函數的相關屬性如類型名+函數名+函數簽名的形式來區分。而在FART中我直接使用了函數的method_idx屬性來確定(對于一個dex中的所有函數都由method_idx來編號,這也是單個dex文件能包含的最大方法數為65536的原因)。
其中,可以看到最關鍵的一個變量的初始化:
dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());
該語句對當前函數所指向的內存中的smali指令的地址進行了初始化。當前一些函數抽取類殼一般有兩種策略來處理:
第一種屬于占坑型,提前將dex中的函數體部分進行加密或者直接置為無效,在函數執行前再進行該部分空間的解密從而供函數調用執行;
第二種則在加固過程中對dex進行了重構,導致原有的函數體的空間已經無效,在函數執行前直接修改該ArtMethod對象中的CodeItemOffse指向來達成函數的調用執行。接下來再看LinkCode源碼:
void ClassLinker::LinkCode(ArtMethod* method, const?OatFile::OatClass* oat_class,
uint32_t class_def_method_index) {
Runtime* const?runtime = Runtime::Current();
if?(runtime->IsAotCompiler()) {
// The following code only applies to a non-compiler runtime.
return;
}
// Method shouldn’t have already been linked.
DCHECK(method->GetEntryPointFromQuickCompiledCode() == nullptr);
if?(oat_class != nullptr) {
// Every kind of method should at least get an invoke stub from the oat_method.
// non-abstract methods also get their code pointers.
const?OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
oat_method.LinkMethod(method);
}

// Install entry point from interpreter.
bool enter_interpreter = NeedsInterpreter(method, method->GetEntryPointFromQuickCompiledCode());
if?(enter_interpreter && !method->IsNative()) {
method->SetEntryPointFromInterpreter(artInterpreterToInterpreterBridge);
} else?{
method->SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
}

if?(method->IsAbstract()) {
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
return;
}

if?(method->IsStatic() && !method->IsConstructor()) {
// For static methods excluding the class initializer, install the trampoline.
// It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
// after initializing class (see ClassLinker::InitializeClass method).
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
} else?if?(enter_interpreter) {
if?(!method->IsNative()) {
// Set entry point from compiled code if there’s no code or in interpreter only mode.
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
} else?{
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
}
}

if?(method->IsNative()) {
// Unregistering restores the dlsym lookup stub.
method->UnregisterNative();

if?(enter_interpreter) {
// We have a native method here without code. Then it should have either the generic JNI
// trampoline as entrypoint (non-static), or the resolution trampoline (static).
// TODO:?this doesn’t handle all the cases where trampolines may be installed.
const?void* entry_point = method->GetEntryPointFromQuickCompiledCode();
DCHECK(IsQuickGenericJniStub(entry_point) || IsQuickResolutionStub(entry_point));
}
}
}<span style=“color:rgb(0, 0, 0); font-family:none; font-size:15px;”>
</span>

LinkCode函數對不同函數類型進行了不同的處理,進而完成對ArtMethod中相關變量的初始化工作,如針對native函數進行method->UnregisterNative(),針對以quick模式或interpreter模式執行的函數的不同的初始化工作。
當然,ArtMethod類提供了一個函數:GetDexFile(),該函數也可以獲取到當前ArtMethod對象所在的DexFile對象引用,在獲得了當前DexFile對象引用后,也依然可以dump得到當前內存中的dex。
實現及實驗驗證
上面對ART環境下的類加載執行流程簡單做了介紹,從而說明這幾種通用dump方案的原理。實現部分就不在這里貼了,具體可以看上一篇文章《FART:ART環境下基于主動調用的自動化脫殼方案》。
最終FART使用的是通過運行過程中ArtMethod來使用GetDexFile()函數從而獲取到DexFile對象引用進而達成dex的dump。這里同時給出四種實現思路(具體的dump時機和方法上一節部分已經給出):
① 通過修改Android系統源代碼,在這些dump點插入dump整體dex的代碼。
② 使用frida來hook這些函數,然后通過指針對這些對象中的變量進行訪問,最終定位到內存中的dex的起始點并完成dump。
③ 使用ida在過掉前期的反調試之后,對這些函數下斷即可(過反調試是個繁瑣的任務)。
④ 使用xposed或者virtualxposed結合native層函數的hook技術實現。
【via@看雪社區