本文主要参考博客:
Android APK加壳技术方案【2】 APK加壳【1】初步方案实现详解
由于之前没有接触过安卓编程,所以即便有两篇这么详细的教程,但是还是走了不少弯路,都折腾了大概一个星期左右。而且两个博主都没有放出demo,所以就想回顾一下这个学习的过程,并给出一个Demo。
配置环境 本文的编译环境如下:
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
程序时的编译工具变了,自己给自己挖坑。
应用加壳 先说一下加壳的程序,假如两个程序都编译完成后,要把crackme0201
的apk放到dexunshell
应用的dex程序里面,由于修改dex文件,也要修改dex_header
的checksum
、signature
等信息。这个加壳程序可以用其他语言编写,文章的示例是用java写的。 建立一个DexShellTool.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 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 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文件。
被加壳应用 本文给出的Demo只是project里app文件夹的压缩包,由于使用了ndk编译共享库,为了编译成功,需要在local.properties
里加入ndk.dir
的路径。 crackme0201
的manifests
如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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
判断一下就好了。
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 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文件夹的代码包链接 。
解壳应用 其实参考的csdn的博主已经说得挺清楚的,就是加一个APPLICATION_CLASS_NAME
,然后后面value就是payload应用的application的classname
。但是我之前不太理解替换的流程,而且我的crackme0201
程序没有继承application。导致manifests
的正确写法折腾了很久,后来发现如果没有classname
就写默认的android.app.Application
。然后后面要添加一个activity,因为没有activity就不能打开程序。正确的写法如下: 而且还有一点就是,dexunshell
程序的res文件要跟crackme0201
的保持一致,要不编译出来后,dexunshell
替换成crackme0201
程序后,crackme0201
会提示找不到资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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是编译不过的,需要加下如下判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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中,并重新生成签名,最后安装调试。
最后 我这个编写的加壳程序,在装有百度卫士的手机上,会被识别为恶意应用,毕竟有一个加壳的过程,这个也正常。在电脑上也会被一些例如ESET NOD32的杀毒原件查杀,如果发现刚打包好,就被杀毒软件删除,可以试一下暂时关闭杀毒软件。 可能有人留意到解壳应用的manifest
文件中package
为com.droider.crackme0201
,其实也可以写成com.droider.dexunshell
,不过如果写成后者,activity
的name就要写成com.droider.crackme0201.MainActivity
。因为如果写成.MainActivity
,默认会加载package
名字的.MainActivity
,这样就会导致出错。