Android脱壳之整体脱壳原理与实践(2)

前面说过壳加载完原始的dex以后还需要对ClassLoader进行修正,否则加载组件类运行的时候会报ClassNotFoundException,为什么会报这种错误呢? 这就涉及到了组件类的创建过程,比如对于Activity来说,应用程序的Activity对象是在ActivityThread类的performLaunchActivity()方法中通过调用mInstrumentation.newActivity()创建出来的,这个函数的实现逻辑为:

2021-05-14_16-36.png 可以看到是通过ClassLoader先加载Activity类,再通过newInstance()来实现化类对象。
这个方法传递进来的ClassLoader是加载应用程序的ClassLoader,它所加载的dex为应用程序的主体的dex,对应的DexPathList是没有原始的dex路径的,因此会报ClassNotFoundException。这里也可以看出,一个BaseClassLoader对应着其实是一个Dex文件的列表,如果尝试让BaseClassLoader加载不在这个列表中的类,就会报ClassNotFoundException
为了解决上面的问题,可以有两种解决方案:

  1. 替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器
  2. 打包原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们的DexClassLoader,即加载原始dex的DexClassLoader作为PathClassLoader的parent

第一种解决方案是将系统组件类替换,这样通过mInstrumentation.newActivity()试图加载类的时候,就能找到相应的类。
第二种解决方案将ClassLoader的继承关系修改为: BootClassLoader --> DexClassLoader --> PathClassLoader,由于双亲委派机制的存在,当PathClassLoader找不到动态加载的原始dex时,它会请求parent即DexClassLoader加载,因此可以加载成功。
还有一些壳是通过对PathClassLoader中的Elements数组进行合并来达到目的,这样的好处是当你试图用Frida枚举ClassLoader对象的时候,看不到额外的ClassLoader对象,也就无法轻易的得到Dex文件加载的路径。


五. 通用整体脱壳方法:

从上面的内容可以得知,加载Dex有很多种实现方式,每种壳的实现都不一定相同,如果想实现一个比较通用的整体脱壳方法,就必须寻找一个壳绕不开的方式来实现。这个关键的数据结构就是art虚拟机中定义的类art::DexFile,它的定义位于art/runtime/dex_file.h文件中。
这个类有两个成员变量,分别代表着dex文件加载到内存当中的起始地址以及大小,得到这两个信息以后通过内存dump的方式就可以轻轻松松的将dex文件dump下来。
这就表示一个事实: 在某个时机点内存当中一定会有解密后的完整dex存在。

2021-05-14_17-26.png

art::DexFile是加载dex绕不开的类,不论是使用BaseClassLoader,还是自定义类加载器,最终都需要art::DexFile,这一结论可以通过查阅class_linker.cc文件中的函数得到,像ClassLinker::DefineClassClassLinker::LoadMethodClassLinker::LoadClassMembers这些重量级函数都需要以DexFile对象做为参数:

2021-05-14_17-34.png

2021-05-14_17-34_1.png

2021-05-14_17-34.png

通过BaseDexClassLoader来加载类会执行art/runtime/native/dalvik_system_DexFile.ccDexFile_defineClassNative函数,这个函数的实现就是遍历Java层的DexFilemCookie对象所表示的native层的art::DexFile,因为这个art::DexFile对象代表了dex文件在内存中的结构,所以可以通过类名在art::DexFile列表中查找到DexFile::ClassDef结构,才能继续调用ClassLinker::DefineClass()函数,最终才能得到一个虚拟机实现中的Class对象表示mirror::Class*,因此这个流程仍然离不开虚拟机中的art::DexFile类。

2021-05-14_17-46.png

这还会引发出一个问题:
art虚拟机执行smail指令可以解释执行,这种模式叫Interpreter模式(很显然解释模式下需要dex文件存在于内存当中才能解释执行)
也可以执行oat以后elf文件中的本地机器指令,这种模式叫quick code模式。
在quick code模式中,dex编译出来的机器指令包含在dex2oat生成出来的oat文件中(对于app来说,oat文件以.odex结尾),那么对于quick code模式是否还需要dex文件呢?如果不需要dex文件,是不是就无法在内存中dump出dex文件了?

事实上quick code模式内存中也是会有原始的dex文件存在的,具体的执行逻辑在Runtime的GetOatFileManager().OpenDexFilesFromOat()函数中,在这个函数中如果走dex2oat流程的话就会执行oat_file_assistant.MakeUpToDate()调用dex2oat命令生成odex和vdex文件,并生成OatFile类的对象,这个类就代表着dex2oat以后oat文件在内存中的映射表示,oat文件其实就是可执行文件,只不过它有特殊的几个符号:oatdata,oatlastword,oatbss,oatbsslastword等,OatFile类中的成员变量oat_dex_files_storage_是个std::vector<const OatDexFile*>类型的变量,而OatDexFile类表示的是这个oat文件所对应的dex文件的信息(列表),通过OatDexFiledex_file_pointer_即可以找到对应的art::DexFile对象的地址。
在android8.0以后,oat文件存放着dex文件编译出来的可执行指令,而原始的dex内容其实存放在vdex文件中,这一点可以在后面的dump程序中看到。
在quick code模式下也需要原始dex的存在的原因是dex文件还存放着类相关的信息,如class_def_item,method_id_item,对于类方法的执行还离不开这些信息。

六. 整体脱壳实践:

既然知道虚拟机中的art::DexFile类是dex在内存中的表示,那么得到这个对象就可以dump出dex文件。
接下来就是寻找一个合适的点可以得到art::DexFile对象,得到对象以后通过hook的方式或者修改源码的方式都可以dump下来了。
下面是脱函数抽取型壳fart的作者寒冰大佬所提出的办法:
找到libart.so文件中所有导出函数中带有art::DexFile参数或者返回值的函数,那么这就是一个可以脱壳的点
我写了一个命令可以查找满足这种条件的函数:

arm64-readelf -s libart.so -W | tr -s ’ ’ | cut -f9 -d ’ '| c++filt | grep “art::DexFile”

比如在art/runtime/dex_file.ccDexFile::OpenCommon或者DexFile::DexFile中添加如下代码即可脱壳:

pid_t pid = getpid();
char dexfilepath[100] = {0};
sprintf(dexfilepath,“/sdcard/drdump_%d_%d_DexFile.dex”,(int)size,(int) pid);
int fd = open(dexfilepath, O_CREAT | O_RDWR , 666);
if (fd > 0){
int number = write(fd,base,size);
if(number > 0){
}
close(fd);
}

这种是修改源码的方式来脱壳。


还有一种方式是通过frida hook来脱壳,它的优点是简单有效,不需要重新编译rom。
它的原理如下:
对于通过使用BaseDexClassLoader来加载的程序来说,DexFile.java类的mCookie变量在native层的表现其实是一个jlong类型的指针的数组,数组的个数为此ClassLoader加载的dex文件个数 + 1,第一个元素类型为OatFile*,剩余的元素为对应的art::DexFile*,因此可以通过获取mCookie变量来得到art::DexFile*列表,并且通过art::DexFile的begin_和size_来dump。
代码如下:

function hasOwnProperty(obj, name) {
try {
return obj.hasOwnProperty(name) || name in obj;
} catch (e) {
return obj.hasOwnProperty(name)
}
}

function getHandle(object) {
var result = null;
if (hasOwnProperty(object, "KaTeX parse error: Expected '}', got 'EOF' at end of input: …esult = object.handle;
if (result) {
return result;
}
}
if (hasOwnProperty(object, "KaTeX parse error: Expected '}', got 'EOF' at end of input: … return object.h;
}

return null;
}

function dump_dex(packagename, dexfilebegin, dexfilesize) {
var dexfile_path = “/sdcard/my_frida_dump_” + packagename + “_” + dexfilesize + “.dex”;
var dexfile_handle = new File(dexfile_path, “w”);
if (dexfile_handle && dexfile_handle != null) {
var dex_buffer = ptr(dexfilebegin).readByteArray(dexfilesize);
dexfile_handle.write(dex_buffer);
dexfile_handle.flush();
dexfile_handle.close();
}
}

function dealwithClassLoader(classloaderobj, packagename) {
if (Java.available) {
Java.perform(function () {
try {
var dexfileclass = Java.use(“dalvik.system.DexFile”);
var BaseDexClassLoaderclass = Java.use(“dalvik.system.BaseDexClassLoader”);
var DexPathListclass = Java.use(“dalvik.system.DexPathList”);
var Elementclass = Java.use(“dalvik.system.DexPathList$Element”);
var basedexclassloaderobj = Java.cast(classloaderobj, BaseDexClassLoaderclass);
var tmpobj = basedexclassloaderobj.pathList.value;
var pathlistobj = Java.cast(tmpobj, DexPathListclass);
console.log(“pathlistobj->” + pathlistobj);
var dexElementsobj = pathlistobj.dexElements.value;
console.log(“dexElementsobj->” + dexElementsobj);
for (var i in dexElementsobj) {
var obj = dexElementsobj[i];
var elementobj = Java.cast(obj, Elementclass);
console.log(“elementobj->” + elementobj);
tmpobj = elementobj.dexFile.value;
var dexfileobj = Java.cast(tmpobj, dexfileclass);
var mCookie = dexfileobj.mInternalCookie.value;
var mInternalCookie = dexfileobj.mInternalCookie.value;
if (mCookie != null) {
var jnienv = Java.vm.tryGetEnv();
var cookiePtr = getHandle(mCookie);
var arrayLength = jnienv.getArrayLength(cookiePtr);
var long_data = jnienv.getLongArrayElements(cookiePtr, 0);
console.log(“arrayLength:” + arrayLength + “,long_data:” + long_data);
for (var i = 1; i < arrayLength; i++) {
var dexfileptr = Memory.readPointer(ptr(long_data).add(8 * i));
var dexfilebegin = Memory.readPointer(ptr(dexfileptr).add(Process.pointerSize * 1));
var dexfilesize = Memory.readU32(ptr(dexfileptr).add(Process.pointerSize * 2));
console.log(“pointer:” + dexfileptr + “,dexfilebegin:” + dexfilebegin + “,dexfilesize:” + dexfilesize);
dump_dex(packagename, dexfilebegin, dexfilesize);
}
}
}
} catch (e) {
console.log(e);
}

});
}

}

function tuoke(packagename) {
if (Java.available) {
Java.perform(function () {
console.log(“go into enumerateClassLoaders!”);
Java.enumerateClassLoadersSync().forEach(function (loader) {
if (loader.toString().indexOf(“BootClassLoader”) >= 0) {
console.log(“this is a BootClassLoader!”)
} else {
try {
console.log("classloader : " + loader);
dealwithClassLoader(loader, packagename);
} catch (e) {
console.log(e);
}
}
})
});
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的14套腾讯、字节跳动、阿里、百度等2021最新面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。

2020面试真题解析
腾讯面试真题解析

阿里巴巴面试真题解析

字节跳动面试真题解析
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

(img-Gz0rek1l-1712732836759)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

首先,查壳,使用PEID或者是FI,为UPX v1.08 接下来用OD载入,提示为“压缩代码是否继续分析”,我们选择否 我在这里介绍几种方法,请看我的操作。 方法1:单步跟踪(最常见的方法) 0040E8C0 N> 60 pushad //停在这里了,我们F8单步 0040E8C1 BE 15B04000 mov esi,NOTEPAD.0040B015 0040E8C6 8DBE EB5FFFFF lea edi,dword ptr ds:[esi+FFFF> 0040E8CC 57 push edi 0040E8CD 83CD FF or ebp,FFFFFFFF 0040E8D0 EB 10 jmp short NOTEPAD.0040E8E2 //跳 。。。。。。。。。。 0040E8E2 8B1E mov ebx,dword ptr ds:[esi] //跳到这里,继续单步 0040E8E4 83EE FC sub esi,-4 0040E8E7 11DB adc ebx,ebx 0040E8E9 ^ 72 ED jb short NOTEPAD.0040E8D8 //这里要往回跳了 0040E8EB B8 01000000 mov eax,1 //F4,然后继续F8 0040E8F0 01DB add ebx,ebx 0040E8F2 75 07 jnz short NOTEPAD.0040E8FB //跳 。。。。。。。。。。。 0040E8FB 11C0 adc eax,eax //来到这里,F8继续 0040E8FD 01DB add ebx,ebx 0040E8FD 01DB add ebx,ebx 0040E8FF ^ 73 EF jnb short NOTEPAD.0040E8F0 0040E901 75 09 jnz short NOTEPAD.0040E90C //跳 。。。。。。。。。。。 0040E90C 31C9 xor ecx,ecx //跳到这里,继续F8 0040E90E 83E8 03 sub eax,3 0040E90E 83E8 03 sub eax,3 0040E911 72 0D jb short NOTEPAD.0040E920 //跳 。。。。。。。。。。。 0040E920 01DB add ebx,ebx //跳到这里,继续F8 0040E922 75 07 jnz short NOTEPAD.0040E92B //跳 。。。。。。。。。。。 0040E92B 11C9 adc ecx,ecx //跳到了这里,继续F8 0040E92D 01DB add ebx,ebx 0040E92F 75 07 jnz short NOTEPAD.0040E938 //跳 。。。。。。。。。。。 0040E938 11C9 adc ecx,ecx //跳到这里,继续F8 0040E93A 75 20 jnz short NOTEPAD.0040E95C //跳 。。。。。。。。。。。 0040E95C 81FD 00F3FFFF cmp ebp,-0D00 //来到这,继续F8 0040E962 83D1 01 adc ecx,1 0040E965 8D142F lea edx,dword ptr ds:[edi+ebp] 0040E968 83FD FC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值