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

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函數

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函數。

public static void fart() {
    ClassLoader appClassloader = getClassloader();
    List<Object> dexFilesArray = new ArrayList<Object>();
    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對象

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,它是這么定義的:

private static native void dumpMethodCode(Object m);

可見,它是一個native方法,它的實際代碼在:art\runtime\native\dalvik_system_DexFile.cc,代碼為:

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文件里

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函數,並立即返回

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的代碼了。

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/<pid>/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的結尾地址給算出來

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函數

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的大小計算出來:

code_item_len = 16 + code_item->insns_size_in_code_units_ * 2;

CodeItem的大小計算出來之后,接下來可以看到,有幾個變量以格式化的方式打印到dexfilepath

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為后綴的文件中:

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文件中

	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函數

namespace art {
extern "C" void dumpDexFileByExecute(ArtMethod* artmethod);
namespace interpreter {
        ......
    }
}

同時在文件其中找到了對dumpDexFileByExecute函數的調用:

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(),"<clinit>")!=nullptr)
  {
	  dumpDexFileByExecute(shadow_frame.GetMethod());
  }
  ......
}

在Execute函數中,通過判斷函數名稱中是否為<clinit>決定要不要調用dumpDexFileByExecute,即判斷傳入的是否為靜態代碼塊,對於加了殼的App來說靜態代碼塊是肯定存在的。如果Execute傳入的是靜態代碼塊則調用dumpDexFileByExecute函數,並傳入一個ArtMethod指針。

dumpDexFileByExecute中對dex進行了整體dump,可以把它看作是dumpArtMethod方式的互補,有時dumpArtMethod中得不到想得到的dex,用dumpDexFileByExecute或許能得到驚喜。

0x3 結語

非常感謝FART作者能夠開源FART,這使得人們對抗ART環境下App殼得到了良好的思路。FART脫殼機理論上來講能脫大多數殼,但是仍有例外,需要自行摸索。

0x4 參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM