前几天在网上看到一篇不错的介绍关于apk加壳的介绍,Android中的Apk的加固(加壳)原理解析和实现,针对里面关于资源加载这块自己研究了下,给出了一个方案,下面结合那篇文章的内容做一下apk加壳流程介绍
一、将目标apk加密放进壳apk的classes.dex里面,代码如下
package com.example;
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 MyClass {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File("C:\\Users\\Desktop\\force\\ForceApkObj.apk"); //需要加壳的程序
System.out.println("apk size:"+payloadSrcFile.length());
File unShellDexFile = new File("C:\\Users\\Desktop\\force\\ForceApkObj.dex"); //解客dex
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex
int payloadLen = payloadArray.length;
int unShellDexLen= unShellDexArray.length;
int totalLen= payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
byte[] newdex = newbyte[totalLen]; // 申请了新的长度
//添加解壳代码
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
//添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
//添加解壳数据长度
System.arraycopy(intToByte(payloadLen),0, newdex, totalLen-4, 4);//最后4为长度
//修改DEXfile size文件头
fixFileSizeHeader(newdex);
//修改DEXSHA1 文件头
fixSHA1Header(newdex);
//修改DEXCheckSum文件头
fixCheckSumHeader(newdex);
String str = "C:\\Users\\jalen_yang\\Desktop\\force\\classes.dex";
File file = new File(str);
if (!file.exists()){
file.createNewFile();
}
FileOutputStreamlocalFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex);
localFileOutputStream.flush();
localFileOutputStream.close();
} catch (Exceptione) {
e.printStackTrace();
}
}
//直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[]srcdata){
for(int i = 0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
/**
* 修改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 = newbyte[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();
}
/**
* int 转byte[]
* @param number
* @return
*/
public static byte[] intToByte(intnumber) {
byte[] b =new byte[4];
for (int i = 3; i >= 0; i--){
b[i] = (byte) (number % 256);
number >>= 8;
}
return b;
}
/**
* 修改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值
* @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)
}
/**
* 以二进制读出文件内容
* @param file
* @return
* @throws IOException
*/
private static byte[] readFileBytes(File file) throws IOException{
byte[]arrayOfByte = new byte[1024];
ByteArrayOutputStreamlocalByteArrayOutputStream = 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();
}
}
}
}
二、 壳apk动态加载目标apk,注意的是目前发现android studio2.2开始编译的debug apk里面会出现两个dex,然后导致动态加载失败,这是因为studio2.2开始默认打开了 Instant Run选项,所以测试动态加载时不要打开这个开关,2.2开始需要自己手动去Settings里面关掉
package com.example.reforceapk;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;
public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;
//这是context 赋值
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
//创建两个文件夹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);
}
// 配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
String packageName = this.getPackageName();//当前apk的包名
//下面两句不是太理解
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
Log.i("demo","classloader:"+dLoader);
try{
Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
Log.i("demo", "actObj:"+actObj);
}catch(Exception e){
Log.i("demo", "activity:"+Log.getStackTraceString(e));
}
} catch (Exception e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
}
@Override
public void onCreate() {
{
loadResources(apkFileName);
Log.i("demo", "onCreate");
// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
String appClassName = null;
try {
ApplicationInfo ai = this.getPackageManager()
.getApplicationInfo(this.getPackageName(),
PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
} else {
Log.i("demo", "have no application class name");
return;
}
} catch (NameNotFoundException e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
//有值的话调用该Applicaiton
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});
Object mBoundApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mBoundApplication");
Object loadedApkInfo = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$AppBindData",
mBoundApplication, "info");
//把当前进程的mApplication 设置成了null
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
loadedApkInfo, null);
Object oldApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mInitialApplication");
//http://www.codeceo.com/article/android-context.html
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldOjbect("android.app.ActivityThread",
currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);//删除oldApplication
ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
"mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.ActivityThread$AppBindData",
mBoundApplication, "appInfo");
appinfo_In_LoadedApk.className = appClassName;
appinfo_In_AppBindData.className = appClassName;
Application app = (Application) RefInvoke.invokeMethod(
"android.app.LoadedApk", "makeApplication", loadedApkInfo,
new Class[] { boolean.class, Instrumentation.class },
new Object[] { false, null });//执行 makeApplication(false,null)
RefInvoke.setFieldOjbect("android.app.ActivityThread",
"mInitialApplication", currentActivityThread, app);
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mProviderMap");
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$ProviderClientRecord",
providerClientRecord, "mLocalProvider");
RefInvoke.setFieldOjbect("android.content.ContentProvider",
"mContext", localProvider, app);
}
Log.i("demo", "app:"+app);
app.onCreate();
}
}
/**
* 释放被加壳的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包里面获取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();
}
// //直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
for(int i=0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
//以下是加载资源
protected AssetManager mAssetManager;//资源管理器
protected Resources mResources;//资源
protected Theme mTheme;//主题
protected void loadResources(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(super.getResources(), assetManager);
Log.i("demo", "mAssets exist, is "+mAssets);
} catch (Throwable ignore) {
Log.i("demo", "mAssets don't exist ,search mResourcesImpl:");
Field mResourcesImpl = Resources.class
.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(super.getResources());
Log.i("demo", "mResourcesImpl exist, is "+resourceImpl);
Field implAssets = resourceImpl.getClass()
.getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, assetManager);
}
} catch (Exception e) {
Log.i("demo", "loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
}
}
这边重点解释一下壳apk动态加载资源这一块,首先解释一下apk加载资源这块的大概流程:
1、 Resources对象包含一个AssetManager对象,在7.0以前这个变量直接是Resources的一个属性,因此要替换AssetManager就要找它的mAssets成员变量,7.0开始Resources对象不再有mAssets属性,取代的是mResourcesImpl属性,这个属性其实就是一个Resources的实现类,它里面包含了一个AssetManager属性,所以在7.0上面就要多一个步骤来替换AssetManager了,也就是上面代码中那样
2、 android系统加载apk资源时主要通过AssetManager来解析Resources.arsc文件,AssetManager的c++类的成员变量mResources指向的是一个ResTable对象,这个ResTable就是解析后的资源索引表,每次加载一个apk就会将这个apk的Resources.arsc解析后放到这个ResTable里面,以及字符串资源池变量里面
3、 在android5.0以前AssetManager对于相同的package id,比如0x7f,搜索资源时是按照逆序,也就是从后往前,第一个找到的就是返回的对象,因此要先加载当前应用的资源,再加载patch的资源,才能实现覆盖,不过如果找到最后一个发现不存在,就会抛异常,因为android系统不允许新增资源,只允许覆盖已有资源;android 5.0以后就不一样了,搜索相同package id,按照从前往后,因此你要先加载patch资源,再加载当前应用资源,这样你就得重新创建一个AssetManager了
4、 我们平常看到的R.java里面的每个id都是由三部分组成的。分别是:mpackage、type、configurelist,mpackage代表的是资源包,由一个字节表示,比如系统资源包、当前应用资源包、第三方资源包等等,默认情况下系统资源包用0x01表示,当前应用资源包用0x7f,在这两个值范围内的都是合法的id,否则是不合法的。Type代表的是资源类型,同样是一个字节,比如layout、drawable、string等等;最后两个字节代表的是次序,也就是偏移,用来定位具体的资源位置。所以搜索资源时首先分解id成上面三个类型,然后在资源索引表里搜索,注意的是相同的mpackage可能存在多个组,也就是上面提到的覆盖资源问题,每个package对应的是一个PakcageGroup,而这个PakcageGroup包含多个Package,这里的Package不是包名,也和mpackage不同,可以看看5.0以前的系统源码:
ssize_tResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag,
uint32_t* outSpecFlags,ResTable_config* outConfig) const
{
......
const ssize_t p =getResourcePackageIndex(resID);
const int t = Res_GETTYPE(resID);
const int e = Res_GETENTRY(resID);
......
const Res_value* bestValue = NULL;
const Package* bestPackage = NULL;
ResTable_config bestItem;
memset(&bestItem, 0, sizeof(bestItem));// make the compiler shut up
if (outSpecFlags != NULL) *outSpecFlags =0;
// Look through all resource packages,starting with the most
// recently added.
const PackageGroup* const grp =mPackageGroups[p];
......
size_t ip = grp->packages.size();
while (ip > 0) {
ip--;
int T = t;
int E = e;
const Package* const package =grp->packages[ip];
if (package->header->resourceIDMap){
uint32_t overlayResID = 0x0;
status_t retval =idmapLookup(package->header->resourceIDMap,
package->header->resourceIDMapSize,
resID, &overlayResID);
if (retval == NO_ERROR &&overlayResID != 0x0) {
// for this loop iteration,this is the type and entry we really want
......
T =Res_GETTYPE(overlayResID);
E =Res_GETENTRY(overlayResID);
} else {
// resource not present inoverlay package, continue with the next package
continue;
}
}
const ResTable_type* type;
const ResTable_entry* entry;
const Type* typeClass;
ssize_t offset = getEntry(package, T,E, &mParams, &type, &entry, &typeClass);
if (offset <= 0) {
// No {entry, appropriate config}pair found in package. If this
// package is an overlay package(ip != 0), this simply means the
// overlay package did not specifya default.
// Non-overlay packages are stillrequired to provide a default.
if (offset < 0 && ip ==0) {
......
return offset;
}
continue;
}
if((dtohs(entry->flags)&entry->FLAG_COMPLEX) != 0) {
......
continue;
}
......
const Res_value* item =
(const Res_value*)(((constuint8_t*)type) + offset);
ResTable_config thisConfig;
thisConfig.copyFromDtoH(type->config);
if (outSpecFlags != NULL) {
if (typeClass->typeSpecFlags !=NULL) {
*outSpecFlags |=dtohl(typeClass->typeSpecFlags[E]);
} else {
*outSpecFlags = -1;
}
}
if (bestPackage != NULL &&
(bestItem.isMoreSpecificThan(thisConfig) || bestItem.diff(thisConfig) ==0)) {
// Discard thisConfig not only ifbestItem is more specific, but also if the two configs
// are identical (diff == 0), oroverlay packages will not take effect.
continue;
}
bestItem = thisConfig;
bestValue = item;
bestPackage = package;
}
......
if (bestValue) {
outValue->size =dtohs(bestValue->size);
outValue->res0 =bestValue->res0;
outValue->dataType =bestValue->dataType;
outValue->data =dtohl(bestValue->data);
if (outConfig != NULL) {
*outConfig = bestItem;
}
......
returnbestPackage->header->index;
}
return BAD_VALUE;
}