AndroidFART脱壳机流程分析
本⽂⾸发于安全客
链接:
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。它的整体思路是通过主动调⽤的⽅式来实现脱壳,项⽬地址:。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, rpatInfo,
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 ");
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 : DeclaredMethods()) {
if (Name().equals("getClassNameList")) {
getClassNameList_method = field;
getClassNameList_method.setAccessible(true);
}
defineClass_method = field;
defineClass_method.setAccessible(true);
}
if (Name().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) (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[] = DeclaredConstructors();
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 = DeclaredMethods();
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_,代码为:
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函数传过来的flect.Method对象,传进来的Java层Method对象传⼊FromReflectedMethod函数得到ArtMethod结构指针,再将ArtMethod结构指针传⼊myfartInvoke函数。
myfartInvoke实际代码在art/runtime/⽂件⾥
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/⽂件⾥,在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) {
"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->
制作android软件流程
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();
"/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);
}
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。