github.com/guanchao/ap…
2.dex2C:
github.com/amimo/dcc
3.VMP :
github.com/chago/ADVMP
二.判断壳的类型:
判断App是不是加壳很简单,使用jadx打开Apk以后,查看App的四大组件类,如果组件类找不到实现,那么就是整体加壳了。
比如这里只能看到qihoo相关的壳代码,App的代码被隐藏了:
判断是不是函数抽取型壳就是看函数体是不是有意义的代码,比如下面就是一个抽取型壳,可以看到函数体都是nop指令:
判断是不是dex2C/VMP壳,就是要看是不是有Java方法被native化了,至于到底是dex2C还是VMP则需要更一步的判断,比如native函数注册地址是否相同:
三. 加壳角度来看整体型壳
整体壳需要解决如下几个问题:
1.如何将原始的dex文件加密隐藏起来
2.壳代码如何在执行前获取控制权并可访问到被隐藏加密的dex从而可以解密dex文件
3.如何动态加载解密后的dex文件并且让虚拟机可以识别到dex文件中的四大组件
问题1:
可以将原始的dex文件加密添加到壳dex文件的末尾,并且更新壳dex文件的checksum,signature和file_size反映出这些变化,因为在dex文件的头部信息中会保留这些值来检查dex文件格式的正确性:
修改了这三个值以后,壳dex文件的末尾就可以添加一些和dex格式定义中无关的信息,比如原始dex文件的个数,大小以及加密以后原始dex文件的内容。然后将壳dex替换掉原始的dex文件对Apk进行重打包即可。
问题2:
壳代码会将自己的Application类替换掉原来的Application类,因此壳的Application类的onCreate()
和attachBaseContext()
会在App进程启动的时候率先执行拿到控制权。而访问被隐藏加密的dex可以通过getApplicationInfo().sourceDir
来获取apk的路径,解压apk得到壳dex文件从而解密出壳dex文件里边附带的原始dex文件。
问题3:
通过Android中的类加载器动态加载解密后的dex文件,为了让四大组件可以正常走生命周期函数,还需要对类加载器进行修正,修正的方式大概有两种方式,下面的内容中会详细的描述。
四. Android类加载器:
类加载器在dex整体加壳与脱壳中起到了重要的作用。
网上有很多的文章介绍Java中的类加载器,因此我这里就不写了,这里主要说的是Android里边关于dex加载的"类"加载器。
类加载器可以在运行的时候加载在编译时未知的类,对于c/c++来说也有类似的功能dlopen与dlsym,而Android里边的BaseDexClassLoader
可以实现在运行的时候加载在编译时未知的dex文件,经过此加载器的加载,ART虚拟机内存中会形成相应的数据结构,对应的dex文件也会由mmap映射到虚拟内存当中,通过此加载器的loadClass(String className, boolean resolve)
方法就可以得到类的Class对象,从而可以使用该类。
对于壳来说,它解密出来的dex文件也是编译时未知的,因此需要通过类加载器来加载dex文件,系统提供的BaseDexClassLoader
可以满足要求,也可以自定义ClassLoader
来实现。
通过打印测试可以看到JDK API中的类如java.lang.String
,以及Android系统提供的核心类如android.app.Activity
(位于/system/framework/framework.jar中)都是由同一个类加载器java.lang.BootClassLoader
所加载的。
而由应用程序开发者自己所编写的代码由dalvik.system.PathClassLoader
加载器加载,打印出来的信息如下:
dalvik.system.PathClassLoader[DexPathList[[directory “.”],nativeLibraryDirectories=[/system/lib64, /vendor/lib64, /system/lib64, /vendor/lib64]]]
查看源码可以看到PathClassLoader
是继承自BaseDexClassLoader
的,而PathClassLoader
还有另外两个兄弟: InMemoryDexClassLoader
以及DexClassLoader
,而壳程序很多都使用了这两个类加载器来加载解密后的dex文件。其中InMemoryDexClassLoader
是Android8.0以后新增的类,可以实现所谓的"不落地加载"。
总结一下,主要有如下几个ClassLoader:
- BootClassLoader: 加载系统核心类的加载器,由系统创建
- BaseDexClassLoader: 加载dex文件的基类加载器
- PathClassLoader: 加载应用程序开发者自己编写的代码的加载器,比如四大组件类,它继承自BaseDexClassLoader
- DexClassLoader: 从源码中可以看到几乎和BaseDexClassLoader没有区别,它继承自BaseDexClassLoader,一般用于实现插件化和加壳
- InMemoryDexClassLoader: 通过ByteBuffer数组来加载dex的加载器,它继承自BaseDexClassLoader
- 自定义ClassLoader: 可以实现想实现的任何功能
前面说过壳加载完原始的dex以后还需要对ClassLoader
进行修正,否则加载组件类运行的时候会报ClassNotFoundException
,为什么会报这种错误呢? 这就涉及到了组件类的创建过程,比如对于Activity来说,应用程序的Activity对象是在ActivityThread类的performLaunchActivity()
方法中通过调用mInstrumentation.newActivity()
创建出来的,这个函数的实现逻辑为:
可以看到是通过
ClassLoader
先加载Activity类,再通过newInstance()
来实现化类对象。
这个方法传递进来的ClassLoader
是加载应用程序的ClassLoader,它所加载的dex为应用程序的主体的dex,对应的DexPathList
是没有原始的dex路径的,因此会报ClassNotFoundException
。这里也可以看出,一个BaseClassLoader
对应着其实是一个Dex文件的列表,如果尝试让BaseClassLoader
加载不在这个列表中的类,就会报ClassNotFoundException
。
为了解决上面的问题,可以有两种解决方案:
- 替换系统组件类加载器为我们的
DexClassLoader
,同时设置DexClassLoader
的parent为系统组件类加载器 - 打包原有的双亲关系,在系统组件类加载器和
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存在。
art::DexFile
是加载dex绕不开的类,不论是使用BaseClassLoader
,还是自定义类加载器,最终都需要art::DexFile
,这一结论可以通过查阅class_linker.cc文件中的函数得到,像ClassLinker::DefineClass
,ClassLinker::LoadMethod
,ClassLinker::LoadClassMembers
这些重量级函数都需要以DexFile对象做为参数:
通过BaseDexClassLoader
来加载类会执行art/runtime/native/dalvik_system_DexFile.cc
的DexFile_defineClassNative
函数,这个函数的实现就是遍历Java层的DexFile
的mCookie
对象所表示的native层的art::DexFile
,因为这个art::DexFile
对象代表了dex文件在内存中的结构,所以可以通过类名在art::DexFile
列表中查找到DexFile::ClassDef
结构,才能继续调用ClassLinker::DefineClass()
函数,最终才能得到一个虚拟机实现中的Class对象表示mirror::Class*
,因此这个流程仍然离不开虚拟机中的art::DexFile
类。
这还会引发出一个问题:
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文件的信息(列表),通过OatDexFile
的dex_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.cc
的DexFile::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){
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司21年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
详细整理在GitHub可以见;
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。