前言
Android兴起以来,应用数量越来越多;当厂商或者个人开发者发现应用本身也可能存在被破解的可能时,他们就需要找一种解决方案来防止应用被破解,应用加固这个产品就是用来解决这个问题。Android的加固大致分为:dex整体加固、指令抽取和虚拟指令(VMP)。保护强度依次递增。我们这里只分析整体加固。
厂商对app做了加固之后想要对app做渗透测试,或者探索app某些功能的实现就会难很多,所以我们有这些需求的时候就需要去脱壳。一般比较多的教程是教你如何用ida动态调试,然后在关键函数下断点脱壳,这种方式确实可以增强动手能力。我们这里教你如何自己写一个脱壳机,免去绕过反调试的烦恼。
环境及工具
-
- Android Studio
-
- Android sdk
-
- ndk
-
- cmake
-
- Android设备(5.0 - 7.0)
-
- IDA
-
- jadx-gui
寻找脱壳点
对于整体加固,他要执行代码,在代码被执行之前,他肯定会做解密操作,把真实的dex或者指令恢复出来。所以只要找到传入参数有dex并且已经在解密之后都可以作为脱壳点。
以Android5.1为例分析dex加载流程
Dex加载流程(基于Android5.1源码)
DexClassloader
我们要动态加载一个dex文件,需要用到的是DexClassloader
,而DexClassloader
是BaseDexClassLoader
的子类,BaseDexClassLoader
还有另外一个子类PathClassloader
,PathClassloader
是系统默认用来加载apk文件dex的类,而我们要动态加载一个dex的话,需要使用DexClassloader
来加载。使用很简单,直接new一个DexClassloader
对象就行了。其代码如下
http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java:
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,ClassLoader parent)
{
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
DexClassloader的代码就只有下面短短的几行,直接调用的父类的构造方法。
BaseClassloader
BaseClassloader
就是DexClassloader
的父类,在这里面实现了dex的加载逻辑。BaseClassloader
是Classloader
的子类,Classloader
有两个重要的方法:findClass
和loadClass
,其中findClass
是用来实现类加载的逻辑,而loadClass
是先从父Classloader
里面去寻找,如果找不到就调用自己的findClass
来找。也就是说当前加载dex的class是由loadClass
来实现的。
BaseClassloader
的源码
http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
在BaseClassloader
里面我们需要关注的有两个方法,其一是构造方法,另外一个是findClass
方法。请自己参看源码看下面的解释。
在构造方法中,将自己的pathList这个成员变量赋值,其值是一个新创建的DexPathList对象。在findClass
方法中是调用的pathList
里面的findClass
方法,所以dex加载的逻辑应该是在DexPathList
里面实现的。
DexPathList
DexPathList的源码http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java(下面对照源码讲解其核心逻辑)
http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
在DexPathList
里面主要需要关注三个函数:构造方法、findClass
和makeDexElements
。我们先看findClass
,这个方法是用来找类的,其代码里面是从自己的dexElements
去找的类。然后我们看正好是在其构造方法中调用makeDexElements
这个方法类给dexElements
来赋值的。在makeDexElements
这个类里面,就是将apk或者zip或者jar或者裸的dex加载起来,放到Elements
对象里面。dexFile就是放到这个element里面,在makeDexElements
里面是调用的自身的loadDexFile
来加载dex,在loadDexFile
里面判断了文件是否是zip或者apk jar,如果是就调用构造方法来加载dex,否则就使用loadDex
方法来加载dex。
DexFile
DexFile的源码
http://androidxref.com/5.1.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
下面对照源码看dexfile的主要逻辑。
DexFile这个类就是java层最终加载dex的类,在其构造方法中调用的openDexFile方法来加载dex,而openDexFile是调用的openDexFileNative
方法来加载dex,这个方法是一个native方法。其实现在dalvik_system_DexFile.cc
中。
dalvik_system_DexFile.cc
在DexFile.java
中调用的openDexFileNative
的实现就在dalvik_system_DexFile.cc
中的DexFile_openDexFileNative
函数里面,源码http://androidxref.com/5.1.0_r1/xref/art/runtime/native/dalvik_system_DexFile.cc)。
在这里是由class_inker.cc
的OpenDexFilesFromOat
来实现的。
class_linker.cc
源码
http://androidxref.com/5.1.0_r1/xref/art/runtime/class_linker.cc#827
这里的代码很长,就不一一解释,大概就是查看当前dex文件是否被解析成oat,如果被解析成了oat文件,就直接下一步,如果没有被解析成oat就先调用dex2oat把dex解析成oat文件。
dex_file.cc
源码
http://androidxref.com/5.1.0_r1/xref/art/runtime/dex_file.cc
在dex_file.cc
这个文件里面有很多关于dex的操作,比如创建新的dex对象,从内存中加载dex。这个文件中也就是我们可以找到很多脱壳点的地方了。我们前面说过只要传递的参数有dex就有可能是脱壳点,我们看有dex参数的函数有:
std::unique_ptr<const DexFile> DexFile::Open(const uint8_t* base, size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
bool verify,
std::string* error_msg)
std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file,
std::string* error_msg)
还有DexFile的构造函数
DexFile::DexFile(const uint8_t* base, size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file)
其中DexFile::Open
这个函数直接调用的DexFile::OpenMemory
,所以我们比较好的脱壳点就是DexFile::OpenMemory
这个函数。当然也有修改dex2oat
做脱壳的,因为会传递解密好的dex给dex2oat
这个时候也能拿到dex,但是这个会改系统文件,对于没有root的手机,这个肯定是做不到的,而我们用hook这种方式借助VirtualApp
可以实现在无root的手机上脱壳,适用范围更广。
编写代码实现脱壳
环境准备
我们编写的脱壳机基于VirtualApp
,含有native代码,所以需要下载ndk来编译native的代码,在Android studio
的Preferences
里面的Android SDK
中勾选安装CMake、LLDB以及NDK,其中CMake是构建工具,LLDB是调试工具,NDK是编译工具链。所需下载安装的如下图所示如下图所示。
克隆
到本地,使用Android studio打开工程。在Android studio
中选择打开已经存在的Android工程
然后选择克隆的目录工程的build.gradle导入
在gradle构建的过程中会报如下的错(我这里用的比较高版本的Android Studio
,如果用低版本的可以直接加载工程,不会报错,不用下面修改build.gradle
这些步骤):
Could not find manifest-merger.jar (com.android.tools.build:manifest-merger:26.0.0)
我们需要改一下整个工程的build.gradle文件
在buildscript
下的repositories
和allprojects
下的repositories
都加上google()
和mavenCentral()
,修改之后如下图:
做了这些操作之后我们就能够成功编译VirtualApp
了,连接上Android手机点击运行就可以运行在手机上了,运行结果如下图:
整体加固脱壳代码(基于Android6.0)
我们前面分析到DexFile::OpenMemory
这个函数的参数里面有传递进来dex以及dex的长度,我们可以做一下hook来把传递的参数中的dex写道文件中去,如果这个时候dex已经被解密了,那么我们获取到的dex就是脱壳过的dex了。
在VirtualApp
中VirtualApp/lib/src/main/jni/Foundation/IOUniformer.cpp
这个源码文件中做了很多的hook操作。我们也可以模仿着他来将DexFile::OpenMemory
函数hook上,代码如下:
在IOUniformer.cpp
里面有个onSoLoaded
函数,当so被加载了之后,这个函数就会被调用,当libart.so被加载之后,我们就可以去hook上DexFile::OpenMemory
,然后将传递参数中的dex写到文件中去。
要去hook这个函数,第一步需要找到这个函数的导出符号,把/system/lib/libart.so
导出出来,用ida打开,等ida分析完成之后再左侧函数栏搜索OpenMemory
,我们发现有两个结果。根据前面分析,确实是有两个。我们需要hook的是直接传递的dex那个,也就是第一个参数为char *
的,所以我们应该hook搜索到的第一个函数。
我们双击函数跳转到他的反汇编代码,函数开头的那一长串就是他的符号,符号为
_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_
有了符号就可以开始写hook代码了,写hook代码也很简单,先定义一个函数指针,这个函数指针用来存放原来的函数(注意: 在Android5.0和5.1参数就和源码中的一样,在Android6.0和7.0上第一个参数为this指针):
void * (*old_DexFile_OpenMemory)(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
void * mem_map,
const void * oat_dex_file,
std::string* error_msg);
void * (*old_DexFile_OpenMemory)(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
void * mem_map,
const void * oat_dex_file,
std::string* error_msg);
然后创建一个新的函数来替代原始的DexFile::OpenMemory
void * new_DexFile_OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
void * mem_map,
const void * oat_dex_file,
std::string* error_msg){
// 把参数传递的dex写入到文件中
// 拼接文件名
char file_path[128];
// 存文件名的char数字全部置为0
memset(file_path, 0, sizeof(file_path));
// 拼接文件名
sprintf(file_path, "/sdcard/%x.dex", size);
// 写入到文件中
writeToFile(file_path, base, size);
// 调用原来的方法
return (*old_DexFile_OpenMemory)(thiz, base, size, location, location_checksum, mem_map, oat_dex_file, error_msg);
}
在这个函数里面我们先把内存中的dex写入到文件中,然后调用了原来的DexFile::OpenMemory
函数,这样就能保证原来的逻辑不变。在写文件的时候我们直接写入到了/sdcard/
但是这么多文件我们找起来挺麻烦的,所以我们获取一下当前VirtualApp
容器内运行应用的/proc/self/cmdline
获取下进程名,然后写道/sdcard/{进程名}
这个文件夹下面,我们需要读取当前的cmdline
,然后拼接,最终的代码如下:
// 写入到文件
void writeToFile(const char *path, const uint8_t *base, size_t size) {
int dex = open(path, O_WRONLY | O_CREAT);
if(dex < 0) {
__android_log_print(ANDROID_LOG_ERROR, UNTAG, "打开文件是吧 %s, %s", path, strerror(errno));
return;
}
int wlen = write(dex, base, size);
if(wlen != size) {
__android_log_print(ANDROID_LOG_ERROR, UNTAG, "写入dex失败%s", path);
}
close(dex);
__android_log_print(ANDROID_LOG_INFO, UNTAG, "写入dex成功 %s", path);
}
size_t getProcessName(char *name) {
char buff[128];
memset(buff, 0, sizeof(buff));
int fp = open("/proc/self/cmdline", O_RDONLY);
if(fp < 0){
__android_log_print(ANDROID_LOG_ERROR, UNTAG, "读取文件失败%s, %s",name, strerror(errno));
sprintf(name, "_default");
return 0;
}
size_t len = read(fp, buff, sizeof(buff));
if(len > 0) {
strncpy(name, buff, len);
}
return len;
}
void * (*old_DexFile_OpenMemory)(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
void * mem_map,
const void * oat_dex_file,
std::string* error_msg);
void * new_DexFile_OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
void * mem_map,
const void * oat_dex_file,
std::string* error_msg){
// 把参数传递的dex写入到文件中
// 拼接文件名
char file_path[256];
// 存文件名的char数字全部置为0
memset(file_path, 0, sizeof(file_path));
// 拼接文件名
char process_name[128];
memset(process_name, 0, sizeof(process_name));
getProcessName(process_name);
sprintf(file_path, "/sdcard/%s", process_name);
int ret = mkdir(file_path, 0777);
if(ret == 0) {
sprintf(file_path, "%s/%x.dex", file_path, size);
// 写入到文件中
writeToFile(file_path, base, size);
} else {
__android_log_print(ANDROID_LOG_ERROR, UNTAG, "创建文件夹失败 %s, %s", file_path, strerror(errno));
}
// 调用原来的方法
return (*old_DexFile_OpenMemory)(base, size, location, location_checksum, mem_map, oat_dex_file, error_msg);
}
现在用来替换DexFile::OpenMemory
的代码已经写好了,下面我们就需要用Substrate
来让我们的hook代码生效,在IOUniformer
里面有个函数叫onSoLoaded
,当so被加载的时候这个函数就会被调用,在加载了libart.so
之后我们就把DexFile::OpenMemory
去hook上,等带壳的程序执行到这里就会进入到我们的hook函数里面,然后dex就会被写入到/sdcard/
中。代码如下:
void onSoLoaded(const char *name, void *handle) {
__android_log_print(ANDROID_LOG_INFO, UNTAG, "加载so: %s", name);
if(strstr(name, "libart.so") != NULL) {
void *symbol = NULL;
if(findSymbol("_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_",
"libart.so", (unsigned long *) &symbol) == 0 ) {
MSHookFunction(symbol, (void *) new_DexFile_OpenMemory, (void **)&old_DexFile_OpenMemory);
}
}
}
现在我们已经完成代码了,点击运行运行在手机上。然后在logcat里面把最右边选为No Filters,然后logcat里面搜索UNPACK_TEST就可以看到我们自己打的日志。
然后运行应用到手就上,将加固的应用放到/sdcard/
点击Add app,在external storage
里面选择加固了的应用,勾选之后点击install,等安装完成之后启动应用即可看到logcat中已经有输出写入dex
到/sdcard
了。加固后的dex如下:
脱壳之后在sdcard下写入了几个dex
脱壳之后的dex用jadx打开如下:
推荐阅读
Android运行时ART加载OAT文件的过程分析。
https://blog.csdn.net/Luoshengyang/article/details/39307813
Android运行时ART加载类和方法的过程分析。
https://blog.csdn.net/luoshengyang/article/details/39533503
Android动态加载DEX文件流程分析。
https://blog.csdn.net/zhu929033262/article/details/78281592
dvm,art模式下的dex文件加载流程
https://blog.csdn.net/m0_37344790/article/details/78523147?utm_source=blogkpcl0