0%

APK加固技术初探


加密技术这块不仅涉及到很多JAVA基础,加密技术还涉及到很多Android底层知识,JAVA反射,JAVA IO,apk的启动流程,类的加载机制,dex文件的构造,APK打包的过程,而这些东西又正好是面试的常考点,是深入学习Android的必经之路,这次从原理入手,手写一个简单的加固框架,在这里做一个记录,如果有不对的地方欢迎指出和交流。


加固的主要目的是为了防止反编译,代码遭到阅读和窃取甚至重新打包上架的事情发生,那反编译的过程是什么呢?

  1. zip解压apk
  2. dex2jar把class.dex转成jar包
  3. jd-gui看class文件源码

加固的原理

所以加固的关键是对dex文件用加密算法进行加密,防止可执行部分的源码被阅读,此时就需要一个壳程序负责解密原dex文件,然后再合并原dex和壳dex重新签名打包成新的apk,运行时壳程序解密,获得原dex重新手动类加载

apk的打包流程

  1. APT工具处理资源文件(xml资源如布局、AndroidManifest),生成R.java
  2. AIDL工具处理AIDL文件,生成相应的Java文件
  3. Javac工具编译Java,生成Class文件
  4. DX工具将Class文件转换成DEX文件
  5. ApkBuilder工具将资源文件和DEX文件打包成APK
  6. KeyStore签名APK
  7. 正式版APK用ZipAlign工具对齐

实现加固

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
public static void main(String[] args) throws Exception {

byte[] mainDexData; // 存储源apk中的源dex文件
byte[] aarData; // 存储壳中的壳dex文件
byte[] mergeDex; // 存储壳dex 和源dex 的合并的新dex文件

// 删除source/apk/temp目录下所有文件
File tempFileApk = new File("source/apk/temp");
if (tempFileApk.exists()) {
File[]files = tempFileApk.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}
// 删除source/aar/temp目录下所有文件
File tempFileAar = new File("source/aar/temp");
if (tempFileAar.exists()) {
File[]files = tempFileAar.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}

//第一步 处理原始apk 加密dex
AES.init(AES.DEFAULT_PWD);
//待加固的apk
File apkFile = new File("source/apk/app-debug.apk");
//创建临时文件夹
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
if(!newApkFile.exists()) {
newApkFile.mkdirs();
}
//加密apk文件并写入到临时文件夹获取主dex
File mainDexFile = AES.encryptAPKFile(apkFile,newApkFile);
//临时文件夹存在,重命名dex文件
if (newApkFile.isDirectory()) {
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println("rename step1:"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("rename step2:"+newName);
file.renameTo(new File(newName));
}
}
}
}

// 第二步 处理aar 获得壳dex,其实这就是一个解密程序
File aarFile = new File("source/aar/mylibrary-debug.aar");
// jar包转dex文件
File aarDex = Dx.jar2Dex(aarFile);
//读取dex文件为byte数组
aarData = Utils.getBytes(aarDex);
// 创建一个classes.dex文件
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
tempMainDex.createNewFile();
}
// 写入byte数组到classes.dex文件
FileOutputStream fos = new FileOutputStream(tempMainDex);
byte[] fbytes = Utils.getBytes(aarDex);
fos.write(fbytes);
fos.flush();
fos.close();


/**
* 第三步 打包签名
*/
// 创建未签名apk的文件夹
File unsignedApk = new File("result/apk-unsigned.apk");
unsignedApk.getParentFile().mkdirs();
// 合并壳dex和加密dex,压缩newApkFile中的文件为unsignedApk
Zip.zip(newApkFile, unsignedApk);
// 对unsignedApk文件签名输出签名后的文件apk-signed.apk
File signedApk = new File("result/apk-signed.apk");
Signature.signature(unsignedApk, signedApk);
}
}

Zip压缩工具类

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
public class Zip {

// 解压文件到目标文件夹
public static void unZip(File zip, File dir) {
try {
// 删除已存在的目标文件夹
dir.delete();
// 包装成压缩文件对象
ZipFile zipFile = new ZipFile(zip);
// 获取被压缩的所有文件
Enumeration<? extends ZipEntry> entries = zipFile.entries();
// 遍历被压缩的文件
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
// 如果是META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF文件就跳过
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
// 如果当前压缩文件不是一个文件夹,就输出到目标文件夹
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void zip(File dir, File zip) throws Exception {
// 删除已存在的压缩文件
zip.delete();
// 压缩文件并对输出文件做CRC32校验
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
compress(dir, zos, "");
zos.flush();
zos.close();
}

private static void compress(File srcFile, ZipOutputStream zos,
String basePath) throws Exception {
if (srcFile.isDirectory()) {
compressDir(srcFile, zos, basePath);
} else {
compressFile(srcFile, zos, basePath);
}
}

private static void compressDir(File dir, ZipOutputStream zos,
String basePath) throws Exception {
File[] files = dir.listFiles();
// 文件夹为空,构建空目录
if (files.length < 1) {
ZipEntry entry = new ZipEntry(basePath + dir.getName() + "/");
zos.putNextEntry(entry);
zos.closeEntry();
}
// 递归压缩
for (File file : files) {
compress(file, zos, basePath + dir.getName() + "/");
}
}

private static void compressFile(File file, ZipOutputStream zos, String dir)
throws Exception {
// 当前文件路径
String dirName = dir + file.getName();
// 文件新名称拼接
String[] dirNameNew = dirName.split("/");
StringBuffer buffer = new StringBuffer();

if (dirNameNew.length > 1) {
for (int i = 1; i < dirNameNew.length; i++) {
buffer.append("/");
buffer.append(dirNameNew[i]);
}
} else {
buffer.append("/");
}
// 创建压缩文件并写入数据
ZipEntry entry = new ZipEntry(buffer.toString().substring(1));
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
file));
int count;
byte data[] = new byte[1024];
while ((count = bis.read(data, 0, 1024)) != -1) {
zos.write(data, 0, count);
}
bis.close();
zos.closeEntry();
}
}

AES对称加密工具类

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
public class AES {
// 默认密码
public static final String DEFAULT_PWD = "abcdefghijklmnop";
//
private static final String algorithmStr = "AES/ECB/PKCS5Padding";

private static Cipher encryptCipher;
private static Cipher decryptCipher;

public static void init(String password) {
try {
// 创建加密对象,ECB模式,PKCS5Padding填充方式
encryptCipher = Cipher.getInstance(algorithmStr);
// 创建解密对象
decryptCipher = Cipher.getInstance(algorithmStr);
// 获取密码字节数组
byte[] keyStr = password.getBytes();
// 生成加密密钥
SecretKeySpec key = new SecretKeySpec(keyStr, "AES");
// 初始化加密对象
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
// 初始化解密对象
decryptCipher.init(Cipher.DECRYPT_MODE, key);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}

/**
*
* @param srcAPKfile 源文件所在位置
* @param dstApkFile 目标文件
* @return 加密后的新dex 文件
* @throws Exception
*/
public static File encryptAPKFile(File srcAPKfile, File dstApkFile) throws Exception {
if (srcAPKfile == null) {
System.out.println("encryptAPKFile :srcAPKfile null");
return null;
}
// 解压源文件到目标文件夹
Zip.unZip(srcAPKfile, dstApkFile);
// 获得目标文件夹所有的dex
File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});

File mainDexFile = null;
byte[] mainDexData = null;
// 遍历所有的dex文件,找到并记录主dex文件并获得加密后的字节数组
for (File dexFile: dexFiles) {
// 获取dex的字节数组
byte[] buffer = Utils.getBytes(dexFile);
// 加密后的字节数组
byte[] encryptBytes = AES.encrypt(buffer);

if (dexFile.getName().endsWith("classes.dex")) {
mainDexData = encryptBytes;
mainDexFile = dexFile;
}
//用加密后的字节数组替换原来的数据
FileOutputStream fos = new FileOutputStream(dexFile);
fos.write(encryptBytes);
fos.flush();
fos.close();
}

// 返回主dex文件
return mainDexFile;
}

// 对字节数组加密返回
public static byte[] encrypt(byte[] content) {
try {
byte[] result = encryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}

// 字节数组解密返回
public static byte[] decrypt(byte[] content) {
try {
byte[] result = decryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
}

dx转换工具类

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
public class Dx {

public static File jar2Dex(File aarFile) throws IOException, InterruptedException {
// 创建临时文件夹
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
// 解压aar到临时文件夹下
Zip.unZip(aarFile, fakeDex);
// 过滤找到classes.jar
File[] files = fakeDex.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.equals("classes.jar");
}
});
// aar文件不存在抛异常
if (files == null || files.length <= 0) {
throw new RuntimeException("the aar is invalidate");
}
// 将classes.jar转classes.dex
File classes_jar = files[0];
// 创建classes.dex文件
File aarDex = new File(classes_jar.getParentFile(), "classes.dex");

//使用android tools里面的dx.bat,调windows下的命令
Dx.dxCommand(aarDex, classes_jar);
return aarDex;
}

public static void dxCommand(File aarDex, File classes_jar) throws IOException, InterruptedException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +
classes_jar.getAbsolutePath());

try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
// 转换失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("dx run failed");
}
process.destroy();
}
}

签名工具类

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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;


public class Signature {
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
// 执行windows下的签名命令
String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "C:/Users/allen/.android/debug.keystore",
"-storepass", "android",
"-keypass", "android",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"androiddebugkey"};
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
// 执行失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("sign run failed");
}
System.out.println("finish signed");
process.destroy();
}
}

实现壳程序

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
public class ShellApplication extends Application {
private static final String TAG = "ShellApplication";

public static String getPassword(){
return "abcdefghijklmnop";
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 初始化AES加密
AES.init(getPassword());
// 待解密文件路径data/data/包名/files/fake_apk/
File apkFile = new File(getApplicationInfo().sourceDir);
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
// 待解密的文件目录data/data/包名/files/fake_apk/app
File app = new File(unZipFile, "app");
// 如果不存在待解密文件目录,解压apk
if (!app.exists()) {
Zip.unZip(apkFile, app);
// 过滤不为classes.dex的.dex文件,对读取的字节数组解密写出到文件
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.equals("classes.dex")) {

} else if (name.endsWith(".dex")) {
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
byte[] decrypt = AES.decrypt(bytes);
fos.write(decrypt);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 获取待解密的文件目录.dex文件列表
List list = new ArrayList<>();
Log.d("FAKE", Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
}
}

Log.d("FAKE", list.toString());
try {
V19.install(getClassLoader(), list, unZipFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

// 反射获取对象某个变量的值
private static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();

while (clazz != null) {
try {
Field e = clazz.getDeclaredField(name);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchFieldException var4) {
clazz = clazz.getSuperclass();
}
}

throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}

// 反射获取对象的某个方法
private static Method findMethod(Object instance, String name, Class... parameterTypes)
throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method e = clazz.getDeclaredMethod(name, parameterTypes);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchMethodException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
(parameterTypes) + " not found in " + instance.getClass());
}

// 扩展某个对象的某个变量数组
private static void expandFieldArray(Object instance, String fieldName, Object[]
extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
.getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}

// 动态加载类
private static final class V19 {
private V19() {
}
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException,
IllegalAccessException, NoSuchFieldException, InvocationTargetException,
NoSuchMethodException {
// 获取ClassLoader对象的pathList变量的值
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
// 根据当前SDK版本动态批量加载类
if (Build.VERSION.SDK_INT >= 23) {
expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
} else {
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}
// 如果有异常,遍历打印
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();

while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
// 获取dexElementsSuppressedExceptions变量的值
Field suppressedExceptionsField1 = findField(loader,
"dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
suppressedExceptionsField1.get(loader));
// 如果值为空,赋值为异常数组
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
// 否则扩展异常数组,合并dexElementsSuppressedExceptions并重新赋值
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}

suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}

}

// 调用dexPathList对象的makeDexElements方法
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files, File
optimizedDirectory,
ArrayList<IOException> suppressedExceptions) throws
IllegalAccessException, InvocationTargetException, NoSuchMethodException {

Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
Class[]{ArrayList.class, File.class, ArrayList.class});
return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
optimizedDirectory, suppressedExceptions}));
}
}
// 调用dexPathList对象的makePathElements方法
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
// 查找返回类型为List的makePathElements方法
Method makePathElements;
try {
makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
// 查找返回类型为ArrayList的makePathElements方法
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
// 调用dexPathList对象的makeDexElements方法
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}

private byte[] getBytes(File file) throws Exception {
RandomAccessFile r = new RandomAccessFile(file, "r");
byte[] buffer = new byte[(int) r.length()];
r.readFully(buffer);
r.close();
return buffer;
}
}