首页
社区
课程
招聘
[原创] Android 简单加密壳
2022-8-1 02:09 36390

[原创] Android 简单加密壳

2022-8-1 02:09
36390

APK 加固

参考

http://www.15pb.com.cn/my/course/915

 

https://www.kanxue.com/book-section_list-73.htm

开发中常用的加固手段

在开发阶段就嵌入安全代码&验证&混淆, 主要有以下几种方式

  • proguard配置项(release版本默认开启) 对apk中源码进行混淆
  • 对Apk反编译之后的smali进行混淆
  • 对Apk中的字符串进行加密
  • 对Apk中的文件进行校验

image-20220415104418372

proguard混淆

  • 开启之后会将系统库名&类名全部修改为随机字符串

    image-20220417170224448

  • release默认开启, 在项目目录app下的build.gradle中有配置信息minifyEnabled

    build.gradle(module)配置 minifyEnabled:true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    配置完后进行编译, 反编译就能观察到变化

    release 版本生成: Build--> Generate Signed Bundle/APK --> APK 生成

  • Proguard的混淆结果会输出到 <module-name>/build/outputs/mapping/release或者debug/ 中的3个文件和一个混淆配置文件

    image-20220417164326902

    • mapping.txt: 提供混淆前后类名&方法名&成员变量名的对应关系
    • seeds.txt: 列出没有被混淆的类和成员
    • usage.txt: 列出从APK中移除的代码(没有使用到)

proguard包括四个功能,shrinker(压缩), optimizer(优化),obfuscator(混淆),preverifier(预校验)。

  • shrink: 检测并移除没有用到的类,变量,方法和属性;
  • optimize: 优化代码,非入口节点类会加上private/static/final, 没有用到的参数会被删除,一些方法可能会变成内联代码。
  • obfuscate: 使用短又没有语义的名字重命名非入口类的类名,变量名,方法名。入口类的名字保持不变。
  • preverify: 预校验代码是否符合Java1.6或者更高的规范(唯一一个与入口类不相关的步骤)

想要自定义proguard混淆可以在 proguard-rules.pro文件中

smali代码乱序

通过对smali代码进行乱序来干扰代码逆向

1
2
3
4
5
6
7
8
9
10
11
    goto :sign1
:sign3
    指令3
    goto :end
:sign2
    指令2
    goto :sign3
:sign1
    指令1
    goto :sign2
:end

实例

 

新建一个工程, 新建类Hello.java

1
2
3
4
5
6
7
8
9
10
11
public class Hello {
    public static void main(String[] argc){
        // 字符串的声明和定义
        String a="1";
        String b="2";
        // 字符串拼接
        String c = a+b;
        // 打印字符串
        System.out.println(c);
    }
}

image-20220417220751389

禁用mutidex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
 
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
 
// Enabling multidex support.
multiDexEnabled false
}
...
}
 

编译生成APK

 

使用Android Killer(或者使用apktool)打开apk, 打开解压后的smali工程

1
java -jar apktool_latest.jar d app-debug.apk

观察Hello.smali代码如下(使用反汇编工具如jd和jadx也能够正常反汇编)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.class public Lcom/example/myapplication/Hello;
.super Ljava/lang/Object;
.source "Hello.java"
 
 
# direct methods
.method public constructor <init>()V
    .locals 0
 
    .line 3
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V
 
    return-void
.end method
 
.method public static main([Ljava/lang/String;)V
    // 字符串的声明和定义
    .locals 4
    .param p0, "argc"    # [Ljava/lang/String;
 
    .line 5
    const-string v0, "1"
 
    .line 6
    .local v0, "a":Ljava/lang/String;
    const-string v1, "2"
    // 字符串拼接
    .line 7
    .local v1, "b":Ljava/lang/String;
    new-instance v2, Ljava/lang/StringBuilder;
 
    invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
 
    invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
 
    invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
 
    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
 
    move-result-object v2
    // 打印字符串
    .line 8
    .local v2, "c":Ljava/lang/String;
    sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;
 
    invoke-virtual {v3, v2}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
 
    .line 9
    return-void
.end method

代码分为三部分, 对这三部分进行乱序

  1. 字符串的声明和定义
  2. 字符串拼接
  3. 打印字符串

image-20220417223420422

 

对smali代码进行乱序之后,重新编译, 在app-debug/build/dist/目录下生成apk

1
java -jar apktool_latest.jar b app-debug

对smali代码进行乱序后,重新编译

 

image-20211127184447788

 

然后dex2jar就不能用了,但是jadx可以识破

对APK字符串进行加密

为了应对逆向分析中常用的字符串分析, 对使用到的字符串进行加密, 主要有如下方法:

  • 编码混淆
  • 加密处理, 在使用时才动态解密字符串

普通方式定义字符串

1
2
3
4
5
6
public class Hello {
    public static void main(String[] argc){
        String str = "Hello World";
        System.out.println(str);
    }
}

反编译的smali代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.method public static main([Ljava/lang/String;)V
    .registers 3
    .param p0, "argc"    # [Ljava/lang/String;
 
    .line 5
    const-string v0, "Hello World"
 
    .line 6
    .local v0, "str":Ljava/lang/String;
    sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
 
    invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
 
    .line 7
    return-void
.end method

可以看到 "Hello World" 被直接内联到代码里面

 

编码混淆

1
2
3
4
5
6
7
public class Hello {
    public static void main(String[] argc){
        byte[] strBytes = {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 0x72, 0x6c, 0x64};
        String str = new String(strBytes);
        System.out.println(str);
    }
}

反编译的smali代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.method public static main([Ljava/lang/String;)V
    .registers 4
    .param p0, "argc"    # [Ljava/lang/String;
 
    .line 5
    const/16 v0, 0xa
 
    new-array v0, v0, [B
 
    fill-array-data v0, :array_12
 
    .line 6
    .local v0, "strBytes":[B
    new-instance v1, Ljava/lang/String;
 
    invoke-direct {v1, v0}, Ljava/lang/String;-><init>([B)V
 
    .line 7
    .local v1, "str":Ljava/lang/String;
    sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;
 
    invoke-virtual {v2, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
 
    .line 8
    return-void
 
    :array_12
    .array-data 1
        0x48t
        0x65t
        0x6ct
        0x6ct
        0x6ft
        0x57t
        0x6ft
        0x72t
        0x6ct
        0x64t
    .end array-data
.end method

对apk中的文件进行校验

  • 一般对apk中的dex & 签名 & apk本身进行校验
  • 这种校验办法最终还是通过返回 true/false, 容易被暴力破解

对apk中的dex文件进行校验

在演示过程中可以直接创建crc_value资源保存dex中的crc,然后与dex文件中的crc进行比对 在实践应用中可以从服务端远程获取crc校验值进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*校验Dex CRC值*/
private void verifyDex(){
    // 获取String.xml中的value, 实践中应该联网获取用于比对的CRC值
    Long dexCrc = Long.parseLong(this.getString(R.string.crc_value));
    String apkPath = this.getPackageCodePath();
    Log.d(TAG, "verifyDex: PackageCodePath: " + apkPath);
    try {
        ZipFile zipFile = new ZipFile(apkPath);
        ZipEntry dexEntry = zipFile.getEntry("classes.dex");
        // 计算classes.dex的crc
        long dexEntryCrc = dexEntry.getCrc();
        Log.d(TAG, "verifyDex: dexEntryCrc: "+ dexEntryCrc);
        if(dexCrc == dexEntryCrc){
            Log.d(TAG, "dex has not been modified'");
        }else{
            Log.d(TAG, "dex has been modified");
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

对apk中的apk进行校验

与dex校验不同, apk检验必须把计算好的Hash值放到网络服务器, 因为对APK的任何改动都会影响到最后的Hash值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*校验APK MD5值
*与dex校验不同, apk检验必须把计算好的Hash值放到网络服务器, 因为对APK的任何改动都会影响到最后的Hash
* */
private void verifyApk(){
    // /data/app/com.example.myapplication-1/base.apk
    String apkPath = this.getPackageCodePath();
    MessageDigest msgDigest;
    try {
        // 获取apk并计算MD5值
        msgDigest = MessageDigest.getInstance("MD5");
        byte[] bytes = new byte[4096];
        int count;
        FileInputStream fis = new FileInputStream(new File(apkPath));
        while((count = fis.read(bytes)) > 0){
            msgDigest.update(bytes, 0, count);
        }
        // 计算出MD5值
        BigInteger bInt = new BigInteger(1, msgDigest.digest());
        String md5 = bInt.toString(16);
        fis.close();
        Log.d(TAG, "verifyApk: md5: "+ md5);
        // 与服务端的MD5值进行对比
        // code ....
    }catch (Exception e){
        e.printStackTrace();
    }

对apk中的签名进行校验

  • 每个APK都会经过开发者的证书签名, 如果破解者对APK进行二次打包会用自己的签名
  • 打包。必须要在在服务端存储MD5, 因为签名中含有对文件的hash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*获取签名的Md5*/
public void verifySignature(){
    String packageName = this.getPackageName();
    PackageManager pm = this.getPackageManager();
    PackageInfo pi;
    String md5 = "";
    try{
        pi = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
        Signature[] s = pi.signatures;
        // 计算出MD5值
        MessageDigest msgDigest = MessageDigest.getInstance("MD5");
        msgDigest.reset();
        msgDigest.update(s[0].toByteArray());
        BigInteger bInt = new BigInteger(1, msgDigest.digest());
        md5 = bInt.toString();
    }catch(Exception e){
        e.printStackTrace();
    }
    Log.d(TAG, "verifySignature: md5" + md5);
    // 与服务端的MD5值进行对比
    // code ....
}

签名校验工具: keytool , 解压apk/META_INF/CERT.RSA

1
keytool -printcert -file CERT.RSA

Android加固-壳

Android安全人鱼对APK加固主要有以下几个方面

  • 将可执行代码dex文件加密, 能够动态解密并执行
  • 能够检测当前状态是被调试, 反调试技术

APK加固主要有如下技术点:

  • 使用ClassLoader动态加载dex文件
  • 设计傀儡dex替换原dex, 动态加载dex
  • 傀儡dex中的各种地方添加反调试代码

dex动态加载

基础知识

JVM ClassLoader 类加载器
  • JAVA虚拟机类加载过程: ClassLoader负责把需要的类加载到内存中,然后进行实例化

    image-20220530105819361

  • 类加载时机

    image-20220530132322920

安卓 ClassLoader类加载器
  • 安卓Dalvik/APT虚拟机类加载过程: 通过对android源码的分析, ClassLoader工作原理类似,用于加载jar和Dex

  • 类加载器的种类和个数:一个运行的APP的生命周期中存在着多个ClassLoader, 所有的ClassLoader呈现出树状结构

    • BootClassLoader: Android系统启动时创建, 用于加载Framework层级的系统类
    • PathClassLoader: 每个App有自己的类, App启动的时候会创建自己的ClassLoader实例, 一般 PathClassLoader.getParent()==BootClassLoader

    image-20220428214241228

image-20220530132835022

 

代码查看ClassLoader层级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showClassloaderTree();
    }
 
    private static final String TAG = "ayuan-log";
    public void showClassloaderTree(){
        int count = 0;
        ClassLoader classLoader = getClassLoader();
        while(classLoader != null){
            Log.d(TAG, "[onCreate ClassLoader]: "+count+": "+classLoader.toString());
            classLoader = classLoader.getParent();
            count++;
        }
    }
}

输出

1
2
[onCreate ClassLoader]: 0: dalvik.system.PathClassLoader
[onCreate ClassLoader]: 1: java.lang.BootClassLoader@1ca27e8

一个应用至少存在两个ClassLoader: BootClassLoader 和 PathClassLoader

双亲代理模型
  • 创建自己的ClassLoader: 通常用于动态加载外部的dex文件中的Class, 需要创建的ClassLoader实例加入到ClassLoader树中(双亲代理模型)

  • 双亲代理模型加载类的特点和作用:

    • JVM中ClassLoader通过defineClass方法加载jar中的Class

    • Android中ClassLoader通过loadClass方法加载Dex中的Class, 根据ClassLoader.loadClass 源码分析,loadClass类加载流程如下

      1. 首先, 检查当前ClassLoader 是否已经加载该类了, 有就返回
      2. 如果没有, 查询 Parent_ClassLoader 是否加载该类了, 有就返回
      3. 如果都没有加载过该类, 就通过 findCLass 通过名称加载该类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      protected Class<?> loadClass(String name, boolean resolve)
              throws ClassNotFoundException
      {
          // 1. 首先, 检查当前ClassLoader 是否已经加载该类了, 有就返回
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              try {
                  // 2. 如果没有, 查询 Parent_ClassLoader 是否加载该类了, 有就返回
                  if (parent != null) {
                      c = parent.loadClass(name, false);
                  } else {
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }
              // 3. 如果都没有加载过该类, 就通过 findCLass 通过名称加载该类
              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // to find the class.
                  c = findClass(name);
              }
          }
          return c;
      }

      这种实现方式导致了如下类加载的特点:

      1. 如果一个类被父ClassLoader加载过, 那么不会被再次加载, 一个类只会被加载过一次
      2. 同一个类的定义:两个类若是相同的类,等价于 PackageName.ClassName + ClassLoader 相同。 作用:
        1. 可以防止用户类冒充核心类, 因为核心类在BootClassLoader加载
        2. 当加载一个新的dex时, 若Class重复则不会加载新的dex中的Class, 这时就需要新建一个与原ClassLoader没有继承关系的新ClassLoader

DexClassLoader 动态加载简单dex文件

测试:把一个简单类(不包含四大组件)的Dex加载并调用

  • ClassLoader是一个抽象类, 具体实现的类加载器有:

    • DexClassLoader 可以加载 jar/apk/dex/sd卡中未安装的apk
    • PathClassLoader 系统中已经安装的apk
  • 动态加载简单dex(不包含四大组件), 步骤如下:

    1. 创建一个类, 制作dex文件
    2. 在项目中加载dex文件
制作傀儡dex文件

dex代码:创建一个class动态设置view(没有四大组件), 方便动态加载成功时显示

  1. 创建新项目

  2. 新建类: 包名 右键--> new Class: MyTestClass

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MyTestClass {
        public void createView(Activity ac){
            // 创建布局,设置参数
            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.WRAP_CONTENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT );
            params.topMargin = 0;
            params.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
            // 动态创建TextView
            TextView tv = new TextView(ac);
            tv.setText("MyTestClass Textview");
            // 添加textVIew 到Activity中
            ac.addContentView(tv, params);
        }
    }
  3. 编译, 然后反编译apk

    1
    java -jar apktool_latest.jar d app-debug.apk

    在逆向工程中把 MyTestClass.smali 单独提取出来, 删除其他所有不需要的smali文件

    image-20220418203555018

    将仅含MyTestClass.smali的项目工程重新编译为dex文件

    1
    ayuan@ayuan-X5:~/Downloads/demo/app-debug$ java -jar smali.jar ./smali

    将生成的out.dex改名为 mytestclass.dex

  4. 在main文件夹 右键--> new Directory: assets (自定义资源目录) --> 把mytest.dex 复制过来

    image-20220418204309400

  5. 然后MyTestClass.java 就没用了,直接删除即可, 这样就保持了相同包名和项目结构

动态加载dex文件

确认总体代码功能

  1. 拷贝自定义资源(assets)的dex到程序目录下
  2. 创建一个DexClassLoader, 加载dex
  3. 调用加载dex中的class方法

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: MainActivity启动");
        loadDex();
    }
 
    private static final String TAG = "ayuan-log";
 
    private void loadDex() {
        Log.d(TAG, "loadDex: 开始加载");
        //1. 拷贝自定义资源(assets)的dex到程序目录下
        String dexPath = copyDex("mytestclass.dex");
        //2. 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
        DexClassLoader dexClassLoader = getLoader(dexPath);
        //3. 通过反射, 调用加载dex中的class方法
        execClassMethod(
                dexClassLoader,         // classLoader
                "com.example.myapplication.MyTestClass", //类名
                "createView",           // 方法名
                this);                  // 参数: MainActivity.this
    }
 
    private void execClassMethod(DexClassLoader dexClassLoader,  // 类所属的ClassLoader
                                 String className,              // 函数所处的类名
                                 String methodName,             // 函数名
                                 Activity as ) {                // 函数参数, 此处传入了MainActivity.this 用于显示
        try{
            // 类类型
            Class testDex = dexClassLoader.loadClass(className);
            // 调用构造函数(无参构造), 获取实例 getConstructor(参数类型数组)
            Constructor cons = testDex.getConstructor(new Class[]{});
            // 调用构造方法创建对象, newInstance(参数数组)
            Object instance = cons.newInstance(new Object[]{});
            // 获取成员方法
            Method methodTest = testDex.getDeclaredMethod(methodName,
                    new Class[]{Activity.class});
            // 取消java 访问检查
            methodTest.setAccessible(true);
            // 调用方法 invoke(实例, 方法参数数组)
            methodTest.invoke(instance, new Object[]{as});
        }catch(Exception e){
            e.printStackTrace();
        }
    }
 
    // 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
    private DexClassLoader getLoader(String dexPath) {
        DexClassLoader dexClassLoader = new DexClassLoader(
                dexPath,            // 要加载的Dex文件路径
                getCacheDir().toString(),   // 优化之后的文件路径
                null,       // native 库路径, 由于没有使用到native,null即可
                getClassLoader()        // 父ClassLoader
        );
        Log.d(TAG, "getLoader: "+dexClassLoader);
        return dexClassLoader;
    }
 
    // 拷贝assets/文件 到 app/files/
    private String copyDex(String dexName) {
        // 获取assets目录管理器
        AssetManager as = getAssets();
        // 目的地址
        String path = getFilesDir() + File.separator + dexName;
        // /data/user/0/com.example.myapplication/files/mytestclass.dex
        Log.d(TAG, "copyDex: path: " + path);
        // 将文件拷贝到目的地址
        try {
            // 创建文件流
            FileOutputStream out = new FileOutputStream(path);
            // 打开文件 assets/dexName
            InputStream is = as.open(dexName);
            // 循环读取并写入文件
            byte[] buffer = new byte[1024];
            int len =0;
            while((len = is.read(buffer))!= -1){
                out.write(buffer, 0, len);
            }
            // 关闭文件
            out.close();
        }catch(Exception e){
            e.printStackTrace();
            return "";
        }
        return path;
    }
}

image-20220421101246593

加密壳的实现

含Activity的dex动态加载

  1. 之前完成了对简单dex文件(不包含四大组件)的动态加载
  2. 对含有Activity的dex文件的加载, 注意在AndroidManifest.xml中有对Activty的注册和布局文件

加壳的流程: 修改application:name 优先启动壳, 壳加载 源dex 并调用Activity。 由于第一步很简单, 所以本节主要测试源dex加载和调用功能

制作傀儡dex
  1. 新建空项目: Empty Activity

  2. 创建一个含有Activity的dex文件

    新建一个Activity: 包名 右键 --> New Activity--> Empty Activity:Main2Activity

    image-20220421104230711

    修改main布局文件

    • activity_main.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="vertical"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_margin="5dp"
          android:padding="5dp"
          >
       
          <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal">
              <Button
                  android:id="@+id/btn1"
                  android:onClick="onClick"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_weight="1"
                  android:text="启动Activity" />
       
          </LinearLayout>
       
      </LinearLayout>

      对应的按钮事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public class MainActivity extends AppCompatActivity {
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
          }
       
          public void onClick(View view) {
              Intent intent = new Intent(this, Main2Activity.class);
              startActivity(intent);
          }
      }
  • activity_main2.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5dp"
        android:padding="5dp"
        >
     
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            >
     
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="第二个Activity"
                android:textSize="20dp" />
        </LinearLayout>
    </LinearLayout>

    编译运行, 功能正常

  1. 编译程序,得到apk, 使用Android Killer/ apktool反编译得到项目

    1
    java -jar apktool_latest.jar d app-debug.apk

    在逆向工程中把 Main2Activity.smali 单独提取出来, 删除其他所有不需要的smali文件

    image-20220418203555018

    将仅含Main2Activity.smali的项目工程重新编译为dex文件

    1
    java -jar smali.jar ./smali_classes3

    将生成的out.dex改名为 Main2Activity.dex

  2. 在main文件夹 右键--> new Directory: assets (自定义资源目录) --> 把mytest.dex 复制过来

    image-20220418204309400

    然后Main2Activity.java 就没用了,直接删除即可(注意不能在IDE中删除,这样会把AndroidManifest中的Activity声明给删除) , 这样就保持了相同包名和项目结构

动态加载dex文件

确认总体代码功能

  1. 拷贝自定义资源(assets)的dex到程序目录下
  2. 创建一个DexClassLoader, 加载dex
  3. 调用加载dex中的Activity

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "ayuan-log";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: MainActivity启动");
    }
 
    public void onClick(View view) {
        loadDex();
    }
 
    private void loadDex() {
        Log.d(TAG, "loadDex: 开始加载");
        //1. 拷贝自定义资源(assets)的dex到程序目录下
        String dexPath = copyDex("Main2Activity.dex");
        //2. 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
        DexClassLoader dexClassLoader = getLoader(dexPath);
        //3. 通过反射, 启动dex中的Activity
        execClassMethod(
                dexClassLoader,         // classLoader
                "com.example.myapplication.Main2Activity", //类名
                "onCreate",           // 方法名
                this);                  // 参数: MainActivity.this
    }
 
    private void execClassMethod(DexClassLoader dexClassLoader,  // 类所属的ClassLoader
                                 String className,              // 函数所处的类名
                                 String methodName,             // 函数名
                                 Activity as ) {                // 函数参数, 此处传入了MainActivity.this 用于显示
        try{
            // 类类型
            Class clazz = dexClassLoader.loadClass(className);
            Intent intent  = new Intent(MainActivity.this, clazz);
            startActivity(intent);
            // 调用构造函数(无参构造), 获取实例 getConstructor(参数类型数组)
//            Constructor cons = clazz.getConstructor(new Class[]{});
            // 调用构造方法创建对象, newInstance(参数数组)
//            Object instance = cons.newInstance(new Object[]{});
            // 获取成员方法
//            Method methodTest = clazz.getDeclaredMethod(methodName, new Class[]{Activity.class});
            // 取消java 访问检查
//            methodTest.setAccessible(true);
            // 调用方法 invoke(实例, 方法参数数组)
//            methodTest.invoke(instance, new Object[]{as});
        }catch(Exception e){
            e.printStackTrace();
        }
    }
 
    // 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
    private DexClassLoader getLoader(String dexPath) {
        DexClassLoader dexClassLoader = new DexClassLoader(
                dexPath,            // 要加载的Dex文件路径
                getCacheDir().toString(),   // 优化之后的文件路径
                null,       // native 库路径, 由于没有使用到native,null即可
                getClassLoader()        // 父ClassLoader
        );
        Log.d(TAG, "getLoader: "+dexClassLoader);
        return dexClassLoader;
    }
 
    // 拷贝assets/文件 到 app/files/
    private String copyDex(String dexName) {
        // 获取assets目录管理器
        AssetManager as = getAssets();
        // 目的地址
        String path = getFilesDir() + File.separator + dexName;
        // /data/user/0/com.example.myapplication/files/Main2Activity.dex
        Log.d(TAG, "copyDex: path: " + path);
        // 将文件拷贝到目的地址
        try {
            // 创建文件流
            FileOutputStream out = new FileOutputStream(path);
            // 打开文件 assets/dexName
            InputStream is = as.open(dexName);
            // 循环读取并写入文件
            byte[] buffer = new byte[1024];
            int len =0;
            while((len = is.read(buffer))!= -1){
                out.write(buffer, 0, len);
            }
            // 关闭文件
            out.close();
        }catch(Exception e){
            e.printStackTrace();
            return "";
        }
        return path;
    }
}

此时编译运行会报错, 无法实例化Main2Activity

1
2
Unable to instantiate activity ComponentInfo{com.example.myapplication/com.example.myapplication.Main2Activity}:
java.lang.ClassNotFoundException: Didn't find class "com.example.myapplication.Main2Activity" on path: DexPathList[[zip file "/data/app/com.example.myapplication-1/base.apk"]

这是因为虽然我们用自己的DexClassLoader加载了Main2Activity, 但是startActivity等系统函数依然无法实例化Main2Activity, 系统函数是从程序的PathClassLoader中寻找并加载类的

 

解决方法:

  • 直接使用PathClassLoader加载我们需要的类

  • 使用ClassLoader替换掉PathClassLoader

    分析Android源码, 利用java的反射机制修改保存的PathClassLoader

  • 把自己的DexClassLoader 改为 PathClassLoader的父ClassLoader

树状结构 BootClassLoader <-- PathClassLoader <-- 自定义的DexClassLoader

分析apk启动的过程
程序启动过程分析
 

参考

 

安卓源码分析: 应用程序的启动过程 https://blog.csdn.net/Luoshengyang/article/details/6689748

 

整个应用程序的启动过程要执行很多步骤,但是整体来看,主要分为以下五个阶段:

  1. Step1 - Step 11:Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity;

    1. Launcher.startActivitySafely: 点击应用图标时,Launcher就会对应的应用程序启动起来
    2. Activity.startActivity: 调用startActivityForResult来进一步处理
    3. Activity.startActivityForResult:定义成员变量 Activity.Intrumentation : 监控应用程序和系统的交互; Activity.mMainThread: Launcher应用程序运行的进程; 调用Instrumentation.execStartActivity 来进一步处理
    4. Instrumentation.execStartActivity:调用ActivityManagerProxy.startActivity
    5. ActivityManagerProxy.startActivity: 通过Binder调用ActivityManagerService.startActivity
    6. ActivityManagerService.startActivity:调用mMainStack.startActivityMayWait
    7. ActivityStack.startActivityMayWait: 定义ActivityInfo aInfo 局部变量保存MainActivity的信息
    8. ActivityStack.startActivityLocked: 定义ActivityRecord r 创建要启动的Activity的信息
    9. ActivityStack.startActivityUncheckedLocked: 根据launchMode和Stack 新建Task
    10. Activity.resumeTopActivityLocked 根据栈顶Activity是否为本Activity来启动Activity
    11. ActivityStack.startPausingLocked: Launcher进入Paused状态
  2. Step 12 - Step 16:ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;

    1. ApplicationThreadProxy.schedulePauseActivity: 通过Binder进程间通信机制进入到ApplicationThread.schedulePauseActivity函数
    2. ApplicationThread.schedulePauseActivity: 调用queueOrSendMessage
    3. ActivityThread.queueOrSendMessage: 整理信息, 发送给ActivityThread.H.handleMessage
    4. ActivityThread.H.handleMessage:
    5. ActivityThread.handlePauseActivity: 做了三个事情:1. 通过调用performUserLeavingActivity函数来Activity用户要离开它了;2. 调用performPauseActivity函数来调用Activity.onPause函数;3. 它通知ActivityManagerService,这个Activity已经进入Paused状态了,ActivityManagerService现在可以完成未竟的事情,即启动MainActivity了
  3. Step 17 - Step 24:Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;

    1. ActivityManagerProxy.activityPaused:
    2. ActivityManagerService.activityPaused
    3. ActivityStack.activityPaused
    4. ActivityStack.completePauseLocked
    5. ActivityStack.resumeTopActivityLokced
    6. ActivityStack.startSpecificActivityLocked 检查是否已经有以process + uid命名的进程存在
    7. ActivityManagerService.startProcessLocked: 调用Process.start接口来创建一个新的进程,新的进程会导入android.app.ActivityThread类,并且执行它的main函数
    8. ActivityThread.main 这个函数在进程中创建一个ActivityThread实例,然后调用它的attach函数,接着就进入消息循环了,直到最后进程退出。attach函数调用ActivityManagerProxy的attachApplication函数
  4. Step 25 - Step 27:ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;

    1. ActivityManagerProxy.attachApplication 通过Binder驱动程序,最后进入ActivityManagerService的attachApplication函数中
    2. ActivityManagerService.attachApplication 转发给attachApplicationLocked
    3. ActivityManagerService.attachApplicationLocked 对app的成员进行初始化, 最后调用mMainStack.realStartActivityLocked执行真正的Activity启动操作。
  5. Step 28 - Step 35:ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。

    1. ActivityStack.realStartActivityLocked
    2. ApplicationThreadProxy.scheduleLaunchActivity 通过Binder驱动程序进入到ApplicationThread的scheduleLaunchActivity函数中
    3. ApplicationThread.scheduleLaunchActivity 创建一个ActivityClientRecord实例,并且初始化它的成员变量,然后调用ActivityThread类的queueOrSendMessage函数进一步处理。
    4. ActivityThread.queueOrSendMessage 把消息内容放在msg中,然后通过mH把消息分发出去,这里的成员变量mH我们在前面已经见过,消息分发出去后,最后会调用H类的handleMessage函数。
    5. ActivityThread.H.handleMessage 调用ActivityThread类的handleLaunchActivity函数进一步处理。
    6. ActivityThread.handleLaunchActivity 这里首先调用performLaunchActivity函数来加载这个Activity类,即shy.luo.activity.MainActivity,然后调用它的onCreate函数,最后回到handleLaunchActivity函数时,再调用handleResumeActivity函数来使这个Activity进入Resumed状态,即会调用这个Activity的onResume函数,这是遵循Activity的生命周期的。
    7. ActivityThread.performLaunchActivity 函数前面是收集要启动的Activity的相关信息,主要package和component信息, 然后通过ClassLoader将shy.luo.activity.MainActivity类加载进来,接下来是创建Application对象,这是根据AndroidManifest.xml配置文件中的Application标签的信息来创建的, 通过mInstrumentation的callActivityOnCreate函数来间接调用MainActivity的onCreate函数
    8. MainActivity.onCreate 这样,MainActivity就启动起来了,整个应用程序也启动起来了。
onCreate 函数调用分析
 

之前分析了整个app的调用过程, 现在详细分析onCreate调用栈,了解安卓启动过程中的关键数据

 

image-20220428102526529

1
2
3
4
5
6
7
8
9
10
11
12
13
onCreate:11, MainActivity (com.example.myapplication)
performCreate:6679, Activity (android.app)
callActivityOnCreate:1118, Instrumentation (android.app)
performLaunchActivity:2618, ActivityThread (android.app)
handleLaunchActivity:2726, ActivityThread (android.app)
-wrap12:-1, ActivityThread (android.app)
handleMessage:1477, ActivityThread$H (android.app)
dispatchMessage:102, Handler (android.os)
loop:154, Looper (android.os)
main:6119, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:886, ZygoteInit$MethodAndArgsCaller (com.android.internal.os)
main:776, ZygoteInit (com.android.internal.os)

从外向内部分析,

  1. ActivityThread.main 这个函数在进程中创建一个ActivityThread实例,然后调用它的attach函数,接着就进入消息循环了 Looper.loop(),直到最后进程退出。
  2. Looper.loop() 调用 msg.target.dispatchMessage(msg) 分发消息
  3. Handler.dispatchMessage(msg) 调用 子类的 handleMessage(msg) 继续分发消息
  4. ActivityThread$H.handleMessage 启动活动LAUNCH_ACTIVITY, ActivityClientRecord r记录Activity状态, 其中r.packageInfo 存储大量数据, 并传输到ActivityThread.handleLaunchActivity
  5. ActivityThread.handleLaunchActivity 紧接着调用 ActivityThread.performLaunchActivity
  6. ActivityThread.performLaunchActivity 对ActivityClientRecord r中的数据进行一系列操作,然后调用 mInstrumentation.callActivityOnCreate 启动Activity.onCreate()
  7. Instrumentation.callActivityOnCreate 进入消息回调, 进入Activity.performCreate, 然后调用onCreate
定位apk进程中的PathClassLoader 变量

如下显示如何一步一步的找到 PathClassLoader 变量, 方便后面修改

  1. ActivityThread 唯一实例 被保存在静态变量ActivityThread.sCurrentActivityThread

    image-20220428221450527

    该唯一实例可以通过调用静态函数直接获取

    ActivityThread.currentActivityThread

    image-20220428221729280

  2. ActivityThread 类中有一个实例成员变量 sCurrentActivityThread.mBoundApplication 变量类型为 AppBindData, 如下

    image-20220429141530905

    image-20220429141325036

  3. 含有包含了Apk信息的 sCurrentActivityThread.mBoundApplication.info 变量类型为LoadedApk

    image-20220429141853100

    含有包含了Apk PathClassLoader的变量 sCurrentActivityThread.mBoundApplication.info.mClassLoader

    值得注意的是, 在java中, 如果一个类没有定义toClone 函数自定义深拷贝, 默认类的赋值如 Test t1= new Test; Test t2=t1; 那么t2 与 t1 中的所有成员依然指向同一个引用。 所以修改了mClassLoader, 那么所有的mClassLoader都被修改了, 参考如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      Test t1 = new Test();
      Test t2 = t1;
      t1.str = "hello ayuan";
      t1.number = 2;
      // onCreate: t1.str: hello ayuant2.str: hello ayuan
      Log.d(TAG, "onCreate: t1.str: " + t1.str + "t2.str: " + t2.str);
      // onCreate: t1.number: 2t2.number: 2
      Log.d(TAG, "onCreate: t1.number: " + t1.number + "t2.number: " + t2.number);
      Log.d(TAG, "onCreate: ------------------------------------------------");
    }

    序列化来实现深拷贝

逐步获取如下

1
2
3
4
1. private static ActivityThread sCurrentActivityThread = ActivityThread.currentActivityThread()
2. private ActivityThread.AppBinData mBoundApplication = sCurrentActivityThread 反射 mBoundApplication
3. private LoadedApk info = mBoundApplication 反射 info
4. private ClassLoader mClassLoader = info 反射 mClassLoader

修改代码,替换classLoader为自定义的,这样startactivity 才能调用自定义的dex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "ayuan-log";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: MainActivity启动");
    }
 
    public void onClick(View view) {
        loadDex();
    }
 
    private void loadDex() {
        Log.d(TAG, "loadDex: 开始加载");
        //1. 拷贝自定义资源(assets)的dex到程序目录下
        String dexPath = copyDex("Main2Activity.dex");
        //2. 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
        DexClassLoader dexClassLoader = getLoader(dexPath);
        //3. 通过反射, 启动dex中的Activity
        execClassMethod(
                dexClassLoader,         // classLoader
                "com.example.myapplication.Main2Activity", //类名
                "onCreate",           // 方法名
                this);                  // 参数: MainActivity.this
    }
 
    private void execClassMethod(DexClassLoader dexClassLoader,  // 类所属的ClassLoader
                                 String className,              // 函数所处的类名
                                 String methodName,             // 函数名
                                 Activity as ) {                // 函数参数, 此处传入了MainActivity.this 用于显示
        try{
            // 替换PathClassLoader为自定义classLoader, startActivity 底层会从PathClassLoader寻找加载的类
            replaceClassLoader(dexClassLoader);
            // 获取类类型, 构造Intent
            Class clazz = dexClassLoader.loadClass(className);
            Intent intent  = new Intent(MainActivity.this, clazz);
            startActivity(intent);
            // 调用构造函数(无参构造), 获取实例 getConstructor(参数类型数组)
//            Constructor cons = clazz.getConstructor(new Class[]{});
            // 调用构造方法创建对象, newInstance(参数数组)
//            Object instance = cons.newInstance(new Object[]{});
            // 获取成员方法
//            Method methodTest = clazz.getDeclaredMethod(methodName, new Class[]{Activity.class});
            // 取消java 访问检查
//            methodTest.setAccessible(true);
            // 调用方法 invoke(实例, 方法参数数组)
//            methodTest.invoke(instance, new Object[]{as});
        }catch(Exception e){
            e.printStackTrace();
        }
    }
 
    private void replaceClassLoader(DexClassLoader dexClassLoader) {
        try {
            //1. private static ActivityThread sCurrentActivityThread = ActivityThread.currentActivityThread()
            // 获取类类型
            Class clzActivityThread = Class.forName("android.app.ActivityThread");
            // 获取静态方法
            Method methodCurrentActivityThread = clzActivityThread.getDeclaredMethod("currentActivityThread");
            // 调用静态方法, 返回静态成员变量
            Object sCurrentActivityThread = methodCurrentActivityThread.invoke(null, new Object[]{});
            //2. private ActivityThread$AppBindData mBoundApplication = sCurrentActivityThread 反射 mBoundApplication
            // 获取类类型
            Class clzAppBinData = Class.forName("android.app.ActivityThread$AppBindData");
            // 获取字段类型
            Field fieldBoundApplication = clzActivityThread.getDeclaredField("mBoundApplication");
            // 获取实例sCurrentActivityThread 对应的字段对象
            fieldBoundApplication.setAccessible(true);
            Object mBoundApplication = fieldBoundApplication.get(sCurrentActivityThread);
            //3. private LoadedApk info = mBoundApplication 反射 info
            // 获取字段类类型
            Class clzLoadedApk = Class.forName("android.app.LoadedApk");
            // 获取字段类型
            Field fieldInfo = clzAppBinData.getDeclaredField("info");
            // 获取实例mBoundApplication 对应的字段对象
            fieldInfo.setAccessible(true);
            Object info = fieldInfo.get(mBoundApplication);
            //4. private ClassLoader mClassLoader = info 反射 mClassLoader
            // 获取类类型
            Class clzClassLoader = Class.forName("java.lang.ClassLoader");
            // 获取字段类型
            Field fieldClassLoader = clzLoadedApk.getDeclaredField("mClassLoader");
            // 获取实例mBoundApplication 对应的字段对象
            fieldClassLoader.setAccessible(true);
            fieldClassLoader.set(info, dexClassLoader);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
    }
 
    // 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
    private DexClassLoader getLoader(String dexPath) {
        DexClassLoader dexClassLoader = new DexClassLoader(
                dexPath,            // 要加载的Dex文件路径
                getCacheDir().toString(),   // 优化之后的文件路径
                null,       // native 库路径, 由于没有使用到native,null即可
                getClassLoader()        // 父ClassLoader
        );
        Log.d(TAG, "getLoader: "+dexClassLoader);
        return dexClassLoader;
    }
 
    // 拷贝assets/文件 到 app/files/
    private String copyDex(String dexName) {
        // 获取assets目录管理器
        AssetManager as = getAssets();
        // 目的地址
        String path = getFilesDir() + File.separator + dexName;
        // /data/user/0/com.example.myapplication/files/Main2Activity.dex
        Log.d(TAG, "copyDex: path: " + path);
        // 将文件拷贝到目的地址
        try {
            // 创建文件流
            FileOutputStream out = new FileOutputStream(path);
            // 打开文件 assets/dexName
            InputStream is = as.open(dexName);
            // 循环读取并写入文件
            byte[] buffer = new byte[1024];
            int len =0;
            while((len = is.read(buffer))!= -1){
                out.write(buffer, 0, len);
            }
            // 关闭文件
            out.close();
        }catch(Exception e){
            e.printStackTrace();
            return "";
        }
        return path;
    }
}

运行

 

image-20220429160631564

 

测试Activity加载功能完成

手工加固APK

一般手工加密壳的实现(推荐后一种,因为代码实现的时候比较方便)

  1. 设计傀儡dex(启动器): apk启动时首先启动傀儡dex文件(壳代码), 在使用自定义的ClassLoader加载原apk的classes.dex后替换apk的 PathClassLoader, 如此即可正常调用原dex的onCreate
  2. 获取待加密的apk, 例如demo.apk
  3. 使用 apktool 反编译demo.apk, 修改 AndroidManifest.xml的 application.name ,指向 壳dex: DummyApplication
  4. 将原apk中的classes.dex放入assets目录并改名 assets/src.dex
  5. 使用apktool 重新打包修改后的apk目录
  6. 替换重打包后的classes.dex为傀儡dex
  7. 为新打包的apk 进行签名

优化步骤如下(直接植入smali代码,省略了把傀儡dex重新打包的步骤)

  1. 设计傀儡dex(启动器): apk启动时首先启动傀儡dex文件(壳代码), 在使用自定义的ClassLoader加载原apk的classes.dex后替换apk的 PathClassLoader, 如此即可正常调用原dex的onCreate
  2. 获取待加密的apk, 例如demo.apk
  3. 使用 apktool 反编译demo.apk, 修改 AndroidManifest.xml的 application.name ,指向 壳dex: DummyApplication
  4. 将原apk中的classes.dex放入assets目录并改名 assets/src.dex
  5. 将反编译的smali代码替换为傀儡dex的smali代码
  6. 使用apktool 重新打包修改后的apk目录
  7. 为新打包的apk 进行签名
设计傀儡dex(启动器)
  1. 之前已经成功实现了一个启动器,可以加载目标dex的Activity
  2. 在Apk启动流程中, 在创建并启动MainActivity之前, 会先创建Application对象,并调用其attachBaseContext方法以及onCreate方法, 可以在其中加载原dex

新建一个工程: DummyDex

 

image-20220503211024854

 

设置禁用mutidex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
 
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
 
// Enabling multidex support.
multiDexEnabled false
}
...
}

在清单文件中添加Application对象: .DummyApplication

 

image-20220503211249890

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DummyApplication extends Application {
    private static final String TAG = "ayuan-log";
    // 在attachBaseContext方法中添加加载原dex和替换ClassLoader的代码
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

在DummyApplication: attachBaseContext方法中添加加载原dex和替换ClassLoader的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class DummyApplication extends Application {
    private static final String TAG = "ayuan-log";
    // 在attachBaseContext方法中添加加载原dex和替换ClassLoader的代码
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        loadDex();
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
    private void loadDex() {
        Log.d(TAG, "DummyApplication.attachBaseContext: 开始加载");
        //1. 拷贝自定义资源(assets/src.dex)的dex到程序目录下(app/files/)
        String dexPath = copyDex("src.dex");
        //2. 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
        DexClassLoader dexClassLoader = getLoader(dexPath);
        //3. 替换PathClassLoader为自定义classLoader, startActivity 底层会从PathClassLoader寻找加载的类
        replaceClassLoader(dexClassLoader);
        Log.d(TAG, "DummyApplication.attachBaseContext: 加载结束");
    }
 
 
    private void replaceClassLoader(DexClassLoader dexClassLoader) {
        try {
            //1. private static ActivityThread sCurrentActivityThread = ActivityThread.currentActivityThread()
            // 获取类类型
            Class clzActivityThread = Class.forName("android.app.ActivityThread");
            // 获取静态方法
            Method methodCurrentActivityThread = clzActivityThread.getDeclaredMethod("currentActivityThread");
            // 调用静态方法, 返回静态成员变量
            Object sCurrentActivityThread = methodCurrentActivityThread.invoke(null, new Object[]{});
            //2. private ActivityThread$AppBindData mBoundApplication = sCurrentActivityThread 反射 mBoundApplication
            // 获取类类型
            Class clzAppBinData = Class.forName("android.app.ActivityThread$AppBindData");
            // 获取字段类型
            Field fieldBoundApplication = clzActivityThread.getDeclaredField("mBoundApplication");
            // 获取实例sCurrentActivityThread 对应的字段对象
            fieldBoundApplication.setAccessible(true);
            Object mBoundApplication = fieldBoundApplication.get(sCurrentActivityThread);
            //3. private LoadedApk info = mBoundApplication 反射 info
            // 获取字段类类型
            Class clzLoadedApk = Class.forName("android.app.LoadedApk");
            // 获取字段类型
            Field fieldInfo = clzAppBinData.getDeclaredField("info");
            // 获取实例mBoundApplication 对应的字段对象
            fieldInfo.setAccessible(true);
            Object info = fieldInfo.get(mBoundApplication);
            //4. private ClassLoader mClassLoader = info 反射 mClassLoader
            // 获取类类型
            Class clzClassLoader = Class.forName("java.lang.ClassLoader");
            // 获取字段类型
            Field fieldClassLoader = clzLoadedApk.getDeclaredField("mClassLoader");
            // 获取实例mBoundApplication 对应的字段对象
            fieldClassLoader.setAccessible(true);
            fieldClassLoader.set(info, dexClassLoader);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
    }
 
    // 创建一个加载了该Dex文件的Dex类加载器, 可以反射获取其中的类和函数
    private DexClassLoader getLoader(String dexPath) {
        DexClassLoader dexClassLoader = new DexClassLoader(
                dexPath,            // 要加载的Dex文件路径
                getCacheDir().toString(),   // 优化之后的文件路径
                null,       // native 库路径, 由于没有使用到native,null即可
                getClassLoader()        // 父ClassLoader
        );
        Log.d(TAG, "getLoader: "+dexClassLoader);
        return dexClassLoader;
    }
 
    // 拷贝assets/文件 到 app/files/
    private String copyDex(String dexName) {
        // 获取assets目录管理器
        AssetManager as = getAssets();
        // 目的地址
        String path = getFilesDir() + File.separator + dexName;
        // /data/user/0/com.example.myapplication/files/Main2Activity.dex
        Log.d(TAG, "copyDex: path: " + path);
        // 将文件拷贝到目的地址
        try {
            // 创建文件流
            FileOutputStream out = new FileOutputStream(path);
            // 打开文件 assets/dexName
            InputStream is = as.open(dexName);
            // 循环读取并写入文件
            byte[] buffer = new byte[1024];
            int len =0;
            while((len = is.read(buffer))!= -1){
                out.write(buffer, 0, len);
            }
            // 关闭文件
            out.close();
        }catch(Exception e){
            e.printStackTrace();
            return "";
        }
        return path;
    }
}

使用apktool 反编译 傀儡dex, 保留smali代码

1
java -jar apktool_latest.jar d dummydex.apk

image-20220503220344017

如果傀儡dex需要dex文件格式, 会生成out.dex

1
java -jar smali.jar ./smali_classes3

然后放到项目根目录下即可

image-20220718201457718

手工加固

自己创建一个demo.apk(不演示了), 以demo.apk为例开始手工加固

demo注意要是单dex文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
 
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
 
// Enabling multidex support.
multiDexEnabled false
}
...
}
 

反编译apk

1
java -jar apktool_latest.jar d demo.apk

修改 AndroidManifest.xml的 application.name ,指向 壳dex: DummyApplication (可以从smali代码中找到类路径)

 

image-20220504003247673

 

读取APK中的dex文件到assets目录, 并改名为src.dex

 

image-20220504004215784

 

删除原有smali文件夹,拷贝傀儡dex的smali 文件到smali目录中(必须删除无用代码,防止重复

 

image-20220504005030592

 

使用apktool重新打包, 生成的apk在demo/dist

1
java -jar apktool_latest.jar b demo

签名

1
java -jar signapk.jar testkey.x509.pem testkey.pk8 demo.apk update_signed.apk

安装测试

1
adb install update_signed.apk

代码实现加固

代码加固流程, 与手工加固流程相同(直接植入smali代码,省略了把傀儡dex重新打包的步骤)

  1. 设计傀儡dex(启动器): apk启动时首先启动傀儡dex文件(壳代码), 在使用自定义的ClassLoader加载原apk的classes.dex后替换apk的 PathClassLoader, 如此即可正常调用原dex的onCreate
  2. 获取待加密的apk的路径, 例如 /home/ayuan/demo.apk , 使用 apktool 反编译demo.apk
  3. 修改 AndroidManifest.xml的 application.name ,指向 壳dex: DummyApplication
  4. 将原apk中的classes.dex放入assets目录并改名 assets/src.dex
  5. 将反编译的smali代码替换为傀儡dex的smali代码
  6. 使用apktool 重新打包修改后的apk目录
  7. 为新打包的apk 进行签名
设计傀儡dex

参考: 设计傀儡dex)

反编译

java 命令执行的函数Runtime.getRuntime().exec有如下重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 直接拼接命令
调用程序
Process proc =Runtime.getRuntime().exec("ls -al");
 
# 字符串数组形式
Windows下调用命令
String [] cmd={"cmd","/C","copy exe1 exe2"};
Process proc =Runtime.getRuntime().exec(cmd);
Linux下调用系统命令就要改成下面的格式, 实测不需要/bin/sh -c
String [] cmd={"ln -s exe1 exe2"};
Process proc =Runtime.getRuntime().exec(cmd);
 
# 设置工作目录exec(命令, 环境变量, 工作目录)
Process proc =Runtime.getRuntime().exec("exeflie",null, new File("workpath"));
  1. IDEA新建工程: ApkPacker

    image-20220505152827778

    Main.java 测试HelloWorld

    1
    2
    3
    4
    5
    6
    7
    public class Main {
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            // 向屏幕输出文本:
            System.out.println("Hello, world!");
        }
    }
  1. 封装命令执行工具类: CmdUtils.java

    image-20220505154035825

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.InputStreamReader;
     
    public class CmdUtils {
        // 输入:命令
        // 输出:命令的输出
        public static void runCMD(String cmdline){
            try {
                // 执行命令
                Process process  = Runtime.getRuntime().exec(cmdline);
                // Process process  = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmdline});
                // 等待命令行结束执行
                process.waitFor();
                // 获取返回数据
                BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
                // 循环输出返回数据
                String line = null;
                while((line = br.readLine())!=null){
                    System.out.println(line);
                }
                if(br != null){
                    br.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 输入:命令, 工作目录
        // 输出:命令的输出
        public static void runCMD(String cmdline, String dir) {
            try {
                // 执行命令
                Process process = Runtime.getRuntime().exec(cmdline, null, new File(dir));
                // Process process  = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmdline});
                // 等待命令行结束执行
                process.waitFor();
                // 获取返回数据
                BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
                // 循环输出返回数据
                String line = null;
                while ((line = br.readLine()) != null) {
                    System.out.println(line);
                }
                if (br != null) {
                    br.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    测试工具类的功能

    Main.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Main {
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            // 向屏幕输出文本:
            CmdUtils.runCMD("ls -al");
            // 工作目录测试
            CmdUtils.runCMD("pwd", "/home/ayuan/");
        }
    }
  2. 对apk进行反编译的功能

    复制apktool.jar到项目目录下

    image-20220505163548559

    测试功能

    1
    2
    3
    4
    5
    6
    7
    public class Main {
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            // 向屏幕输出文本:
            CmdUtils.runCMD("java -jar apktool.jar");
        }
    }

    运行, 成功返回结果

    image-20220505212352640

  3. 封装文件类: FileUtils,用于获取文件的文件名和拓展名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class FileUtils {
        /*
        * 输入: /home/ayuan/demo.apk
        * 输出: /home/ayuan/demo
        */
        public static String getPathNoEx(String filepath){
            if((filepath != null) && (filepath.length() > 0)){
                int dot = filepath.lastIndexOf('.');
                if((dot > -1) && (dot < (filepath.length()))){
                    return filepath.substring(0, dot);
                }
            }
            return filepath;
        }
        /*
         * 输入: /home/ayuan/demo.apk
         * 输出: apk
         */
        public static String getPathExtensionName(String filepath){
            if((filepath != null) && (filepath.length() > 0)){
                int dot = filepath.lastIndexOf('.');
                if((dot > -1) && (dot < (filepath.length()))){
                    return filepath.substring(dot+1);
                }
            }
            return filepath;
        }
        /* 获取工作路径
        * 输出: 当前工作路径
        */
        public static String getWorkpath(){
            return System.getProperty("user.dir");
        }
    }
  4. 打包功能

    Main.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Main {
        // 要加壳的文件名, 在程序主目录中
        static String fileName = "demo.apk";
     
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            // 构造 demo.apk的绝对路径
            String dir = FileUtils.getWorkpath();
            String filePath = dir + File.separator + fileName;
            // apk反编译, 默认解包文件夹名:去掉后缀的文件名
            CmdUtils.runCMD("java -jar apktool.jar d "+ filePath);
            // apk重新打包
            CmdUtils.runCMD("java -jar apktool.jar b "+
                    FileUtils.getPathNoEx(filePath) +
                    " -o app.apk");
        }
    }

    运行测试, 生成app.apk

修改 AndroidManifest

修改 AndroidManifest.xml的 application.name ,有就修改,没有就创建,指向 壳dex: DummyApplication

  1. 由于需要对xml文件进行操作, 使用xml解析库: org.dom4j:dom4j:2.0.0-RC1

    右键项目 --> open module setting (记得勾选下载到这个项目模块)

    image-20220505222435096

    导出启用

    image-20220505222804881

  2. 新建封装XML文件处理类: XMLUtils.java

    image-20220505223120143

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    import org.dom4j.Attribute;
    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    import org.dom4j.io.XMLWriter;
     
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileWriter;
    import java.util.List;
     
    public class XMLUtils {
        /*
        * 输入: xml文件路径
        * 输出: Document 文件对象
        * */
        public static Document getDocument(String path){
            try {
                // 获取Document对象
                SAXReader reader = new SAXReader();
                FileInputStream in = new FileInputStream(path);
                Document doc = reader.read(in);
                return doc;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        /*输入: Document文件对象, 节点名
        * 输出: Element 节点对象
        * */
        public static Element getElement(Document doc, String name){
            Element root = doc.getRootElement();
            if(root.getName().equals(name))
                return root;
            return getChildElement(root,name);
        }
        /*输入: Element父节点, 子节点名
        * 输出: Element 节点对象
        * */
        public static Element getChildElement(Element node, String name){
            List<Element> childNodes = node.elements();
            for (Element e: childNodes){
                if(e.getName().equals(name))
                    return e;
            }
            return null;
        }
        /*输入: Element 节点对象, 属性名
        * 输出: Attribute 属性对象
        * */
        public static Attribute getAttribute(Element node, String name){
            List<Attribute> attrs = node.attributes();
            for(Attribute attr: attrs){
                if(attr.getName().equals(name))
                    return attr;
            }
            return null;
        }
        /*输入: AndroidManifest.xml文件路径
        * 输出:修改AndroidManifest.xml中的application节点的android:name, 指向傀儡dex*/
        public static void modifyApplication(String xmlPath, String dexPath){
            Document doc = XMLUtils.getDocument(xmlPath);
            Element eleApplication = XMLUtils.getElement(doc, "application");
            Attribute application_name = XMLUtils.getAttribute(eleApplication, "name");
            // 判断是否存在application:name , 存在则修改,不存在则创建
            if(application_name == null){
                // 不存在, 添加
                eleApplication.addAttribute("android:name", dexPath);
            } else{
                // 存在, 修改,保存原有的application:name
                String old_name = application_name.getValue();
                Element eleMata = eleApplication.addElement("meta-data");
                eleMata.addAttribute("android:name", "SCR_APPLICATION");
                eleMata.addAttribute("android:value", old_name);
                // 设置新值
                application_name.setValue(dexPath);
            }
            try {
                // 保存修改的document到AndroidManifest.xml,
                File file = new File(xmlPath);
                // 删除重写
                if(file.exists())
                    file.delete();
                file.createNewFile();
                XMLWriter out = new XMLWriter(new FileWriter(file));
                out.write(doc);
                out.flush();
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    运行测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import java.io.File;
     
    public class Main {
        // 要加壳的文件名, 在程序主目录中
        static String fileName = "demo.apk";
        // 傀儡dex的包名.类名
        static String dexPath = "com.example.dummydex.DummyApplication";
     
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            String dir = FileUtils.getWorkpath(); // 工作目录 /home/ayuan/
            String filePath = dir + File.separator + fileName; // apk绝对路径 /home/ayuan/demo.apk
            String projectPath = dir + File.separator + FileUtils.getPathNoEx(fileName); // apktool 反编译路径 /home/ayuan/demo
            String xmlPath = projectPath+"/AndroidManifest.xml";   // /home/ayuan/demo/AndroidManifest.xml
            // 1. 反编译apk
            CmdUtils.runCMD("java -jar apktool.jar d "+ filePath);
            // 2. 解析AndroidManifest.xml并获取application 节点, 指向 壳dex: DummyApplication
            XMLUtils.modifyApplication(xmlPath, dexPath);
     
            // apk重新打包
    //        CmdUtils.runCMD("java -jar apktool.jar b "+
    //                FileUtils.getPathNoEx(filePath) +
    //                " -o app.apk");
        }
    }

    运行两次结果如下, 第一次出现如下1的android:name, 第二次出现meta-data

    image-20220506145004763

读取APK中的dex文件到assets目录

将原apk中的classes.dex放入assets目录并改名 assets/src.dex

 

封装Zip工具类: ZipUtils.java, 从apk中读取classes.dex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
 
public class ZipUtils {
    /*输入: apk路径=/home/ayuan/demo.apk, 要解压的文件名=classes.dex, 目的文件夹=/home/ayuan/demo/assets, 解压后的文件名=src.dex
    * 输出: 无,解压特定文件到指定文件夹*/
    public static void copyFileFromZip(String zipPath, String filename, String newPath, String newFilename){
        try {
            // 创建Zip输入流
            ZipFile zf = new ZipFile(zipPath);
            InputStream in = new BufferedInputStream(new FileInputStream(zipPath));
            ZipInputStream zin = new ZipInputStream(in);
            ZipEntry ze;
            // 遍历Zip文件
            while((ze = zin.getNextEntry())!=null){
                if(ze.isDirectory()){
                    // 只检测根目录下的文件,直接跳过
                    continue;
                }else{
                    // 检测文件名
                    if(!ze.getName().equals(filename)){
                        continue;
                    }
                    // 检测目标文件夹是否存在,不存在就创建
                    File file = new File(newPath);
                    if(!file.exists()) file.mkdir();
                    // 循环读取写入
                    FileOutputStream outputStream = new FileOutputStream(newPath + File.separator + newFilename);
                    InputStream inputStream = zf.getInputStream(ze);
                    int len = 0;
                    byte[] bytes = new byte[1024];
                    while((len = inputStream.read(bytes))!=-1){
                        outputStream.write(bytes, 0, len);
                    }
                    outputStream.close();
                    break;
                }
            }
            zin.closeEntry();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试 Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.File;
 
public class Main {
    // 要加壳的文件名, 在程序主目录中
    static String fileName = "demo.apk";
    // 傀儡dex的包名.类名
    static String dexPath = "com.example.dummydex.DummyApplication";
 
    // main 是static, 要调用的函数有必须是static
    public static void main(String[] args) {
        String dir = FileUtils.getWorkpath(); // 工作目录 /home/ayuan/
        String filePath = dir + File.separator + fileName; // apk绝对路径 /home/ayuan/demo.apk
        String projectPath = dir + File.separator + FileUtils.getPathNoEx(fileName); // apktool 反编译路径 /home/ayuan/demo
        String xmlPath = projectPath+File.separator+"AndroidManifest.xml";   // /home/ayuan/demo/AndroidManifest.xml
        String assetsPath = projectPath+File.separator+"assets";   // /home/ayuan/demo/assets
        String dexName = "src.dex";
        // 1. 反编译apk
        CmdUtils.runCMD("java -jar apktool.jar d "+ filePath);
        // 2. 解析AndroidManifest.xml并获取application 节点, 指向 壳dex: DummyApplication
        XMLUtils.modifyApplication(xmlPath, dexPath);
        // 3. 解压classes.dex到 assets/src.dex
        ZipUtils.copyFileFromZip(filePath, "classes.dex", assetsPath, dexName);
        // apk重新打包
//        CmdUtils.runCMD("java -jar apktool.jar b "+
//                FileUtils.getPathNoEx(filePath) +
//                " -o app.apk");
    }
}

运行, 成功复制

 

image-20220506160612007

拷贝傀儡dex的smali 文件到smali目录中
  1. 把傀儡dex的smali代码复制,并改名为 dummyDexSmali

    image-20220506211843457

  2. 继续封装工具类 FileUtils.java , 用于1.递归删除原有的smali文件夹, 2.复制傀儡dex的smali文件夹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
     
    public class FileUtils {
        /*
        * 输入: /home/ayuan/demo.apk
        * 输出: /home/ayuan/demo
        */
        public static String getPathNoEx(String filepath){
            if((filepath != null) && (filepath.length() > 0)){
                int dot = filepath.lastIndexOf('.');
                if((dot > -1) && (dot < (filepath.length()))){
                    return filepath.substring(0, dot);
                }
            }
            return filepath;
        }
        /*
         * 输入: /home/ayuan/demo.apk
         * 输出: apk
         */
        public static String getPathExtensionName(String filepath){
            if((filepath != null) && (filepath.length() > 0)){
                int dot = filepath.lastIndexOf('.');
                if((dot > -1) && (dot < (filepath.length()))){
                    return filepath.substring(dot+1);
                }
            }
            return filepath;
        }
        /* 获取工作路径
        * 输出: 当前工作路径
        */
        public static String getWorkpath(){
            return System.getProperty("user.dir");
        }
        /*
        输入: 文件夹路径=/home/ayuan/demo/smali
        输出: 无, 递归删除文件夹及文件
         */
        public static void deleteFolder(String path){
            File file = new File(path);
            deleteFile(file);
        }
     
        private static void deleteFile(File file) {
            if(file.exists()){
                if(file.isFile()){
                    // 如果是文件,直接删除
                    file.delete();
                }else{
                    // 文件夹, 先递归删除文件
                    File[] files = file.listFiles();
                    for(File item: files){
                        deleteFile(item);
                    }
                    // 删除文件夹
                    file.delete();
                }
            }else{
                System.out.println("指定的文件路径不存在");
            }
        }
        /*
        输入: 源文件夹=/home/ayuan/smali, 目标文件夹=/home/ayuan/demo/smali
        输出: 无, 递归复制源文件夹到目标文件夹
         */
        public static void copyFolder(String src, String target){
            // 创建目标文件夹
            (new File(target)).mkdirs();
            // 获取当前目录下的所有文件名
            String[] filesname = (new File(src)).list();
            File file = null;
            for(String filename: filesname){
                // 拼接节点名
                if(src.endsWith(File.separator)){
                    file = new File(src + filename);
                }else {
                    file = new File(src + File.separator+ filename);
                }
                if(file.isFile()){
                    // 如果是文件, 则直接复制
                    copyFile(file.getAbsolutePath(), target + File.separator+ file.getName());
                }else{
                    // 目录递归
                    copyFolder(src + File.separator+filename, target+File.separator + filename);
                }
            }
     
        }
        private static void copyFile(String src, String target){
            try {
                FileInputStream input = new FileInputStream(src);
                FileOutputStream output = new FileOutputStream(target);
                byte[] bytes = new byte[1024];
                int len;
                while((len= input.read(bytes)) != -1){
                    output.write(bytes, 0, len);
                }
                output.flush();
                output.close();
                input.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    功能测试 Main.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import java.io.File;
     
    public class Main {
        // 要加壳的文件名, 在程序主目录中
        static String fileName = "demo.apk";
        // 傀儡dex的包名.类名
        static String dexPath = "com.example.dummydex.DummyApplication";
     
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            String dir = FileUtils.getWorkpath(); // 工作目录 /home/ayuan/
            String filePath = dir + File.separator + fileName; // apk绝对路径 /home/ayuan/demo.apk
            String projectPath = dir + File.separator + FileUtils.getPathNoEx(fileName); // apktool 反编译路径 /home/ayuan/demo
            String xmlPath = projectPath+File.separator+"AndroidManifest.xml";   // /home/ayuan/demo/AndroidManifest.xml
            String assetsPath = projectPath+File.separator+"assets";   // /home/ayuan/demo/assets
            String demoSmaliPath = projectPath+File.separator+"smali";   // /home/ayuan/demo/smali
            String dummydexSmaliPath = dir+File.separator+"dummyDexSmali";   // /home/ayuan/dummyDexSmali
            // 1. 反编译apk
            CmdUtils.runCMD("java -jar apktool.jar d "+ filePath);
            // 2. 解析AndroidManifest.xml并获取application 节点, 指向 壳dex: DummyApplication
            XMLUtils.modifyApplication(xmlPath, dexPath);
            // 3. 解压classes.dex到 assets/src.dex
            ZipUtils.copyFileFromZip(filePath, "classes.dex", assetsPath, "src.dex");
            // 4. 拷贝傀儡dex的smali 文件到smali目录中
            // 删除原有的 /home/ayuan/demo/smali 文件夹
            FileUtils.deleteFolder(demoSmaliPath);
            // 拷贝傀儡dex的smali 文件到smali目录中
            FileUtils.copyFolder(dummydexSmaliPath, demoSmaliPath);
        }
    }

    运行完后, 成功将dummydex的smali文件夹复制过去了

    image-20220506211556558

跟踪调试, 确保删除smali

对重打包的apk文件进行签名完成加固
  1. 把sign签名工具文件夹复制到项目目录下

    image-20220506214045955

    image-20220506214057285image-20220506214045955

  2. 测试代码 Main.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import java.io.File;
     
    public class Main {
        // 要加壳的文件名, 在程序主目录中
        static String fileName = "demo.apk";
        // 傀儡dex的包名.类名
        static String dexPath = "com.example.dummydex.DummyApplication";
     
        // main 是static, 要调用的函数有必须是static
        public static void main(String[] args) {
            String dir = FileUtils.getWorkpath(); // 工作目录 /home/ayuan/
            String filePath = dir + File.separator + fileName; // apk绝对路径 /home/ayuan/demo.apk
            String projectPath = dir + File.separator + FileUtils.getPathNoEx(fileName); // /home/ayuan/demo apktool反编译路径
            String xmlPath = projectPath+File.separator+"AndroidManifestimage-20220506214045955.xml";   // /home/ayuan/demo/AndroidManifest.xml
            String assetsPath = projectPath+File.separator+"assets";   // /home/ayuan/demo/assets
            String demoSmaliPath = projectPath+File.separator+"smali";   // /home/ayuan/demo/smali
            String dummydexSmaliPath = dir+File.separator+"dummyDexSmali";   // /home/ayuan/dummyDexSmali
            String outPath = dir + File.separator + "out.apk"// /home/ayuan/out.apk
            String outSignPath = dir + File.separator + "signed.apk"// /home/ayuan/signed.apk
            // 1. 反编译apk
            CmdUtils.runCMD("java -jar apktool.jar d "+ filePath);
            // 2. 解析AndroidManifest.xml并获取application 节点, 指向 壳dex: DummyApplication
            XMLUtils.modifyApplication(xmlPath, dexPath);
            // 3. 解压classes.dex到 assets/src.dex
            ZipUtils.copyFileFromZip(filePath, "classes.dex", assetsPath, "src.dex");
            // 4. 拷贝傀儡dex的smali 文件到smali目录中
            // 删除原有的 /home/ayuan/demo/smali 文件夹
            FileUtils.deleteFolder(demoSmaliPath);
            // 拷贝傀儡dex的smali 文件到smali目录中
            FileUtils.copyFolder(dummydexSmaliPath, demoSmaliPath);
            // 5. 重打包
            CmdUtils.runCMD("java -jar apktool.jar b "+projectPath+" -o out.apk");
            // 6. 签名
            CmdUtils.runCMD("java -jar signapk.jar testkey.x509.pem testkey.pk8 "+outPath+" "+outSignPath, "sign");
            // 7. 清理
            FileUtils.deleteFolder(projectPath);
            FileUtils.deleteFile(new File(outPath));
        }
    }

    加固成功, 输出signed.apk

完整项目代码: https://github.com/overturncat/reverse_android/tree/master/ApkPacker


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞11
打赏
分享
最新回复 (7)
雪    币: 3777
活跃值: (5535)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huangjw 2022-8-1 08:44
2
0
图片看不到
雪    币: 1988
活跃值: (2750)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 3 2022-8-3 10:37
3
0
请整理一下,图片已经挂了
雪    币: 1058
活跃值: (550)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
TrumpWY 2022-8-6 16:58
4
0
nice ths
雪    币: 495
活跃值: (3594)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
我只是没人要 2022-8-28 02:22
5
0
LoadedApk 方式的不太靠谱吧, 遇到分包不太好处理
雪    币: 429
活跃值: (665)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
mb_svatpqwc 2022-9-30 10:00
6
0
我只是没人要 LoadedApk 方式的不太靠谱吧, 遇到分包不太好处理
是的,这种方法需要手动关闭apk的分包功能,请问有比较好的处理方法吗
雪    币: 62
活跃值: (518)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2022-10-1 23:50
7
0
厉害了 感谢分享
游客
登录 | 注册 方可回帖
返回