【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解
【胖虎的逆向之路】01——动态加载和类加载机制详解
【胖虎的逆向之路】02——Android整体加壳原理详解&实现
【胖虎的逆向之路】03——Android一代壳脱壳办法&实操
文章目录
- 【胖虎的逆向之路】04——脱壳(一代壳)原理&脱壳相关概念详解
- 前言
- 一、Dex加载流程
- 二、Dex2Oat编译流程
- 三、类加载流程
- 四、DexFile详解
- (1)直接查找法
- (2)间接查找法
- 五、ArtMethod详解
- 总结
- 参考文献
前言
提示:这里可以添加本文要记录的大概内容:
在上文中,我们讲解了关于Android脱壳的基本办法和实际操作,现在我们来针对脱壳(一代壳)的原理和脱壳相关的基础知识介绍,由于作者能力有限,会尽力的详细描述 一代壳脱壳 的流程及原理,如本文中有任何错误,烦请指正,感谢~
一、Dex加载流程
在日常分析脱壳点过程中,Dex加载的基本流程也是要明白熟悉的
DexPathList:该类主要用来查找Dex、SO库的路径,并这些路径整体呈一个数组
Element:根据多路径的分隔符“;”将dexPath转换成File列表,记录所有的dexFile
DexFile:用来描述Dex文件,Dex的加载以及Class的查找都是由该类调用它的native方法完成的
我们依次来分析这个过程中的源码
DexPathList
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
**********************
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
**********************
}
makeDexElements
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
**********************
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
**********************
}
loadDexFile
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
loadDex
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
DexFile
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
//System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
}
这里出现的mCookie,mCookie在C/C++层中是DexFile的指针,我们在下面详细讲解
openDexFile
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
这里就进入了C/C++层
openDexFileNative
为了节约篇幅,我们快速分析,中间再经过一些函数
OpenDexFilesFromOat()
MakeUpToDate()
GenerateOatFileNoChecks()
Dex2Oat()
最后进入了Dex2Oat,这就进入了Dex2Oat的编译流程
反之如果我们在下面Dex2Oat的流程中通过Hook相关方法或execv或execve导致dex2oat失败,我们就会返回到OpenDexFilesFromOat
OpenDexFilesFromOat
会先在HasOriginalDexFiles里尝试加载我们的Dex,也就是说,倘若我们的壳阻断了dex2oat的编译流程,然后又调用了DexFile的Open函数。
DexFile::Open
校验dex的魔术字字段,然后调用DexFile::OpenFile
DexFile::OpenFile
/art/runtime/dex_file.cc
std::unique_ptr<const DexFile> DexFile::OpenFile(int fd,
const std::string& location,
bool verify,
bool verify_checksum,
std::string* error_msg) {
**************************************
std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
map->Size(),
location,
dex_header->checksum_,
kNoOatDexFile,
verify,
verify_checksum,
error_msg);
**************************************
}
OpenCommon
最后又再次回到DexFile类,这里我们的dex文件加载基本流程分析完毕
二、Dex2Oat编译流程
Dex2oat是google公司为了提高编译效率的一种机制,从Android8.0开始实施,一些加壳厂商实现抽取壳往往会禁用Dex2oat,而针对整体加壳没有禁用的Dex2Oat也成为了脱壳点
Exec
/art/runtime/exec_utils.cc
bool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) {
int status = ExecAndReturnCode(arg_vector, error_msg);
if (status != 0) {
const std::string command_line(android::base::Join(arg_vector, ' '));
*error_msg = StringPrintf("Failed execv(%s) because non-0 exit status",
command_line.c_str());
return false;
}
return true;
}
ExecAndReturnCode
而我们就可以通过Hook execv或execve来禁用Dex2Oat,而如果我们不禁用dex2oat,execve函数是用来调用dex2oat的二进制程序实现对dex文件的加载,我们这时候找到dex2oat.cc这个文件,找到main函数
/art/dex2oat/dex2oat.cc
int main(int argc, char** argv) {
int result = static_cast<int>(art::Dex2oat(argc, argv));
if (!art::kIsDebugBuild && (RUNNING_ON_MEMORY_TOOL == 0)) {
_exit(result);
}
return result;
这里我们调用了Dex2oat
Dex2Oat
/art/dex2oat/dex2oat.cc
static dex2oat::ReturnCode Dex2oat(int argc, char** argv) {
**************************************
dex2oat::ReturnCode setup_code = dex2oat->Setup();
dex2oat::ReturnCode result;
if (dex2oat->IsImage()) {
result = CompileImage(*dex2oat);
} else {
result = CompileApp(*dex2oat);
}
**************************************
}
Dex2oat中会对dex文件进行逐个类逐个函数的编译,setup()函数完成对dex的加载
然后顺序执行,就会进入CompileApp
编译过程中会按照逐个函数进行编译,就会进入CompileMethod
到这里Dex2oat的基本流程就分析完毕
三、类加载流程
要理解DexFile为什么如此重要,首先我们要清除Android APP的类加载流程。Android的类加载一般分为两类隐式加载和显式加载
1.隐式加载:
(1)创建类的实例,也就是new一个对象
(2)访问某个类或接口的静态变量,或者对该静态变量赋值
(3)调用类的静态方法
(4)反射Class.forName("android.app.ActivityThread")
(5)初始化一个类的子类(会首先初始化子类的父类)
2.显示加载:
(1)使用LoadClass()加载
(2)使用forName()加载
我们详细看一下显示加载:
Class.forName 和 ClassLoader.loadClass加载有何不同:
(1)ClassLoader.loadClass也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作)
(2)Class.forName这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作
我们在详细来看一下在类加载过程中的流程:
java层
我们可以发现类加载中关键的DexFile,该类用来描述Dex文件,所以我们的脱壳对象就是DexFile
这里从DexFile进入Native层中,还有一个关键的字段就是mCookie
后面我们详细的介绍mCookie的作用
我们进一步分析,进入Native层
Native层
/art/runtime/native/[dalvik_system_DexFile.cc
ConvertJavaArrayToDexFiles对cookie进行了处理
通过这里的分析,我们可以知道mCooike转换为C/C++层指针后,就是dexfile的索引
我们继续分析DefineClass
art/runtime/class_linker.cc
mirror::Class* ClassLinker::DefineClass(Thread* self,
const char* descriptor,
size_t hash,
Handle<mirror::ClassLoader> class_loader,
const DexFile& dex_file,
const DexFile::ClassDef& dex_class_def) {
***************
LoadClass(self, *new_dex_file, *new_class_def, klass);
***************
}
LoadClass
art/runtime/class_linker.cc
void ClassLinker::LoadClass(Thread* self,
3120 const DexFile& dex_file,
3121 const DexFile::ClassDef& dex_class_def,
3122 Handle<mirror::Class> klass) {
3123 const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
3124 if (class_data == nullptr) {
3125 return; // no fields or methods - for example a marker interface
3126 }
3127 LoadClassMembers(self, dex_file, class_data, klass);
3128}
LoadClassMembers
art/runtime/class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
***************
LoadMethod(dex_file, it, klass, method);
LinkCode(this, method, oat_class_ptr, class_def_method_index);
***************
}
LoadMethod
art/runtime/class_linker.cc
void ClassLinker::LoadMethod(const DexFile& dex_file,
const ClassDataItemIterator& it,
Handle<mirror::Class> klass,
ArtMethod* dst) {
}
LinkCode
我们可以发现这里就进入了从linkcode后就进入了解释器中,并对是否进行dex2oat进行了判断,我们直接进入解释器中继续分析
我们知道Art解释器分为两种:解释模式下和quick模式下,而我们又知道Android8.0开始进行dex2oat
如果壳没有禁用dex2oat,那类中的初始化函数运行在解释器模式下
如果壳禁用dex2oat,dex文件中的所有函数都运行在解释器模式下
则类的初始化函数运行在解释器模式下
所以一般的加壳厂商会禁用掉dex2oat,这样可以是所有的函数都运行在解释模式下,所以一些脱壳点选在dex2oat流程中,可能针对禁用dex2oat的情况并不使用,我们这里主要针对整体加壳,就不展开讲述,最后我们得知解释器中会运行在Execute下
Execute
art/runtime/interpreter/interpreter.cc
static inline JValue Execute(
Thread* self,
const DexFile::CodeItem* code_item,
ShadowFrame& shadow_frame,
JValue result_register,
bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_){
***************
ArtMethod *method = shadow_frame.GetMethod();
***************
}
这里我们大致分析完成了类加载的思路
四、DexFile详解
前面我们分析了很多,对dex加载、类加载等都已经有了一个很详细的了解,而最终一切的核心就是DexFile,DexFile就是我们脱壳所关注的重点,寒冰大佬在拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点中提到,在ART下只要获得了DexFile对象,那么我们就可以得到该dex文件在内存中的起始地址和大小,进而完成脱壳。
我们先查看一些DexFile的结构体
只要我们能获得起始地址begin和大小size,就可以成功的将dex文件脱取下来,这里我们记得DexFile含有虚函数表,所以根据C++布局,要偏移一个指针
而DexFile类还给我们提供了方便的API
这样只要我们找到函数中有DexFile对象,就可以通过调用API来进一步dump dex文件,由此按照寒冰大佬的思想,大量的脱壳点由此产生
(1)直接查找法
我们通过直接在Android源码中搜索DexFile,就可以获得海量的脱壳点
我们通过在IDA中搜索libart.so导出的DexFile,同样可以获得大量的脱壳点
(2)间接查找法
这里就是寒冰大佬在文章中提到的通过ArtMethod对象的getDexFile()获取到ArtMethod所属的DexFile对象的这种一级间接法,通过Thread的getCurrentMethod()函数首先获取到ArtMethod或者通过ShadowFrame的getMethod获取到ArtMethod对象,然后再通过getDexFile获取到ArtMethod对象所属的DexFile的二级间接法
getDexFile()
getMethod()
五、ArtMethod详解
上面我们已经详细分析了DexFile的文件结构,我们知道通过ArtMethod可以获得DexFile,那么为啥又要单独提ArtMethod呢,因为ArtMethod在抽取壳和VMP等壳中扮演了重要的角色
ArtMethod结构体
我们通过ArtMethod可以获得codeitem的偏移和方法索引,熟悉dex结构的朋友知道codeitem就是代码实际的值,而codeitem则再后续加壳技术扮演了至关重要的地址,而且ArtMethod还有非常丰富的方法,可以帮助大家实现很多功能,所以在脱壳工作中也是十分重要的
总结
以上就是今天要讲的内容,主要借鉴了 随风而行 大佬的一些思路及文章,将文章在复写一遍目的是为了加深记忆,在以后的学习过程中可以很方便的查看~
参考文献
https://security-kitchen.com/2022/12/04/Packer5/
https://bbs.kanxue.com/thread-254555.htm#msg_header_h2_2