本文主要参考博客:
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如下

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.droider.crackme0201">
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

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会提示找不到资源。


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.droider.crackme0201" >
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.droider.dexunshell.ProxyApplication">
        <meta-data android:name="APPLICATION_CLASS_NAME" android:value="android.app.Application"/>
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

其他文件的编写可以参考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,这样就会导致出错。

更多