前言
环境及设备
●电脑已具备 ADB 环境。
●虚拟机或实体机已有Root权限。
鉴于实体机性能更加出色(脱壳过程中可以节省大量时间),我还是选择在某鱼上以310元购买一台战损外观的Pixel3作为我的测试机。为何更推荐使用Google手机,因为他们搭载着原生安卓,并且它们的镜像都可以在谷歌开发者网站上找到,Root后你可以随意地魔改自己的系统。
实体机刷机
拿到手机,先进行刷机。刷机有很多方式。但主要分为两种,线刷和卡刷。我不过多论述,大致原理查看这个视频。
如果你的设备也是Pixel 3,你可以查看此博客。
准备工作
手机电脑连接上数据线,打开手机的OEM开关以及USB调试开关。如果没有adb工具,那么请先在Android Studio里下载好Android SDK以及Google USB Driver(不然之后fastboot会连接不上手机)。输入命令 adb devices来查看连接是否成功。
刷机步骤
解除bootloader锁(会清除手机数据)
输入adb reboot bootloader,手机会进入到fastboot模式下。
此时,在命令框中输入fastboot flashing unlock。
通过音量键选择Unlock the bootloader。
安装Magisk面具
下载链接
adb+install+Magisk在你电脑上的地址。
下载与你系统版本号相同的谷歌镜像,两次解压后,取boot.img镜像,传输到手机,使用Magisk软件对boot.img进行修补。
修补后的镜像重新传回电脑。再次进入fastboot模式下,执行命令fastboot+boot+boot.img的地址。开机后,进入Magisk软件,再选择直接安装。
我们能发现,Magisk的超级用户界面已经可以使用,并且adb也能获得root权限了。
学习目录
加壳和脱壳
01.FART的初步使用
了解FART
总所周知,从Android5.0开始,ART取代dalvik,成为默认虚拟机。dalvik和ART运行机制的不同,脱壳方法自然也不同。而FART就是通过主动调用的方式来实现ART环境下的脱壳。FART的代码是通过修改少量Android源码文件而成的,经过修改的Android源码编译成系统镜像,刷入手机,这样的手机启动后,就成为一台可以用于脱壳的脱壳机。
此节并不会谈其原理,只是简单配置FART以及使用方法。
想了解其原理的请click博客链接。
安装FART
项目地址
环境:Android 8.0
设备:Pixel 1
安装镜像
镜像链接(拒绝度盘,从我做起)。
手机开启USB调试并连接电脑,进入fastboot模式(bl锁要解开),下载镜像包后解压,直接运行flash-all.sh。
自动脱壳
理论上,你运行的apk,它都会自动进行脱壳。我们拿看雪的一道题来演示。
题目链接
adb install apk地址,直接安装。
手机上点开apk,然后直接进入data/data目录下。
cd com.kanxue.craceme,就能找到脱壳下来的dex文件了。
那么这么多dex,哪个包含MainActivity呢?
我们使用grep -ril “MainActivity” ./*dex 命令,直接找到我们所需要的dex
然后cp到sdcard目录下,便于之后pull到计算机中。
如果此时提示没有读写权限,可以看这篇论坛地址
只要之前加上mount -o rw,remount /sys命令即可。
pull 到本地,jadx/jeb打开分析。
02.ClassLoader与动态加载
安卓中常见的类加载器
3.1 BootClassLoader:单例模式,用来加载系统类
3.2 BaseDexClassLoader:是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类 dex和类加载的主要逻辑都是在BaseDexClassLoader完成的
3.3 PathClassLoader:是默认使用的类加载器,用于加载app自身的dex
3.4 DexClassLoader:用于实现插件化、热修复、dex加固等
3.5 InMemoryDexClassLoader:安卓8.0以后引入,用于内存加载dex
我们可以去验证一下,来到Android的在线源码网站
Link:http://www.aospxref.com/
如图,在Defination中输入你要查的类即可。
查看ClassLoader源代码
红框处的代码,记录了parent,实现了后续的双亲委派。
查看BaseDexClassLoader源码
继承了ClassLoader
查看DexClassLoader源码
继承了BaseDexClassLoader
限于篇幅,其他的请自行探究…
APP从点击图标到运行至第一个Activity的过程,ClassLoader的情况和双亲委派的关系是怎么样的呢?
验证调用的顺序以及双亲委派的关系
我们使用Android Studio进行演示
package com.example.classloadertest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testclassloader();
}
public void testclassloader(){
ClassLoader thisclassloader=MainActivity.class.getClassLoader();
Log.i("Fup1p1","thisclassloader: "+thisclassloader);
ClassLoader tmpclassloader=null;
ClassLoader parentclassloader=thisclassloader.getParent();
while(parentclassloader!=null){
Log.i("Fup1p1","this:"+thisclassloader+"- - -"+parentclassloader);
tmpclassloader=parentclassloader.getParent();
thisclassloader=parentclassloader;
parentclassloader=tmpclassloader;
}
Log.i("Fup1p1","root:"+thisclassloader);
}
/*
output:
2023-01-07 14:26:45.730 29165-29165/com.example.classloadertest I/Fup1p1: this:classloader: dalvik.system.PathClassLoader[DexPathList[[dex file "/data/data/com.example.classloadertest/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~opxAMwK2t0T-zvgjwnQaMw==/com.example.classloadertest-9IxjuL8mBmeHnJbr0nW5xQ==/base.apk"],nativeLibraryDirectories=[/data/app/~~opxAMwK2t0T-zvgjwnQaMw==/com.example.classloadertest-9IxjuL8mBmeHnJbr0nW5xQ==/lib/arm64, /system/lib64, /system_ext/lib64, /product/lib64]]]
2023-01-07 14:26:45.731 29165-29165/com.example.classloadertest I/Fup1p1: this:dalvik.system.PathClassLoader[DexPathList[[dex file "/data/data/com.example.classloadertest/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~opxAMwK2t0T-zvgjwnQaMw==/com.example.classloadertest-9IxjuL8mBmeHnJbr0nW5xQ==/base.apk"],nativeLibraryDirectories=[/data/app/~~opxAMwK2t0T-zvgjwnQaMw==/com.example.classloadertest-9IxjuL8mBmeHnJbr0nW5xQ==/lib/arm64, /system/lib64, /system_ext/lib64, /product/lib64]]]- - -java.lang.BootClassLoader@dcac047
2023-01-07 14:26:45.731 29165-29165/com.example.classloadertest I/Fup1p1: root:java.lang.BootClassLoader@dcac047
可以发现最后的根加载类就是BootLoader
*/
}
编写一个动态加载
Android Studio新建一个empty项目,新建一个TestClass类。
TestClass类:
package com.example.test02;
import android.util.Log;
public class TestClass {
public void testfunc(){
Log.i("Fup1p1","i am from com.example.test02.TestClass.testfunc");
}
}
然后构建中选择build apk,解压后提取其dex。
然后adb push ./classes3.dex /sdcard 4.dex
我们的目的就是让APP去加载这个dex。
MainActivity:
package com.example.test02;
import androidx.appcompat.app.AppCompatActivity;
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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context appcontext=this.getApplicationContext();
testdexClassloader(appcontext,"/sdcard/4.dex");
}
public void testdexClassloader(Context context,String dexfilepath){
File optfile=context.getDir("opt_dex",0);
File libfile=context.getDir("lib_path",0);
DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(), libfile.getAbsolutePath(),MainActivity.class.getClassLoader());
Class<?> clazz=null;
try {
clazz = dexClassLoader.loadClass("com.example.test02.TestClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (clazz!=null){
try {
Method testfuncMethod=clazz.getDeclaredMethod("testfunc");
Object obj=clazz.newInstance();
testfuncMethod.invoke(obj);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
给apk写入需要读写权限,运行后需要在手机上授予程序对应的权限。
03.加壳App运行流程和ClassLoader修正
解决方案1
包含组件的插件Dex
package com.example.test03;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
Log.i("Fup1p1","i am from TestActivity.oncreate");
}
}
build apk,解压后将classes.dex push到sdcard中,重命名为66.dex
adb push classes3.dex /sdcard/66.dex
反射替换
package com.example.test02;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.ArrayMap;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context appcontext = this.getApplicationContext();
startTestActivityFirstmethod(this, "/sdcard/66.dex");
}
public void replaceClassloader(ClassLoader classloader){
try {
Class<?> Activitythreadclazz=classloader.loadClass("android.app.ActivityThread");
Method currenTActivityThreadMethod=Activitythreadclazz.getDeclaredMethod("currentActivityThread");
currenTActivityThreadMethod.setAccessible(true);
Object Activitythreadobj=currenTActivityThreadMethod.invoke(null);
Field mpackagesField=Activitythreadclazz.getDeclaredField("mPackages");
mpackagesField.setAccessible(true);
ArrayMap mpackagesobj=(ArrayMap) mpackagesField.get(Activitythreadobj);
WeakReference wr=(WeakReference) mpackagesobj.get(this.getPackageName());
Object loadedApkobj=wr.get();
Class LoadedApkclazz=classloader.loadClass("android.app.LoadedApk");
Field mClassloader=LoadedApkclazz.getDeclaredField("mClassLoader");
mClassloader.setAccessible(true);
mClassloader.set(loadedApkobj,classloader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public void startTestActivityFirstmethod(Context context, String dexfilepath) {
File optfile = context.getDir("opt_dex", 0);
File libfile = context.getDir("lib_path", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath, optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader());
replaceClassloader(dexClassLoader);
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.test03.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context, clazz));
}
实现结果
解决方案二
包含组件的插件Dex
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class TestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
Log.i("Fup1p1","i am from TestActivity.oncreate");
}
}
方案二代码
package com.example.test02;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.ArrayMap;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context appcontext = this.getApplicationContext();
// startTestActivityFirstmethod(this, "/sdcard/66.dex");
startTestActivitySecondmethod(this,"/sdcard/77.dex");
}
public void startTestActivityFirstmethod(Context context, String dexfilepath) {
File optfile = context.getDir("opt_dex", 0);
File libfile = context.getDir("lib_path", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath, optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader());
replaceClassloader(dexClassLoader);
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.test03.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context, clazz));
}
public void startTestActivitySecondmethod(Context context, String dexfilepath) {
File optfile = context.getDir("opt_dex", 0);
File libfile = context.getDir("lib_path", 0);
ClassLoader pathclassloader=MainActivity.class.getClassLoader();
ClassLoader bootclassloader=MainActivity.class.getClassLoader().getParent();
DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath, optfile.getAbsolutePath(), libfile.getAbsolutePath(),bootclassloader);
try {
Field parentField=ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathclassloader,dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.test03.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context, clazz));
}
}
实现结果
验证关系
添加代码
ClassLoader tmpclassloader=pathclassloader;
ClassLoader parentclassloader=pathclassloader.getParent();
while(parentclassloader!=null){
Log.i("Fup1p1","this:"+tmpclassloader+"--parent:"+parentclassloader);
tmpclassloader=parentclassloader;
parentclassloader=parentclassloader.getParent();
}
Log.i("Fup1p1","root:"+tmpclassloader);
output:
/com.example.test02 I/Fup1p1: this:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.test02-pMLEUUkjX85dw_l9WeN5rg==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.test02-pMLEUUkjX85dw_l9WeN5rg==/lib/arm64, /system/lib64, /vendor/lib64]]]--parent:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/77.dex"],nativeLibraryDirectories=[/data/user/0/com.example.test02/app_lib_path, /system/lib64, /vendor/lib64]]]
/com.example.test02 I/Fup1p1: this:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/77.dex"],nativeLibraryDirectories=[/data/user/0/com.example.test02/app_lib_path, /system/lib64, /vendor/lib64]]]--parent:java.lang.BootClassLoader@e9d1226
/com.example.test02 I/Fup1p1: root:java.lang.BootClassLoader@e9d1226
即:
BootClassLoader
↑
DexClassLoader
↑
PathClassLoader
04.一二三代壳和加壳技术分类识别
动态加载是dex加壳、插件化、热更新的基础
动态加载就是用到的时候才去加载,也叫懒加载。
动态加载的dex不具有生命周期特征,APP中的Activity,Service等组件无法正常工作,只能完成一般函数的调用;
这时就需要对ClassLoader进行修正,APP才能够正常运行,两种修正手段,上文已提及。
壳史
第一代壳 Dex加密
Dex整体加固,主要有两种方式,落地加载和内存加载。
落地加载:动态解密后候会用到DexClassLoader函数,第一个参数要传入一个路径,也就是说解密后的dex必须出现在硬盘中,加载完后会删除这个解密后的dex。也就是说会有一段时间,解密后的dex会出现。
类似于下图
内存加载:Dex解密后加载的时候会全部映射到内存中,所以可以从内存中dump下来
第二代壳 Dex抽取与so混淆
对重要的代码进行抽取,只有当函数和代码需要被执行的时候,才会被加载。
第三代壳 Dex动态解密与so混淆
Dex在内存中始终不是一个完整的状态。二代壳可以使用dexhunter工具遍历dex中所有的类然后初始化,使得内存中的dex变得完整。
第四代壳 vmp壳与dex2c(Java函数Native化)
05.ART下的整体脱壳原理
Dex的加载流程去脱壳
通过mCookie脱壳,因为mCookie是一个jlong类型的数组,存放了DexFile类型的指针,其中有begin和size,即可保存dex文件
通过openCommen等函数脱壳,这个函数调用时传入两个参数,base和size,可以保存dex文件
通过DexFile 的构造函数脱壳,因为最后加载的时候还是要变成DexFile对象的,调用构造函数进行实例化
youpk则是通过ClassLinker来得到DexFile
总结一下就是只要能获取带dexfile的地方,都可以尝试脱壳。
也可以是间接得到DexFile的地方,比如ArtMethod->getDexFile()
加固厂商加载dex的方式,也决定了脱壳点,有些脱壳点可能会被禁用掉
InMemoryDexClassLoader源码分析
找到其父类BaseDexClassLoader,然后发现initByteBufferDexPath函数传入了我们关心的dexFiles
继续查看initByteBufferDexPath的实现
继续跟进,发现mCookie这个比较关键的值(后续可知mCookie是一个jlong数组,每个数组都存放着每个dex文件的指针)
查看openInMemoryDexFiles这个函数,发现最后会返回一个JNI函数的值
进一步查看
所以其实在createCookieFromOatFileManagerResult之前,就已经加载了dex,这个函数只是进行一个转换
分析其中的脱壳点
本质上就是找到DexFile对象,然后通过begin和size就能得到dex文件的起始位置和大小,便可以脱壳。
接下来我们关注一下dex的加载
DexClassLoader源码分析
DexClassLoader继承了BaseDexClassLoader
其中这个openDexFile 和之前分析的openInMemoryDexFiles是差不多的,接下来我们就仔细分析分析。
youpk脱壳原理
看到这一段源码,也是通过获得DexFile对象,然后再进行脱壳的。
dex2oat
早期的脱壳原理:https://www.jianshu.com/p/7af31cc5130e
不推荐,Android10不再从应用进程调用dex2oat,只接受系统生成的oat。对于早期的系统也有可能被加固厂商给禁用掉了
(但是也有好多骚操作,也有人绕过了Link)
fdex2
适用于8.0以下的系统
脱壳原理 classObj.getDex().getBytes()
类的加载和初始化流程
如 DexHunter在defineClass进行类解析
比如下图Class_linker中已经开始加载类的方法了,一般来说加固厂商都会使用这些函数,自己实现就太麻烦了。如果这也不行,那就只能走下面的ART虚拟机,加固厂商不会连ART虚拟机都要自己实现吧!
可以看到dex_file,这也都是可以保存的。包括LinkCode也是脱壳点,art_method->getDexFIle()
函数执行过程中的脱壳点
Fart源码分析
我们查看Fart的作者的源码可以发现,其在interpret.cc的Execute函数中添加了一个自实现的函数dumpDexFileByExecute()函数。
无论有无dex2oat对Dex进行编译,但是类的初始化函数都没有被编译,所以始终运行在解释模式下,这时候就会经过interpreter.cc的Execute()函数,然后再进入到ART下的解释器解释执行,因此我们可以在Execute()函数中执行Dump操作。
Fart迁移到安卓10
现在的Fart的特征被很多加固厂商检测了,而且直接把Fart 8.0移植到Fart 10.0会报错,所以需要小小地修改一下。变量名也可以改一改。
Android 10 中有些函数有改动主要是两个地方
问题一
根据提示,进行修改即可
问题二
flags中要两个参数了,提示说要加上O_CREAT,但是加上就行了吗?O_CREAT参数是存在就访问,不存在就新建。
看看左图的代码,这里是判断是否已经加载过这个dex,如果没加载过,再执行后面的代码。但是如果你加上了O_CREAT这个参数,如果这个Dex不存在,就直接新建一个空的dex,后面的代码也不会执行了。
所以我们可以用其他的函数,比如access,改成下图即可
成品测试
06.ART下的指令抽取原理
本质其实就是提取出dex中方法体的字节码,然后在方法运行的时候去还原
实现形式
1.抽空方法体代码,运行方法之后再去回填,回填后不再抽取 solution:延时保存
2.抽空方法体代码,运行方法之后再去回填,回填之后又抽取 solution:Fart、youpk主动调用
3.将原有函数体替换为解密代码,运行时解密执行
对dex的处理形式
有用的函数体数据空间置为0,但是保留原有空间
对dex文件进行重构,不保留原有空间,在还原的时候没修改CodeItemOffset·
解决方案
1.解决思路:在函数运行时,保存被抽取的数据
2.被动调用 app正常运行过程中所发生的函数调用只对dex中部分的类完成加载,只对dex中的部分函数完成调用调用函数不全,导致能够恢复的函数有限
3.主动调用构造虚拟调用,对app中所有函数完成调用在这些函数执行时,保存函数体Code|tem数据保存数据的时机越晚,效果越好
4.常见的抽取加固脱壳系统 DexHunter、Fupk3、FART、youpk
在分析Fart源码之前,还是再先回顾一下双亲委派和类加载器的知识
双亲委派
类加载:
隐式加载
显式加载 : 明确的说明要加载某个类 ,
- 使用 Class.forName() 加载指定的类 ;
- 使用 ClassLoader.loadClass 加载指令的类 ,后者可以设置是否在加载的时候初始化;
有的加固厂商的壳会生成这类代码,若对这个函数进行了主动调用,而且默认了加载时初始化,那么就会执行static中的代码,就会退出.
双亲委派机制的工作原理
1.如果一个类加载器收到了类加载请求,会先把这个请求委托给父类的加载器去执行
2.知果父类加载器还存在其父类加载器,则进一步向上委托,依次类推,最终到达顶层的启动类加载器
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
为什么要有双亲委派?
1.避免重复加载,已经加载的Class,可以直接读取。
2.更加安全,无法自定义类来替代系统的类,可以防止核心API库被随意篡改 。因为系统类都是BootClassLoader加载的,而用户定义的类是由pathClassLoader去加载的,其又会交给父加载类BootClassLoader去加载,如果BootClassLoader里面已经有了,就不会再次加载了。所以替换成实现的核心库是不可能的,除非hook。
* 3.方便脱壳机主动调用。因为抽取加固的解决方案就是主动调用app中的所有函数,而想调用app中的所有函数,就要获得所有类—>所有Dex—>所有ClassLoader。而我们只要得到一个classloader,就可以顺着这个classloader的parent属性向上遍历。
那么又如何得到一个ClassLoader呢?
在PathClassLoader加载dex以后,会记录在LoadedApk的mClassLOader属性中,默认会使用这个ClassLoader去寻找类
普通app与加固app运行流程的区别
一开始都是BootClassLoader去加载系统相关的类。
普通app先加载app自身的dex
加壳app加载壳的dex,然后在壳的dex/so中再去加载原先app的dex。但是需要你对类加载器进行修正,02那章其实已经提及。
未修正之前:
BootClassLoader 系统核心库
PathClassLoader 壳的dex mClassLoader保存PathClassLoader
DexCLassLoader(也有可能是自实现的ClassLoader) app自身dex
DexClassLoader 加载的类是没有组件生命周期的,即 DexClassLoader 即使通过对 APK 的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常。又因为双亲委派的存在,如果如上图一样,默认的类加载器PathClassLoader又加载不到app自身的dex,所以是不可取的。
解决方案一. 插入ClassLoader
将DexCLassLoader(也有可能是自实现的ClassLoader) 的父加载器设置成BootClassLoader, 然后将PathClassLoader的父加载器设置成自己,就会形成下面这个结构
BootClassLoader 系统核心库
DexCLassLoader(也有可能是自实现的ClassLoader) app自身dex
PathClassLoader 壳的dex mClassLoader保存PathClassLoader
解决方案二. 反射替换
直接替换mClassLoader的属性,修改成 DexCLassLoader(也有可能是自实现的ClassLoader)
BootClassLoader 系统核心库
PathClassLoader 壳的dex
DexCLassLoader(也有可能是自实现的ClassLoader) app自身dex mClassLoader被替换成改类加载器
Fart源码分析
遍历所有的ClassLoader
先得到一个ClassLoader,然后根据双亲委派得到向上的所有ClassLoader
(如果仅仅只是动态加载,没有加入到双亲委派中,那么只能用Frida枚举类加载器)1
遍历所有的类名
public static void fartwithClassloader(ClassLoader appClassloader) { //dex -->element-->Dexelements-->Dexpathlist-->pathlist 所以想获取dex就要通过反射一步一步得到
List<Object> dexFilesArray = new ArrayList<Object>();
Field pathList_Field = (Field) getClassField(appClassloader, "dalvik.system.BaseDexClassLoader", "pathList");
Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");
Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");
Field dexFile_fileField = null;
try {
dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
Class DexFileClazz = null;
try {
DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
Method getClassNameList_method = null;
Method defineClass_method = null;
Method dumpDexFile_method = null;
Method dumpMethodCode_method = null;
for (Method field : DexFileClazz.getDeclaredMethods()) { //遍历方法,保存之后需要反射调用的的方法。
if (field.getName().equals("getClassNameList")) {
getClassNameList_method = field;
getClassNameList_method.setAccessible(true);
}
if (field.getName().equals("defineClassNative")) {
defineClass_method = field;
defineClass_method.setAccessible(true);
}
if (field.getName().equals("dumpDexFile")) {
dumpDexFile_method = field;
dumpDexFile_method.setAccessible(true);
}
if (field.getName().equals("dumpMethodCode")) {
dumpMethodCode_method = field;
dumpMethodCode_method.setAccessible(true);
}
}
Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");
Log.v("ActivityThread->methods", "dalvik.system.DexPathList.ElementsArray.length:" + ElementsArray.length);//5个
for (int j = 0; j < ElementsArray.length; j++) {
Object element = ElementsArray[j];
Object dexfile = null;
try {
dexfile = (Object) dexFile_fileField.get(element);
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
if (dexfile == null) {
Log.e("ActivityThread", "dexfile is null");
continue;
}
if (dexfile != null) {
dexFilesArray.add(dexfile);
Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie"); //有的时候加固厂商会禁用其中一个
if (mcookie == null) {
Object mInternalCookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mInternalCookie"); //有的时候加固厂商会禁用其中一个
if(mInternalCookie!=null)
{
mcookie=mInternalCookie;
}else{
Log.v("ActivityThread->err", "get mInternalCookie is null");
continue;
}
}
String[] classnames = null;
try {
classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);//通过之前保存的函数去得到Dex中的类名
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
if (classnames != null) {
for (String eachclassname : classnames) {
loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method); //类名的遍历与加载
}
}
}
}
return;
}
遍历所有的类,调用类中的所有函数
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
Class resultclass = null;
Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);
try {
resultclass = appClassloader.loadClass(eachclassname); //使用loadClass,避免初始化类的时候加载加固厂商写的垃圾类中的static代码块
} catch (Exception e) {
e.printStackTrace();
return;
} catch (Error e) {
e.printStackTrace();
return;
}
if (resultclass != null) {
try {
Constructor<?> cons[] = resultclass.getDeclaredConstructors();
for (Constructor<?> constructor : cons) { //遍历所有的构造函数
if (dumpMethodCode_method != null) {
try {
dumpMethodCode_method.invoke(null, constructor);
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
} else {
Log.e("ActivityThread", "dumpMethodCode_method is null ");
}
}
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
try {
Method[] methods = resultclass.getDeclaredMethods();
if (methods != null) {
for (Method m : methods) { //遍历所有的方法
if (dumpMethodCode_method != null) {
try {
dumpMethodCode_method.invoke(null, m);
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
} else {
Log.e("ActivityThread", "dumpMethodCode_method is null ");
}
}
}
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
}
}
遍历所有的方法
Thread参数传递了一个nulptr,作为主动调用的标识,并在Invoke函数的开头加了对Thread参数的判断。
extern "C" void dumpArtMethod(ArtMethod* artmethod) REQUIRES_SHARED(Locks::mutator_lock_) {
char *dexfilepath=(char*)malloc(sizeof(char)*1000);
if(dexfilepath==nullptr)
{
LOG(ERROR) << "ArtMethod::dumpArtMethodinvoked,methodname:"<<artmethod->PrettyMethod().c_str()<<"malloc 1000 byte failed";
return;
}
int result=0;
int fcmdline =-1;
char szCmdline[64]= {0};
char szProcName[256] = {0};
int procid = getpid();
sprintf(szCmdline,"/proc/%d/cmdline", procid);
fcmdline = open(szCmdline, O_RDONLY,0644);
if(fcmdline >0)
{
result=read(fcmdline, szProcName,256);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file file error";
}
close(fcmdline);
}
if(szProcName[0])
{
const DexFile* dex_file = artmethod->GetDexFile();
const uint8_t* begin_=dex_file->Begin(); // Start of data.
size_t size_=dex_file->Size(); // Length of data.
memset(dexfilepath,0,1000);
int size_int_=(int)size_;
memset(dexfilepath,0,1000);
sprintf(dexfilepath,"%s","/sdcard/fart");
mkdir(dexfilepath,0777);
memset(dexfilepath,0,1000);
sprintf(dexfilepath,"/sdcard/fart/%s",szProcName);
mkdir(dexfilepath,0777);
memset(dexfilepath,0,1000);
sprintf(dexfilepath,"/sdcard/fart/%s/%d_dexfile.dex",szProcName,size_int_);
int dexfilefp=open(dexfilepath,O_RDONLY,0666);
if(dexfilefp>0){
close(dexfilefp);
dexfilefp=0;
}else{
int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
if(fp>0)
{
result=write(fp,(void*)begin_,size_);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath file error";
}
fsync(fp);
close(fp);
memset(dexfilepath,0,1000);
sprintf(dexfilepath,"/sdcard/fart/%s/%d_classlist.txt",szProcName,size_int_);
int classlistfile=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
if(classlistfile>0)
{
for (size_t ii= 0; ii< dex_file->NumClassDefs(); ++ii)
{
const DexFile::ClassDef& class_def = dex_file->GetClassDef(ii);
const char* descriptor = dex_file->GetClassDescriptor(class_def);
result=write(classlistfile,(void*)descriptor,strlen(descriptor));
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
}
const char* temp="\n";
result=write(classlistfile,(void*)temp,1);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
}
}
fsync(classlistfile);
close(classlistfile);
}
}
}
//----------------------------上面又脱了一次整体加固-----------------------
const DexFile::CodeItem* code_item = artmethod->GetCodeItem(); //得到codeItem的指针
if (LIKELY(code_item != nullptr))
{
// ----------//计算codeItem的长度---------------- 也可以一句代码去替代 dex_file->GetCodeitemSize(const CodeItem& code)
int code_item_len = 0;
uint8_t *item=(uint8_t *) code_item;
if (code_item->tries_size_>0) {
const uint8_t *handler_data = (const uint8_t *)(DexFile::GetTryItems(*code_item, code_item->tries_size_));
uint8_t * tail = codeitem_end(&handler_data);
code_item_len = (int)(tail - item);
}else{
code_item_len = 16+code_item->insns_size_in_code_units_*2; //16是codeItem的头部信息 后面每个code
}
// --------------------------------------------
memset(dexfilepath,0,1000);
int size_int=(int)dex_file->Size();
uint32_t method_idx=artmethod->GetDexMethodIndexUnchecked();//代表这个codeItem属于哪个函数的
sprintf(dexfilepath,"/sdcard/fart/%s/%d_ins_%d.bin",szProcName,size_int,(int)gettidv1());
int fp2=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
if(fp2>0){
lseek(fp2,0,SEEK_END);
memset(dexfilepath,0,1000);
int offset=(int)(item - begin_);
sprintf(dexfilepath,"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",artmethod->PrettyMethod().c_str(),method_idx,offset,code_item_len);
int contentlength=0;
while(dexfilepath[contentlength]!=0) contentlength++;
result=write(fp2,(void*)dexfilepath,contentlength);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
}
long outlen=0;
char* base64result=base64_encode((char*)item,(long)code_item_len,&outlen); //对指令数据进行编码
result=write(fp2,base64result,outlen);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
}
result=write(fp2,"};",2);
if(result<0)
{
LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";
}
fsync(fp2);
close(fp2);
if(base64result!=nullptr){
free(base64result);
base64result=nullptr;
}
}
}
}
if(dexfilepath!=nullptr)
{
free(dexfilepath);
dexfilepath=nullptr;
}
}
评论区