逆向分析APP的一散流程:
- 使用自动化检测工具检测apk是否加壳,或者借助一些反编译工具依靠经验推断是否加壳,
- 如果apk加壳,则需要首先对apk进行脱壳﹔(Fart、Youpk、Dex-Dump三种常用方案)
- 使用jeb、jadx , apktool等反编译工具对apk进行反编译;
- 先依据静态分析中得到的关键字符串、关键api调用等方法快速定位需要分析的关键函数和流程;
- 如果依据简单的字符串、关键api无法快速定位,则apk可能使用了字符串加密、反射调用等手段,此时可结合h o o k 、动态调试等
- 定位到关键函数后,再根据是java实现还是jni实现进一步分析,其中so中的函数逻辑分析难度较大下面通过几个实例来看下流程
- JNI分析到IDA当中,IDA搜索不到响应的函数则为动态注册,动态注册是在JNI_Onload当中注册的,这个时候我们可以搜一下我们想要的目标字符串,倘若进行了字符串加密(ollvm),则搜索不到。此时可以尝试hook registerNative、art相关的地址进行监控
JVM的类加载器包括3种:
- Bootstrap ClassLoader(引导类加载器)
C/C++代码实现的加载器,用于加载指定的JDK的核心类库,比如java.lang.、java.uti.等这些系统类。Java虚拟机的启动就是通过Bootstrap,该Classloader在java里无法获取,负责加载/lib下的类。 - Extensions ClassLoader(拓展类加载器)
Java中的实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在java里获取,负责加载/lib/ext下的类。 - Application ClassLoader(应用程序类加载器)
Java中的实现类为AppClassLoader,是与我们接触对多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
自定义类加载器
通过继承java.lang.ClassLoader来实现自己的ClassLoader
双亲委派
类加载器通过双亲委派(也叫向上委托)的方式来进行类的加载。
双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。
为什么采用双亲委派
- 避免重复执行类加载器, 如果类已经被加载了,则可以直接读取已经加载的Class。
- 更加安全,无法用自定义的类替代系统类,防止系统级API被篡改。
当年我老师给我上课时,跟我说过,以前有个黑客通过替换java.lang.String成自己的String从而引发安全危机。所以通过双亲委派就杜绝了这种问题的发生。
类加载的时机
- 隐式加载
类不由开发人员进行加载的。
触发时机:- 创建类的实例
- 访问类的静态变量(读写)
- 调用类的静态方法
- 使用反射方式强制创建某个类或接口对应的java.lang.Class对象
- 初始化某个类的子类
- 显示加载(在反射的过程当中用的频率较高)
- 使用LoadClass()加载
- 使用forName()加载
两者有所区别:
JVM当中加载类的流程
- 装载:查找和导入Class文件
- 链接:其中解析步骤是可以选择的
- 检查:检查载入的class文件数据的正
确性 - 准备:给类的静态变量分配存储空间
- 解析:将符号引用转成直接引用
- 检查:检查载入的class文件数据的正
- 初始化:即调用
函数,对静态变
量,静态代码块执行初始化工作(反编译过程当中,这些静态代码将变成clinit函数,该函数由编译器自动生成)
Android系统当中的ClassLoader的继承关系
其中InMemoryDexClassLoader为Android8.0新引入的ClassLoader.
采用InMemoryDexClassLoader写加壳代码非常方便。
ClassLoader: 抽象类
BootClassLoader: 继承自ClassLoader,与JVM中的BootStrapClassLoader作用一样,用于预加载系统级别的类,采用单例模式,确保只需加载一次核心的类即可。由Java实现。
BaseDexClassLoader:该类非常关键。对一个d
ex加载的过程当中,大部分的逻辑都在该ClassLoader当中实现,其子类InMemoryDexClassLoader、PathClassLoader、DexClassLoader
只是简单的继承了BaseDexClassLoader。
PathClassLoader: 当一个App从点击到第一个Activity呈现的过程当中,APP当中的类由PathClassLoader来加载。
SecureClassLoader继承自ClassLoader,主要加入权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。
InMemoryDexClassLoader: 在Android 8.0 引入的,在APP的加固当中使用最多的就是在InMemoryDexClassLoader当中进行。顾名思义,从内存当中直接加载dex
PathClassLoader: 是Android默认使用的类加载器,四大组件Activity、Service等等都是在PathClassLoader当中进行加载。
DexClassLoader: 可以加载任意目录下的dex、jar、apk、zip文件,甚至可以从网络、SD卡当中加载,也是目前实现插件化、热修复以及dex加壳的重点!
Android的源码目录
art: 存放Android runtime的实现,以C++代码为主
bionic: Android的C库
libcore: ClassLoader的存放处,编译到手机时以jar包的形式呈现
framework: Android四大组件的管理处
tips: android的源码可以到aospxref.com和androidxref.com当中查看,aosp为国内镜像且以更新到android 10
Android ClassLoader源码解析
ClassLoader位于其他所有ClassLoader的根节点。其中一个关键的变量parent
是用于实现双亲委派的关键。该参数在每个ClassLoader当中都有存在,用于表示它的父节点是哪个ClassLoader,对于Android的BootClassLoader来说,parent是空的。再看看parent是一个final类型的变量,意味着只能赋值一次。该变量是否重要,对于如何插件化加载dex,并且让dex当中的组件生效。
BaseDexClassLoader源码:
DexClassLoader源码:
PathClassLoader源码:
InMemoryDexClassLoader源码:
以上三个子类均继承自BaseDexClassLoader,且仅仅只有自己的构造函数。
证明:BootClassLoader为根ClassLoader
通过Android Studio写一个程序:
frida与xpose对ClassLoader的加载
xpose: 需要加载ClassLoader才能进行操作
frida: 通过反射找到app所在的ClassLoader, 自动处理ClassLoader
动态加载dex
之前我们有提到DexClassLoader 可以加载任意目录的dex、zip、jar、apk文件,所以我们需要得到一个DexClassLoader实例,先看看源码部分。
第一个参数dexPath,即要加载的那4个类型的文件路径,第二个
我们先创建一个Android项目,然后我们再创建一个类,类中创建一个方法,用来输出已经被调用到。
编译整个项目成apk,然后解压apk,找到当前classxx.dex,把其导入到手机的/sdcard/目录
接着,我们创建一个用于Load刚才我们导入进sdcard的项目,记得添加读写sd卡的权限.(使用时还要记得在设置里面打开本app读取sdcard的权限,因为时常因为这个没读取报错)
编写程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62package com.example.loadsdcardcode;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context context = this.getApplicationContext();
testDexClassLoader(context, "/sdcard/3.dex");
}
/**
*
* @param context 获取当前app的私有目录
* @param dexPath dex、zip、apk、jar这四种中的一种文件格式的路径
*/
public void testDexClassLoader(Context context, String dexPath){
// 在当前的私有文件下新建一个私有目录,用于存放其dex
File optfile = context.getDir("opt_dex", 0);
File libFile = context.getDir("lib_dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
optfile.getAbsolutePath(),
libFile.getAbsolutePath(),
MainActivity.class.getClassLoader());
try {
// 读取dex中的class
Class clazz = dexClassLoader.loadClass("com.example.beloadedproject.TestClass");
try {
// 反射出需要的method
Method method = clazz.getDeclaredMethod("functionBeLoaded");
// 反射出我们要load的class
Object obj = clazz.newInstance();
// 调用方法
method.invoke(obj);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}看看结果:
DexClassLoader方法参数:
dexPath:目标所在的apk或者jar文件的路径,装载器将从路径中寻找指定的目标类。
dexOutputDir:由于dex文件在APK或者jar文件中,所以在装载前面前先要从里面解压出dex文件,这个路径就是dex文件存放的路径,在android系统中,一个应用程序对应一个linux用户id ,应用程序只对自己的数据目录有写的权限,所以我们存放在这个路径中。
libPath:目标类中使用的C/C++库。
最后一个参数是该装载器的父装载器,一般为当前执行类的装载器。
APP运行的过程
ActivityThread.main()是进入APP世界的大门,只有经过这个方法之后才会进入到加壳app的自己的代码当中。
接下来我们开始讲ActivityThread:
ActivityThread
ActivityThread是一个单例模式的类,sActivityThread用于保留这个唯一的实例。
我们要想获取到当前的ActivityThread,需要调用其静态函数currentActivityThread
通过该函数,我们将获取这个全局、单例的实例,通过该实例我们可以获取一些比较重要的变量。
LoadedApk:
在ActivityThread的内部这个部分有LoadedApk这个类的变量,其中这个类中有一个变量叫做mClassLoader
,就是我们加载APP用的ClassLoader,即PathClassLoader.
我们通过反射获取一个ActivityThread这个仅有的实例,接下来,再通过反射获取mPackages这个ArrayMap,接下来就可以通过当前APP的包名获取到它的LoadedApk,最后就可以通过这个LoadedApk获取到其中的一个mClassLoader。
这个实际上就是PathClassLoader,就是接下来APP用于加载四大组件这些类的ClassLoader。
而什么时候才会进入到app的代码当中?(何时进行dex解密)
在handleBindApplication
当中,最先进入到app自身代码当中。
在hangbingle老师的这片文章链接就有提到,此处上他的代码截取
1 | private void handleBindApplication(AppBindData data) { |
接下来我们看看那个newApplication做了什么。
跟到这里发现app.attach,我们接着跟到attach里。
这里实际上调用了attachBaseContext
这个函数。
一个正常的APP的哪一部分最先被执行?在AndroidManifest.xml当中,所声明的ApplicationBaseContext和onCreate函数是最先获取到执行权的。
在这个过程当中,涉及到两个ClassLoader,BootClassLoader用来加载系统核心库,而PathClassLoader用于加载APP自身dex,其中包含有app所声明的Application,如果APP没有加壳,自然而然拥有APP这些类信息;如果加壳了呢?此时PathClassLoader加载的,只有壳的代码!而且,当前呢,也还没有加载真正的代码也就是壳解密后释放的代码。接下来就进入到Application的attachBaseContext这个函数执行,再往下就是onCreate函数进行执行。对于壳程序来说需要找到一个比较早的时机进行加密dex交付。自然而然就会选择这两个函数做文章。
加壳应用的运行流程
如何解决动态加载中加壳dex的类的生命周期
要解决这个问题,我们首先来看看dex的解密源程序在哪里进行。
在自定义的Application中的attachBaseContext以及onCreate两个函数当中进行
实战讲解
首先我们创建要动态导入的dex, 并导入到sdcard当中。
创建一个新项目,并先在AndroidManififest.xml当中注册我们要动态加载的Activity.
- 动态加载dex,并启动activity
报错: java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.juziss.loadactivity/com.juziss.loadeddex.TestActivity}: java.lang.ClassNotFoundException: Didn’t find class “com.juziss.loadeddex.TestActivity” on path: DexPathList[[zip file “/data/app/com.juziss.loadactivity-R-aobRMym_MNBJ9GchrmZg==/base.apk”],nativeLibraryDirectories=[/data/app/com.juziss.loadactivity-R-aobRMym_MNBJ9GchrmZg==/lib/arm64, /system/lib64, /vendor/lib64]]
报错的原因:
由于组件相关的Activity实际上是由上面提到的mClassLoader(实际上就是PathClassLoader)进行加载的,虽然我们通过反射拿到了我们想要动态加载的Activity,但是却造成ActivityManager在管理上无法找到我们要加载的Activity,因为别忘了是先加载AndroidManifest才到onCreate的!
两种解决动态加载组件相关的Activity的生命周期问题
替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统当前的组件类加载器即
PathClassLoader
。
通过反射区替换当前的ClassLoader为我们定义的DexClassLoader,当进入到App的Activity相关的生命周期切换的过程时,就会使用DexClassLoader进行加载。自然而然就找到了我们需要的类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// 先传入我们的DexClassLoader
public void replaceClassLoader(ClassLoader classLoader){
try {
// 通过反射获取ActivityThread
Class<?> ActivityThreadClass = classLoader.loadClass("android.app.ActivityThread");
// public static ActivityThread currentActivityThread() {
// return sCurrentActivityThread;
// }
// 由以上可以得知,需要通过currentActivityThread来获得当前的ActivityThread,因为ActivityThread是单例的
Method currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod("currentActivityThread");
// 执行这个Method对象即可得到ActivityThread
Object activityThread = currentActivityThreadMethod.invoke(null);
// 寻找LoadedApk类(因为这个类里面存在当前的mClassLoader),关键要找到 final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
// 即要找到mPackages
Field mPackages = ActivityThreadClass.getDeclaredField("mPackages");
// 关闭类型检查
mPackages.setAccessible(true);
// 通过包名来获取WeakReference<LoadedApk>
ArrayMap mPackagesObj = (ArrayMap) mPackages.get(activityThread);
// 参数为一个String类型,实际上为当前的包名,得到的是一个弱引用对象,该对象对应着LoadedApk,可以理解为python的弱引用,能引用到,但是垃圾收集器不计数
WeakReference weakReference = (WeakReference) mPackagesObj.get(this.getPackageName());
Object loadedApk = weakReference.get();
// 通过反射先获取LoadedApk类
Class<?> loadedApkClass = classLoader.loadClass("android.app.LoadedApk");
Field mClassLoader = loadedApkClass.getDeclaredField("mClassLoader");
mClassLoader.setAccessible(true);
// 替换ClassLoader为我们的DexClassLoader
mClassLoader.set(loadedApk, classLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}把该函数加到我们写的那个实战当中
寒冰老师牛逼!!!
打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader。
这种解决方案和
双亲委派
密切相关,我们依然保留当前组件的ClassLoader,但是我们把它的parent设置成DexClassLoader,通过双亲委派的特性,PathClassLoader会把加载Activity的时机交由我们创建的DexClassLoader进行处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27private void startTestActivity2(Context context, String dexFilePath) {
Class clazz = null;
File optFile = context.getDir("opt_dex", 0);
File libFile = context.getDir("lib_dex", 0);
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
ClassLoader bootClassLoader = MainActivity.class.getClassLoader().getParent();
// 将我们的DexClassLoader的parent设置成BootClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, optFile.getAbsolutePath(), libFile.getPath(), bootClassLoader);
try {
//通过反射获取PathClassLoader的parent
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassLoader, dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
try {
clazz = dexClassLoader.loadClass("com.juziss.loadeddex.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
startActivity(new Intent(context, clazz));
}寒冰老师牛逼!!!
Dalvik下一代壳通用解决方案
Dalvik下的DexClassLoader的加载流程。
Dalvik在Android4.4以下的源代码位于libcore里,我们找到DexClassLoader.
第一个参数为要加载的dex的路径,第二个是dex在优化过程当中产生的odex的路径,第三个是当前要加载的so的路径,第四个则为为了双亲委派而设置的父节点的ClassLoader。
然后我们跟到BaseDexClassLoader当中,这里才是真正的逻辑处理的地方。其跟前面所提到的BaseClassLoader一样,继承于ClassLoader这个抽象类。
先看其构造函数。这里先调用父类的构造函数,我们看看在父类的实现。
此时网站上可以看到有两个ClassLoader,其实不奇怪,我们看的Android4.4版本的源码,在那个版本当中Android开始引入来ART,与Dalvik共存,我们只需看看Dalvik下的实现。
这里ClassLoader这个构造函数只是把传入的父节点设置为parent
,让我们回到BaseDexClassLoader当中。
设置完成父节点之后,又new了一个DexPathList,我们跟进去。
在这个构造函数当中,第一个参数为传入的ClassLoader,第二个则是要加载的Dex文件,其他三与四个则为DexClassLoader所设置的opt和lib,这个构造函数真正做了事情的是:
跟进去,
这个makeElements返回的是一个Element[]数组,其定义在DexPathList.java这个文件里,
this.file是dexFile对象。
跳回来makeElements这个函数里,当我们动态加载dex时一般只加载一个dex,此时传递的就是一个文件,此时的后缀就是dex,所以走的是第一个逻辑。
调用loadDexFile,之后把这个dex添加到要返回的Element[]数组当中,我们跟进loadDexFile这个函数
其仍旧在DexPathList这个文件当中, 第一个参数就是Dex文件,其调用来DexFile类当中的loadDex,我们跟着跳过去。
第一个参数是我们的Dex文件路径,第三个文件参数是0,第二个是DexClassLoader当中的opt这个File,不用去管这个先。
看到其new了一个DexFile,我们跟到相应位置。
我们看到这里调用了一个openDexFile,然后返回来mCookie,我们接着跟进openDexFile
发现剩下的处理都在native层当中进行了。既使用C/C++来实现。第一个参数就是要加载的Dex的文件路径
接着往下跟进到JNI函数当中。这个函数所在的路径其实对应了Java的包的路径
其对应的JNI函数所在路径就是dalvik_system_DexFile
来到native函数
第一个参数是Dex文件的路径。
往下拉来到这里,由于我们传进来的是dex结尾的,所以会走这条流程
可以看到这里还走了一个函数dvmRawDexFileOpen
,跟过去看看。
需要注意的是加载的Dex路径。
然后可以看到他用了open这个函数打开这个dex文件。然后尝试访问这个dex.
首先对这个dex的魔术进行了校验。
接着往下拉。
这里呢,将生成要优化的文件(在动态加载的dex有一个优化的过程,最终会生成odex文件),这里是产生odex的路径。
这里是优化的流程,我们跟进dvmOptimizeDexFile
第一个fd是我们打开的dex的id,第三个则是当前dex文件的大小。
接着往下看,这里还用到了fork(),新建了一个子进程用于调用/bin/dexopt
文件对当前的dex文件进行一个优化,最后产生odex文件,直到产生odex文件之后才开始一步步返回。
这里我们看看dexopt这个文件对dex怎么优化的。
找到这个cpp文件之后直接搜main即可。
当我们传入的是dex文件时将调用fromDex
,我们找到这个fromDex
往下拉
然后这里调用了dvmContinueOptimization
对当前这个fd进行优化(真正的优化逻辑也就在这里),跟进去。
先是进行了dex文件进行判断,判断文件的长度是不是比dexHeader的长度还小,倘若还小则不合法,则直接返回。
这里先对内存进行映射。往下走
跟进这个函数,第一个参数是映射到内存当中这个dex的起始地址,第二个则为dex文件的长度。
紧接着,我们来到了第一个脱壳点函数dvmDexFileOpenPartial
跟踪进去
可以看到这个函数又调用了dexFileParse
其中也包含了dex的起始地址。
这样就是整个动态加载dex时,dalvik帮助我们进行的一些逻辑处理。在这个处理的流程当中,出现了加载的dex的起始地址,这些就是非常好的脱壳时机。
通用脱壳点:编译源码,对dexFileParse、dvmDexFileOpenPartial脱壳点的验证。
一般来说对这两函数进行hook,或者说取出这两个函数参数,第一个参数就是要dump的dex的起始地址,第二个就是我们要dump的内存区域的长度。
当然,除了这两个脱壳点之外还有海量的脱壳点。可以通过有传入或者函数内部有调用dex的起始地址和长度的函数进行脱壳。通过对起始地址和长度进行dump即可脱下完整的dex.当然这个思想适用于脱整体壳(所有壳都有整体壳,所以这个思想是通用的)
手动实现关键时机的hook
下载Android 4.4的源码
在linux的虚拟机下找到相应的源码部分(通过androidxref站找到源码所在的位置)
编写脱壳代码
编译源码
这个是谷歌提供的标准编译
选择设备
- 编译
-jn是用来选择线程数的,这里给出了8个线程,编译的时候最好给虚拟机分配高点的内存,使其编译的时候快一些。
- 找到编译好的镜像
其所在位置位于/out/target/product/
由于这里只编译了hammerhead即nexus5所以就只有这个镜像的文件 - 把这些文件拷出来然后到平时刷机用的虚拟机里刷到手机即可。
- ps:写刷机文件
把这一串写上,然后chmod啥的都呼一遍,然后就可以刷了,看起来镜像刷都挺有规律的,就是顺序不知道有没有要求,盲猜没有。
ART下的脱壳原理
InMemoryDexClassLoader加载一个内存中的解密的字节流的过程当中ART是怎么的流程
这里存在着两个构造函数,一个用于加载多个dex,一个用于加载单个dex.我们主要关注单个的即可。
跟到BaseDexClassLoader
,跟我们讲Dalvik一样,这里的super是用来给ClassLoader的parent属性赋值用的,这里就不跟进去了。
这里初始化了pathList
这个属性
接着,我们又看到了熟悉的DexPathList
,看到这个我又想到了熟悉的dex优化流程dex2oat,不知道这里是不是也有这个流程。
我们先看这里传入的对象,ByteBuffer[],当我们传入一个dex时这个长度就是1。
我们进入到这个方法,因为这个传入了dex
遍历了dexFile这个数组,然后新建一个DexFile,我们在跟去看看。
openInMemeryDexFile
被调用,跟进去
这里调用了两个地方,分别查看一下走了哪里。
都是两native函数
那我们就跟到native!!DexFile_createCookieWithDirectBuffe
因为其是一个静态函数,所以第二个参数是一个jClass, 第三个是我们传入的字节流。这个函数里面进行了一次**memcpy(dex_mem_map->Begin(), base_address, length);**进行了一次拷贝,而传入的这个Begin()和length其实就是我们的dex文件的其实地址(意味着这个函数是一个脱壳点!)
DexFile_createCookieWithArray
这两个函数共同调用了CreateSingleDexFileCookie
,跟进去看看。
这里传入了MemMap,即我们的Dex相关信息以后,通过它创建了一个DexFile,即一个Dex实例,*最后返回了这个DexFile.**这意味着什么?这意味这这就是整个InMemeryDexClassLoader*的加载流程!!
我们跟进去看看DexFile的初始化。在这之前,我们先看看我们传入的CreateDexFile
.
这里的第二个参数就是包含我们dex文件在内存当中的映射的一个地址,而CreateDexFile最终又调用了DexFile的Open这个函数,我们传入了location,即一个字符串string(要加载的Dex的路径),即是这个函数。
第二个参数是dex在内存当中的映射。这个Open函数,又调用了OpenCommon
这个函数,跟进去看看。
这个函数也包含着我们加载的Dex文件的起始地址。这个OpenCommon最后又new DexFile对象,在这个DexFile又包含了Dex的起始地址和大小。
也就是说InMemoryDexClassLoader在对内存中的Dex信息进行加载的流程当中,实际上涉及的函数都有Dex的起始信息和大小。回想一下Dalvik下的方案,也就是说,这些函数就是一个脱壳点!
还有一个重要的点:**InMemoryDexClassLoader并没有内存中的Dex信息进行一个编译生成相应的oat文件!**这一点是和DexClassLoader加载dex的不同之处!
此时
InMemoryDexClassLoader暴露出来的dex脱壳点有:
- CreateSingleDexFileCookie
- CreateDexFile
- DexFile::Open(location….)
- OpenCommon
- DexFile::DexFile(const uint8 t* base, …)
Art下的DexClassLoader
该ClassLoader的分析流程会比InMemoryDexClassLoader要复杂,因为多了一步我们用到过的脱壳流程,即dex2oat
的编译过程。
老套路,跟到BaseDexClassLoader当中
其构造函数跟Dalvik下的BaseDexClassLoader是一样的,我们接着跟到DexPathList.
可以看到,到目前为止跟Dalvik下是一样的。我们接着跟到makeDexElements
当中。
然后又跟到loadDexFile
,其最后调了DexFile.loadDex
,跟过去。
再跟进。
跟到这里,我们就来到了Native层。该层体现了与Dalvik下的区别。
因为其是静态函数,所以第二个参数是jclass, 第三个参数是java要加载的Dex文件。
在这里我们也看到了第一次出现oat的字符,我们看到了这里有调用一个OpenDexFilesFromOat
,跟进去。当我们第一次DexClassLoader去解密一个动态的Dex,此时必然还没有生成oat。
先是生成了OatFileAssistant这个类,初始化这个对象。接着这个实例调用了isUpToDate()
当我们第一次调用的时候,是必然还没有生成oat的。
然后我们接着跟到MakeUpToDate
当中。
接着这个函数最后调用了GenerateOatFileNoChecks
最终进入到,调用Dex2Oat编译生成oat文件的流程。
接下来我们来看看是怎么进行dex2oat这个流程。
先判断一下Dex2Oat是否是可用的状态。若不可用直接返回。
接着,是生成oat路径的一些函数。
接着往下走,直到Dex2Oat
这个函数。
跟进去。
我们先看到了一些二进制的程序的参数的相关信息。
最终调用了Exec
来完成Dex2Oat的编译的过程。
然后跟进到ExecAndReturnCode
在这函数当中首次进行了进程的fork
,在子进程当中,使用execve
这个函数来执行Dex2Oat编译的过程。
也就是说,在整个流程当中,我们把其中的某个函数的逻辑进行修改,或者对某个函数(比如execve)进行hook都会导致dex2oat编译流程的结束,实际上,强制结束dex2oat的编译流程是可以让我们的DexClassLoader在第一次加载Dex的流程当中变成非常的快速,减少dex2oat的编译流程。
实际上,要实现Art下的函数抽取技术,我们也是要阻断dex2oat的编译流程的。这也是art下实现函数抽取的方案与Dalvik下的区别所在,因为Dalvik下不存在dex2oat的编译流程,所以Dalvik下实现函数抽取流程稍微简单些。
接着回到源码,当阻断了dex2oat的编译流程之后,最终会返回到OpenDexFilesFromOat
,并最后来到了这个控制流上。
会先在HasOriginalDexFiles
里尝试加载我们的Dex,也就是说,倘若我们的壳阻断了dex2oat的编译流程(当前好多厂商的函数抽取流程都是这么做的),然后又调用了DexFile的Open函数。
来到了这个Open函数,这里有调用了OpenAndReadMagic
这个函数(这个函数的第一个参数是Dex文件路径)!这个是 2017DefCon
上提出的通过修改DexFile的构造函数DexFile::DexFile()
,以及OpenAndReadMagic()
函数来实现对加壳应用的内存中的dex的dump来脱壳的原理。
接着往下走:
由于我们加载的是一个dex文件,所以必然是走OpenFile这个函数的,跟进去!
第一个参数fd是文件的描述符,接着我们往下走,进入到OpenCommon
这个函数当中。在这之前,我们可以看到,MemMap::MapFile将这个文件进行了内存映射。
进入到OpenCommon
当中。
这个函数的参数也有文件的映射区域,
第一个参数是文件的起始地址,
接着往下走又进入到了new DexFile
函数当中,那我们就跟进去。
这里又有文件的起始地址,把这个函数记录下来,因为有起始地址的地方都有机会成为我们的脱壳点!
以上就是Dex文件的加载流程。涉及到了三个重要的脱壳点这里。
DexClassLoader的三个重要脱壳点
- OpenAndReadMagic(filename, &magic, error_msg);
- DexFile::OpenCommon(const uint8_t* base,
- DexFile::DexFile(const uint8_t* base,
刷入aosp 8.0碰到的坑
我按往常那样刷了aosp,但是却一直在recovery无法进入系统,后来我对着官方的flash-all.sh
看了看,发现官方包其实也是刷了编译出来的那几个.img文件,但是它还多刷了两个东西。
这两个.img是在刷编译出来的文件的时候先刷入的,而这两个.img你单纯编译源码是没有的,需要在官方的刷机包里拿,先刷入这两个.img之后再刷入我们编译出来的.img。
上面我们提到的DexClassLoader走的是没有走dex2oat的路线,但是如果有些壳没有禁用dex2oat的编译流程,而且又是走DexClassLoader时改怎么办?
实际上dex2oat的流程也是可以进行脱壳的。
刚才我们有说过这一步execve函数是用来调用dex2oat
的二进制程序实现对dex文件的加载,我们这时候找到dex2oat.cc这个文件,找到main函数。
我们可以看到这里调用了dex2oat
这个程序,跟进去。SetUp
这个函数里最后出现了我们要编译的dex文件的处理。
在这里
这个地方也可以完成对dex文件的脱壳。
实际上dex2oat存在的脱壳点还是非常多的,比如
在这个dex2oat编译流程里,它对函数粒度的编译流程当中,都出现了dexFile这个对象,实际上都可以进行脱壳。
ART下抽取壳实现
函数抽取是宣告一代壳即整体保护的结束,由此进入二代壳的时代。
函数抽取壳的实现
二代壳的出现标志着一代壳的时代结束。
Dalvik下抽取壳实现
四哥的博客有写:
这篇是下面一篇的基础
Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑
实现函数抽取壳的时机要早于函数被调用的的时机,倘若晚于被调用的时机,APP的逻辑就被破坏掉了,APP自然就崩溃了。
函数被调用之前经历的阶段
- Dex被加载,例如Dex被动态加载
- 类的准备
- 装载
- 链接
- 初始化
类的加载流程(loadClass)
通过DexClassLoader进行,遂跟到相应源码(本次选取4.4,流程是一致的,变化几乎很小)。
再往上找BaseDexClassLoader
,在这个类当中没有找到loadClass,那再往上找,ClassLoader
在这个类中就找到了。
这里可以体现出双亲委派,如果Class之前已经被加载,则直接返回这个Class,否则尝试使用当前ClassLoader的父类ClassLoader(父类的ClassLoader被存放于parent属性当中)进行加载,如果加载成功直接就返回,由于我们第一次执行是由DexClassLoader进行加载,其父节点ClassLoader为PathClassLoader(或者被我们自定义为BootClassLoader),此时我们的类没有找到,因为我们的类只由当前的ClassLoader进行加载,所以会走到下一个分支,findClass(),即来到了DexClassLoader的findClass当中,而DexClassLoader的逻辑都被封装于BaseDexClassLoader当中,我们找到它。
可以看到内部逻辑实际上由pathList的findClass来执行,而pathList被实例化于DexClassLoader加载Dex的时候已经被实例化,跟进去找到DexPathList
内部的findClass()。
可以看到findClass当中存在dexElements,并对其进行遍历,这个dexElements当中存放着的都是DexFile对象,然后又调用该对象中的loadClassBinaryName
,接着往上跟。
可以看到其内部实际上调用的是defineClass(第三个参数mCookie实际上存放的是Native层的指针,这个mCookie在往后的版本当中演变成了Object),而defineClass内部又调用了defineClassNative
方法进行Class的加载,我们要跟到Native层当中了。
跟到这个cpp文件。
实际上这个注释也说了,从DEX文件当中加载一个类。
也可以看到,这个cookie被转换成了DexOrJar
指针,由于我们当前是加载Dex文件,所以会走dvmGetRawDexFileDex
函数
跟进来,发现只是取到这个指针中的pDvmDex
也就意味着这里实际上没做啥实际的工作,接着刚才的逻辑往下走。
走进这个函数。
在深入到findClassNoInit
函数,又跳过去。
这里的第一个参数是一个类名,即一个字符串,第二个为当前的ClassLoader。
首先对类名进行HASH计算,然后通过已经加载的类进行查询,倘若查询不到则返回null。由于当前,我们是第一次加载一个类,所以返回null。
所以接上刚才的逻辑往下走。
这里的dexFindClass
是姜维在Android中实现「类方法指令抽取方式」加固方案原理解析这篇文章当中提到的,hook这个类来实现抽取函数的恢复的解决方案(其原理就是通过hook类被加载的时机来实现类的还原,这个时机肯定要早于函数被执行的时机的,因此可以保证APP正常的运行)。
实际上,下面的一些函数也是可以进行Hook的,但是由于可能没有导出符号,所以hook的难度不如dexFindClass容易,这也是姜维选择这个函数进行hook的原因之一。
ART下抽取壳实现
ART下的抽取壳首要解决的是禁用dex2oat
dex加载流程当中,dex2oat是可以实现脱壳的。当dex完成对dex编译以后——即生成了oat文件之后,后续的函数调用的过程当中,就会直接从oat取出编译生成的二进制代码去执行。因此,如果我们对dex进行函数抽取和填充的时机不对的话,我们填充的代码就无法生效,因为我们后面在运行时,调用的真正代码就会从dex2oat编译生成的oat文件当中获取,则此时跟以前的dex没有关系了。所以要想使得我们填充回去的指令生效,要么,禁用dex2oat;要么,还原的时机早于dex2oat。
- ps : 第一种解决方案是加固厂商最常使用的,因为不禁用dex2oat时,我们的还原时机就要在dex2oat之前,而此时,可以利用dex2oat的编译过程就可以使得壳被脱下来。dex2oat实际上是用来提上app的运行效率的,但是加壳则是需要禁用掉它。
让我们来到之前分析的ART下dex2oat的脱壳点时的这个函数GenerateOatFileNoChecks
,该函数用于开始调用dex2oat进行编译。
该函数当中有一个Dex2Oat
函数,我们跟进去。
再跟进Exec实际上就是跟进ExecAndReturnCode
函数
我们可以看到这个函数里fork
了一个进程,在这个进程当中,倘若发生任一报错,则执行_exit(1)来退出dex2oat流程。所以,我们只需要在这个函数的fork之后的这个过程当中使其报错即可终止所有的dex2oat。
我们这里通过hook execve
来干掉整个dex2oat。
该项目也实现了干掉dex2oat: TurboDex,达到让我们的dex在第一次加载的过程当中快速的加载完成。倘若不干掉dex2oat,我们的ART虚拟机将会调用dex2oat进行编译,而该编译过程非常耗时,所以通过干掉它达到提升DexClassLoader的加载效率。该项目是通过Hook execv来解决。具体hook execv还是execve是由安卓的版本来决定的。有的版本不适用execv。该项目通过适用inline hook库来进行hook,也可以使用爱奇艺的xhook,该库通过JOT可以实现干掉dex2oat.
这里我们通过hook libc的execve来实现Hook
寻找时机点——还原被抽空函数
FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
可以参考寒冰老师的这篇文章。
抽空函数:
首先先抽空codeItem,即在32位的codeitme的 后16位全部置0,然后重新计算校验位使其合法。
FART学习
FART由三个组件组成
脱壳组件
与前面介绍的脱整体DEX保护的方案差不多,但是脱壳点不一样,这个只是为了获取DEX的结构体,而对于其中每一个函数的子类部分,即code_item部分只作为参考,真正使用的code_item不在脱壳组件拖出来的code_item中产生。主动调用组件
主要用于解决函数抽取的代码解决方案,该组件用于构造一条让壳以为自己的APP在调用其函数的路径,从而使得壳把APP源代码还原出来。此时前两部使得我们拥有了两种信息:
- DEX的结构性信息(来自脱壳组件)
- DEX当中每一个函数的指令信息(来自主动调用组件Dump)
修复组件
用于整合DEX的结构体信息和每一个函数的函数体信息,从而修复DEX被抽取掉的内容。
脱壳组件
第一个脱壳点
与dex2oat的原理有很大的关系,但是又不在dex2oat的流程当中脱下来的。
apk安装时进行的dex2oat编译流程
流程的最后会来到CompileMethod
,
在源码当中可以看到,每一个函数(除了初始化函数),都会进行基本检查
- dex2oat编译流程:函数粒度进行编译
并不是所有函数都会被编译。比如类初始化函数
ART下函数执行模式
- interpreter模式:由ART下的解释器执行
- quick模式:直接允许dex2oat编译生成的arm指令
类的初始化函数始终运行在解释模式下的两种情况
一个壳用DexClassLoader进行加载一个dex文件时,如果没有禁用dex2oat,类的初始化函数运行在interpreter模式(解释模式)下,如果禁用掉了dex2oat则所有的函数都运行在解释模式下。
- 如果运行在解释模式下,必然会运行在ART下的解释器去完成一个取址操作——取出每个函数对应的code_item中的每一条smali去解释执行。
ART下解释器不同版本存在不同的实现
8-10版本:
7及以下版本存在三种实现:存在两种实现: 1. Switch-interpreter 2. Assembly interpreter (汇编),默认是走汇编解释器。这里汇编实现的调用及效率高于Switch-interpreter的实现。
1. Switch-interpreter 2. Assembly 3. Computed-goto-based interpreter
的ArtMethod对象
即使流程走到了dex2oat下,所有除
我们可以通过ArtMethod
对象的GetDexFile
函数获取其所属的DexFile
对象
DexFile
存在Size()和Begin()两个函数。
Begin() -> 用于获取当前的DexFile这个对象对应的Dex文件在内存中的起始地址。
Size() -> 获取当前Dex文件的大小。
因此,可以通过ArtMethod下的GetDexFile()获取所属的DexFile对象,进而通过Begin()函数和Size()函数获取Dex起始文件的大小和地址。
FART选择的第一个脱壳点是建立在interpreter模式流程中。
一个函数在进入到解释器之前,会先进入到interpreter (其实现类是InterpreterImplKind,写出来方便后续搜索)的Execute
在这个函数当中会调用给具体的解释器进行解释执行。在这里,我们对当前的初始化函数所属于的Dex文件进行dump,则可实现脱壳。
修改AOSP源码的小技巧
当我们修改一个AOSP里的一个很通用的函数时,可能会导致一个函数被脱出多次的情况,此时可以通过判断当前的函数名来进行过滤即可,如何判断函数名呢,ArtMethod类里有一个函数可以返回当前ArtMethod函数的函数名PrettyMethod
现有脱壳方法的根本原理
对于每一个加载Dex的文件来说,要么是加载一个DexFile对象要么就是DexFile结构体。
常用的方法就是在源码当中搜索DexFile,可以看到海量的点都可以用来脱壳,然后添加相应的脱壳代码,优先找到:参数中出现DexFile对象,其次找到返回值出现DexFile对象,最后在函数执行过程中出现DexFile对象。这些都是潜在的脱壳点。还可以通过ArtMethod对象来获取其对应的DexFile对象。
举例:
1 |
|
如何判断一个脱壳点的好坏
Execute为什么是一个很好的脱壳点
不管加固厂商有没有禁用dex2oat的流程,对于一个Dex文件来说,它其中的类是很多的,而对于其中的类的初始化函数来说,都会进入到解释执行的流程当中。对于Execute这个函数来说,它还是一个inline函数,即内联函数,加固厂商无法通过Hook的方法修改函数的逻辑的。
与FART中主动调用相关的概念
- 被动调用
指app正常运行过程中所发生的调用,该过程只对dex中部分类完成加载,同时也只是对dex中的部分函数完成了加载调用。
- 被动调用脱壳的缺点:存在修复函数不全的问题。
- 主动调用
通过构造虚拟调用,从而达到欺骗壳,让壳误认为app在执行正常的函数调用流程从而达成对dex中所有类函数的虚拟调用。
- 主动调用的优点:能够覆盖dex中所有函数,从而完成更彻底的函数粒度的修复。同时,函数的修复准确度同主动调用链的构造深度有关。
FART是模拟调用所有类,而并非真实的调用,免去了复杂的参数构造。
如何获取每一个函数的CodeItem
一个Java函数最终在内存当中对应的codeitem
每一个CodeItem在内存当中的前16个字节是固定的,
我画圈的部分是固定的部分。
uint_debugInfoOff: 这一部分对于一个发布的或者说是加壳后的应用来说是无用的,一般会进行抹除。
16个字节之后的部分才是指令的部分,具体的指令数为:指令数 = insnsSize * 2
。
指令相关的长度并非为CodeItem的长度,还与其是否有异常处理有关。
CodeItem的长度的计算
没有异常处理的函数:
其长度为32个字节,前16个字节包含需要的寄存器的个数(ushort registers_size的数值,这里为3),还有参数的个数(ushort ins_size的数值,这里为1),调用时候的寄存器的个数(ushort outs_size的数值,这里为2),以及刚才提到的一般会被抹去的uint debug_info_off。
*由于这里没有异常处理,所以可以直接用uint insns_size * 2 得到除前16个字节外该codeitem的长度为16 + insns_size * 2 = 16 + 8 * 2 = 32*,由上图也可以看到010 Editor也已经帮我们把该codeitem的范围长度也给标亮出来了。
接下来要判断有异常处理的函数的codeitem长度了。
在这之前,我们要清楚的知道怎样识别我们要判断的codeitem是不是包含异常处理。起始codeitem的结构是有相应的标识的ushort tries_size
为1时,则为包含异常处理的codeitem。
有异常处理的函数
除了前16个字节以外,我们先看看insns_size,这里总共有23个字节,除异常处理外的字节长度就为23 * 2 + 16 = 62。我们可以通过图片的高亮处,可以得出这个codeitem总长为77个字节,也就是说现在还有15个字节没有算出来。
实际上看这个结构也可以得出
有异常处理的codeitem_size = ins_size * 2 + 16 + try_item_tries的长度 + encoded_catch_handler_list handle的长度
主动调用组件
FART的使用及应用场景
FART生成的bin文件即CodeItem的格式
5元组填充
{name:函数名,method_idx:函数索引,offset:偏移,code_item_len:长度,ins:函数体codeltem的base64字符串};
method_idx:
为图中的这些下标
offset:
偏移,当前函数体CodeItem在当前Dex文件的偏移在哪个位置,这个字段方便我们定位Dex文件的偏移,方便我们填充函数
code_item_len:
当前CodeItem的长度
ins:
CodeItem的Base64编码。意让CodeItem成为可视的编码,存放进txt文件里可直接打开,修复时直接对其进行Base64解码。
红色的字段为必需的,其他的不必需,但是可以作为参考。即只需要method_idx进行区分函数以及该函数的Codeltem内容即可
手动回填抽空的函数
该函数需要修复,则在相应的bin文件搜索MemoryCache.checkSize
然后把相应的dex拖入010 Editor
找到相应的method_idx,这里为10554,即可找到要修复的函数,然后找到0E这里
解码五元组里面的BASE64,解码的内容写进一个.bin文件里
这里总共是16行 + 6个字节,然后通过偏移找到需要替换的函数
先替换16行,再替换最后6个字节,即可还原!
FART的适用场景
- 整体加固壳
- FART对dex的修复粒度为函数粒度
frida FART脱壳问题
使用frida FART进行脱壳碰到一个问题,so不管怎么赋权限始终报没有权限。经高人指点,发现似乎只要push so到手机都有类似问题。
原因在于:
selinux(安全增强的Linux)
SELinux被描述为在内核中执行的强制访问控制(MAC)安全结构。SELinux提供了一种强制执行某些安全策略的方法,否则系统管理员将无法有效地实现这些策略。
就是这个机制导致的so跑不起来!
网上搜了如何禁用selinux,但是永久的方法:
“””
要永久禁用SELinux,请使用您最喜欢的文本编辑器打开/etc/sysconfig/selinux文件,如下所示:
vi /etc/sysconfig/selinux
然后将配置SELinux=enforcing改为SELinux=disabled,如下图所示。
SELINUX=disabled
然后,保存并退出文件,为了使配置生效,需要重新启动系统,然后使用sestatus命令检查SELinux的状态,如下所示:
sestatus
“””
用不了!!!,会提示说没有这个文件。。。。。
只好临时禁用selinux来解决问题~
解决方法:
adb shell -> setenforce 0临时禁用selinux