灵魂一问,如何彻底防止反编译?(1)

  • 需要加密的APK(源程序APK)
  • 壳程序APK(负责解密APK工作)
  • 加密工具(将源APK进行加密和壳程序的DEX合并)

主要步骤 用加密算法对源程序APK进行加密,再将其与壳程序APK的DEX文件合并生成新的DEX文件,最后替换壳程序中的原DEX文件即可。得到新的APK也叫做脱壳程序APK,它已经不是一个完整意义上的APK程序了,它的主要工作是:负责解密源程序APK,然后加载APK,让其正常运行起来。 在这个过程中需要了解的知识是:如何将源程序APK和壳程序APK进行合并 这需要了解DEX文件的格式,下面简单介绍一下:

现在只要关注其中三个部分:

  • checksum(文件校验码)使用alder32算法校验文件,除去magic、checksum外余下的所有文件区域,用于检查文件错误。
  • signature 使用SHA-1算法hash出去magic、checksum和signature外余下的所有文件区域,用于唯一识别本文件。
  • file_size DEX文件大小。

我们需要将加密之后的源程序APK文件写入到DEX中,那么就需要修改checksum,因为它的值和文件内容有关。signature也是一样,也是唯一识别文件的算法,还有DEX文件的大小。 还需要一个操作,就是标注加密之后的源程序APK文件的大小,因为运行解密的时候,需要知道APK的大小,才能正确得到源程序APK。这个值直接放到文件的末尾就可以了。 修改之后的DEX文件的格式如下:

知道了原理,下面就是代码实现了。这里有三个工程:

  • 源程序项目(需要加密的APK)
  • 壳项目(解密源程序APK和加载APK)
  • 对源APK进行加密和壳项目的DEX的合并

二.项目案例

下面先来看一下源程序

1.需要加密的源程序项目:SourceApk


需要一个Application类,这个到后面说为什么需要: MyApplication.java

package com.example.sourceapk;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i(“demo”, “source apk onCreate:” + this);
}
}

就是打印一下onCreate方法。 MainActivity.java

package com.example.sourceapk;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

TextView content = new TextView(this);
content.setText(“I am Source Apk”);
content.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MainActivity.this, SubActivity.class);
startActivity(intent);
}});
setContentView(content);

Log.i(“demo”, “app:”+getApplicationContext());
}
}

2.加壳程序项目:DexPackTool


加壳程序其实就是一个Java工程,它的工作就是加密源程序APK,然后将其写入到壳程序的DEX文件里,修改文件头,得到一个新的DEX文件。 看一下代码:

package com.example.packdex;

public class mymain {
public static void main(String[] args) {
try {
File payloadSrcFile = new File(“files/SourceApk.apk”); // 需要加壳的源程序
System.out.println(“apk size:”+payloadSrcFile.length());
File packDexFile = new File(“files/SourceApk.dex”); // 壳程序dex
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile)); // 以二进制形式读出源apk,并进行加密处理
byte[] packDexArray = readFileBytes(packDexFile); // 以二进制形式读出dex
/* 合并文件 */
int payloadLen = payloadArray.length;
int packDexLen = packDexArray.length;
int totalLen = payloadLen + packDexLen + 4; // 多出4字节是存放长度的
byte[] newdex = new byte[totalLen]; // 申请了新的长度
// 添加解壳代码
System.arraycopy(packDexArray, 0, newdex, 0, packDexLen); // 先拷贝dex内容
// 添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, packDexLen, payloadLen); // 再在dex内容后面拷贝apk的内容
// 添加解壳数据长度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4); // 最后4字节为长度
// 修改DEX file size文件头
fixFileSizeHeader(newdex);
// 修改DEX SHA1 文件头
fixSHA1Header(newdex);
// 修改DEX CheckSum文件头
fixCheckSumHeader(newdex);

String str = “files/classes.dex”; // 创建一个新文件
File file = new File(str);
if (!file.exists()) {
file.createNewFile();
}

FileOutputStream localFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex); // 将新计算出的二进制dex数据写入文件
localFileOutputStream.flush();
localFileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

// 直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){
for (int i = 0; i < srcdata.length; i++) {
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}

}

加密算法很简单,只是对每个字节进行异或一下

这里是为了简单,所以就用了很简单的加密算法,其实为了增加破解难度,我们应该使用更高效的加密算法,同时最好将加密操作放到native层去做。

这里需要两个输入文件:

  • 源程序APK文件:SourceApk.apk
  • 壳程序的DEX文件:SourceApk.dex

第一个文件就是源程序项目编译之后的APK文件,第二个文件是下面要讲的第三个项目:壳程序项目中的classes.dex文件,修改名称之后得到。

3.壳程序项目:PackApk


先来了解一下壳程序项目的工作:

  • 通过反射置换android.app.ActivityThread中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序,另一方面以原mClassLoader为父节点,这就保证即加载了源程序,又没有放弃原先加载的资源与系统代码。 关于这部分内容不了解的可以看一下Android动态加载之免安装运行程序这篇文章。

  • 找到源程序的Application,通过反射建立并运行。 这里需要注意的是,我们现在是加载一个完整的APK,让他运行起来。一个APK运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类,所以我们必须找到解密之后的源程序APK的Application类,运行它的onCreate方法,这样源程序APK才开始它的运行生命周期。后面会说如何得到源程序APK的Application类:使用meta标签进行设置。

下面看一下整体流程:

下面看一下代码: ProxyApplication.java

  • 得到壳程序APK中的DEX文件,然后从这个文件中得到源程序APK进行解密、加载

// 这是context赋值
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
// 创建两个文件夹payload_odex、payload_lib,私有的,可写的文件目录
File odex = this.getDir(“payload_odex”, MODE_PRIVATE);
File libs = this.getDir(“payload_lib”, MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + “/payload.apk”;
File dexFile = new File(apkFileName);
Log.i(“demo”, “apk size:”+dexFile.length());
if (!dexFile.exists())
{
dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk
// 读取程序classes.dex文件
byte[] dexdata = this.readDexFileFromApk();

// 分离出解壳后的apk文件已用于动态加载
this.splitPayLoadFromDex(dexdata);
}
// 配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
“android.app.ActivityThread”, “currentActivityThread”,
new Class[] {}, new Object[] {});//获取主线程对象
String packageName = this.getPackageName();//当前apk的包名
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
“android.app.ActivityThread”, currentActivityThread,
“mPackages”);
WeakReference wr = (WeakReference) mPackages.get(packageName);
// 创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
“android.app.LoadedApk”, wr.get(), “mClassLoader”));
//把当前进程的mClassLoader设置成了被加壳apk的DexClassLoader
RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mClassLoader”,
wr.get(), dLoader);

Log.i(“demo”,“classloader:”+dLoader);
try{
Object actObj = dLoader.loadClass(“com.example.sourceapk.MainActivity”);
Log.i(“demo”, “actObj:”+actObj);
}catch(Exception e){
Log.i(“demo”, “activity:”+Log.getStackTraceString(e));
}
} catch (Exception e) {
Log.i(“demo”, “error:”+Log.getStackTraceString(e));
e.printStackTrace();
}
}

这里需要注意的一个问题,就是我们需要找到一个时机,就是在壳程序还没有运行起来的时候,来加载源程序的APK,执行它的onCreate方法,那么这个时机不能太晚,不然的话,就是运行壳程序,而不是源程序了。查看源码我们知道。Application中有一个方法:attachBaseContext这个方法,它在Application的onCreate方法执行前就会执行了,所以我们的工作就需要在这里进行。
A) 从APK中获取到DEX文件

/**

  • 从apk包里面获取dex文件内容(byte)
  • @return
  • @throws IOException
    */
    private byte[] readDexFileFromApk() throws IOException {
    ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
    ZipInputStream localZipInputStream = new ZipInputStream(
    new BufferedInputStream(new FileInputStream(
    this.getApplicationInfo().sourceDir)));
    while (true) {
    ZipEntry localZipEntry = localZipInputStream.getNextEntry();
    if (localZipEntry == null) {
    localZipInputStream.close();
    break;
    }
    if (localZipEntry.getName().equals(“classes.dex”)) {
    byte[] arrayOfByte = new byte[1024];
    while (true) {
    int i = localZipInputStream.read(arrayOfByte);
    if (i == -1)
    break;
    dexByteArrayOutputStream.write(arrayOfByte, 0, i);
    }
    }
    localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
    return dexByteArrayOutputStream.toByteArray();
    }

B) 从壳程序DEX中得到源程序APK文件

/**

  • 释放被加壳的apk文件,so文件
  • @param data
  • @throws IOException
    */
    private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
    int ablen = apkdata.length;
    //取被加壳apk的长度 这里的长度取值,对应加壳时长度的赋值都可以做些简化
    byte[] dexlen = new byte[4];
    System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
    ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
    DataInputStream in = new DataInputStream(bais);
    int readInt = in.readInt();
    System.out.println(Integer.toHexString(readInt));
    byte[] newdex = new byte[readInt];
    //把被加壳的源程序apk内容拷贝到newdex中
    System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
    //这里应该加上对于apk的解密操作,若加壳是加密处理的话

// 对源程序Apk进行解密
newdex = decrypt(newdex);

// 写入apk文件
File file = new File(apkFileName);
try {
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(newdex);
localFileOutputStream.close();
} catch (IOException localIOException) {
throw new RuntimeException(localIOException);
}

// 分析被加壳的apk文件
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(file)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry(); // 这个也遍历子目录
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
// 取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib)
String name = localZipEntry.getName();
if (name.startsWith(“lib/”) && name.endsWith(“.so”)) {
File storeFile = new File(libPath + “/”

  • name.substring(name.lastIndexOf(‘/’)));
    storeFile.createNewFile();
    FileOutputStream fos = new FileOutputStream(storeFile);
    byte[] arrayOfByte = new byte[1024];
    while (true) {
    int i = localZipInputStream.read(arrayOfByte);
    if (i == -1)
    break;
    fos.write(arrayOfByte, 0, i);
    }
    fos.flush();
    fos.close();
    }
    localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
    }

C) 解密源程序APK

//直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
for(int i=0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;

面试复习路线,梳理知识,提升储备

自己的知识准备得怎么样,这直接决定了你能否顺利通过一面和二面,所以在面试前来一个知识梳理,看需不需要提升自己的知识储备是很有必要的。

关于知识梳理,这里再分享一下我面试这段时间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

  • 架构师筑基必备技能
  • Android高级UI与FrameWork源码
  • 360°全方面性能调优
  • 解读开源框架设计思想
  • NDK模块开发
  • 微信小程序
  • Hybrid 开发与Flutter

知识梳理完之后,就需要进行查漏补缺,所以针对这些知识点,我手头上也准备了不少的电子书和笔记,这些笔记将各个知识点进行了完美的总结:

Android开发七大模块核心知识笔记

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?

1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

[外链图片转存中…(img-TMaOWXLL-1715340043010)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值