Android APK加壳技术的研究

本文主要参考博客: Android APK加壳技术方案【2】 APK加壳【1】初步方案实现详解 由于之前没有接触过安卓编程,所以即便有两篇这么详细的教程,但是还是走了不少弯路,都折腾了大概一个星期左右。而且两个博主都没有放出demo,所以就想回顾一下这个学习的过程,并给出一个Demo。

1.配置环境

本文的编译环境如下: Android Studio 1.2.1.1 JDK 1.7.0_79 SDK NDK Android Studio都出了这么久了,应该都没有什么bug了;JDK网上的人都说不要选择java8,用java7就够了;然后SDK是必须的,NDK是用来编译底层c/c++的共享库的。 建议大家把上面两个博主的前后几篇文章都看一下,因为原理和实现都已经描述的很清楚了。 现在说一下自己的程序,文中有两个应用,一个是com.droider.crackme0201程序,是作为被加壳的应用。还有一个是com.droider.dexunshell程序,是作为动态解壳的应用。原理是运行时先加载dexunshell程序,然后通过dexunshell程序动态加载crackme0201程序,通过建立一系列的反射,使crackme0201正常运行。 先说一下自己走过的坑,这里两个程序编译的build-tool要一致,就是app文件夹里的build.gradle(project里面有两个build.gradle,一个在app文件夹里面,一个在app文件夹的那一层目录下。)。确保compile编译工具一致,我是第一个程序crackme0201建立后,还升级了SDK,导致建立dexunshell程序时的编译工具变了,自己给自己挖坑。

2.应用加壳

先说一下加壳的程序,假如两个程序都编译完成后,要把crackme0201的apk放到dexunshell应用的dex程序里面,由于修改dex文件,也要修改dex_header的checksum、signature等信息。这个加壳程序可以用其他语言编写,文章的示例是用java写的。 建立一个DexShellTool.java的文件,填入一下代码。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;

public class DexShellTool {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File(“I:/payload.apk”);
File unShellDexFile = new File(“I:/unshell.dex”);
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));
byte[] unShellDexArray = readFileBytes(unShellDexFile);
int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;
byte[] newdex = new byte[totalLen];
//添加解壳代码
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
//添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, unShellDexLen,
payloadLen);
//添加解壳数据长度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);
//修改DEX file size文件头
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件头
fixSHA1Header(newdex);
//修改DEX CheckSum文件头
fixCheckSumHeader(newdex);
String str = “I:/classes.dex”;
File file = new File(str);
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream localFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex);
localFileOutputStream.flush();
localFileOutputStream.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//直接返回数据,读者可以添加自己加密方法
private static byte\[\] encrpt(byte\[\] srcdata){
    return srcdata;
}

private static void fixCheckSumHeader(byte\[\] dexBytes) {
    Adler32 adler = new Adler32();
    adler.update(dexBytes, 12, dexBytes.length - 12);
    long value = adler.getValue();
    int va = (int) value;
    byte\[\] newcs = intToByte(va);
    byte\[\] recs = new byte\[4\];
    for (int i = 0; i < 4; i++) {
        recs\[i\] = newcs\[newcs.length - 1 - i\];
        System.out.println(Integer.toHexString(newcs\[i\]));
    }
    System.arraycopy(recs, 0, dexBytes, 8, 4);
    System.out.println(Long.toHexString(value));
    System.out.println();
}

public static byte\[\] intToByte(int number) {
    byte\[\] b = new byte\[4\];
    for (int i = 3; i >= 0; i--) {
        b\[i\] = (byte) (number % 256);
        number >>= 8;
    }
    return b;
}

private static void fixSHA1Header(byte\[\] dexBytes)
        throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(dexBytes, 32, dexBytes.length - 32);
    byte\[\] newdt = md.digest();
    System.arraycopy(newdt, 0, dexBytes, 12, 20);
    String hexstr = "";
    for (int i = 0; i < newdt.length; i++) {
        hexstr += Integer.toString((newdt\[i\] & 0xff) + 0x100, 16)
                .substring(1);
    }
    System.out.println(hexstr);
}

private static void fixFileSizeHeader(byte\[\] dexBytes) {
    byte\[\] newfs = intToByte(dexBytes.length);
    System.out.println(Integer.toHexString(dexBytes.length));
    byte\[\] refs = new byte\[4\];
    for (int i = 0; i < 4; i++) {
        refs\[i\] = newfs\[newfs.length - 1 - i\];
        System.out.println(Integer.toHexString(newfs\[i\]));
    }
    System.arraycopy(refs, 0, dexBytes, 32, 4);
}

private static byte\[\] readFileBytes(File file) throws IOException {
    byte\[\] arrayOfByte = new byte\[1024\];
    ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
    FileInputStream fis = new FileInputStream(file);
    while (true) {
        int i = fis.read(arrayOfByte);
        if (i != -1) {
            localByteArrayOutputStream.write(arrayOfByte, 0, i);
        } else {
            return localByteArrayOutputStream.toByteArray();
        }
    }
}

}

通过javac DexshellTool.java命令编译,通过java DexshellTool运行。代码中的payload.apk为crackme0201改程序的apk,unshell.dex为dexunshell程序的DEX文件,生成的classes.dex为拼接后的DEX文件。

2.被加壳应用

本文给出的Demo只是project里app文件夹的压缩包,由于使用了ndk编译共享库,为了编译成功,需要在local.properties里加入ndk.dir的路径。 crackme0201的manifests如下










crackme0201的build.gradle如下,其实build.gradle指明了编译的sdk版本,编译的buildtool版本,还有支持的最低sdk版本,其实csdn的博主的编译环境是2.3,如果用4.0以上的sdk版本编译会出现一些小问题,说某些结构体不存在,用Build.VERSION.SDK_INT判断一下就好了。

apply plugin: ‘com.android.application’

android {
compileSdkVersion 22
buildToolsVersion “22.0.1”

defaultConfig {
    applicationId "com.droider.crackme0201"
    minSdkVersion 9
    targetSdkVersion 22
    versionCode 1
    versionName "1.0"

    ndk {
        moduleName "helloNDK"
        abiFilters "armeabi-v7a"
    }
}
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

}

dependencies {
compile fileTree(dir: ‘libs’, include: [‘*.jar’])
compile ‘com.android.support:appcompat-v7:22.1.1’
}

crackme0201程序app文件夹的代码包链接

4.解壳应用

其实参考的csdn的博主已经说得挺清楚的,就是加一个APPLICATION_CLASS_NAME,然后后面value就是payload应用的application的classname。但是我之前不太理解替换的流程,而且我的crackme0201程序没有继承application。导致manifests的正确写法折腾了很久,后来发现如果没有classname就写默认的android.app.Application。然后后面要添加一个activity,因为没有activity就不能打开程序。正确的写法如下: 而且还有一点就是,dexunshell程序的res文件要跟crackme0201的保持一致,要不编译出来后,dexunshell替换成crackme0201程序后,crackme0201会提示找不到资源。











其他文件的编写可以参考csdn的博主的,这里就不重复了。有两个地方使用KITKAT版本的SDK是编译不过的,需要加下如下判断:

@TargetApi(Build.VERSION_CODES.KITKAT)
protected void attachBaseContext(Context base) {
……
        if(Build.VERSION.SDK\_INT >= Build.VERSION\_CODES.KITKAT){
            // 使用KITKAT的API
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            wr = (WeakReference) mPackages.get(packageName);
        } else {
            // 使用低版本的API
            HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            wr = (WeakReference) mPackages.get(packageName);
        }

dexunshell程序app文件夹的代码包链接。 当编译的时候,会提示Default Activity not found,这是完全正常的,因为当前的程序并没有activity,被加壳的程序才有,所以可以忽视这个error去编译。完成dexunshell编译后,编译好的apk是无法运行的,需要把里面的dex抽取出来,使用DexShellTool把payload.apk加载到DEX文件后面,然后把生成的DEX文件放回apk中,并重新生成签名,最后安装调试。

5.最后

我这个编写的加壳程序,在装有百度卫士的手机上,会被识别为恶意应用,毕竟有一个加壳的过程,这个也正常。在电脑上也会被一些例如ESET NOD32的杀毒原件查杀,如果发现刚打包好,就被杀毒软件删除,可以试一下暂时关闭杀毒软件。 可能有人留意到解壳应用的manifest文件中package为com.droider.crackme0201,其实也可以写成com.droider.dexunshell,不过如果写成后者,activity的name就要写成com.droider.crackme0201.MainActivity。因为如果写成.MainActivity,默认会加载package名字的.MainActivity,这样就会导致出错。