1
前言
2
Android壳简介
1.加密壳
-
一代 动态加载型 DexClassLoader 落地加载 -
二代 动态加载型 InMemoryDexClassLoader 不落地加载 -
三代 代码抽取型 Dex代码抽取与回填 落地加载
落地指释放Dex文件到文件系统,不落地则不释放Dex文件,直接在内存中加载
2.四代壳
DexVMP Dex2C
3.混淆壳
OLLVM
在学习Android加固之前,一定要了解以下知识:
1.Java反射机制
2.ClassLoader机制
3.Android应用程序启动流程
4.Dex文件结构
3
Java反射机制
参考java反射技术学习和Java反射机制-十分钟搞懂
示例文件: attachmentsJavaReflectionDemoReflectionDemo.java
反射机制简介
反射的原理:
1.Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法
本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息
2.Java属于先编译再运行的语言
程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM.通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁.
3.反射是实现动态加载的技术之一
反射的优缺点:
1.优点
在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
2.缺点
反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题.
反射的作用: 反射机制在实现壳程序动态加载被保护程序时非常关键,它可以突破默认的权限访问控制,访问安卓系统默认情况下禁止用户代码访问的以及不公开的部分.
反射的本质
正常类的加载过程:
1.执行Student student=new Student(),向JVM请求创建student实例
2.JVM寻找Student.class文件并加载到内存中
3.JVM创建Student对应的Class对象(一个类只对应一个Class对象)
4.JVM创建student实例
对于同一个类而言,无论有多少个实例,都只对应一个Class对象
Java反射的本质: 获取Class对象后,反向访问实例对象
反射的入口-Class类
反射相关文件:
Java.lang.Class;Java.lang.reflect.Constructor;Java.lang.reflect.Field;Java.lang.reflect.Method;Java.lang.reflect.Modifier;
JDK中,主要由以下类来实现Java反射机制,这些类都位于java.lang.reflect包中
◆Class类: 代表一个类
◆Constructor 类: 代表类的构造方法
◆Field 类: 代表类的成员变量(属性)
◆Method类: 代表类的成员方法
反射的入口-Class类:
1.Class类的对象称为类对象
2.Class类是Java反射机制的起源和入口
-
用于获取与类相关的各种信息
-
提供了获取类信息的相关方法
-
Class类继承自Object类
3.Class类是所有类的共同的图纸
-
每个类有自己的对象,好比图纸和实物的关系
-
每个类也可看做是一个对象,有共同的图纸Class,存放类的结构信息,比如类的名字、属性、方法、构造方法、父类和接口,能够通过相应方法取出相应信息
示例类,用于后续操作:
// 示例类class Person implements Runnable{public String name;private int age;//默认构造函数和有参构造函数public Person() {}private Person(String str){ System.out.println("Private constructor:"+str); }public Person(String name, int age) {this.name = name;this.age = age; }// 不同访问权限的方法public void introduce() { System.out.println("我是" + name + ",年龄" + age); }private void privateMethod(String name,int age) { System.out.println("这是Person的私有方法,"+"name="+name+","+"age="+age); } @Overridepublic void run() { System.out.println("Hello World"); }}
反射获取Class
非动态加载时,可通过.class属性或实例.getClass()方法获取Class类
动态加载时,可使用Class.forName()和ClassLoader.loadClass()加载并获取类对象
//1. 获取类对象// 动态加载 Class<?> clazz= Class.forName("MyUnidbgScripts.Person"); // 通过类的完整名加载 Class<?> clazz2=ClassLoader.getSystemClassLoader().loadClass("MyUnidbgScripts.Person");// 通过classloader加载// 非动态加载 Class<?> clazz3=Person.class; Class<?> clazz4=new Person().getClass(); System.out.println("Load Class:"); System.out.println(clazz); System.out.println(clazz2); System.out.println(clazz3); System.out.println(clazz4); System.out.println();//2. 从类对象获取类的各种信息 System.out.println("Class info:"); System.out.println(clazz.getName()); // 完整类名 System.out.println(clazz.getSimpleName()); // 类名 System.out.println(clazz.getSuperclass()); // 父类类对象 System.out.println(Arrays.toString(clazz.getInterfaces())); //接口类对象数组 System.out.println();
输出如下
Load Class:class MyUnidbgScripts.Personclass MyUnidbgScripts.Personclass MyUnidbgScripts.Personclass MyUnidbgScripts.PersonClass info:MyUnidbgScripts.PersonPersonclass java.lang.Object[interface java.lang.Runnable]
反射获取Constructor
◆class.getConstructor(Class<?>... ParameterTypes) 获取class类指定参数类型的public构造方法
◆class.getConstructors() 获取class类中的所有public权限的构造方法
◆class.getDeclaredConstructor(Class<?>... ParameterTypes) 获取class类中的任意构造方法
◆class.getDeclaredConstructors()获取class类中的所有构造方法
//3. 获取构造方法// 获取无参构造方法(默认构造方法) System.out.println("Get constructor:"); Constructor<?> constructor=clazz.getConstructor(); System.out.println(constructor); System.out.println();// 获取public构造方法 System.out.println("Get public constructors:"); Constructor<?>[] constructors=clazz.getConstructors(); System.out.println(Arrays.toString(constructors)); System.out.println();// 获取所有构造方法 System.out.println("Get all constructors:"); constructors=clazz.getDeclaredConstructors(); System.out.println(Arrays.toString(constructors)); System.out.println("Print All Constructors:");for(Constructor<?> cons:constructors){ System.out.println("constructor: "+cons); System.out.println("tname: "+cons.getName()+"ntModifiers: "+Modifier.toString(cons.getModifiers())+"ntParameterTypes: "+Arrays.toString(cons.getParameterTypes())); } System.out.println();
输出如下
Get constructor:public MyUnidbgScripts.Person()Get public constructors:[public MyUnidbgScripts.Person(java.lang.String,int), public MyUnidbgScripts.Person()]Get all constructors:[public MyUnidbgScripts.Person(java.lang.String,int), private MyUnidbgScripts.Person(java.lang.String), public MyUnidbgScripts.Person()]Print All Constructors:constructor:public MyUnidbgScripts.Person(java.lang.String,int) name: MyUnidbgScripts.Person Modifiers: public ParameterTypes: [class java.lang.String, int]constructor:private MyUnidbgScripts.Person(java.lang.String) name: MyUnidbgScripts.Person Modifiers: private ParameterTypes: [class java.lang.String]constructor:public MyUnidbgScripts.Person() name: MyUnidbgScripts.Person Modifiers: public ParameterTypes: []
反射获取Field
◆class.getField(FieldName) 获取class类中的带public声明的FieldName变量
◆class.getFields() 获取class类中的带public声明的所有变量
◆class.getDeclaredField(FieldName) 获取class类中的FieldName变量
◆class.getDeclaredFields() 获取class类中的所有变量
//3. 获取属性// 获取所有public属性 System.out.println("Get public fields:"); Field[] fields=clazz.getFields(); System.out.println(Arrays.toString(fields)); System.out.println();// 获取所有属性 System.out.println("Get all fields:"); fields=clazz.getDeclaredFields(); System.out.println(Arrays.toString(fields)); System.out.println("Print all fields:");for(Field field:fields){ System.out.println("field: "+field); System.out.println("ttype: "+field.getType()+"ntname: "+field.getName()); } System.out.println(); System.out.println("Get specific field:");// 获取public权限的指定属性 Field field=clazz.getField("name"); System.out.println(field);// 获取任意权限的指定属性 field=clazz.getDeclaredField("age"); System.out.println(field);
输出如下
Get public fields:[public java.lang.String MyUnidbgScripts.Person.name]Get all fields:[public java.lang.String MyUnidbgScripts.Person.name, private int MyUnidbgScripts.Person.age]Print all fields:field:public java.lang.String MyUnidbgScripts.Person.name type: class java.lang.String name: namefield:private int MyUnidbgScripts.Person.age type: int name: ageGet specific field:public java.lang.String MyUnidbgScripts.Person.nameprivate int MyUnidbgScripts.Person.age
反射获取Method
◆class.getMethod(MethodName,...ParameterTypes) 获取指定方法名和指定参数的public方法
◆class.getMethods() 获取class类中所有public方法
◆class.getDeclaredMethod(MethodName,...ParameterTypes) 获取class类中指定方法名和指定参数的任意方法
◆class.getDeclaredMethods() 获取class类的所有方法
//4. 获取方法 System.out.println("Get public methods:"); Method[] methods=clazz.getMethods(); // 注意会获取所实现接口的public方法 System.out.println(Arrays.toString(methods)); System.out.println(); System.out.println("Get all methods:"); methods=clazz.getDeclaredMethods(); // 获取所有声明的方法 System.out.println(Arrays.toString(methods)); System.out.println(); System.out.println("Print all methods:");for(Method method:methods){ System.out.println("method: "+method); System.out.println("tname: "+method.getName()); System.out.println("treturnType: "+method.getReturnType()); System.out.println("tparameterTypes: "+Arrays.toString(method.getParameterTypes())); } System.out.println();// 获取public的指定方法 Method method=clazz.getMethod("introduce"); System.out.println(method);// 获取任意权限的指定方法 method=clazz.getDeclaredMethod("privateMethod",String.class,int.class); System.out.println(method); System.out.println();
输出如下
Get public methods:[public void MyUnidbgScripts.Person.run(), public void MyUnidbgScripts.Person.introduce(), public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public java.lang.String java.lang.Object.toString(), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll()]Get all methods:[public void MyUnidbgScripts.Person.run(), public void MyUnidbgScripts.Person.introduce(), private void MyUnidbgScripts.Person.privateMethod(java.lang.String,int)]Print all methods:method: public void MyUnidbgScripts.Person.run() name: run returnType: void parameterTypes: []method: public void MyUnidbgScripts.Person.introduce() name: introduce returnType: void parameterTypes: []method: private void MyUnidbgScripts.Person.privateMethod(java.lang.String,int) name: privateMethod returnType: void parameterTypes: [class java.lang.String, int]public void MyUnidbgScripts.Person.introduce()private void MyUnidbgScripts.Person.privateMethod(java.lang.String,int)
反射创建对象
1.通过Class.newInstance() 调用无参构造方法创建实例 不能传递参数
2.通过Constructor.newInstance() 调用指定构造方法创建实例 可传递参数
//5. 反射创建对象 System.out.println("Create instance by reflection:");//5.1 Class.newInstance() 要求Class对象对应类有无参构造方法,执行无参构造方法创建实例 System.out.println("Create instance by Class.newInstance():"); Object obj=clazz.newInstance(); System.out.println(obj.toString()); System.out.println();
//5.2 Constructor.newInstance() 通过Class获取Constructor,再创建对象,可使用指定构造方法 System.out.println("Create instance by Constructor.newInstance():"); Constructor<?> cons=clazz.getConstructor();// 获取无参构造方法 obj=cons.newInstance(); System.out.println(obj.toString()); cons=clazz.getDeclaredConstructors()[0];// 获取有参构造方法 obj=cons.newInstance("张三",18); System.out.println(obj.toString()); System.out.println();
输出如下
Create instance by reflection:Create instance by Class.newInstance():MyUnidbgScripts.Person@30dae81Create instance by Constructor.newInstance():MyUnidbgScripts.Person@1b2c6ec2MyUnidbgScripts.Person@4edde6e5
反射操作属性
1.Class.getField(FieldName) 获取指定名称的public属性
2.Class.getDeclaredField(FieldName) 获取指定名称的任意属性
3.Field.get(Object obj) 获取指定实例的值
4.Field.set(Object obj,Object value) 设置指定实例的值
5.Field.setAccessible(true) 突破属性权限控制
//6. 反射操作属性 System.out.println("Access field by reflection:"); Field nameField=clazz.getField("name"); nameField.set(obj,"王五"); // 修改指定对象的指定属性 Field ageField=clazz.getDeclaredField("age"); ageField.setAccessible(true);// 突破权限控制 ageField.set(obj,20); System.out.println(nameField.get(obj));// get方法获取字段值 System.out.println(ageField.get(obj));
输出如下
Access field by reflection:王五20
反射调用方法
1.Class.getMethod(String name,Class<?>... parameterTypes) 获取指定名称和参数类型的public方法
2.Class.getDeclaredMethod(String name,Class<?>... parameterTypes) 获取指定名称和参数类型的方法
3.Method.setAccessible(true) 突破访问权限控制
4.Method.invoke(Object obj,Object... args) 调用指定实例的方法,可传递参数
//7. 反射调用方法 System.out.println("Run method by reflection:"); Method introduceMethod=clazz.getMethod("introduce"); introduceMethod.invoke(obj); //person.introduce() Method privateMethod=clazz.getDeclaredMethod("privateMethod",String.class,int.class);// person.privateMethod("赵四",19) privateMethod.setAccessible(true); privateMethod.invoke(obj,"赵四",19);
输出如下
Run method by reflection:我是王五,年龄20这是Person的私有方法,name=赵四,age=19
封装反射类
封装Reflection.java反射类便于后续使用,提供以下功能:
1.调用静态/实例方法
2.访问静态/实例字段
package com.example.androidshell;import android.util.Log;import java.lang.reflect.Field;import java.lang.reflect.Method;public class Reflection {private static final String TAG="glass";public static Object invokeStaticMethod(String class_name,String method_name,Class<?>[] parameterTypes,Object[] parameterValues){try {Class<?> clazz = Class.forName(class_name); //反射获取Class类对象Method method = clazz.getMethod(method_name,parameterTypes);//反射获取方法 method.setAccessible(true);//突破权限访问控制return method.invoke(null,parameterValues);//反射调用,静态方法无需指定所属实例,直接传参即可 }catch (Exception e){Log.d(TAG, e.toString());return null; } }public static Object invokeMethod(String class_name,String method_name,Object obj,Class<?>[] parameterTypes,Object[] parameterValues) {try {Class<?> clazz = Class.forName(class_name);Method method = clazz.getMethod(method_name,parameterTypes); method.setAccessible(true);//突破权限访问控制return method.invoke(obj,parameterValues);// 反射调用,动态方法需要指定所属实例 }catch (Exception e) {Log.d(TAG, e.toString());return null; } }public static Object getField(String class_name,Object obj,String field_name) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true);return field.get(obj); //获取实例字段,需要指定实例对象 }catch(Exception e) {Log.d(TAG, e.toString());return null; } }public static Object getStaticField(String class_name,String field_name) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true);return field.get(null); }catch (Exception e) {Log.d(TAG, e.toString());return null; } }public static void setField(String class_name,String field_name,Object obj,Object value) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true); field.set(obj,value); }catch (Exception e) {Log.d(TAG, e.toString()); } }public static void setStaticField(String class_name,String field_name,Object value) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true); field.set(null,value); }catch (Exception e){Log.d(TAG, e.toString()); } }}
4
ClassLoader机制
热修复和插件化技术依赖于ClassLoader,JVM虚拟机运行class文件,而Dalvik/ART运行dex文件,所以它们的ClassLoader有部分区别
Java中的ClassLoader
ClassLoader的类型
Java的ClassLoader分为两种:
◆系统类加载器
BootstrapClassLoader, ExtensionsClassLoader, ApplicationClassLoader
◆自定义类加载器
Custom ClassLoader, 通过继承java.lang.ClassLoader实现
具体分析如下
1.Bootstrap ClassLoader (引导类加载器)
是使用C/C++实现的加载器(不能被Java代码访问),用于加载JDK的核心类库,例如java.lang和java.util等系统类
会加载JAVA_HOME/jre/lib和-Xbootclasspath参数指定的目录
JVM虚拟机的启动就是通过BootstrapClassLoader创建的初始类完成的
可通过如下代码得出其加载的目录(java8)
public
class
Test0 {
public
static
void
main(String[] args) {
System.out.println(System.getProperty(
"sun.boot.class.path"
));
}
}
效果如下
2.Extensions ClassLoader (拓展类加载器)
该类在Java中的实现类为ExtClassLoader,用于加载java的拓展类,提供除系统类之外的额外功能
用于加载JAVA_HOME/jre/lib/ext和java.ext.dir指定的目录
以下代码可以打印ExtClassLoader的加载目录
public
class
JavaClassLoaderTest {
public
static
void
main(String[] args) {
System.out.println(System.getProperty(java.ext.dirs));
}
}
3.Application ClassLoader (应用程序类加载器)
对应的实现类为AppClassLoader,又可以称作System ClassLoader(系统类加载器),因为它可以通过ClassLoader.getSystemClassLoader()方法获取
用于加载程序的Classpath目录和系统属性java.class.path指定的目录
4.Custom ClassLoader (自定义加载器)
除了以上3个系统提供的类加载器之外,还可以通过继承java.lang.ClassLoader实现自定义类加载器
Extensions和Application ClassLoader也继承了该类
ClassLoader的继承关系
运行一个Java程序需要几种类型的类加载器呢?可以使用如下代码验证
public class JavaClassLoaderTest {public static void main(String[] args) { ClassLoader loader=JavaClassLoaderTest.class.getClassLoader();while(loader!=null){ System.out.println(loader); loader=loader.getParent(); } }}
打印出了AppClassLoader和ExtClassLoader,由于BootstrapClassLoader由C/C++编写,并非Java类,所以无法在Java中获取它的引用。
注意:
1.系统提供的类加载器有3种类型,但是系统提供的ClassLoader不止3个
2.并且,AppClassLoader的父类加载器为ExtClassLoader,不代表AppClassLoader继承自ExtClassLoader
ClassLoader的继承关系如图所示:
1.ClassLoader 是一个抽象类,定义了ClassLoader的主要功能
2.SecureClassLoader 继承自ClassLoader,但并不是ClassLoader的实现类,而是拓展并加入了权限管理方面的功能,增强了安全性
3.URLClassLoader 继承自SecureClassLoader 可通过URL路径从jar文件和文件夹中加载类和资源
4.ExtClassLoader和AppClassLoader都继承自URLClassLoader
ClassLoader的双亲委托机制
类加载器查找Class采用了双亲委托模式:
1.先判断该Class是否加载,如果没有则先委托父类加载器查找,并依次递归直到顶层的Bootstrap ClassLoader
2.如果Bootstrap ClassLoader找到了则返回该Class,否则依次向下查找
3.如果所有父类都没找到Class则调用自身findClass进行查找
双亲委托机制的优点:
1.避免重复加载
如果已经加载过Class则无需重复加载,只需要读取加载的Class即可
2.更加安全
保证无法使用自定义的类替代系统类,并且只有两个类名一致并且被同一个加载器加载的类才会被认为是同一个类
ClassLoader.loadClass方法源码如下(Java17)
1.注释1处 检查传入的类是否被加载, 如果已经加载则不执行后续代码
2.注释2处 若父类加载器不为null则调用父类loadClass方法加载Class
3.注释3处 如果父类加载器为null则调用findBootstrapClassOrNull方法
该方法内部调用了Native方法findBootstrapClass,最终用Bootstrap ClassLoader检查该类是否被加载
注意: 在Android中,该方法直接返回null,因为Android中没有BootstrapClassLoader
4.注释4处 调用自身的findClass查找类
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name);//1if (c == null) {longt0 = System.nanoTime();try {if (parent != null) { c = parent.loadClass(name, false);//2. } else { c = findBootstrapClassOrNull(name);//3 } } catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader }if (c == null) {// If still not found, then invoke findClass in order// to find the class.longt1 = System.nanoTime(); c = findClass(name);//4// this is the defining class loader; record the stats PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } }if (resolve) { resolveClass(c); }return c; } }
以上流程示意图如下
Android中的ClassLoader
Java中的ClassLoader可以加载jar和class文件(本质都是加载class文件)
而在Android中,无论DVM还是ART加载的文件都是dex文件,所以需要重新设计ClassLoader的相关类。
Android中的ClassLoader分为系统类加载器和自定义加载器:
◆系统类加载器
包括 BootClassLoader, PathClassLoader, DexClassLoader等
◆自定义加载器
通过继承BaseDexClassLoader实现,它们的继承关系如图所示:
各个ClassLoader的作用:
1.ClassLoader
抽象类,定义了ClassLoader的主要功能.
2.BootClassLoader
继承自ClassLoader,用于Android系统启动时预加载常用类.
3.SecureClassLoader
继承自ClassLoader扩展了类权限方面的功能,加强了安全性.
4.URLClassLoader
继承自SecureClassLoader,用于通过URL路径加载类和资源.
5.BaseDexClassLoader
继承自ClassLoader,是抽象类ClassLoader的具体实现类.
6.InMemoryDexClassLoader(Android8.0新增)
继承自BaseDexClassLoader,用于加载内存中的dex文件.
7.PathClassLoader
继承自BaseDexClassLoader,用于加载已安装的apk的dex文件.
8.DexClassLoader
继承自BaseDexClassLoader,用于加载已安装的apk的dex文件,以及从SD卡中加载未安装的apk的dex文件.
实现Android加固时,壳程序动态加载被保护程序的dex文件主要使用以下3个类加载器:
1.DexClassLoader 可以加载未安装apk的dex文件
它是一代加固——整体加固(落地加载)的核心之一
2.InMemoryDexClassLoader 可以加载内存中的dex文件
它是二代加固——整体加固(不落地加载)的核心之一
3.BaseDexClassLoader是ClassLoader的具体实现类
实际上DexClassLoader,PathClassLoader以及InMemoryDexClassLoader加载类时,均通过委托BaseDexClassLoader实现
ClassLoader加载Dex流程简介
Dex文件的加载依赖于前文提到的PathClassLoader,DexClassLoader和InMemoryDexClassLoader
加载Dex文件的功能均通过委托父加载器BaseDexClassLoader实现.其中PathClassLoader和DexClassLoader调用相同的BaseDexClassLoader构造函数,InMemoryDexClassLoader调用另一个构造函数。
最终通过ArtDexFileLoader::OpenCommon方法在ART虚拟机中创建DexFile::DexFile对象,该对象是Dex文件在内存中的表示,用于安卓程序运行时加载类以及执行方法代码,也是后续第三代加固——代码抽取加固,进行指令回填时的关键对象。
三种ClassLoader加载Dex文件的流程如下(基于Android10.0):
1.Java层
PathClassLoader和DexClassLoader委托BaseDexClassLoader最终执行JNI方法DexFile.openDexFileNative进入Native层.
而InMemoryDexClassLoader委托BaseDexClassLoader后则执行DexFile.openInMemoryDexFiles进入Native层.
2.Native层
PathClassLoader和DexClassLoader这条委托链会根据不同情况,调用ArtDexFileLoader::Open的不同重载,或者调用OpenOneDexFileFromZip.
InMemoryDexClassLoader调用ArtDexFileLoader::Open的第3种重载.
无论是调用哪个函数,最终都会调用ArtDexFileLoader::OpenCommon.
经过以上调用流程后进入ArtDexFileLoader::OpenCommon,经过DexFile的初始化和验证操作后便成功创建了DexFile对象:
创建DexFile对象后,Class对应的文件便被加载到ART虚拟机中
ClassLoader加载Class流程简介
前文通过ClassLoader.loadClass讲解了双亲委托机制,那么一个Class具体是如何被加载到JVM中的呢?
首先,继承自BaseDexClassLoader的3种ClassLoader调用自身loadClass方法时,委托父类查找,委托到ClassLoader.loadClass时返回。
BaseDexClassLoader.findClass调用DexPathList.findClass,其内部调用Element.findClass,最终调用DexFile.loadClassBinaryName进入DexFile中,该流程如图所示:
进入DexFile后,主要执行以下操作:
1.DexFile
通过JNI函数defineClassNative进入Native层
2.DexFile_defineClassNative
通过FindClassDef枚举DexFile的所有DexClassDef结构并使用ClassLinker::DefineClass创建对应的Class对象.
之后调用ClassLinker::InsertDexFileInToClassLoader将对应的DexFile对象添加到ClassLoader的ClassTable中.
3.ClassLinker::DefineClass
调用LoadField加载类的相关字段,之后调用LoadMethod加载方法,再调用LinkCode执行方法代码的链接
该流程如图所示:
综上所述,ClassLoader最终通过ClassLinker::DefineClass创建Class对象,并完成Field和Method的加载以及Code的链接。
调用链中有一个核心函数——ClassLinker::LoadMethod,通过该函数可以获取方法字节码在DexFile中的偏移值code_off,它是实现指令回填的核心之一。
LoadDexDemo
参考动态:加载示例
示例文件: attachmentsLoadDexDemo
经过前文的介绍,我们知道Android中可使用ClassLoader加载dex文件,并调用其保存的类的方法
创建空项目,编写一个测试类用于打印信息,编译后提取apk文件和该类所在的dex文件并推送至手机的tmp目录
package com.example.emptydemo;import android.util.Log;public class TestClass{public void print(){ Log.d("glass","com.example.emptydemo.print is called!"); }}
创建另一个项目,通过DexClassLoader加载apk和提取的dex文件并反射执行print方法
1.创建私有目录用于创建DexClassLoader:分别是odex和lib目录
2.创建DexClassLoader
3.加载指定类
4.反射加载并执行类的方法
package com.example.testdemo;import android.content.Context;import android.os.Bundle;import androidx.appcompat.app.AppCompatActivity;import java.io.File;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import dalvik.system.DexClassLoader;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);ContextappContext = getApplicationContext();//com.example.emptydemo loadDexClassAndExecuteMethod(appContext, "/data/local/tmp/classes3.dex"); //直接加载dex文件 loadDexClassAndExecuteMethod(appContext, "/data/local/tmp/EmptyDemo.apk"); //加载apk文件 本质还是加载dex }public void loadDexClassAndExecuteMethod(Context context, String strDexFilePath) {//1.创建优化私有目录app_opt_dex和app_lib_dex,用于ClassLoaderFileoptFile = context.getDir("opt_dex", 0);// /data/user/0/com.example.testdemo/app_opt_dexFilelibFile = context.getDir("lib_dex", 0);// /data/user/0/com.example.testdemo/app_lib_dex//2.创建ClassLoader用于加载Dex文件 依次指定dex文件路径 Odex目录 lib库目录 父类加载器DexClassLoaderdexClassLoader =new DexClassLoader( strDexFilePath, optFile.getAbsolutePath(), libFile.getAbsolutePath(), MainActivity.class.getClassLoader()); Class<?> clazz = null;try {//3.通过创建的DexClassLoader加载dex文件的指定类 clazz = dexClassLoader.loadClass("com.example.emptydemo.TestClass");if (clazz != null) {try {//3.反射获取并调用类的方法Objectobj = clazz.newInstance();Methodmethod = clazz.getDeclaredMethod("print"); method.invoke(obj); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
效果如下
DexClassLoader构造函数如下
/* 参数一: String dexPath, Dex文件路径 参数二: String optimizedDirectory, 优化后的dex即Odex目录 Android中内存中不会出现上述参数一的Dex文件, 会先优化,然后运行,优化后为.odex文件 参数三: String librarySearchPath, lib库搜索路径 参数四: ClassLoader parent, 父类加载器*/public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!"); }}
5
Android应用程序启动流程
Android应用程序启动流程相关知识是实现壳程序加载被保护程序的核心,只有了解该机制,才能理解壳程序的工作原理。
Android应用程序启动涉及4个进程: Zygote进程,Launcher进程,AMS所在进程(SystemServer进程),应用程序进程
总体流程如下:
1.点击桌面应用图标后,Launcher进程使用Binder通信,向SystemServer进程的AMS发起startActivity请求创建根Activity
2.AMS接收Launcher请求后,判断应用程序进程是否存在,不存在则通过Socket通信,调用zygoteSendArgsAndGetResult请求Zygote创建应用程序进程
3.Zygote进程接收AMS的Socket请求后,通过forkAndSpecialize创建应用程序进程,之后进行初始化工作
4.应用程序进程启动后,AMS通过ApplicationThread代理类与应用程序进程的ApplicationThread进行Binder通信,发送scheduleLaunchActivity请求
5.应用程序进程的Binder线程ApplicationThread接受请求后,通过sendMessage与主线程ActivityThread进行Handler通信,发送LAUNCH_ACTIVITY消息
6.主线程ActivityThread接收消息后,调用handleLaunchActivity,通过反射机制创建Activity实例后调用Activity.onCreate方法
调用Activity.onCreate后便进入应用程序的生命周期,至此,成功启动应用程序
以上流程的核心可总结为三个部分,后续将围绕这三部分展开:
1.创建应用程序进程
2.创建Application
3.启动根Activity
创建应用程序进程简介
要想启用一个Android应用程序,要保证该程序所需的应用程序进程存在
当点击应用程序图标后,触发Launcher.onClick方法,经过一系列方法调用后,在ActivityStackSupervisor.startSpecificActivityLocked中会判断当前应用程序进程是否存在
若不存在则调用AMS代理类ActivityManagerProxy的startProcessLocked请求AMS创建应用程序进程,AMS接收Launcher的请求后又向Zygote发送请求
这一过程可分为两部分:
◆AMS向Zygote发送启动应用程序进程的请求
◆Zygote接收AMS的请求并创建应用程序进程.
AMS向Zygote发送启动应用程序进程的请求的流程如下:
Launcher请求AMS,AMS请求Zygote,通过ZygoteInit.main进入Zygote
Zygote接收AMS的请求并创建应用程序进程的流程如下:
1.在进行一系列处理后进入ZygoteConnection.handleChildProc,该函数内部配置子进程的初始环境后并调用ZygoteInit.zygoteInit初始化应用程序进程
2.ZygoteInit.zygoteInit中调用ZygoteInit.nativeZygoteInit启动了应用程序进程的Binder线程池,使得进程可以进行Binder通讯,之后调用RuntimeInit.applicationInit
3.RuntimeInit调用invokeStaticMain并抛出Zygote.MethodAndArgsCaller异常,经过异常处理清空设置过程中的堆栈帧后,调用主线程管理类ActivityThread的main方法,至此,应用程序进程被成功创建
创建Application简介
ActivityThread.main进行部分初始化配置后,创建并启动主线程消息循环管理类Looper用于进行消息处理,并且调用attach方法通知AMS附加自身ApplicationThread类
AMS中通过ApplicationThread的代理类IApplicationThread发送BIND_APPLICATION消息到应用程序的消息队列
之后应用程序主线程管理类ActivityThread的handleMessage处理该消息,并调用handleBindApplication创建Application并绑定,主要执行以下4个步骤:
1.ActivityThread.getPackageInfoNoCheck
创建LoadedApk对象,设置ApplicationInfo和ClassLoader并加入mPackages中,该对象是应用程序apk包的管理类
2.ContextImpl.createAppContext
创建应用程序的上下文环境Context类
3.LoadedApk.makeApplication
创建Application,并调用Application.attachBaseContext方法
4.Instrumentation.callApplicationOnCreate
调用Application.onCreate方法
至此,Application创建完成,以上流程如图所示:
启动根Activity简介
经过以上步骤后,创建了应用程序进程和对应的Application,回到Launcher请求AMS过程,在ActivityStackSupervisor.startSpecificActivityLocked中调用了ActivityStackSupervisor.realStartActivityLocked,其内部通过调用ApplicationThread.scheduleLaunchActivity通知应用程序启动Activity
ApplicationThread通过sendMessage向H类(应用程序进程中主线程的消息管理类)发送H.LAUNCH_ACTIVITY消息,H.handleMessage接收到消息后调用ActivityThread.handleLaunchActivity将控制权转移给ActivityThread
handleLaunchActivity调用performLaunchActivity执行启动Activity的步骤:
1.获取Activity信息类ActivityInfo
2.获取Apk文件描述类LoadedApk
3.为Activity创建上下文环境
4.获取Activity完整类名并通过ClassLoader创建Activity实例
5.调用LoadedApk.makeApplication
这一步需要解释,Application类是Android的应用程序描述类,每个应用程序只有一个全局单例的Application.
此处调用makeApplication时,由于ActivityThread.main中已经创建Application,所以会直接返回已创建的Application.
而performLaunchActivity用于启动任意Activity,根Activity必定使用ActivityThread.main创建的Application,但其他Activity可能在子进程中运行,所以此处的调用主要用于处理一般Activity.
6.调用Activity.attach初始化Activity
7.调用Instrumentation.callActivityOnCreate启动Activity
以上流程如图所示:
经过以上步骤后,安卓应用程序正式启动(根Activity启动),其中最值得关注的便是ActivityThread,Application和LoadedApk类.
1.ActivityThread是安卓应用程序进程的主线程管理类
保存了很多关键信息,通过该类可以反射获取应用程序进程的Application和LoadedApk.
2.Application用于描述当前的应用程序
应用程序启动过程中,Application实例是最先创建的,早于应用程序启动以及任何Activity或Service,它的生命周期是整个应用程序中最长的,等于应用程序的生命周期
安卓应用程序开发时,可通过继承该类并重写attachBaseContext和onCreate方法进行部分资源的初始化配置.如果被保护程序没有指定自定义的Application,使用系统创建的默认Application即可;如果存在自定义的Application,则脱壳程序需要替换
3.LoadedApk用于描述当前应用程序对应的APK文件
其mClassLoader字段保存了当前APK对应的类加载器,mApplication字段保存了当前Application
6
Dex文件结构和代码抽取
关于Dex文件结构和解析器,由于篇幅限制此处不展开,可参考我的另一篇文章Dex文件结构-ReadDex解析器实现
在解析功能的基础之上,添加代码抽取功能,支持抽取指定dex文件的所有方法代码,并输出classes.dex置空指令后的.patched文件和字节码.codes文件
原理:
1.DexClassDef结构定义了一个ART虚拟机中的Class对象,其classDataOff字段,指向DexClassData结构.
2.DexClassData结构保存了类的数据,字段和方法的数据由4个按顺序连续排列的字段和方法数组保存,DexClassDataHeader结构的header保存了4个数组结构的大小.值得注意的是DexClassData除header之外的成员不一定有效,只有当header中指定了对应结构体数组的大小才有效.
3.DexMethod.codeOff字段指向了DexCode结构,是代码抽取的关键结构,该结构的insnsSize和insns字段分别保存了指令码的个数和指令码数组.
代码抽取的步骤如下:
1.遍历所有DexClassDef,通过classDataOff访问对应DexClassData结构.
2.对于每个DexClassData,解析header,确定DexField和DexMethod的大小,忽略DexField结构数组后解析DexMethod结构数组.
3.对于每个DexMethod结构,通过其codeOff字段访问DexCode的insnsSize和insns,执行代码抽取并将原始的insns内容设置为空指令NOP(字节码0x0000).
对应ReadDex项目中的关键函数如下:
1.extractAllClassDefCodes
功能: 抽取所有DexClassDef的代码.
逻辑: 遍历所有DexClassDef并过滤系统类,并解析对应DexClassData结构从而抽取代码.
2.extractClassDataCodes
功能: 抽取单个DexClassData的代码.
逻辑: 先通过readAndGetSizeUleb128读取获取DexClassDataHeader结构的staticFieldsSize,instanceFieldsSize,directMethodsSize和virtualMethodsSize.
再计算DexField数组占据的长度得到偏移值,最后访问DexMethod数组并进行代码抽取.
3.extractDexMethodArrayCodes
功能: 抽取DexMethod结构体数组的代码.
逻辑: 遍历DexMethod数组,对于每个DexMethod结构,通过DexFile基地址加codeOff得到DexCode的引用,之后根据insnsSize指定的大小抽取insns数组保存的指令字节码,每个DexCode抽取后保存为CodeItem并写入文件.自定义CodeItem结构用于后续代码回填时解析codes文件.
经过代码抽取后的Dex文件示意图如下:
详情可参考ReadDex项目,编译为ReadDex.exe后供加壳程序调用,通过-file参数指定dex文件路径,-extractAllCodes参数指定抽取所有代码。
7
一代加固-整体加固(落地加载)
一代加固是所有加固的基础,了解一代加固的基本思路有助于理解后续加固技术的演进
原理
主要涉及三个对象:
1.待加固的源程序APK
2.(脱)壳程序APK 负责加载,解密和执行被保护的源程序
3.加壳程序 负责解析源程序并利用壳程序对其进行加固保护,将壳程序和源程序合并为新的程序
加壳程序:
1.解包壳程序和源程序:得到壳程序的dex以及源程序的AndroidManifest.xml和资源文件等
2.复制源程序解包后的文件到新apk临时目录(忽略部分文件)
3.处理源程序AndroidManifest.xml 写入新apk临时目录
判断是否存在application标签的name属性指定了自定义Application
若存在则添加meta-data标签保存原始application
无论是否存在都要指定name值为壳的Application
4.合并壳dex和源程序apk 写入新apk临时目录
5.重打包新的Apk
6.对新Apk签名
7.删除临时目录
加壳后的壳Dex文件示意图如下
最终新APK中包括以下内容:
1.修改过的AndroidManifest.xml
2.由壳dex和源APK合并而来的classes.dex
3.源APK的所有其他资源文件(包括lib,assets,resources等)
(脱)壳程序:
1.在壳程序dex末尾追加源程序所有dex
2.在壳程序Application的attachBaseContext方法释放源程序所有dex并替换mClassLoader
3.在壳程序Application的onCreate方法注册源程序Application并开始生命周期
源程序
源程序即一般的用户程序,我们的主要目的是对源程序进行加固保护,所以需要注意源程序可能涉及到的技术点:
◆源程序自定义Application
Application.attachBaseContext是app进程真正的入口点
如果源程序没有使用自定义Application,则可以直接复用壳程序Application
如果源程序有自定义Application,必定在AndroidManifest.xml文件中由**<application android:name="">**标签声明指定,可以替换属性值为壳application,同时添加meta-data标签保存源程序application
◆Native
源程序使用NDK开发时会生成lib文件夹和对应so文件,需要进行处理
主要是创建lib库的释放文件夹,提供给新的ClassLoader
源程序主要包括以下文件:
1.MainActivity.java
2.MyApplication.java
3.native-lib.cpp
4.AndroidManifest.xml
5.activity_main.xml
注意关闭Multidex支持,保证只编译出一个Dex文件
MainActivity.java
主要功能:
1.Java层组件TextView
2.Native层JNI函数调用
注意点:
1.MainActivity继承自Activity类而非AppCompatActivity,这一步是为了方便资源处理
2.使用到了native函数,所以要处理lib文件
3.关闭MultiDex支持,只生成一个dex文件便于处理
package com.example.nativedemo;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.widget.TextView;public class MainActivity extends Activity {static { System.loadLibrary("nativedemo"); }public native String stringFromJNI();public native intadd(int x,int y);public native intsub(int x,int y);@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);// Example of a call to a native methodTextViewtv = findViewById(R.id.sample_text) ; String result=stringFromJNI()+" add(111,222)="+add(111,222)+" sub(999,333)="+sub(999,333); tv.setText(result); Log.d("glass","Run source MainActivity.onCreate "+this); }}
MyApplication.java
主要功能: log输出便于定位执行流程
注意:
1.程序执行的顺序为Application.attachBaseContext->Application.onCreate->MainActivity.onCreate
2.该类主要用于模拟存在自定义Application的情况
如果源程序不存在自定义Application则使用默认的Application即可,否则需要解析源Application并进行创建和替换
package com.example.nativedemo;import android.app.Application;import android.content.Context;import android.util.Log;public class MyApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base); Log.d("glass", "Run source MyApplication.attachBaseContext "+this); }@Overridepublic void onCreate() {super.onCreate(); Log.d("glass", "Run source MyApplication.onCreate "+this); }}
native-lib.cpp
定义了静态注册的方法stringFromJNI和动态注册的方法add,sub
#include <jni.h>#include <string>// 静态注册extern "C" JNIEXPORT jstring JNICALLJava_com_example_nativedemo_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++";return env->NewStringUTF(hello.c_str());}//native函数对应的java方法所属类staticconstchar *ClassName="com/example/nativedemo/MainActivity";jint add(JNIEnv* env,jobject obj,jint x,jint y){return x+y;}jint sub(JNIEnv* env,jobject obj,jint x,jint y){return x-y;}//定义本地方法数组 建立映射关系//JNINativeMethod结构体成员为函数名,函数签名,函数指针static JNINativeMethod methods[]={ {"add","(II)I",(void*)add}, {"sub","(II)I",(void*)sub}};//编写加载注册方法jint JNI_OnLoad(JavaVM* vm,void* reserved){ JNIEnv* env=nullptr;//获取JNIEnv对象if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK)return -1;//获取对应java方法所属的类对象 jclass clazz=env->FindClass(ClassName);//调用RegisterNatives注册方法if(clazz){ env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));return JNI_VERSION_1_6;//注意必须返回JNI版本 }elsereturn -1;}
AndroidManifest.xml
主要功能:
1.指定根activity为MainActivity
2.指定android:name为MyApplication
<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.NativeDemo"tools:targetApi="29"android:name=".MyApplication" ><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><actionandroid:name="android.intent.action.MAIN" /><categoryandroid:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
activity_main.xml
LinearLayout
<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"tools:context="com.example.nativedemo.MainActivity" ><TextViewandroid:id="@+id/sample_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"android:textAlignment="center" /></LinearLayout>
加壳程序
加壳程序主要需要完成以下工作:
1.解包壳程序和源程序
得到壳程序的dex以及源程序的AndroidManifest.xml和资源文件等
2.复制源程序解包后的文件到新apk临时目录(忽略部分文件)
3.处理源程序AndroidManifest.xml 写入新apk临时目录
判断是否存在application标签的name属性指定了自定义Application
若存在则添加meta-data标签保存原始application
无论是否存在都要指定name值为壳的Application
4.合并壳dex和源程序apk 写入新apk临时目录
5.重打包新的Apk
6.对新Apk签名
7.删除临时目录
FirstShell.py代码如下
1.封装Paths类,保存全局使用到的路径 主要是apk路径和临时目录
2.封装Apktool类,通过apktool提供apk解包和打包功能,通过uber-apk-signer提供apk签名功能
3.封装ManifeseEditor类,提供ManifestEditor解析功能,支持获取和修改标签属性,添加新标签
4.combineShellDexAndSrcApk 合并壳dex和源apk
将原apk加密后填充到壳dex后方,并添加4字节标识apk大小
新dex=壳dex+加密后的源APK+源APK大小
5.handleManifest 处理AndroidManifest
分别提取源apk和壳的manifest文件,以源manifest为基准
根据源apk是否指定自定义application确定是否添加meta-data标签保存
最后修改application:name为壳application
6.start 完整的处理函数
from zlib import adler32from hashlib import sha1from binascii import unhexlifyfrom lxml import etreeimport subprocessimport shutilfrom pathlib import Pathimport argparse# 封装path类,保存全局用到的路径class Paths:def __init__(self, srcApk:Path, shellApk:Path, outputApk:Path):# Apk file paths self.srcApkPath= srcApk.resolve() # 解析为绝对路径 self.shellApkPath= shellApk.resolve() self.newApkPath= outputApk.resolve()# Temp directories default python file path self.tmpdir= Path(__file__).parent/'temp' self.srcApkTempDir= self.tmpdir/ 'srcApkTemp' self.shellApkTempDir= self.tmpdir/'shellApkTemp' self.newApkTempDir= self.tmpdir/ 'newApkTemp'# Apktool类 提供解包,打包,签名功能class Apktool:def __init__(self): self.apktool_path= Path(__file__).parent/'tools/apktool/apktool.bat' self.signer_path=Path(__file__).parent/'tools/uber-apk-signer-1.3.0.jar'def signApk(self,unsignedApkPath:Path): self.runCommand(['java','-jar', self.signer_path, '--apk',unsignedApkPath])# 使用apktool解包apk 只解包资源不解包dexdef extractApk(self,apkPath:Path, outputDir:Path): self.runCommand([self.apktool_path, '-s', 'd' , apkPath, '-o', outputDir])# 重打包apkdef repackApk(self,inputDir:Path, outApk:Path): self.runCommand([self.apktool_path, 'b' , inputDir, '-o', outApk])def runCommand(self,args): subprocess.run(args,stdout=subprocess.DEVNULL) #仅调用工具,不需要输出,重定向stdout到os.devnull# 参数列表 捕获输出 输出转为字符串#print(subprocess.run(args, capture_output=True,text=True).stdout)# AndroidManifest.xml的editor 用于获取和修改标签属性,以及添加标签class ManifestEditor:def __init__(self, xml_content: bytes): self.ns = {'android': 'http://schemas.android.com/apk/res/android'} self.tree = etree.fromstring(xml_content)# 获取指定标签的android属性值 examples: get_attr('application', 'name') get_attr('activity', 'name')def getTagAttribute(self, tag_name: str, attr_name: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.get(f'{attr_name}') # 寻找标签的属性else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None:return elem.get(f'{{{self.ns["android"]}}}{attr_name}') # 根标签之外的属性位于android命名空间下return None# 设置指定标签的属性值 example:set_attr('application','name',"com.example.ProxyApplication")def setTagAttribute(self, tag_name: str, attr_name: str, new_value: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.set(f'{attr_name}', new_value) # 设置属性值else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None: elem.set(f'{{{self.ns["android"]}}}{attr_name}', new_value)return Truereturn False# 在指定父标签下添加新子标签 example: add_tag('application',"meta-data",{'name': 'android.permission.CAMERA','value':'hello'})def addTagWithAttributes(self, parent_tag: str, new_tag: str, attrs: dict):if parent_tag == 'manifest': parent = self.treeif parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): # 支持一次给添加的标签设置多个属性 new_elem.set(f'{k}', v)return Trueelse: parent = self.tree.find(f'.//{parent_tag}', namespaces=self.ns)if parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): new_elem.set(f'{{{self.ns["android"]}}}{k}', v)return Truereturn False# 不以壳manifest为基准操作则用不到该函数,以源apk的manifest为基准自带,无需额外设置def getMainActivity(self): activities = self.tree.findall('.//activity', namespaces=self.ns)for activity in activities: intent_filters = activity.findall('.//intent-filter', namespaces=self.ns)for intent_filter in intent_filters: action = intent_filter.find('.//action[@android:name="android.intent.action.MAIN"]', namespaces=self.ns) category = intent_filter.find('.//category[@android:name="android.intent.category.LAUNCHER"]', namespaces=self.ns)if action is not None and category is not None:return activity.get(f'{{{self.ns["android"]}}}name')return Nonedef getApplication(self):return self.getTagAttribute('application', 'name')def setApplication(self, application: str): self.setTagAttribute('application', 'name', application)def addMetaData(self, name: str, value: str): self.addTagWithAttributes('application', 'meta-data', {'name': name, 'value': value})def getManifestData(self):"""返回XML字符串"""return etree.tostring(self.tree, pretty_print=True, encoding='utf-8', xml_declaration=True).decode()# 合并壳dex和源apkdef combineShellDexAndSrcApk(sourceApkPath:Path, shellApkTempDir:Path, newApkTempDir:Path):def fixCheckSum(dexBytesArray):# dexfile[8:12]# 小端存储 value = adler32(bytes(dexBytesArray[12:])) valueArray = bytearray(value.to_bytes(4, 'little'))for i in range(len(valueArray)): dexBytesArray[8 + i] = valueArray[i]def fixSignature(dexBytesArray):# dexfile[12:32] sha_1 = sha1() sha_1.update(bytes(dexBytesArray[32:])) value = sha_1.hexdigest() valueArray = bytearray(unhexlify(value))for i in range(len(valueArray)): dexBytesArray[12 + i] = valueArray[i]def fixFileSize(dexBytesArray, fileSize):# dexfile[32:36]# 小端存储 fileSizeArray = bytearray(fileSize.to_bytes(4, "little"))for i in range(len(fileSizeArray)): dexBytesArray[32 + i] = fileSizeArray[i]def encrypto(file):for i in range(len(file)): file[i] ^= 0xffreturn file# 获取源apkwith open(sourceApkPath, 'rb') as f: SourceApkArray=bytearray(f.read())# 获取shelldexwith open(shellApkTempDir/'classes.dex', 'rb') as f: shellDexArray=bytearray(f.read()) SourceApkLen = len(SourceApkArray) shellDexLen = len(shellDexArray)# 新的dex文件长度 newDexLen = shellDexLen + SourceApkLen + 4# 加密源文件 enApkArray = encrypto(SourceApkArray)# 新的dex文件内容 = 壳dex + 加密的源apk + 四字节标识加密后源apk大小长度 newDexArray = shellDexArray + enApkArray + bytearray(SourceApkLen.to_bytes(4, 'little'))# 修改filesize fixFileSize(newDexArray, newDexLen)# 修改signature fixSignature(newDexArray)# 修改checksum fixCheckSum(newDexArray)# 导出文件with open(newApkTempDir/'classes.dex', 'wb') as f: f.write(newDexArray)# 提取源apk的Manifest文件,修改application为壳application(可能添加meta-data标签),输出新的Manifest文件def handleManifest( srcApkTempDir:Path,shellApkTempDir:Path,newApkTempDir:Path):# 从源apk提取AndroidManifest.xmlwith open(srcApkTempDir/'AndroidManifest.xml', 'r') as f: srcManifestEditor=ManifestEditor(f.read().encode()) srcApplication=srcManifestEditor.getApplication()# 从壳apk提取AndroidManifest.xmlwith open(shellApkTempDir/'AndroidManifest.xml', 'r') as f: shellManifestEditor=ManifestEditor(f.read().encode())print('ShellApplication:',shellManifestEditor.getApplication())# 修改源AndroidManifest.xml的application为壳的代理application srcManifestEditor.setApplication(shellManifestEditor.getApplication())# 写入meta-data标签 保存源apk的原始applicationif srcApplication != None:print('Source application:',srcApplication) srcManifestEditor.addMetaData('APPLICATION_CLASS_NAME',srcApplication)# 输出新的AndroidManifest.xmlwith open(newApkTempDir/'AndroidManifest.xml', 'w') as f: f.write(srcManifestEditor.getManifestData())def start(paths:Paths): apktool=Apktool()# 1.分别解包源文件和壳文件到临时目录print('Extracting source and shell apk...') apktool.extractApk(paths.srcApkPath,paths.srcApkTempDir)print('Extract source apk success!')print('Extracting shell apk...') apktool.extractApk(paths.shellApkPath,paths.shellApkTempDir)print('Extract shell apk success!')# 2.复制源apk所有文件到新apk临时目录中print('Copying source apk files to new apk temp dir...') shutil.copytree(paths.srcApkTempDir,paths.newApkTempDir )print('Copy source apk files success!')# 3.处理AndroidManifest.xmlprint('Handling AndroidManifest.xml...') handleManifest(paths.srcApkTempDir,paths.shellApkTempDir,paths.newApkTempDir)print('Handle AndroidManifest.xml success!')# 4.合并壳dex和源apk并导出文件print('Combining shell dex and source apk...') combineShellDexAndSrcApk(paths.srcApkPath,paths.shellApkTempDir,paths.newApkTempDir)print('Combine shell dex and source apk success!')# 5.重打包apkprint('Repacking apk...') apktool.repackApk(paths.newApkTempDir,paths.newApkPath)print('Repack apk success!')# 6.签名apkprint('Signing apk...') apktool.signApk(paths.newApkPath)print('Resign apk success!')# 7.删除临时目录print('Deleting temp directories...') shutil.rmtree(paths.tmpdir) # 删除临时目录print('Delete temp directories success!')def main(): parser = argparse.ArgumentParser(description="Android APK Packer") parser.add_argument('-src','--src-apk', required=True, type=Path, help='Path to source APK file') parser.add_argument('-shell','--shell-apk', required=True, type=Path, help='Path to shell APK file') parser.add_argument('-o','-out','--output-apk',type=Path,help='Output path for packed APK (Default: ./out/<src-apk>_protected.apk)') args = parser.parse_args()if args.output_apk == None: args.output_apk = Path('./out')/(args.src_apk.stem+'_protected.apk') # 默认新apk名称及输出路径 paths = Paths(args.src_apk, args.shell_apk, args.output_apk)print('Source APK:', paths.srcApkPath)print('Shell APK:', paths.shellApkPath)print('Output APK:', paths.newApkPath) start(paths)if __name__=="__main__": main()
脱壳程序
FirstProxyApplication.java
注意关闭Multidex支持,保证只生成一个Dex文件
attachBaseContext中执行以下操作
1.创建私有目录,用于保存释放的dex,lib,源apk
2.调用readDexFromApk,从壳apk中提取壳dex文件,保存为字节数组
3.调用extractSrcApkFromShellDex 从壳dex文件提取源程序apk文件 解包lib文件到lib私有目录
4.调用replaceClassLoader替换壳程序的ClassLoader
新的ClassLoader指定了源apk dex文件,lib文件,odex路径 也就是前面释放的源apk和源lib
onCreate调用了replaceApplication
1.判断manifest文件是否通过meta-data标签保存了源apk的application
如果源apk未指定application则使用默认的application(即壳applicaiton)
2.如果源apk指定了自定义application则创建对应实例,替换掉壳的application,之后调用onCreate方法
package com.example.androidshell;import android.app.Application;import android.app.Instrumentation;import android.content.Context;import android.content.pm.ApplicationInfo;import android.content.pm.PackageManager;import android.os.Bundle;import android.util.ArrayMap;import android.util.Log;import java.io.BufferedInputStream;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.lang.ref.WeakReference;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.ArrayList;import java.util.Iterator;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;import dalvik.system.DexClassLoader;public class FirstProxyApplication extends Application {private static final String TAG="glass";private String apkPath;private String dexPath;private String libPath;public void log(String message){Log.d(TAG,message);}@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base); log("FirstProxyApplication.attachBaseContext() running!");try {//1.创建私有目录,保存dex,lib和源apk 具体路径为data/user/0/<package_name>/app_tmp_dexFiledex = getDir("tmp_dex", MODE_PRIVATE);Filelib = getDir("tmp_lib", MODE_PRIVATE); dexPath = dex.getAbsolutePath(); libPath = lib.getAbsolutePath(); apkPath = dex.getAbsolutePath() + File.separator + "Source.apk"; log("dexPath: " + dexPath); log("libPath: " + libPath); log("apkPath: " + apkPath);// 根据文件路径创建File对象FileapkFile =new File(apkPath);// 只有首次运行时需要创建相关文件if (!apkFile.exists()) {// 根据路径创建文件 apkFile.createNewFile();//读取Classes.dex文件byte[] shellDexData = readDexFromApk();//从中分离出源apk文件 extractSrcApkFromShellDex(shellDexData); }//配置加载源程序的动态环境,即替换mClassLoader replaceClassLoader(); } catch (Exception e) { Log.getStackTraceString(e); } }// 从当前程序的apk读取dex文件并存储为字节数组private byte[] readDexFromApk() throws IOException {//1.获取当前应用程序的源码路径(apk),一般是data/app目录下,该目录用于存放用户安装的软件StringsourceDir =this.getApplicationInfo().sourceDir; log("this.getApplicationInfo().sourceDir: " +sourceDir);//2.创建相关输入流FileInputStreamfileInputStream =new FileInputStream(sourceDir);BufferedInputStreambufferedInputStream =new BufferedInputStream(fileInputStream);ZipInputStreamzipInputStream =new ZipInputStream(bufferedInputStream); //用于解析apk文件ByteArrayOutputStreambyteArrayOutputStream =new ByteArrayOutputStream(); //用于存放dex文件//3.遍历apk的所有文件并提取dex文件 ZipEntry zipEntry;while((zipEntry = zipInputStream.getNextEntry()) != null){ //存在下一个文件// 将classes.dex文件存储到bytearray中 壳dex和源apk合并后只保留一个dex便于处理if (zipEntry.getName().equals("classes.dex")){byte[] bytes = new byte[1024];int num;while((num = zipInputStream.read(bytes))!=-1){ //每次读取1024byte,返回读取到的byte数 byteArrayOutputStream.write(bytes,0, num); //存放到开辟的byteArrayOutputStream中 } } zipInputStream.closeEntry(); //关闭当前文件 } zipInputStream.close(); log("Read dex from apk succeed!");return byteArrayOutputStream.toByteArray(); //将读取到的dex文件以字节数组形式返回 }// 从壳dex文件中提取源apk并解析private void extractSrcApkFromShellDex(byte[] shellDexData) throws IOException {intshellDexLen = shellDexData.length;//开始解析dex文件//1.读取源apk的大小byte[] srcApkSizeBytes = new byte[4]; System.arraycopy(shellDexData, shellDexLen - 4, srcApkSizeBytes,0,4);intsrcApkSize =ByteBuffer.wrap(srcApkSizeBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();//转成bytebuffer,方便4 bytes转int 将bytes转成int,加壳时,长度按小端存储//2.读取源apkbyte[] sourceApkData = new byte[srcApkSize]; System.arraycopy(shellDexData, shellDexLen - srcApkSize - 4, sourceApkData, 0, srcApkSize);//注意减4//3.解密源apk sourceApkData = decrypt(sourceApkData);//写入新建的apk文件中Fileapkfile =new File(apkPath);try {FileOutputStreamapkfileOutputStream =new FileOutputStream(apkfile); apkfileOutputStream.write(sourceApkData); apkfileOutputStream.close(); }catch (IOException e){throw new IOException(e); }//分析源apk,取出so文件放入libPath目录中FileInputStreamfileInputStream =new FileInputStream(apkfile);BufferedInputStreambufferedInputStream =new BufferedInputStream(fileInputStream);ZipInputStreamzipInputStream =new ZipInputStream(bufferedInputStream); ZipEntry nextEntry;while ((nextEntry=zipInputStream.getNextEntry())!=null){Stringname = nextEntry.getName();if (name.startsWith("lib/") && name.endsWith(".so")){//获取文件名并创建相应文件 String[] nameSplit = name.split("/");StringsoFileStorePath = libPath + File.separator + nameSplit[nameSplit.length - 1];FilestoreFile =new File(soFileStorePath); storeFile.createNewFile();//读数据到相应so文件中FileOutputStreamfileOutputStream =new FileOutputStream(storeFile);byte[] bytes = new byte[1024];int num;while((num = zipInputStream.read(bytes))!=-1){ fileOutputStream.write(bytes,0,num); } fileOutputStream.flush(); fileOutputStream.close(); } zipInputStream.closeEntry(); } zipInputStream.close(); }// 解密函数private byte[] decrypt(byte[] data) {for (inti =0; i < data.length; i++){ data[i] ^= (byte) 0xff; }return data; }// 替换壳App的ClassLoader为源App的ClassLoaderprivate void replaceClassLoader() {//1.获取当前的classloaderClassLoaderclassLoader =this.getClassLoader(); log("Current ClassLoader: " + classLoader.toString()); log("Parent ClassLoader: " + classLoader.getParent().toString());//2.反射获取ActivityThreadObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread.sCurrentActivity: " + sCurrentActivityThreadObj.toString());//3.反射获取LoadedApk//获取当前ActivityThread实例的mPackages字段 类型为ArrayMap<String, WeakReference<LoadedApk>>, 里面存放了当前应用的LoadedApk对象ArrayMapmPackagesObj = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mPackages"); log( "mPackagesObj: " + mPackagesObj.toString());//获取mPackages中的当前应用包名StringcurrentPackageName =this.getPackageName(); log("currentPackageName: " + currentPackageName);// 获取loadedApk实例也有好几种,mInitialApplication mAllApplications mPackages// 通过包名获取当前应用的loadedApk实例WeakReferenceweakReference = (WeakReference) mPackagesObj.get(currentPackageName);ObjectloadedApkObj = weakReference.get(); log( "LoadedApk: " + loadedApkObj.toString());//4.替换ClassLoaderDexClassLoaderdexClassLoader =new DexClassLoader(apkPath,dexPath,libPath, classLoader.getParent()); //动态加载源程序的dex文件,以当前classloader的父加载器作为parent Reflection.setField("android.app.LoadedApk","mClassLoader",loadedApkObj,dexClassLoader); //替换当前loadedApk实例中的mClassLoader字段 log("New DexClassLoader: " + dexClassLoader); }//替换壳程序LoadedApk的Application为源程序Application,并调用其onCreate方法public booleanreplaceApplication(){// Application实例存在于: LoadedApk.mApplication// 以及ActivityThread的mInitialApplication和mAllApplications和mBoundApplication//判断源程序是否使用自定义Application 若使用则需要进行替换,若未使用则直接返回,使用壳的默认Application即可StringappClassName =null; //源程序的Application类名try {//获取AndroidManifest.xml 文件中的 <meta-data> 元素ApplicationInfoapplicationInfo = getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);BundlemetaData = applicationInfo.metaData;//获取xml文件声明的Application类if (metaData != null && metaData.containsKey("APPLICATION_CLASS_NAME")){ appClassName = metaData.getString("APPLICATION_CLASS_NAME"); } else { log("源程序中没有自定义Application");return false; //如果不存在直接返回,使用壳的application即可 } } catch (PackageManager.NameNotFoundException e) { log(Log.getStackTraceString(e)); }//源程序存在自定义application类,开始替换 log("Try to replace Application");//1.反射获取ActivityThread实例ObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread: " + sCurrentActivityThreadObj.toString());//2.获取并设置LoadedApk//获取mBoundApplication (AppBindData对象)ObjectmBoundApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mBoundApplication") ; log("mBoundApplication: "+mBoundApplicationObj.toString());//获取mBoundApplication.info (即LoadedApk)ObjectinfoObj = Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"info"); log( "LoadedApk: " + infoObj.toString());//把LoadedApk的mApplication设置为null,这样后续才能调用makeApplication() 否则由于已存在Application,无法进行替换 Reflection.setField("android.app.LoadedApk","mApplication",infoObj,null);//3.获取ActivityThread.mInitialApplication 即拿到旧的Application(对于要加载的Application来讲)ObjectmInitialApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mInitialApplication"); log("mInitialApplicationObj: " + mInitialApplicationObj.toString());//4.获取ActivityThread.mAllApplications并删除旧的application ArrayList<Application> mAllApplicationsObj = (ArrayList<Application>) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mAllApplications"); mAllApplicationsObj.remove(mInitialApplicationObj); log("mInitialApplication 从 mAllApplications 中移除成功");//5.重置相关类的Application类名 便于后续创建Application//获取LoadedApk.mApplicationInfoApplicationInfoapplicationInfo = (ApplicationInfo) Reflection.getField("android.app.LoadedApk",infoObj,"mApplicationInfo"); log( "LoadedApk.mApplicationInfo: " + applicationInfo.toString());//获取mBoundApplication.appInfoApplicationInfoappinfoInAppBindData = (ApplicationInfo) Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"appInfo"); log("ActivityThread.mBoundApplication.appInfo: " + appinfoInAppBindData.toString());//此处通过引用修改值,虽然后续没有使用,但是实际上是修改其指向的LoadedApk相关字段的值//设置两个appinfo的classname为源程序的application类名,以便后续调用makeApplication()创建源程序的application applicationInfo.className = appClassName; appinfoInAppBindData.className = appClassName; log("Source Application name: " + appClassName);//6.反射调用makeApplication方法创建源程序的applicationApplicationapplication = (Application) Reflection.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null}); //使用源程序中的application//Application app = (Application)ReflectionMethods.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{true,null}); //使用自定义的application 强制为系统默认 log("Create source Application succeed: "+application);//7.重置ActivityThread.mInitialApplication为新的Application Reflection.setField("android.app.ActivityThread","mInitialApplication",sCurrentActivityThreadObj,application); log("Reset ActivityThread.mInitialApplication by new Application succeed!");//8.ContentProvider会持有代理的Application,需要特殊处理一下ArrayMapmProviderMap = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mProviderMap"); log("ActivityThread.mProviderMap: " + mProviderMap);//获取所有provider,装进迭代器中遍历Iteratoriterator = mProviderMap.values().iterator();while(iterator.hasNext()){ObjectproviderClientRecord = iterator.next();//获取ProviderClientRecord.mLocalProvider,可能为空ObjectmLocalProvider = Reflection.getField("android.app.ActivityThread$ProviderClientRecord",providerClientRecord,"mLocalProvider") ;if(mLocalProvider != null){ log("ProviderClientRecord.mLocalProvider: " + mLocalProvider);//获取ContentProvider中的mContext字段,设置为新的Application Reflection.setField("android.content.ContentProvider","mContext",mLocalProvider,application); } } log( "Run Application.onCreate" ); application.onCreate(); //源程序,启动!return true; }@Overridepublic void onCreate() {super.onCreate(); log("ProxyApplication.onCreate() is running!");if(replaceApplication()) log("Replace application succeed!"); }}
Reflection.java
封装的反射类,用于获取/设置指定类的成员变量,调用指定类的方法
package com.example.androidshell;import android.util.Log;import java.lang.reflect.Field;import java.lang.reflect.Method;public class Reflection {private static final String TAG="glass";public static Object invokeStaticMethod(String class_name,String method_name,Class<?>[] parameterTypes,Object[] parameterValues){try {Class<?> clazz = Class.forName(class_name); //反射获取Class类对象Method method = clazz.getMethod(method_name,parameterTypes);//反射获取方法 method.setAccessible(true);//突破权限访问控制return method.invoke(null,parameterValues);//反射调用,静态方法无需指定所属实例,直接传参即可 }catch (Exception e){Log.d(TAG, e.toString());return null; } }public static Object invokeMethod(String class_name,String method_name,Object obj,Class<?>[] parameterTypes,Object[] parameterValues) {try {Class<?> clazz = Class.forName(class_name);Method method = clazz.getMethod(method_name,parameterTypes); method.setAccessible(true);//突破权限访问控制return method.invoke(obj,parameterValues);// 反射调用,动态方法需要指定所属实例 }catch (Exception e) {Log.d(TAG, e.toString());return null; } }public static Object getField(String class_name,Object obj,String field_name) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true);return field.get(obj); //获取实例字段,需要指定实例对象 }catch(Exception e) {Log.d(TAG, e.toString());return null; } }public static Object getStaticField(String class_name,String field_name) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true);return field.get(null); }catch (Exception e) {Log.d(TAG, e.toString());return null; } }public static void setField(String class_name,String field_name,Object obj,Object value) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true); field.set(obj,value); }catch (Exception e) {Log.d(TAG, e.toString()); } }public static void setStaticField(String class_name,String field_name,Object value) {try {Class<?> clazz = Class.forName(class_name);Field field = clazz.getDeclaredField(field_name); field.setAccessible(true); field.set(null,value); }catch (Exception e){Log.d(TAG, e.toString()); } }}
AndroidManifest.xml
通过name属性指定application
<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.AndroidShell"tools:targetApi="29"android:name="com.example.androidshell.FirstProxyApplication" ></application></manifest>
总结
整体加固-落地加载的核心思路是将源程序apk和壳dex进行合并,运行时动态解密并执行相关环境处理操作重新执行源apk的代码
优点: 易于实现
缺点: 由于文件落地释放,所以非常容易在文件系统中获取到源程序apk;两次加载源程序apk到内存,效率低
8
五一劳动节二代加固-整体加固(不落地加载)
原理
针对落地加载的部分问题,引申出了不落地加载的思路: 即直接加载内存中的dex字节码,避免释放文件
如何在内存中加载dex?Android 8及以上系统中,可以使用系统提供的InMemoryDexClassLoader实现内存加载,Android7及以下则需要手动实现。
另外需要针对第一代加固不支持Multidex进行优化:
源apk每个dex文件前添加4字节标识其大小,之后全部合并成一个文件再合并到壳dex,最后添加4字节标识文件总大小。
结构如下:
源程序
同一代加固,可开启Multidex支持,其他部分不变
加壳程序
主要改动如下:
1.调用combineShellAndSourceDexs合并壳dex和源dex
内部调用readAndCombineDexs读取并合并源apk的多个dex为一个文件
2.复制源apk文件时忽略Manifest和dex文件
shutil.copytree(paths.srcApkTempDir,paths.newApkTempDir,ignore
=
shutil.ignore_patterns(
'AndroidManifest.xml'
,
'classes*.dex'
))
3.ManifeseEditor
添加getEtractNativeLibs,获取application标签的android:extractNativeLibs属性值
添加resetExtractNativeLibs,重置extractNativeLibs=true 强制释放lib文件
4.handleManifest
判断源程序Manifest是否设置了android:extractNativeLibs="true",若为false(默认)则改为true
from zlib import adler32from hashlib import sha1from binascii import unhexlifyfrom lxml import etreeimport subprocessimport shutilfrom pathlib import Pathimport argparse# 封装path类,保存全局用到的路径class Paths:def __init__(self, srcApk:Path, shellApk:Path, outputApk:Path):# Apk file paths self.srcApkPath= srcApk.resolve() # 解析为绝对路径 self.shellApkPath= shellApk.resolve() self.newApkPath= outputApk.resolve()# Temp directories default python file path self.tmpdir= Path(__file__).parent/'temp' self.srcApkTempDir= self.tmpdir/ 'srcApkTemp' self.shellApkTempDir= self.tmpdir/'shellApkTemp' self.newApkTempDir= self.tmpdir/ 'newApkTemp'# Apktool类 提供解包,打包,签名功能class Apktool:def __init__(self): self.apktool_path= Path(__file__).parent/'tools/apktool/apktool.bat' self.signer_path=Path(__file__).parent/'tools/uber-apk-signer-1.3.0.jar'def signApk(self,unsignedApkPath:Path): self.runCommand(['java','-jar', self.signer_path, '--apk',unsignedApkPath])# 使用apktool解包apk 只解包资源不解包dexdef extractApk(self,apkPath:Path, outputDir:Path): self.runCommand([self.apktool_path, '-s', 'd' , apkPath, '-o', outputDir])# 重打包apkdef repackApk(self,inputDir:Path, outApk:Path): self.runCommand([self.apktool_path, 'b' , inputDir, '-o', outApk])def runCommand(self,args): subprocess.run(args,stdout=subprocess.DEVNULL) #仅调用工具,不需要输出,重定向stdout到os.devnull# 参数列表 捕获输出 输出转为字符串#print(subprocess.run(args, capture_output=True,text=True).stdout)# AndroidManifest.xml的editor 用于获取和修改标签属性,以及添加标签class ManifestEditor:def __init__(self, xml_content: bytes): self.ns = {'android': 'http://schemas.android.com/apk/res/android'} self.tree = etree.fromstring(xml_content)# 获取指定标签的android属性值 examples: get_attr('application', 'name') get_attr('activity', 'name')def getTagAttribute(self, tag_name: str, attr_name: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.get(f'{attr_name}') # 寻找标签的属性else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None:return elem.get(f'{{{self.ns["android"]}}}{attr_name}') # 根标签之外的属性位于android命名空间下return None# 设置指定标签的属性值 example:set_attr('application','name',"com.example.ProxyApplication")def setTagAttribute(self, tag_name: str, attr_name: str, new_value: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.set(f'{attr_name}', new_value) # 设置属性值else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None: elem.set(f'{{{self.ns["android"]}}}{attr_name}', new_value)return Truereturn False# 在指定父标签下添加新子标签 example: add_tag('application',"meta-data",{'name': 'android.permission.CAMERA','value':'hello'})def addTagWithAttributes(self, parent_tag: str, new_tag: str, attrs: dict):if parent_tag == 'manifest': parent = self.treeif parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): # 支持一次给添加的标签设置多个属性 new_elem.set(f'{k}', v)return Trueelse: parent = self.tree.find(f'.//{parent_tag}', namespaces=self.ns)if parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): new_elem.set(f'{{{self.ns["android"]}}}{k}', v)return Truereturn False# 不以壳manifest为基准操作则用不到该函数,以源apk的manifest为基准自带,无需额外设置def getMainActivity(self): activities = self.tree.findall('.//activity', namespaces=self.ns)for activity in activities: intent_filters = activity.findall('.//intent-filter', namespaces=self.ns)for intent_filter in intent_filters: action = intent_filter.find('.//action[@android:name="android.intent.action.MAIN"]', namespaces=self.ns) category = intent_filter.find('.//category[@android:name="android.intent.category.LAUNCHER"]', namespaces=self.ns)if action is not None and category is not None:return activity.get(f'{{{self.ns["android"]}}}name')return Nonedef getApplication(self):return self.getTagAttribute('application', 'name')def setApplication(self, application: str): self.setTagAttribute('application', 'name', application)def addMetaData(self, name: str, value: str): self.addTagWithAttributes('application', 'meta-data', {'name': name, 'value': value})def getManifestData(self):"""返回XML字符串"""return etree.tostring(self.tree, pretty_print=True, encoding='utf-8', xml_declaration=True).decode()def getEtractNativeLibs(self):"""返回是否释放so文件"""return self.getTagAttribute('application', 'extractNativeLibs')def resetExtractNativeLibs(self):"""重置etractNativeLibs属性为true""" self.setTagAttribute('application', 'extractNativeLibs', 'true')# 合并壳dex和源apk的dexdef combineShellAndSourceDexs(shellApkTempDir:Path,srcApkTempDir:Path,newApkTempDir:Path):def fixCheckSum(dexBytesArray):# dexfile[8:12]# 小端存储 value = adler32(bytes(dexBytesArray[12:])) valueArray = bytearray(value.to_bytes(4, 'little'))for i in range(len(valueArray)): dexBytesArray[8 + i] = valueArray[i]def fixSignature(dexBytesArray):# dexfile[12:32] sha_1 = sha1() sha_1.update(bytes(dexBytesArray[32:])) value = sha_1.hexdigest() valueArray = bytearray(unhexlify(value))for i in range(len(valueArray)): dexBytesArray[12 + i] = valueArray[i]def fixFileSize(dexBytesArray, fileSize):# dexfile[32:36]# 小端存储 fileSizeArray = bytearray(fileSize.to_bytes(4, "little"))for i in range(len(fileSizeArray)): dexBytesArray[32 + i] = fileSizeArray[i]def encrypto(file):for i in range(len(file)): file[i] ^= 0xffreturn filedef readAndCombineDexs(unpackedApkDir:Path):# 读取解包后的apk的所有dex文件,并合并为一个dex文件 combinedDex = bytearray()# glob方法返回包含所有匹配文件的生成器for dex in unpackedApkDir.glob('classes*.dex'):print('Source Apk Dex file:', dex)with open(dex, 'rb') as f: data = bytearray(f.read()) combinedDex+=bytearray(len(data).to_bytes(4, 'little')) # dex文件的长度,小端序 combinedDex+=data # dex文件内容return combinedDex# 获取shelldexwith open(shellApkTempDir/'classes.dex', 'rb') as f: shellDexArray=bytearray(f.read())# 获取源apk的dex文件 srcDexArray = readAndCombineDexs(srcApkTempDir)# 新的dex文件长度 newDexLen = len(srcDexArray) + len(shellDexArray) + 4# 加密源文件 encSrcDexArray = encrypto(srcDexArray)# 新的dex文件内容 = 壳dex + 加密的源dex + 四字节标识加密后源dex大小长度 newDexArray = shellDexArray + encSrcDexArray + bytearray(len(encSrcDexArray).to_bytes(4, 'little'))# 修改filesize fixFileSize(newDexArray, newDexLen)# 修改signature fixSignature(newDexArray)# 修改checksum fixCheckSum(newDexArray)# 导出文件with open(newApkTempDir/'classes.dex', 'wb') as f: f.write(newDexArray)# 提取源apk的Manifest文件,修改application为壳application(可能添加meta-data标签),输出新的Manifest文件def handleManifest( srcApkTempDir:Path,shellApkTempDir:Path,newApkTempDir:Path):# 从源apk提取AndroidManifest.xmlwith open(srcApkTempDir/'AndroidManifest.xml', 'r') as f: srcManifestEditor=ManifestEditor(f.read().encode()) srcApplication=srcManifestEditor.getApplication() srcExtractNativeLibs=srcManifestEditor.getEtractNativeLibs()print('SourceApplication:',srcApplication)print('SourceExtractNativeLibs:',srcExtractNativeLibs)# 从壳apk提取AndroidManifest.xmlwith open(shellApkTempDir/'AndroidManifest.xml', 'r') as f: shellManifestEditor=ManifestEditor(f.read().encode())print('ShellApplication:',shellManifestEditor.getApplication())# 修改源AndroidManifest.xml的application为壳的代理application srcManifestEditor.setApplication(shellManifestEditor.getApplication())# 写入meta-data标签 保存源apk的原始applicationif srcApplication != None:print('Source application:',srcApplication) srcManifestEditor.addMetaData('APPLICATION_CLASS_NAME',srcApplication)# 如果源apk的manifest中默认设置etractNativeLibs=false,则重置为true,确保释放lib文件if srcExtractNativeLibs=='false': srcManifestEditor.resetExtractNativeLibs() # 输出新的AndroidManifest.xmlwith open(newApkTempDir/'AndroidManifest.xml', 'w') as f: f.write(srcManifestEditor.getManifestData())def start(paths:Paths): apktool=Apktool()# 1.分别解包源文件和壳文件到临时目录print('Extracting source and shell apk...') apktool.extractApk(paths.srcApkPath,paths.srcApkTempDir)print('Extract source apk success!')print('Extracting shell apk...') apktool.extractApk(paths.shellApkPath,paths.shellApkTempDir)print('Extract shell apk success!')# 2.复制源apk所有文件到新apk临时目录中,忽略源dex和manifest文件print('Copying source apk files to new apk temp dir...') shutil.copytree(paths.srcApkTempDir,paths.newApkTempDir,ignore=shutil.ignore_patterns('AndroidManifest.xml','classes*.dex')) print('Copy source apk files success!')# 3.处理AndroidManifest.xmlprint('Handling AndroidManifest.xml...') handleManifest(paths.srcApkTempDir,paths.shellApkTempDir,paths.newApkTempDir)print('Handle AndroidManifest.xml success!')# 4.合并壳dex和源apk并导出文件print('Combining shell dex and source dexs...') combineShellAndSourceDexs(paths.shellApkTempDir,paths.srcApkTempDir,paths.newApkTempDir)print('Combine shell dex and source dexs success!')# 5.重打包apkprint('Repacking apk...') apktool.repackApk(paths.newApkTempDir,paths.newApkPath)print('Repack apk success!')# 6.签名apkprint('Signing apk...') apktool.signApk(paths.newApkPath)print('Resign apk success!')# 7.删除临时目录print('Deleting temp directories...') shutil.rmtree(paths.tmpdir) # 删除临时目录print('Delete temp directories success!')def main(): parser = argparse.ArgumentParser(description="Android APK Packer") parser.add_argument('-src','--src-apk', required=True, type=Path, help='Path to source APK file') parser.add_argument('-shell','--shell-apk', required=True, type=Path, help='Path to shell APK file') parser.add_argument('-o','-out','--output-apk',type=Path,help='Output path for packed APK (Default: ./out/<src-apk>_protected.apk)') args = parser.parse_args()if args.output_apk == None: args.output_apk = Path('./out')/(args.src_apk.stem+'_protected.apk') # 默认新apk名称及输出路径 paths = Paths(args.src_apk, args.shell_apk, args.output_apk)print('Source APK:', paths.srcApkPath)print('Shell APK:', paths.shellApkPath)print('Output APK:', paths.newApkPath) start(paths)if __name__=="__main__": main()
脱壳程序
SecondProxyApplication.java相比FirstProxyApplication.java改动如下:
1.attachBaseContext
读取壳dex文件后,提取源程序dex文件,之后替换ClassLoader
没有设置私有目录和释放文件等操作
2.extractDexFilesFromShellDex
替代splitSourceApkFromShellDex,从壳dex提取源程序dex文件并存储为ByteBuffer[]
3.replaceClassLoader
一代加固使用DexClassLoader从文件加载,二代加固使用InMemoryDexClassLoader
注意:
◆在Android 8.0以下不支持InMemoryDexClassLoader,需要手动实现
◆在Android 10.0以下不支持InMemoryDexClassLoader指定lib目录的重载
默认搜索路径为nativeLibraryDirectories=[/system/lib64, /product/lib64]]]
可参考以下文章修复lib目录的问题
https://blog.csdn.net/q610098308/article/details/105246355
http://www.yxhuang.com/2020/03/28/android-so-load/
◆若源程序设置了android:extractNativeLibs="false",则不会释放lib文件到文件系统,而是直接映射apk文件的lib数据
如果遇到这种情况则需要手动处理,模拟映射加载so的操作,背后的逻辑比较复杂,并且需要考虑兼容性
为了简化处理可以将加壳后apk的android:extractNativeLibs属性改为true,强行指定释放lib
package com.example.androidshell;import android.app.Application;import android.app.Instrumentation;import android.content.Context;import android.content.pm.ApplicationInfo;import android.content.pm.PackageManager;import android.os.Build;import android.os.Bundle;import android.util.ArrayMap;import android.util.Log;import java.io.BufferedInputStream;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.IOException;import java.lang.ref.WeakReference;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.ArrayList;import java.util.Iterator;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;import dalvik.system.InMemoryDexClassLoader;public class SecondProxyApplication extends Application {private final String TAG="glass";public void log(String message){Log.d(TAG,message);}@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base); log("SecondProxyApplication.attachBaseContext is running!");try {byte[] shellDexData = readDexFromApk(); log("成功从源APK中读取classes.dex");//从中分理出源dex文件 ByteBuffer[] byteBuffers = extractDexFilesFromShellDex(shellDexData); log("成功分离出源dex集合");//配置加载源程序的动态环境,即替换mClassLoader replaceClassLoader(byteBuffers); } catch (Exception e) { log( Log.getStackTraceString(e)); } }@Overridepublic void onCreate() {super.onCreate(); log("SecondProxyApplication.onCreate is running!");if(replaceApplication()) log("替换Application成功"); }// 从壳dex文件中提取源apk的dex并封装为ByteBufferprivate ByteBuffer[] extractDexFilesFromShellDex(byte[] shellDexData) throws IOException {intshellDexlength = shellDexData.length;//开始解析dex文件byte[] sourceDexsSizeByte = new byte[4];//读取源dexs的大小 System.arraycopy(shellDexData,shellDexlength - 4, sourceDexsSizeByte,0,4);//转成bytebuffer,方便4byte转intByteBufferwrap = ByteBuffer.wrap(sourceDexsSizeByte);//将byte转成int, 加壳时,长度按小端存储intsourceDexsSizeInt = wrap.order(ByteOrder.LITTLE_ENDIAN).getInt(); Log.d(TAG, "源dex集合的大小: " + sourceDexsSizeInt);//读取源dexsbyte[] sourceDexsData = new byte[sourceDexsSizeInt]; System.arraycopy(shellDexData,shellDexlength - sourceDexsSizeInt - 4, sourceDexsData, 0, sourceDexsSizeInt);//解密源dexs sourceDexsData = decrypt(sourceDexsData);//更新部分//从源dexs中分离dex ArrayList<byte[]> sourceDexList = new ArrayList<>();intpos =0;while(pos < sourceDexsSizeInt){//先提取四个字节,描述当前dex的大小//开始解析dex文件byte[] singleDexSizeByte = new byte[4];//读取源dexs的大小 System.arraycopy(sourceDexsData, pos, singleDexSizeByte,0,4);//转成bytebuffer,方便4byte转intByteBuffersingleDexwrap = ByteBuffer.wrap(singleDexSizeByte);intsingleDexSizeInt = singleDexwrap.order(ByteOrder.LITTLE_ENDIAN).getInt(); Log.d(TAG, "当前singleDex的大小: " + singleDexSizeInt);//读取单独dexbyte[] singleDexData = new byte[singleDexSizeInt]; System.arraycopy(sourceDexsData,pos + 4, singleDexData, 0, singleDexSizeInt);//加入到dexlist中 sourceDexList.add(singleDexData);//更新pos pos += 4 + singleDexSizeInt; }//将dexlist包装成ByteBufferintdexNum = sourceDexList.size(); Log.d(TAG, "源dex的数量: " + dexNum); ByteBuffer[] dexBuffers = new ByteBuffer[dexNum];for (inti =0; i < dexNum; i++){ dexBuffers[i] = ByteBuffer.wrap(sourceDexList.get(i)); }return dexBuffers; }// 从apk读取dex文件并返回dex对应字节数组// 从当前程序的apk读取dex文件并存储为字节数组private byte[] readDexFromApk() throws IOException {//1.获取当前应用程序的源码路径(apk),一般是data/app目录下,该目录用于存放用户安装的软件StringsourceDir =this.getApplicationInfo().sourceDir; log("this.getApplicationInfo().sourceDir: " +sourceDir);//2.创建相关输入流FileInputStreamfileInputStream =new FileInputStream(sourceDir);BufferedInputStreambufferedInputStream =new BufferedInputStream(fileInputStream);ZipInputStreamzipInputStream =new ZipInputStream(bufferedInputStream); //用于解析apk文件ByteArrayOutputStreambyteArrayOutputStream =new ByteArrayOutputStream(); //用于存放dex文件//3.遍历apk的所有文件并提取dex文件 ZipEntry zipEntry;while((zipEntry = zipInputStream.getNextEntry()) != null){ //存在下一个文件// 将classes.dex文件存储到bytearray中 壳dex和源apk合并后只保留一个dex便于处理if (zipEntry.getName().equals("classes.dex")){byte[] bytes = new byte[1024];int num;while((num = zipInputStream.read(bytes))!=-1){ //每次读取1024byte,返回读取到的byte数 byteArrayOutputStream.write(bytes,0, num); //存放到开辟的byteArrayOutputStream中 } } zipInputStream.closeEntry(); //关闭当前文件 } zipInputStream.close(); log("Read dex from apk succeed!");return byteArrayOutputStream.toByteArray(); //将读取到的dex文件以字节数组形式返回 }private byte[] decrypt(byte[] sourceApkdata) {for (inti =0; i < sourceApkdata.length; i++){ sourceApkdata[i] ^= (byte) 0xff; }return sourceApkdata; }//替换壳程序LoadedApk的Application为源程序Application,并调用其onCreate方法public booleanreplaceApplication(){// Application实例存在于: LoadedApk.mApplication// 以及ActivityThread的mInitialApplication和mAllApplications和mBoundApplication//判断源程序是否使用自定义Application 若使用则需要进行替换,若未使用则直接返回,使用壳的默认Application即可StringappClassName =null; //源程序的Application类名try {//获取AndroidManifest.xml 文件中的 <meta-data> 元素ApplicationInfoapplicationInfo = getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);BundlemetaData = applicationInfo.metaData;//获取xml文件声明的Application类if (metaData != null && metaData.containsKey("APPLICATION_CLASS_NAME")){ appClassName = metaData.getString("APPLICATION_CLASS_NAME"); } else { log("源程序中没有自定义Application");return false; //如果不存在直接返回,使用壳的application即可 } } catch (PackageManager.NameNotFoundException e) { log(Log.getStackTraceString(e)); }//源程序存在自定义application类,开始替换 log("Try to replace Application");//1.反射获取ActivityThread实例ObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread: " + sCurrentActivityThreadObj.toString());//2.获取并设置LoadedApk//获取mBoundApplication (AppBindData对象)ObjectmBoundApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mBoundApplication") ; log("mBoundApplication: "+mBoundApplicationObj.toString());//获取mBoundApplication.info (即LoadedApk)ObjectinfoObj = Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"info"); log( "LoadedApk: " + infoObj.toString());//把LoadedApk的mApplication设置为null,这样后续才能调用makeApplication() 否则由于已存在Application,无法进行替换 Reflection.setField("android.app.LoadedApk","mApplication",infoObj,null);//3.获取ActivityThread.mInitialApplication 即拿到旧的Application(对于要加载的Application来讲)ObjectmInitialApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mInitialApplication"); log("mInitialApplicationObj: " + mInitialApplicationObj.toString());//4.获取ActivityThread.mAllApplications并删除旧的application ArrayList<Application> mAllApplicationsObj = (ArrayList<Application>) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mAllApplications"); mAllApplicationsObj.remove(mInitialApplicationObj); log("mInitialApplication 从 mAllApplications 中移除成功");//5.重置相关类的Application类名 便于后续创建Application//获取LoadedApk.mApplicationInfoApplicationInfoapplicationInfo = (ApplicationInfo) Reflection.getField("android.app.LoadedApk",infoObj,"mApplicationInfo"); log( "LoadedApk.mApplicationInfo: " + applicationInfo.toString());//获取mBoundApplication.appInfoApplicationInfoappinfoInAppBindData = (ApplicationInfo) Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"appInfo"); log("ActivityThread.mBoundApplication.appInfo: " + appinfoInAppBindData.toString());//此处通过引用修改值,虽然后续没有使用,但是实际上是修改其指向的LoadedApk相关字段的值//设置两个appinfo的classname为源程序的application类名,以便后续调用makeApplication()创建源程序的application applicationInfo.className = appClassName; appinfoInAppBindData.className = appClassName; log("Source Application name: " + appClassName);//6.反射调用makeApplication方法创建源程序的applicationApplicationapplication = (Application) Reflection.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null}); //使用源程序中的application//Application app = (Application)ReflectionMethods.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{true,null}); //使用自定义的application 强制为系统默认 log("Create source Application succeed: "+application);//7.重置ActivityThread.mInitialApplication为新的Application Reflection.setField("android.app.ActivityThread","mInitialApplication",sCurrentActivityThreadObj,application); log("Reset ActivityThread.mInitialApplication by new Application succeed!");//8.ContentProvider会持有代理的Application,需要特殊处理一下ArrayMapmProviderMap = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mProviderMap"); log("ActivityThread.mProviderMap: " + mProviderMap);//获取所有provider,装进迭代器中遍历Iteratoriterator = mProviderMap.values().iterator();while(iterator.hasNext()){ObjectproviderClientRecord = iterator.next();//获取ProviderClientRecord.mLocalProvider,可能为空ObjectmLocalProvider = Reflection.getField("android.app.ActivityThread$ProviderClientRecord",providerClientRecord,"mLocalProvider") ;if(mLocalProvider != null){ log("ProviderClientRecord.mLocalProvider: " + mLocalProvider);//获取ContentProvider中的mContext字段,设置为新的Application Reflection.setField("android.content.ContentProvider","mContext",mLocalProvider,application); } } log( "Run Application.onCreate" ); application.onCreate(); //源程序,启动!return true; }// 替换壳App的ClassLoader为源App的ClassLoaderprivate void replaceClassLoader(ByteBuffer[] byteBuffers) throws Exception{//1.获取当前的classloaderClassLoaderclassLoader =this.getClassLoader(); log("Current ClassLoader: " + classLoader.toString()); log("Parent ClassLoader: " + classLoader.getParent().toString());//2.反射获取ActivityThreadObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread.sCurrentActivity: " + sCurrentActivityThreadObj.toString());//3.反射获取LoadedApk//获取当前ActivityThread实例的mPackages字段 类型为ArrayMap<String, WeakReference<LoadedApk>>, 里面存放了当前应用的LoadedApk对象ArrayMapmPackagesObj = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mPackages"); log( "mPackagesObj: " + mPackagesObj.toString());//获取mPackages中的当前应用包名StringcurrentPackageName =this.getPackageName(); log("currentPackageName: " + currentPackageName);// 获取loadedApk实例也有好几种,mInitialApplication mAllApplications mPackages// 通过包名获取当前应用的loadedApk实例WeakReferenceweakReference = (WeakReference) mPackagesObj.get(currentPackageName);ObjectloadedApkObj = weakReference.get(); log( "LoadedApk: " + loadedApkObj.toString());//动态加载源程序的dex文件if(Build.VERSION.SDK_INT>=29){ Log.d(TAG,"Library path:"+this.getApplicationInfo().nativeLibraryDir); InMemoryDexClassLoader dexClassLoader=new InMemoryDexClassLoader(byteBuffers,this.getApplicationInfo().nativeLibraryDir,classLoader.getParent()); Log.d(TAG, "New InMemoryDexClassLoader: " + dexClassLoader);//替换当前loadedApk实例中的mClassLoader字段 Reflection.setField("android.app.LoadedApk","mClassLoader",loadedApkObj,dexClassLoader); }else{ Log.d(TAG,"不支持Android 8.0以下版本"); } }}
总结
二代加固使用不落地加载技术,可在内存中直接加载dex,实际上是对落地加载的更新
第一代和第二代加固统称为整体加固,在部分资料中将他们合称为一代加固,将代码抽取加固称为二代加固
优点: 相对一代加固更加高效,不容易从文件系统获取dex文件从而得到关键代码
缺点: 兼容性问题: 源程序的libso处理起来比较复杂;低版本需要手动实现InMemoryDexClassLoader
9
三代加固-抽取加固
基于整体加固遇到的部分问题,引申出了三代加固: 代码抽取加固
思路: 将关键方法的指令抽空,替换为nop指令,运行时动态回填指令执行, 回填的核心是对Android系统核心函数进行hook
1.Hook点
常用的Hook点有ClassLinker::LoadMethod和ClassLinker::DefineClass,二者都可以获取DexFile对象
LoadMethod: 可获取Method对象,从而获取codeOff,方便回填处理,但兼容性差
DefineClass: 可获取ClassDef对象,需要解析得到codeOff,更复杂但兼容性好,变化不大
2.如何抽取代码(参考前文Dex文件结构和代码抽取部分)
Dex文件结构中DexClassDef结构定义了各个类的信息,其中的DexClassData结构记录了类的字段和方法数据
方法由DexMethod结构保存,其codeOff成员保存了方法的字节码数据在文件中的偏移,根据该偏移可以进行抽取
LoadMethod声明如下
//Android 7及以前void LoadMethod(Thread* self,const DexFile& dex_file,const ClassDataItemIterator& it, Handle<mirror::Class> klass, ArtMethod* dst)//Android 8-9void LoadMethod(const DexFile& dex_file,const ClassDataItemIterator& it, Handle<mirror::Class> klass, ArtMethod* dst)//Android 10-14void LoadMethod(const DexFile& dex_file,const ClassAccessor::Method& method, Handle<mirror::Class> klass, ArtMethod* dst)//Android 15void LoadMethod(const DexFile& dex_file,const ClassAccessor::Method& method, ObjPtr<mirror::Class> klass,/*inout*/ MethodAnnotationsIterator* mai,/*out*/ ArtMethod* dst)
DefineClass声明如下
// Android 5.0 Level 21 及之前使用该声明staticvoid* (*g_originDefineClassV21)(void* thiz,constchar* descriptor,void* class_loader,constvoid* dex_file,constvoid* dex_class_def);/*//原始声明mirror::Class* DefineClass(const char* descriptor, Handle<mirror::ClassLoader> class_loader, const DexFile& dex_file, const DexFile::ClassDef& dex_class_def)*/// Android 5.1-14 Level22-34使用该声明staticvoid* (*g_originDefineClassV22)(void* thiz,void* self,constchar* descriptor,size_t hash,void* class_loader,constvoid* dex_file,constvoid* dex_class_def);/*//原始声明ObjPtr<mirror::Class> DefineClass(Thread* self, const char* descriptor, size_t hash, Handle<mirror::ClassLoader> class_loader, const DexFile& dex_file, const dex::ClassDef& dex_class_def)*///Android 15 Level 35 以后使用该声明staticvoid* (*g_originDefineClassV35)(void* thiz,void* self,constchar* descriptor,size_t descriptor_length,size_t hash,void* class_loader,constvoid* dex_file,constvoid* dex_class_def);//原始声明/*ObjPtr<mirror::Class> DefineClass(Thread* self, const char* descriptor, size_t descriptor_length, size_t hash, Handle<mirror::ClassLoader> class_loader, const DexFile& dex_file, const dex::ClassDef& dex_class_def)*/
原理
源程序
主要包括3个技术点(同上):
1.Native层NDK开发
2.Multidex
3.自定义Application
加壳程序
加壳程序主要分为3个模块
1.Apk处理模块: 提供解包Apk,打包Apk,签名Apk的功能
2.Xml处理模块: 提供读取标签,修改标签,添加标签的功能
3.Dex处理模块: 提供合并多Dex,加密Dex,代码抽取,文件修复的功能
加壳程序工作基本流程图如下
加壳程序工作流程总览图如下:
相对于SecondShell.py的改动如下:
1.start
解包源apk后调用extractAllDexFiles抽取所有dex文件的代码
另外复制了壳apk的lib库到新apk的临时目录(hook和代码回填逻辑在native层)
2.extractAllDexFiles
遍历指定目录的所有dex文件,调用ReadDex抽取代码,得到对应的.patched和.codes文件
修复patch后的dex文件并覆写原dex文件,将codes移动到assets目录下
from zlib import adler32from hashlib import sha1from binascii import unhexlifyfrom lxml import etreeimport subprocessimport shutilfrom pathlib import Pathimport argparse# Paths类,管理全局用到的路径class Paths:def __init__(self, srcApk:Path, shellApk:Path, outputApk:Path):# 相关APK文件路径 self.srcApkPath= srcApk.resolve() # 解析为绝对路径 self.shellApkPath= shellApk.resolve() self.newApkPath= outputApk.resolve()# 临时目录 以该脚本文件父目录为根目录 self.tmpdir= Path(__file__).parent/'temp' self.srcApkTempDir= self.tmpdir/ 'srcApkTemp' self.shellApkTempDir= self.tmpdir/'shellApkTemp' self.newApkTempDir= self.tmpdir/ 'newApkTemp'# Apktool类,通过subprocess调用其他工具 提供解包,打包,签名,抽取代码功能class Apktool:def __init__(self): self.apktool_path= Path(__file__).parent/'tools/apktool/apktool.bat' self.signer_path=Path(__file__).parent/'tools/uber-apk-signer-1.3.0.jar' self.readDex_path=Path(__file__).parent/'tools/ReadDex.exe'# 为apk签名def signApk(self,unsignedApkPath:Path): self.runCommand(['java','-jar', self.signer_path, '--apk',unsignedApkPath])# 使用apktool解包apk 只解包资源得到AndroidManifest.xml 不需要解包dex文件得到smali def unpackApk(self,apkPath:Path, outputDir:Path): self.runCommand([self.apktool_path, '-s', 'd' , apkPath, '-o', outputDir])# 重打包apkdef repackApk(self,inputDir:Path, outApk:Path): self.runCommand([self.apktool_path, 'b' , inputDir, '-o', outApk])# 抽取指定dex文件的代码def extractDexCodes(self,dexPath:Path):# 调用ReadDex.exe抽取dex文件代码,输出到同级目录 例如classes.dex抽取后生成classes.dex.patched和classes.dex.codes self.runCommand([self.readDex_path,'-file', dexPath, '-extractCodes'])# 执行命令def runCommand(self,args):#subprocess.run(args) subprocess.run(args,stdout=subprocess.DEVNULL) #仅调用工具,不需要额外输出,重定向stdout到os.devnull# 参数列表 捕获输出 输出转为字符串#print(subprocess.run(args, capture_output=True,text=True).stdout)# AndroidManifest.xml的editor 用于获取和修改标签属性,以及添加标签class ManifestEditor:def __init__(self, xml_content: bytes): self.ns = {'android': 'http://schemas.android.com/apk/res/android'} self.tree = etree.fromstring(xml_content)# 获取指定标签的android属性值 examples: get_attr('application', 'name') get_attr('activity', 'name')def getTagAttribute(self, tag_name: str, attr_name: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.get(f'{attr_name}') # 寻找标签的属性else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None:return elem.get(f'{{{self.ns["android"]}}}{attr_name}') # 根标签之外的属性位于android命名空间下return None# 设置指定标签的属性值 example:s et_attr('application','name',"com.example.ProxyApplication")def setTagAttribute(self, tag_name: str, attr_name: str, new_value: str):if tag_name == 'manifest': # 根标签特殊处理 elem = self.treeif elem is not None:return elem.set(f'{attr_name}', new_value) # 设置属性值else: elem = self.tree.find(f'.//{tag_name}', namespaces=self.ns)if elem is not None: elem.set(f'{{{self.ns["android"]}}}{attr_name}', new_value)return Truereturn False# 在指定父标签下添加新子标签 example: add_tag('application',"meta-data",{'name': 'android.permission.CAMERA','value':'hello'})def addTagWithAttributes(self, parent_tag: str, new_tag: str, attrs: dict):if parent_tag == 'manifest': parent = self.treeif parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): # 支持一次给添加的标签设置多个属性 new_elem.set(f'{k}', v)return Trueelse: parent = self.tree.find(f'.//{parent_tag}', namespaces=self.ns)if parent is not None: new_elem = etree.SubElement(parent, new_tag)for k, v in attrs.items(): new_elem.set(f'{{{self.ns["android"]}}}{k}', v)return Truereturn False# 不以壳manifest为基准操作则用不到该函数,以源apk的manifest为基准自带,无需额外设置def getMainActivity(self): activities = self.tree.findall('.//activity', namespaces=self.ns)for activity in activities: intent_filters = activity.findall('.//intent-filter', namespaces=self.ns)for intent_filter in intent_filters: action = intent_filter.find('.//action[@android:name="android.intent.action.MAIN"]', namespaces=self.ns) category = intent_filter.find('.//category[@android:name="android.intent.category.LAUNCHER"]', namespaces=self.ns)if action is not None and category is not None:return activity.get(f'{{{self.ns["android"]}}}name')return None# 获取application标签的name属性值def getApplicationName(self):return self.getTagAttribute('application', 'name')# 设置application标签的name属性值def setApplicationName(self, application: str): self.setTagAttribute('application', 'name', application)# 添加meta-data标签,并设置name和value属性值def addMetaData(self, name: str, value: str): self.addTagWithAttributes('application', 'meta-data', {'name': name, 'value': value})# 获取AndroidManifest.xml的字符串def getManifestData(self):"""返回XML字符串"""return etree.tostring(self.tree, pretty_print=True, encoding='utf-8', xml_declaration=True).decode()# 获取application标签的extractNativeLibs属性值def getEtractNativeLibs(self):return self.getTagAttribute('application', 'extractNativeLibs')# 设置application标签的extractNativeLibs属性值为truedef resetExtractNativeLibs(self): self.setTagAttribute('application', 'extractNativeLibs', 'true')# 工具函数,注意修复时顺序为: fileSize->signature->checksum# 修复dex文件的checksumdef fixCheckSum(dexBytes:bytearray):# dexfile[8:12] 小端序4字节 value = adler32(bytes(dexBytes[12:])) valueArray = bytearray(value.to_bytes(4, 'little'))for i in range(len(valueArray)): dexBytes[8 + i] = valueArray[i]# 修复dex文件的signaturedef fixSignature(dexBytes:bytearray):# dexfile[12:32] 小端序20字节 sha_1 = sha1() sha_1.update(bytes(dexBytes[32:])) value = sha_1.hexdigest() valueArray = bytearray(unhexlify(value))for i in range(len(valueArray)): dexBytes[12 + i] = valueArray[i]# 修复dex文件的filesizedef fixFileSize(dexBytes:bytearray, fileSize):# dexfile[32:36] 小端存储 fileSizeArray = bytearray(fileSize.to_bytes(4, "little"))for i in range(len(fileSizeArray)): dexBytes[32 + i] = fileSizeArray[i]# 加密函数,使用异或def encrypt(data:bytearray):# todo:使用aes/sm4等加密算法替代for i in range(len(data)): data[i] ^= 0xffreturn data# 抽取指定目录下的所有dex文件的代码 patch所有dex文件并修复,将codes文件移动到assets目录下def extractAllDexFiles(directory:Path): apktool=Apktool()# 1.遍历目录下的所有dex文件,并抽取对应代码for dex in directory.glob('classes*.dex'): apktool.extractDexCodes(dex) # 抽取dex文件代码 得到classes*.dex.patched和classes*.dex.codes# 2.修复抽取后的文件并覆写原dex文件for patchedDex in directory.glob('classes*.dex.patched'): newDexName = str(patchedDex).replace('.patched', '') # 重命名# 读取文件内容with open(patchedDex, 'rb') as f: data = bytearray(f.read())# 修复signature和checksum,注意先后顺序 fixSignature(data) fixCheckSum(data)# 修复后的文件覆写原dex文件with open(newDexName, 'wb') as newf: newf.write(data)# 3.删除patched文件for patchedDex in directory.glob('classes*.dex.patched'): patchedDex.unlink()# 4.移动.codes文件到assets目录下# 如果没有assets目录则创建if not (directory/'assets').exists(): (directory/'assets').mkdir(parents=True)for codes in directory.glob('classes*.dex.codes'): shutil.move(codes,directory/'assets'/codes.name) # 移动到assets目录下# 合并壳dex和源apk的dex,支持多dex文件,合并为一个dexdef combineShellAndSourceDexs(shellApkTempDir:Path,srcApkTempDir:Path,newApkTempDir:Path):# 读取解包后的apk的所有dex文件,并合并为一个dex文件def readAndCombineDexs(unpackedApkDir:Path): combinedDex = bytearray()# glob方法返回包含所有匹配文件的生成器for dex in unpackedApkDir.glob('classes*.dex'):print('Source Apk Dex file:', dex)with open(dex, 'rb') as f: data = bytearray(f.read()) combinedDex+=bytearray(len(data).to_bytes(4, 'little')) # dex文件的长度,小端序 combinedDex+=data # dex文件内容return combinedDex# 获取shelldexwith open(shellApkTempDir/'classes.dex', 'rb') as f: shellDexArray=bytearray(f.read())# 获取源apk的dex文件 srcDexArray = readAndCombineDexs(srcApkTempDir)# 新的dex文件长度 newDexLen = len(srcDexArray) + len(shellDexArray) + 4# 加密源文件 encSrcDexArray = encrypt(srcDexArray)# 新的dex文件内容 = 壳dex + 加密的源dex + 四字节标识加密后源dex大小长度 newDexArray = shellDexArray + encSrcDexArray + bytearray(len(encSrcDexArray).to_bytes(4, 'little'))# 修改filesize fixFileSize(newDexArray, newDexLen)# 修改signature fixSignature(newDexArray)# 修改checksum fixCheckSum(newDexArray) # 注意先后顺序,先修改signature,再修改checksum# 导出文件with open(newApkTempDir/'classes.dex', 'wb') as f: f.write(newDexArray)# 提取源apk的Manifest文件,修改application为壳application(可能添加meta-data标签),输出新的Manifest文件def handleManifest(srcApkTempDir:Path,shellApkTempDir:Path,newApkTempDir:Path):# 从源apk提取AndroidManifest.xmlwith open(srcApkTempDir/'AndroidManifest.xml', 'r') as f: srcManifestEditor=ManifestEditor(f.read().encode()) srcApplication=srcManifestEditor.getApplicationName() # 获取application:name,确定是否存在自定义Application类 srcExtractNativeLibs=srcManifestEditor.getEtractNativeLibs() # 获取application:extractNativeLibs,判断是否释放lib文件print('SourceApplication:',srcApplication)print('SourceExtractNativeLibs:',srcExtractNativeLibs)# 从壳apk提取AndroidManifest.xmlwith open(shellApkTempDir/'AndroidManifest.xml', 'r') as f: shellManifestEditor=ManifestEditor(f.read().encode())print('ShellApplication:',shellManifestEditor.getApplicationName())# 修改源AndroidManifest.xml的application为壳的代理application srcManifestEditor.setApplicationName(shellManifestEditor.getApplicationName())# 写入meta-data标签 保存源apk的原始applicationif srcApplication != None:print('Source application:',srcApplication) srcManifestEditor.addMetaData('APPLICATION_CLASS_NAME',srcApplication)# 如果源apk的manifest中默认设置etractNativeLibs=false,则重置为true,确保释放lib文件if srcExtractNativeLibs=='false': srcManifestEditor.resetExtractNativeLibs() # 输出新的AndroidManifest.xmlwith open(newApkTempDir/'AndroidManifest.xml', 'w') as f: f.write(srcManifestEditor.getManifestData())# 执行加固流程def start(paths:Paths): apktool=Apktool()# 1.分别解包源文件和壳文件到临时目录print('Extracting source and shell apk...') apktool.unpackApk(paths.srcApkPath,paths.srcApkTempDir)print('Extract source apk success!')print('Extracting shell apk...') apktool.unpackApk(paths.shellApkPath,paths.shellApkTempDir)print('Extract shell apk success!')# 2.抽取源dex文件代码print('Exrtracting dex files codes...') extractAllDexFiles(paths.srcApkTempDir) print('Extract dex files codes success!')# 3.复制源apk所有文件到新apk临时目录中 忽略源dex和manifest文件print('Copying source apk files to new apk temp dir...') shutil.copytree(paths.srcApkTempDir,paths.newApkTempDir,ignore=shutil.ignore_patterns('AndroidManifest.xml','classes*.dex')) print('Copy source apk files success!')# 4.复制壳apk的lib库文件到新apk临时目录中 (壳的代码回填逻辑在lib中实现)print('Copying shell apk lib files to new apk temp dir...') shutil.copytree(paths.shellApkTempDir/'lib',paths.newApkTempDir/'lib',dirs_exist_ok=True) # dirs_exist_ok=True 如果目标目录已存在,则覆盖print('Copy shell apk lib files success!')# 5.处理AndroidManifest.xmlprint('Handling AndroidManifest.xml...') handleManifest(paths.srcApkTempDir,paths.shellApkTempDir,paths.newApkTempDir)print('Handle AndroidManifest.xml success!')# 6.合并壳dex和源apk的dex并导出文件print('Combining shell dex and source dexs...') combineShellAndSourceDexs(paths.shellApkTempDir,paths.srcApkTempDir,paths.newApkTempDir)print('Combine shell dex and source dexs success!')# 7.重打包apkprint('Repacking apk...') apktool.repackApk(paths.newApkTempDir,paths.newApkPath)print('Repack apk success!')# 8.签名apkprint('Signing apk...') apktool.signApk(paths.newApkPath)print('Resign apk success!')# 9.删除临时目录print('Deleting temp directories...') shutil.rmtree(paths.tmpdir) print('Delete temp directories success!')def main(): parser = argparse.ArgumentParser(description="Android APK Packer") parser.add_argument('-src','--src-apk', required=True, type=Path, help='Path to source APK file') parser.add_argument('-shell','--shell-apk', required=True, type=Path, help='Path to shell APK file') parser.add_argument('-o','-out','--output-apk',type=Path,help='Output path for packed APK (Default: ./out/<src-apk>_protected.apk)') args = parser.parse_args()if args.output_apk == None: args.output_apk = Path('./out')/(args.src_apk.stem+'_protected.apk') # 默认新apk名称及输出路径 paths = Paths(args.src_apk, args.shell_apk, args.output_apk)print('Source APK:', paths.srcApkPath)print('Shell APK:', paths.shellApkPath)print('Output APK:', paths.newApkPath) start(paths)if __name__=="__main__": main()
脱壳程序
脱壳程序主要分为2个模块:
1.Java层: 提供环境初始化,替换ClassLoader,替换Application的功能
2.Native层: 提供禁用Dex2Oat,设置Dex文件可写,代码回填,代码文件解析的功能
经过前文加壳程序的处理后,源程序AndroidManifest.xml文件的application标签的name属性指定壳的Application,由于Application是安卓应用程序真正的入口类,所以启动加壳后的程序时控制权在壳的代理Application中。
在壳的代理Application中主要执行以下操作:
1.初始化操作
设置相关文件路径,解析相关文件用于后续处理.
2.替换ClassLoader
替换壳程序的ClassLoader为被保护程序的ClassLoader.
3.替换Application
若被保护程序存在自定义Application则创建实例并替换.
4.加载壳so
调用System.loadLibrary()主动加载即可,后续在Native层执行代码回填.
示意图如下
环境初始化
主要执行以下操作:
1.设置相关私有目录,供后续释放文件以及设置DexClassLoader
2.从壳apk文件提取并解析被保护程序的dex文件,写入私有目录:调用copyClassesCodesFiles执行该操作.
3.从assets目录提取codes文件写入私有目录
4.拼接源程序所有dex文件的路径:用“:”分隔,拼接源程序所有dex文件路径,供后续DexClassLoader加载使用.
示意图如下
替换ClassLoader
主要执行以下操作:
1.获取当前ClassLoader:调用this.getClassLoader()获取.
2.反射获取ActivityThread实例:通过反射直接获取ActivityThread.sCurrentActivityThread字段,即当前程序对应的ActivityThread实例.
3.反射获取LoadedApk实例:首先反射获取ActivityThread.mPackages字段,再根据当前程序的包名从中查找获取对应LoadedApk实例.
4.创建并替换ClassLoader:将环境初始化工作中创建的lib和dex文件的私有目录路径以及当前ClassLoader作为参数,新建DexClassLoader,该ClassLoader可用于加载之前释放的源程序Dex文件和libso文件.
最后通过反射修改LoadedApk.mClassLoader实例完成替换.
替换Application
主要执行以下操作:
1.获取自定义Application完整类名
访问3.2.2节中加壳程序为AndroidManifest.xml添加的meta-data标签,其中保存了源程序自定义的Application类名.
1.反射获取ActivityThread实例(同3.3.2)
2.反射获取LoadedApk实例,并设置mApplication为空
获取LoadedApk实例同3.3.2,设置mApplication为空的原因是调用LoadedApk.makeApplication时,如果mApplication不为空,则直接返回当前的Application实例.所以想要替换Application必须先置空再创建.
1.获取ActivityThread.mInitialApplication并删除壳Application
2.反射调用LoadedApk.makeApplication创建源Application
3.重置ActivityThread.mInitialApplication为源Application
4.处理ContentProvider持有的代理Application
5.调用Application.onCreate 源程序,启动!
流程图如下
Native层
调用System.loadLibrary主动加载了壳的SO文件,首先调用init函数,其中依次执行以下Hook操作(劫持对应函数):
1.Hook execve
在hook后添加判断逻辑,匹配到调用dex2oat系统调用时直接返回.
dex2oat是ART将所有Dex文件优化为一个OAT文件(本质为ELF文件)的操作,目的是加快指令执行速度,但这会影响加固工具执行指令回填.
2.Hook mmap
在mmap映射内存时添加写权限,保证可修改DexFile进行指令回填.
3.Hook LoadMethod
LoadMethod有两个关键参数: DexFile* dexFile和Method* method.
通过dexFile获取方法所在的Dex文件路径,从而判断是否为源程序被抽取了代码的Dex文件,如果是则判断是否进行过文件解析.
若没有解析过则调用parseExtractedCodeFiles函数解析Dex文件对应的codes文件后,便成功创建了一组CodeMap(保存codeOff和CodeItem映射).
之后调用目标方法时,根据Method.codeOff从CodeMap中提取对应CodeItem并执行指令回填,dexFile.begin+Method.codeOff即为insns[]指令字节数组的位置.
其中Hook主要使用Bhook和Dobby
1.Dobby
参考https://github.com/luoyesiqiu/dpt-shell/blob/main/shell/src/main/cpp/CMakeLists.txt和https://www.52pojie.cn/thread-1779984-1-1.html
源码编译似乎有点麻烦,静态导入dobby
include_directories(
dobby
)
add_library(local_dobby STATIC IMPORTED)
set_target_properties(local_dobby PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}
/
..
/
..
/
..
/
libs
/
${ANDROID_ABI}
/
libdobby.a)
target_link_libraries(dpt
${log
-
lib}
MINIZIP::minizip
local_dobby
bytehook
${android
-
lib}
)
2.Bhook
参考https://github.com/bytedance/bhook/blob/main/README.zh-CN.md
build.gradle添加bhook依赖
android {
buildFeatures {
prefab true
}
}
dependencies {
implementation
'com.bytedance:bytehook:1.1.1'
}
CMakeLists.txt添加如下设置
/
/
其中mylib 表示需要使用bhook的模块名,也就是将这些模块和bytehook链接
find_package(bytehook REQUIRED CONFIG)
/
/
获取bytehook包
add_library(mylib SHARED mylib.c)
/
/
用户模块
target_link_libraries(mylib bytehook::bytehook)
/
/
链接用户模块和bytehook
Hook后的LoadMethod主要工作如下
ThirdProxyApplication.java
相对FirstProxyApplication.java改动如下:
1.System.loadLibrary("androidshell")
主动加载壳程序的so,设置hook
2.writeByteBuffersToDirectory
用于将壳dex中提取的源dex字节数组写为文件
3.copyClassesCodesFiles
用于将dex文件对应的codes文件复制到和dex相同的私有目录
package com.example.androidshell;import android.app.Application;import android.app.Instrumentation;import android.content.Context;import android.content.pm.ApplicationInfo;import android.content.pm.PackageManager;import android.content.res.AssetManager;import android.os.Bundle;import android.util.ArrayMap;import android.util.Log;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.lang.ref.WeakReference;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.ArrayList;import java.util.Iterator;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;import dalvik.system.DexClassLoader;public class ThirdProxyApplication extends Application {private final String TAG="glass";private String dexPath;private String odexPath;private String libPath;public void log(String message){Log.d(TAG,message);}@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base); log("ThirdProxyApplication.attachBaseContext is running!"); System.loadLibrary("androidshell"); //主动加载so,设置hook,进行指令回填 log("Load libandroidshell.so succeed!");try {//初始化相关环境 initEnvironments();//配置加载源程序的动态环境,即替换mClassLoader replaceClassLoader(); } catch (Exception e) { log( Log.getStackTraceString(e)); } }@Overridepublic void onCreate() {super.onCreate(); log("ThirdProxyApplication.onCreate is running!");if(replaceApplication()) log("Replace Application succeed!"); }private void initEnvironments() throws IOException {//1.设置相关目录和路径Filedex = getDir("tmp_dex", MODE_PRIVATE); // 私有目录,存放dex文件//File lib = getDir("tmp_lib", MODE_PRIVATE); // lib可使用默认目录//libPath = lib.getAbsolutePath(); odexPath = dex.getAbsolutePath(); libPath=this.getApplicationInfo().nativeLibraryDir; //默认lib路径 dexPath =this.getApplicationInfo().sourceDir; //当前base.apk路径//2.从当前base.apk读取classes.dex并读取为字节数组byte[] shellDexData = readDexFromApk(); log("Get classes.dex from base.apk succeed!");//3.从壳dex文件中分离出源dex文件 ByteBuffer[] byteBuffers = extractDexFilesFromShellDex(shellDexData);//4.将源dex文件依次写入私有目录 writeByteBuffersToDirectory(byteBuffers, odexPath);//5.将codes文件依次写入私有目录 copyClassesCodesFiles(this, odexPath);//6.拼接dex目录字符串,设置dex文件路径 DexClassLoader支持传递多个dex文件路径以加载多个dex文件,通过':'分隔路径 StringBuffer dexFiles=new StringBuffer();for(File file:dex.listFiles()){if(file.getName().contains(".codes"))continue; dexFiles.append(file.getAbsolutePath()); dexFiles.append(":"); } dexPath=dexFiles.toString(); }private void writeByteBuffersToDirectory(ByteBuffer[] byteBuffers, String directoryPath) throws IOException {// 创建目录对象Filedirectory =new File(directoryPath);// 检查目录是否存在,不存在则创建if (!directory.exists()) {if (!directory.mkdirs()) {throw new IOException("无法创建目录: " + directoryPath); } }// 遍历 ByteBuffer 数组for (inti =0; i < byteBuffers.length; i++) {// 生成文件名 String fileName;if (i == 0) { fileName = "classes.dex"; } else { fileName = "classes" + (i + 1) + ".dex"; }// 构建文件对象Filefile =new File(directory, fileName);// 创建文件输出流try (FileOutputStreamfos =new FileOutputStream(file)) {// 获取 ByteBuffer 中的字节数组ByteBufferbuffer = byteBuffers[i];byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes);// 将字节数组写入文件 fos.write(bytes); } } }private void copyClassesCodesFiles(Context context, String targetDirectoryPath) {AssetManagerassetManager = context.getAssets();try {// 获取 assets 目录下的所有文件和文件夹 String[] files = assetManager.list("");if (files != null) {// 创建目标目录FiletargetDirectory =new File(targetDirectoryPath);if (!targetDirectory.exists()) {if (!targetDirectory.mkdirs()) {throw new IOException("无法创建目标目录: " + targetDirectoryPath); } }for (String fileName : files) {// 筛选以 classes 开头且以 .codes 结尾的文件if (fileName.startsWith("classes") && fileName.endsWith(".codes")) {try (InputStreaminputStream = assetManager.open(fileName);BufferedInputStreambis =new BufferedInputStream(inputStream);FileOutputStreamfos =new FileOutputStream(new File(targetDirectory, fileName));BufferedOutputStreambos =new BufferedOutputStream(fos)) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = bis.read(buffer)) != -1) { bos.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } } } } } catch (IOException e) { e.printStackTrace(); } }// 从壳dex文件中提取源apk的dex并封装为ByteBufferprivate ByteBuffer[] extractDexFilesFromShellDex(byte[] shellDexData) {intshellDexlength = shellDexData.length;//开始解析dex文件byte[] sourceDexsSizeByte = new byte[4];//读取源dexs的大小 System.arraycopy(shellDexData,shellDexlength - 4, sourceDexsSizeByte,0,4);//转成bytebuffer,方便4byte转intByteBufferwrap = ByteBuffer.wrap(sourceDexsSizeByte);//将byte转成int, 加壳时,长度按小端存储intsourceDexsSizeInt = wrap.order(ByteOrder.LITTLE_ENDIAN).getInt(); Log.d(TAG, "源dex集合的大小: " + sourceDexsSizeInt);//读取源dexsbyte[] sourceDexsData = new byte[sourceDexsSizeInt]; System.arraycopy(shellDexData,shellDexlength - sourceDexsSizeInt - 4, sourceDexsData, 0, sourceDexsSizeInt);//解密源dexs sourceDexsData = decrypt(sourceDexsData);//更新部分//从源dexs中分离dex ArrayList<byte[]> sourceDexList = new ArrayList<>();intpos =0;while(pos < sourceDexsSizeInt){//先提取四个字节,描述当前dex的大小//开始解析dex文件byte[] singleDexSizeByte = new byte[4];//读取源dexs的大小 System.arraycopy(sourceDexsData, pos, singleDexSizeByte,0,4);//转成bytebuffer,方便4byte转intByteBuffersingleDexwrap = ByteBuffer.wrap(singleDexSizeByte);intsingleDexSizeInt = singleDexwrap.order(ByteOrder.LITTLE_ENDIAN).getInt(); Log.d(TAG, "当前Dex的大小: " + singleDexSizeInt);//读取单独dexbyte[] singleDexData = new byte[singleDexSizeInt]; System.arraycopy(sourceDexsData,pos + 4, singleDexData, 0, singleDexSizeInt);//加入到dexlist中 sourceDexList.add(singleDexData);//更新pos pos += 4 + singleDexSizeInt; }//将dexlist包装成ByteBufferintdexNum = sourceDexList.size(); Log.d(TAG, "源dex的数量: " + dexNum); ByteBuffer[] dexBuffers = new ByteBuffer[dexNum];for (inti =0; i < dexNum; i++){ dexBuffers[i] = ByteBuffer.wrap(sourceDexList.get(i)); }return dexBuffers; }// 从当前程序的apk读取dex文件并存储为字节数组private byte[] readDexFromApk() throws IOException {//1.获取当前应用程序的源码路径(apk),一般是data/app目录下,该目录用于存放用户安装的软件StringsourceDir =this.getApplicationInfo().sourceDir; log("this.getApplicationInfo().sourceDir: " +sourceDir);//2.创建相关输入流FileInputStreamfileInputStream =new FileInputStream(sourceDir);BufferedInputStreambufferedInputStream =new BufferedInputStream(fileInputStream);ZipInputStreamzipInputStream =new ZipInputStream(bufferedInputStream); //用于解析apk文件ByteArrayOutputStreambyteArrayOutputStream =new ByteArrayOutputStream(); //用于存放dex文件//3.遍历apk的所有文件并提取dex文件 ZipEntry zipEntry;while((zipEntry = zipInputStream.getNextEntry()) != null){ //存在下一个文件// 将classes.dex文件存储到bytearray中 壳dex和源apk合并后只保留一个dex便于处理if (zipEntry.getName().equals("classes.dex")){byte[] bytes = new byte[1024];int num;while((num = zipInputStream.read(bytes))!=-1){ //每次读取1024byte,返回读取到的byte数 byteArrayOutputStream.write(bytes,0, num); //存放到开辟的byteArrayOutputStream中 } } zipInputStream.closeEntry(); //关闭当前文件 } zipInputStream.close(); log("Read dex from apk succeed!");return byteArrayOutputStream.toByteArray(); //将读取到的dex文件以字节数组形式返回 }// 解密private byte[] decrypt(byte[] data) {for (inti =0; i < data.length; i++){ data[i] ^= (byte) 0xff; }return data; }//替换壳程序LoadedApk的Application为源程序Application,并调用其onCreate方法private booleanreplaceApplication(){// Application实例存在于: LoadedApk.mApplication// 以及ActivityThread的mInitialApplication和mAllApplications和mBoundApplication//判断源程序是否使用自定义Application 若使用则需要进行替换,若未使用则直接返回,使用壳的默认Application即可StringappClassName =null; //源程序的Application类名try {//获取AndroidManifest.xml 文件中的 <meta-data> 元素ApplicationInfoapplicationInfo = getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);BundlemetaData = applicationInfo.metaData;//获取xml文件声明的Application类if (metaData != null && metaData.containsKey("APPLICATION_CLASS_NAME")){ appClassName = metaData.getString("APPLICATION_CLASS_NAME"); } else { log("源程序中没有自定义Application");return false; //如果不存在直接返回,使用壳的application即可 } } catch (PackageManager.NameNotFoundException e) { log(Log.getStackTraceString(e)); }//源程序存在自定义application类,开始替换 log("Try to replace Application");//1.反射获取ActivityThread实例ObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread: " + sCurrentActivityThreadObj.toString());//2.获取并设置LoadedApk//获取mBoundApplication (AppBindData对象)ObjectmBoundApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mBoundApplication") ; log("mBoundApplication: "+mBoundApplicationObj.toString());//获取mBoundApplication.info (即LoadedApk)ObjectinfoObj = Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"info"); log( "LoadedApk: " + infoObj.toString());//把LoadedApk的mApplication设置为null,这样后续才能调用makeApplication() 否则由于已存在Application,无法进行替换 Reflection.setField("android.app.LoadedApk","mApplication",infoObj,null);//3.获取ActivityThread.mInitialApplication 即拿到旧的Application(对于要加载的Application来讲)ObjectmInitialApplicationObj = Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mInitialApplication"); log("mInitialApplicationObj: " + mInitialApplicationObj.toString());//4.获取ActivityThread.mAllApplications并删除旧的application ArrayList<Application> mAllApplicationsObj = (ArrayList<Application>) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mAllApplications"); mAllApplicationsObj.remove(mInitialApplicationObj); log("mInitialApplication 从 mAllApplications 中移除成功");//5.重置相关类的Application类名 便于后续创建Application//获取LoadedApk.mApplicationInfoApplicationInfoapplicationInfo = (ApplicationInfo) Reflection.getField("android.app.LoadedApk",infoObj,"mApplicationInfo"); log( "LoadedApk.mApplicationInfo: " + applicationInfo.toString());//获取mBoundApplication.appInfoApplicationInfoappinfoInAppBindData = (ApplicationInfo) Reflection.getField("android.app.ActivityThread$AppBindData",mBoundApplicationObj,"appInfo"); log("ActivityThread.mBoundApplication.appInfo: " + appinfoInAppBindData.toString());//此处通过引用修改值,虽然后续没有使用,但是实际上是修改其指向的LoadedApk相关字段的值//设置两个appinfo的classname为源程序的application类名,以便后续调用makeApplication()创建源程序的application applicationInfo.className = appClassName; appinfoInAppBindData.className = appClassName; log("Source Application name: " + appClassName);//6.反射调用makeApplication方法创建源程序的applicationApplicationapplication = (Application) Reflection.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null}); //使用源程序中的application//Application app = (Application)ReflectionMethods.invokeMethod("android.app.LoadedApk","makeApplication",infoObj,new Class[]{boolean.class, Instrumentation.class},new Object[]{true,null}); //使用自定义的application 强制为系统默认 log("Create source Application succeed: "+application);//7.重置ActivityThread.mInitialApplication为新的Application Reflection.setField("android.app.ActivityThread","mInitialApplication",sCurrentActivityThreadObj,application); log("Reset ActivityThread.mInitialApplication by new Application succeed!");//8.ContentProvider会持有代理的Application,需要特殊处理一下ArrayMapmProviderMap = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mProviderMap"); log("ActivityThread.mProviderMap: " + mProviderMap);//获取所有provider,装进迭代器中遍历Iteratoriterator = mProviderMap.values().iterator();while(iterator.hasNext()){ObjectproviderClientRecord = iterator.next();//获取ProviderClientRecord.mLocalProvider,可能为空ObjectmLocalProvider = Reflection.getField("android.app.ActivityThread$ProviderClientRecord",providerClientRecord,"mLocalProvider") ;if(mLocalProvider != null){ log("ProviderClientRecord.mLocalProvider: " + mLocalProvider);//获取ContentProvider中的mContext字段,设置为新的Application Reflection.setField("android.content.ContentProvider","mContext",mLocalProvider,application); } } log( "Run Application.onCreate" ); application.onCreate(); //源程序,启动!return true; }// 替换壳App的ClassLoader为源App的ClassLoaderprivate void replaceClassLoader() {//1.获取当前的classloaderClassLoaderclassLoader =this.getClassLoader(); log("Current ClassLoader: " + classLoader.toString()); log("Parent ClassLoader: " + classLoader.getParent().toString());//2.反射获取ActivityThreadObjectsCurrentActivityThreadObj = Reflection.getStaticField("android.app.ActivityThread","sCurrentActivityThread"); log("ActivityThread.sCurrentActivityThread: " + sCurrentActivityThreadObj.toString());//3.反射获取LoadedApk//获取当前ActivityThread实例的mPackages字段 类型为ArrayMap<String, WeakReference<LoadedApk>>, 里面存放了当前应用的LoadedApk对象ArrayMapmPackagesObj = (ArrayMap) Reflection.getField("android.app.ActivityThread",sCurrentActivityThreadObj,"mPackages"); log( "mPackagesObj: " + mPackagesObj.toString());//获取mPackages中的当前应用包名StringcurrentPackageName =this.getPackageName(); log("currentPackageName: " + currentPackageName);// 获取loadedApk实例也有好几种,mInitialApplication mAllApplications mPackages// 通过包名获取当前应用的loadedApk实例WeakReferenceweakReference = (WeakReference) mPackagesObj.get(currentPackageName);ObjectloadedApkObj = weakReference.get(); log( "LoadedApk: " + loadedApkObj.toString());//4.替换ClassLoaderDexClassLoaderdexClassLoader =new DexClassLoader(dexPath, odexPath,libPath, classLoader.getParent()); //动态加载源程序的dex文件,以当前classloader的父加载器作为parent Reflection.setField("android.app.LoadedApk","mClassLoader",loadedApkObj,dexClassLoader); //替换当前loadedApk实例中的mClassLoader字段 log("New DexClassLoader: " + dexClassLoader); }}
shell.cpp
主要提供以下功能:
1.hook execve
主要目的是禁止dex2oat,防止dex文件被优化
2.hook mmap
使dex文件可写,用于后续指令回填
3.hook LoadMethod
loadmethod用于加载dex文件的方法,可获取dex文件引用和codeoff
hook劫持后执行codes文件解析和指令回填
#include <jni.h>#include <string>#include <unistd.h>#include <map>#include <fstream>#include <stdlib.h>#include <elf.h>#include <dlfcn.h>#include "android/log.h"#include "sys/mman.h"#include "bytehook.h"#include "dobby/dobby.h"#include "dex/DexFile.h"#include "dex/CodeItem.h"#include "dex/class_accessor.h"#define TAG "glass"#define logd(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);#define logi(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__);#define loge(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);// sdk版本,用于兼容适配int APILevel;// 函数声明voidhook();voidhookExecve();voidhookMmap();voidhook_LoadMethod();// 抽取代码文件,与源.dex在同一私有目录std::string codeFilePostfix = ".codes";//dex文件名->codeOff->CodeItem 每个dex文件对应一个map,每个map内的codeoff对应一个CodeItemstd::map<std::string,std::map<uint32_t, CodeItem>> codeMapList;//art/runtime/class_linker.h// 函数声明staticvoid (*g_originLoadMethod)(void* thiz,const DexFile* dex_file, ClassAccessor::Method* method,void* klass,void* dst);/*//Android 10-14 原型如下void LoadMethod(const DexFile& dex_file, const ClassAccessor::Method& method, Handle<mirror::Class> klass, ArtMethod* dst);*/// Tool functions// 以二进制形式读取整个文件,返回字节数组并返回文件长度uint8_t* readFileToBytes(const std::string fileName,size_t* readSize) { FILE *file = fopen(fileName.c_str(), "rb");if (file == NULL) {logd("Error opening file");fclose(file);return NULL; }fseek(file, 0,SEEK_END);size_t fileSize = ftell(file);fseek(file, 0,SEEK_SET);uint8_t *buffer = (uint8_t *) malloc(fileSize);if (buffer == NULL) {logd("Error allocating memoryn");fclose(file);return NULL; }size_t bytesRead = fread(buffer, 1, fileSize, file);if(bytesRead!=fileSize) {logd("Read bytes not equal file size!n");free(buffer);fclose(file);return NULL; }fclose(file);if(readSize) *readSize=bytesRead;return buffer;}// 4字节数组转uint32_tuint32_tbytes2uint32(unsignedchar * bytes){uint32_t retnum = 0;for(int i = 3;i >=0;i--){ retnum <<= 8; retnum |= bytes[i]; }return retnum;}constchar * getArtLibPath() {if(APILevel < 29) {return "/system/lib64/libart.so"; } else if(APILevel == 29) {return "/apex/com.android.runtime/lib64/libart.so"; } else {return "/apex/com.android.art/lib64/libart.so"; }}constchar * getArtBaseLibPath() {if(APILevel == 29) {return "/apex/com.android.runtime/lib64/libartbase.so"; } else {return "/apex/com.android.art/lib64/libartbase.so"; }}constchar* find_symbol_in_elf_file(constchar *elf_file,int keyword_count,...) { FILE *elf_fp = fopen(elf_file, "r");if (elf_fp) {// 获取elf文件大小fseek(elf_fp, 0L, SEEK_END);size_t lib_size = ftell(elf_fp);fseek(elf_fp, 0L, SEEK_SET);// 读取elf文件数据char *data = (char *) calloc(lib_size, 1);fread(data, 1, lib_size, elf_fp);char *elf_bytes_data = data;// elf头 Elf64_Ehdr *ehdr = (Elf64_Ehdr *) elf_bytes_data;// 节头 Elf64_Shdr *shdr = (Elf64_Shdr *) (((uint8_t *) elf_bytes_data) + ehdr->e_shoff); va_list kw_list;// 遍历节for (int i = 0; i < ehdr->e_shnum; i++) {// 字符串表if (shdr->sh_type == SHT_STRTAB) {constchar *str_base = (char *) ((uint8_t *) elf_bytes_data + shdr->sh_offset);char *ptr = (char *) str_base;// 遍历字符串表for (int k = 0; ptr < (str_base + shdr->sh_size); k++) {constchar *item_value = ptr;size_t item_len = strnlen(item_value, 128); ptr += (item_len + 1);if (item_len == 0) {continue; }int match_count = 0;va_start(kw_list, keyword_count);for (int n = 0; n < keyword_count; n++) {constchar *keyword = va_arg(kw_list, constchar*);if (strstr(item_value, keyword)) { match_count++; } }va_end(kw_list);if (match_count == keyword_count) {return item_value; } }break; } shdr++; }fclose(elf_fp);free(data); }return nullptr;}constchar * getClassLinkerDefineClassLibPath(){return getArtLibPath();}constchar * getClassLinkerDefineClassSymbol() {constchar * sym = find_symbol_in_elf_file(getClassLinkerDefineClassLibPath(),2,"ClassLinker","DefineClass");return sym;}constchar * getClassLinkerLoadMethodLibPath(){return getArtLibPath();}//获取ClassLinker::LoadMethod真实符号名constchar * getClassLinkerLoadMethodSymbol() {constchar * sym = find_symbol_in_elf_file(getClassLinkerLoadMethodLibPath(),2,"ClassLinker","LoadMethod");return sym;}//获取libart真实名称constchar * getArtLibName() {//Android 10及以后变为libartbase.soreturn APILevel >= 29 ? "libartbase.so" : "libart.so";}// 禁用dex2oatintfakeExecve(constchar *pathname, char *const argv[], char *const envp[]) {BYTEHOOK_STACK_SCOPE();// 禁用dex2oatif (strstr(pathname, "dex2oat") != nullptr) { errno = EACCES;return -1; }return BYTEHOOK_CALL_PREV(fakeExecve, pathname, argv, envp);}voidhookExecve(){bytehook_stub_t stub = bytehook_hook_single(getArtLibName(),"libc.so","execve", (void *) fakeExecve,nullptr,nullptr);if (stub != nullptr) {logd("hook execve done"); }}//为dex文件添加可写权限void* fakeMmap(void * __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset){BYTEHOOK_STACK_SCOPE();int prot = __prot;int hasRead = (__prot & PROT_READ) == PROT_READ;int hasWrite = (__prot & PROT_WRITE) == PROT_WRITE;// 添加写权限if(hasRead && !hasWrite) { prot |= PROT_WRITE; }void * addr = BYTEHOOK_CALL_PREV(fakeMmap, __addr, __size, prot, __flags, __fd, __offset);return addr;}voidhookMmap(){bytehook_stub_t stub = bytehook_hook_single(getArtLibName(),"libc.so","mmap", (void *) fakeMmap,nullptr,nullptr);if(stub != nullptr){logd("hook mmap done"); }}// 解析抽取代码文件,每个dex.codes只解析一次voidparseExtractedCodeFiles(const std::string& dexPath){//1.读取代码文件为字节数组 std::string codeFilePath=dexPath+codeFilePostfix;logd("Code File Path: %s",codeFilePath.c_str());size_t codeBytesLen = 0;uint8_t* codeBytes = readFileToBytes(codeFilePath, &codeBytesLen);if(codeBytes == nullptr || codeBytesLen == 0) {logd("Code file not found!")return; }logd("CodeFile: %s Len:%#llx", codeFilePath.c_str(),codeBytesLen);// 2.解析代码字节数组size_t offset=0;while(offset<codeBytesLen){uint8_t* pointer = codeBytes + offset; //每个结构的起点指针uint32_t codeOff = bytes2uint32(pointer); // 4字节CodeOff和4字节InsnSizeuint32_t insnSize = bytes2uint32(pointer+4);if(codeOff == 0 || insnSize == 0){logd("CodeOff or InsnSize equals 0!")break; }logd("CodeOff: %#x InsnSize: %#x", codeOff, insnSize);// 创建抽取代码对象 CodeItem codeItem = CodeItem(insnSize, pointer+8);// 添加一组CodeOff:CodeItem映射 codeMapList[dexPath].insert(std::pair<uint32_t, CodeItem>(codeOff, codeItem));logd("CodeItem codeOff: %#x insnSize: %#x has created!", codeOff, insnSize); offset += 8 + insnSize*2; //跳过CodeOff,InsnSize和Insn[] }}// 回填dex的方法代码,每次只回填一个MethodvoidinnerLoadMethod(void* thiz, const DexFile* dexFile, ClassAccessor::Method* method, void* klass, void* dest){// dex文件路径 std::string location = dexFile->location_;//logd("Load Dex File Location: %s",location.c_str())// 判断是否为解密释放的dex文件,位于私有目录内if(location.find("app_tmp_dex") == std::string::npos){return; }// 如果未解析过dexCodes文件则进行解析,每个dex文件只解析一次,创建对应的map<CodeOff,CodeItem>映射if(codeMapList.find(location)==codeMapList.end()){logd("Parse dex file %s codes",location.c_str()); codeMapList[location]=std::map<uint32_t,CodeItem>(); //创建新的codeMapparseExtractedCodeFiles(location); }// 不存在DexCode 直接跳过if(method->code_off_==0){return; }// 指令地址uint8_t* codeAddr = (uint8_t*)(dexFile->begin_ + method->code_off_ + 16); //insn结构前面有16字节//logd("MethodCodeOff: %d",method->code_off_);// 回填指令 std::map<uint32_t,CodeItem> codeMap=codeMapList[location];// 似乎没有走到回填指令处 (注意c++浅拷贝问题,不能随意delete)if(codeMap.find(method->code_off_) != codeMap.end()){ CodeItem codeItem = codeMap[method->code_off_];memcpy(codeAddr,codeItem.getInsns(),codeItem.getInsnsSize()*2); //注意指令为u2类型,长度需要*2 }}voidnewLoadMethod(void* thiz, const DexFile* dex_file, ClassAccessor::Method* method, void* klass, void* dest){if(g_originLoadMethod!= nullptr){// 先回填指令,再调用innerLoadMethod(thiz,dex_file,method,klass,dest);g_originLoadMethod(thiz,dex_file,method, klass, dest); }return;}voidhook_LoadMethod(){void * loadMethodAddress = DobbySymbolResolver(getClassLinkerLoadMethodLibPath(),getClassLinkerLoadMethodSymbol());DobbyHook(loadMethodAddress, (void *) newLoadMethod, (void **) &g_originLoadMethod);logd("hook LoadMethod done");}// 初始函数,实现hookextern "C"void _init(){ APILevel = android_get_device_api_level();logd("Android API Level: %d", APILevel)logd("Setting hook...")hook();}// hookvoidhook(){bytehook_init(BYTEHOOK_MODE_AUTOMATIC, false);hookExecve(); // 禁用dex2oathookMmap(); // 使dex文件可写//hook_DefineClass(); //需手动解析ClassDefhook_LoadMethod(); // 加载方法时回填指令}
看雪ID:东方玻璃
https://bbs.kanxue.com/user-home-968342.htm
#
原文始发于微信公众号(看雪学苑):Android从整体加固到抽取加固的实现及原理(上)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论