0%

Android APK加壳技术的研究

本文主要参考博客:

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_headerchecksumsignature等信息。这个加壳程序可以用其他语言编写,文章的示例是用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.apkcrackme0201改程序的apk,unshell.dexdexunshell程序的dex文件,生成的classes.dex为拼接后的dex文件。

被加壳应用

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

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>

crackme0201build.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抽取出来,使用DexShellToolpayload.apk加载到dex文件后面,然后把生成的dex文件放回apk中,并重新生成签名,最后安装调试。

最后

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