Apk加殼實現
前幾天在網上看到一篇不錯的介紹關於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
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;
}