Keep and carry on.

前言

dex的加密应该是android加固中非常简单的加密手段了吧!前段时间在网上看博客,关于android加固的具体实现方法,看了作者的例子想起之前做过的一道华山杯的题好像也是对dex进行加密,然后翻出来发现这两个方法一模一样,连加密的方式都是异或0xff。所以主要写一下它的实现过程,解题思路都非常简单。

dex加固原理

1.待加固程序(apk)

2.加密程序(java层代码,主要用于提取apk中的dex文件,并加密dex文件,再将dex文件和解壳程序合并然后保存到新的dex文件中)

3.解壳程序

1

源程序apk;

加壳程序:

  1. 以二进制方式读出待加固apk,并进行加密处理。
  2. 以二进制方式读出解壳dex。
  3. 创建二进制数组,在结尾增加4个字节,存放待加壳apk的大小,拷贝dex和apk到新的dex中。
  4. 修改DEX file size文件头 ,修改DEX SHA1 文件头 ,修改DEX CheckSum文件头 。
  5. 将二进制数组写入到新建的dex文件中。

解壳程序:

  1. 从apk中读出dex文件,用zipentry写入到二进制数组中。
  2. 取被加壳apk的长度,把被加壳apk内容拷贝到newdex中 ,并解密apk
  3. 动态加载该dex。

整个实现的流程如上,接下来就把华山杯的Sheild.apk拿到jeb中看一下源码,由于我们在做题过程中加壳程序不是我们需要考虑的,我们只用看解壳程序并且找到这其中的解密程序,以及代码段被加密后的数据即可。还是先把上述流程转化为代码实现一下吧。

解壳程序

步骤一:找到apk中的dex文件,用到ZipInputStream,创建两个文件夹用于存储apk和so文件

创建目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建两个文件夹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);
}

从apk中获取dex:

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
/**
* 从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();
}

从dex中得到源Apk文件:System.arraycopy复制待解密apk数据到指定文件中,进行解密,分析解密的apk文件,同样是使用ZipInputStream,得到apk中的dex和so文件

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
/**
* 释放被加壳的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();
}

最后是动态加载运行我们解密出来的apk

关键的decrypt这个函数找到后就是对代码进行一个简单的异或0xff,所以模拟这个过程解密即可。

加壳程序

对于加壳程序要对dex的文件结构有一个大概的了解,主要是要涉及到修改文件头的几个字段。

先来看一下dex的文件结构

2

我们在加壳时需要修改上图中,红线括起来的三个部分,方法如下:

修改DEX CheckSum文件头,使用Adler32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 修改dex头,CheckSum 校验码
* @param dexBytes
*/
private static void fixCheckSumHeader(byte[] dexBytes) {
Adler32 adler = new Adler32();
adler.update(dexBytes, 12, dexBytes.length - 12);//从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);//效验码赋值(8-11)
System.out.println(Long.toHexString(value));
System.out.println();
}

修改DEX SHA1文件头,signature部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 修改dex头 sha1值
* @param dexBytes
* @throws NoSuchAlgorithmException
*/
private static void fixSHA1Header(byte[] dexBytes)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
byte[] newdt = md.digest();
System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
//输出sha-1值,可有可无
String hexstr = "";
for (int i = 0; i < newdt.length; i++) {
hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
.substring(1);
}
System.out.println(hexstr);
}

修改DEX file_size文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 修改dex头 file_size值
* @param dexBytes
*/
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);//修改(32-35)
}

Sheld.apk解题过程

我们打开classes.dex

3

生成的dex文件,上图中选中部分为20字节的signature

文件末尾为apk文件大小:0x0004DAAC

4

加密的方法我们已经分析完了,所以解密时只需要读取最后四位为整个加密apk长度,然后再把加密后的代码读取出来进行解密,存在apk文件中即可,解密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import binascii
arr=''
f= open('classes.dex','rb')
f.seek(0,0)
while True:
byte=f.read(1)
if byte=='':
break
else:
hexstr=byte
arr+=hexstr
f.close()
apksize=int(binascii.b2a_hex(arr[-4:]),16)
apk_addr=apksize+4
apkdata=arr[-apk_addr:-4]
f2=open('original.apk','wb')
for i in range(len(apkdata)):
f2.write(chr(ord(apkdata[i])^0xff))
f2.close()

最后我们生成了一个orignal.apk再把这个apk破解即可,逻辑非常简单,只是简单的base64,就不再叙述他的解密方法了。

总结

dex加密较为简单,特别是这种方法还将他放在java层就直接动态加载了,但是看了他的实现过程还是感觉比较有收获,主要在于实现过程中不仅仅是对加密方案的设计,还需要对整个dex文件的解析,文件结构,安卓的动态加载机制都要熟练掌握,所以难点应该是在开发这一块。而逆向的时候并不会考虑这么多。

Read More
⬆︎TOP