#### 1、需要加密的Apk(源Apk)
#### 2、殼程序Apk(負責解密Apk工作)
#### 3、加密工具(將源Apk進行加密和殼Dex合并成新的Dex)
主要步驟:
#### 我們拿到需要加密的Apk和自己的殼程序Apk,然后用加密算法對源Apk進行加密在將殼Apk進行合并得到新的Dex文件,最后替換殼程序中的dex文件即可,得到新的Apk,那么這個新的Apk我們也叫作脫殼程序Apk.他已經不是一個完整意義上的Apk程序了,他的主要工作是:負責解密源Apk.然后加載Apk,讓其正常運行起來。
在這個過程中我們可能需要了解的一個知識是:??如何將源Apk和殼Apk進行合并成新的Dex
這里就需要了解Dex文件的格式了。下面就來簡單介紹一下Dex文件的格式
具體Dex文件格式的詳細介紹可以查看這個文件:??[http://download.csdn.net/detail/jiangwei0910410003/9102599](http://download.csdn.net/detail/jiangwei0910410003/9102599)
主要來看一下Dex文件的頭部信息,其實Dex文件和Class文件的格式分析原理都是一樣的,他們都是有固定的格式,我們知道現在反編譯的一些工具:
#### 1、jd-gui:可以查看jar中的類,其實他就是解析class文件,只要了解class文件的格式就可以
#### 2、dex2jar:將dex文件轉化成jar,原理也是一樣的,只要知道Dex文件的格式,能夠解析出dex文件中的類信息就可以了
當然我們在分析這個文件的時候,最重要的還是頭部信息,應該他是一個文件的開始部分,也是索引部分,內部信息很重要。
我們今天只要關注上面紅色標記的三個部分:
#### 1) checksum?
#### 文件校驗碼 ,使用alder32 算法校驗文件除去 maigc ,checksum 外余下的所有文件區域 ,用于檢查文件錯誤 。
#### 2) signature?
#### 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件區域 ,用于唯一識別本文件 。
#### 3) file_size
#### Dex 文件的大小 。
為什么說我們只需要關注這三個字段呢?
因為我們需要將一個文件(加密之后的源Apk)寫入到Dex中,那么我們肯定需要修改文件校驗碼(checksum).因為他是檢查文件是否有錯誤。那么signature也是一樣,也是唯一識別文件的[算法](http://lib.csdn.net/base/datastructure "算法與數據結構知識庫")。還有就是需要修改dex文件的大小。
#### 不過這里還需要一個操作,就是標注一下我們加密的Apk的大小,因為我們在脫殼的時候,需要知道Apk的大小,才能正確的得到Apk。那么這個值放到哪呢?這個值直接放到文件的末尾就可以了。
所以總結一下我們需要做:修改Dex的三個文件頭,將源Apk的大小追加到殼dex的末尾就可以了。
我們修改之后得到新的Dex文件樣式如下:
那么我們知道原理了,下面就是代碼實現了。所以這里有三個工程:
#### 1、源程序項目(需要加密的Apk)
#### 2、脫殼項目(解密源Apk和加載Apk)
#### 3、對源Apk進行加密和脫殼項目的Dex的合并
## 三、項目案例?
下面先來看一下源程序
## 1、需要加密的源程序Apk項目:ForceApkObj?
需要一個Application類,這個到后面說為什么需要:
#### MyApplication.java
~~~
package com.example.forceapkobj;
import android.app.Application;
import android.util.Log;
public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
Log.i("demo", "source apk onCreate:"+this);
}
}
~~~
就是打印一下onCreate方法。
#### MainActivity.java
~~~
package com.example.forceapkobj;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView content = new TextView(this);
content.setText("I am Source Apk");
content.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MainActivity.this, SubActivity.class);
startActivity(intent);
}});
setContentView(content);
Log.i("demo", "app:"+getApplicationContext());
}
}
~~~
也是打印一下內容。
## 2、加殼程序項目:DexShellTools?
加殼程序其實就是一個[Java](http://lib.csdn.net/base/javase "Java SE知識庫")工程,因為我們從上面的分析可以看到,他的工作就是加密源Apk,然后將其寫入到脫殼Dex文件中,修改文件頭,得到一個新的Dex文件即可。
看一下代碼:
~~~
package com.example.reforceapk;
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 mymain {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File("force/ForceApkObj.apk"); //需要加殼的程序
System.out.println("apk size:"+payloadSrcFile.length());
File unShellDexFile = new File("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 = new byte[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為長度
//修改DEX file size文件頭
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件頭
fixSHA1Header(newdex);
//修改DEX CheckSum文件頭
fixCheckSumHeader(newdex);
String str = "force/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) {
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 = 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();
}
/**
* int 轉byte[]
* @param number
* @return
*/
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;
}
/**
* 修改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];
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();
}
}
}
}
~~~
下面來分析一下:
紅色部分其實就是最核心的工作:
#### 1>、加密源程序Apk文件
~~~
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進制形式讀出apk,并進行加密處理//對源Apk進行加密操作
~~~
加密算法很簡單:
~~~
//直接返回數據,讀者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){
for(int i = 0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
~~~
對每個字節進行異或一下即可。
#### (說明:這里是為了簡單,所以就用了很簡單的加密算法了,其實為了增加破解難度,我們應該使用更高效的加密算法,同事最好將加密操作放到native層去做)
#### 2>、合并文件:將加密之后的Apk和原脫殼Dex進行合并
~~~
int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;//多出4字節是存放長度的。
byte[] newdex = new byte[totalLen]; // 申請了新的長度
//添加解殼代碼
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷貝dex內容
//添加加密后的解殼數據
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex內容后面拷貝apk的內容
~~~
#### 3>、在文件的末尾追加源程序Apk的長度
~~~
//添加解殼數據長度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4為長度
~~~
4>、修改新Dex文件的文件頭信息:file_size; sha1; check_sum
~~~
//修改DEX file size文件頭
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件頭
fixSHA1Header(newdex);
//修改DEX CheckSum文件頭
fixCheckSumHeader(newdex);
~~~
具體修改可以參照之前說的文件頭格式,修改指定位置的字節值即可。
這里我們還需要兩個輸入文件:
#### 1>、源Apk文件:ForceApkObj.apk
#### 2>、脫殼程序的Dex文件:ForceApkObj.dex
那么第一個文件我們都知道,就是上面的源程序編譯之后的Apk文件,那么第二個文件我們怎么得到呢?這個就是我們要講到的第三個項目:脫殼程序項目,他是一個[Android](http://lib.csdn.net/base/android "Android知識庫")項目,我們在編譯之后,能夠得到他的classes.dex文件,然后修改一下名稱就可。
## 3、脫殼項目:ReforceApk?
在講解這個項目之前,我們先來了解一下這個脫殼項目的工作:
1>、通過反射置換android.app.ActivityThread 中的mClassLoader為加載解密出APK的DexClassLoader,該DexClassLoader一方面加載了源程序、另一方面以原mClassLoader為父節點,這就保證了即加載了源程序又沒有放棄原先加載的資源與系統代碼。
關于這部分內容,不了解的同學可以看一下ActivityThread.java的源碼:
或者直接看一下這篇文章:
[http://blog.csdn.net/jiangwei0910410003/article/details/48104455](http://blog.csdn.net/jiangwei0910410003/article/details/48104455)
如何得到系統加載Apk的類加載器,然后我們怎么將加載進來的Apk運行起來等問題都在這篇文章中說到了。
2>、找到源程序的Application,通過反射建立并運行。?
這里需要注意的是,我們現在是加載一個完整的Apk,讓他運行起來,那么我們知道一個Apk運行的時候都是有一個Application對象的,這個也是一個程序運行之后的全局類。所以我們必須找到解密之后的源Apk的Application類,運行的他的onCreate方法,這樣源Apk才開始他的運行生命周期。這里我們如何得到源Apk的Application的類呢?這個我們后面會說道。使用meta標簽進行設置。
下面來看一下整體的流程圖:
所以我們看到這里還需要一個核心的技術就是動態加載。關于動態加載技術,不了解的同學可以看這篇文章:
[http://blog.csdn.net/jiangwei0910410003/article/details/48104581](http://blog.csdn.net/jiangwei0910410003/article/details/48104581)
下面來看一下代碼:
~~~
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.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;
} catch (Exception e) {
Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
@Override
public Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
}
~~~
首先我們來看一下具體步驟的代碼實現:
#### 1>、得到脫殼Apk中的dex文件,然后從這個文件中得到源程序Apk.進行解密,然后加載
~~~
//這是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();
}
}
~~~
這里需要注意的一個問題,就是我們需要找到一個時機,就是在脫殼程序還沒有運行起來的時候,來加載源程序的Apk,執行他的onCreate方法,那么這個時機不能太晚,不然的話,就是運行脫殼程序,而不是源程序了。查看源碼我們知道。Application中有一個方法:??attachBaseContext
這個方法,他在Application的onCreate方法執行前就會執行了,那么我們的工作就需要在這里進行
#### 1)、從脫殼程序Apk中找到源程序Apk,并且進行解密操作
~~~
//創建兩個文件夾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);
}
~~~
這個脫殼解密操作一定要和我們之前的加殼以及加密操作對應,不然就會出現Dex加載錯誤問題
A) 從Apk中獲取到Dex文件
~~~
/**
* 從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();
}
~~~
其實就是解壓Apk文件,直接得到dex文件即可
B) 從脫殼Dex中得到源Apk文件
~~~
/**
* 釋放被加殼的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();
}
~~~
C) 解密源程序Apk
~~~
////直接返回數據,讀者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
for(int i=0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
~~~
這個解密算法和加密算法是一致的
#### 2>、加載解密之后的源程序Apk
~~~
//配置動態加載環境
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));
}
~~~
2)、找到源程序的Application程序,讓其運行
~~~
@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();
}
}
~~~
直接在脫殼的Application中的onCreate方法中進行就可以了。這里我們還可以看到是通過AndroidManifest.xml中的meta標簽獲取源程序Apk中的Application對象的。
下面來看一下AndoridManifest.xml文件中的內容:
在這里我們定義了源程序Apk的Application類名。
#### 項目下載: http://download.csdn.net/detail/jiangwei0910410003/9102741
## 四、運行程序
那么到這里我們就介紹完了,這三個項目的內容,下面就來看看如何運行吧:
運行步驟:
#### 第一步:得到源程序Apk文件和脫殼程序的Dex文件
運行源程序和脫殼程序項目,之后得到這兩個文件(記得將classes.dex文件改名ForceApkObj.dex),然后使用加殼程序進行加殼:
這里的ForceApkObj.apk文件和ForceApkObj.dex文件是輸入文件,輸出的是classes.dex文件。
#### 第二步:替換脫殼程序中的classes.dex文件
我們在第一步中得到加殼之后的classes.dex文件之后,并且我們在第一步運行脫殼項目的時候得到一個ReforceApk.apk文件,這時候我們使用解壓縮軟件進行替換:
#### 第三步:我們在第二步的時候得到替換之后的ReforceApk.apk文件,這個文件因為被修改了,所以我們需要從新對他簽名,不然運行也是報錯的。
工具下載:??[http://download.csdn.net/detail/jiangwei0910410003/9102767](http://download.csdn.net/detail/jiangwei0910410003/9102767)
下載之后的工具需要用ReforeceApk.apk文件替換ReforceApk_des.apk文件,然后運行run.bat就可以得到簽名之后的文件了。
run.bat文件的命令如下:
#### cd C:\Users\i\Desktop\forceapks??
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei??
del ReforceApk.apk?
這里最主要的命令就是中間的一條簽名的命令,關于命令的參數說明如下:
#### jarsigner -verbose -keystore 簽名文件 -storepass 密碼 ?-keypass alias的密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA ?簽名后的文件 簽名前的apk alias名稱??
eg:??
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei??
簽名文件的密碼:123456??
alais的密碼:123456?
所以這里我們在得到ReforceApk.apk文件的時候,需要簽名,關于Eclipse中如何簽名一個Apk的話,這里就不多說了,自己google一下吧:
那么通過上面的三個步驟之后我們得到一個簽名之后的最終文件:??ReforceApk_des.apk
我們安裝這個Apk,然后運行,效果如下:
看到運行結果的那一瞬間,我們是多么的開心,多么的有成就感,但是這個過程中遇到的問題,是可想而知的。
我們這個時候再去反編譯一下源程序Apk(這個文件是我們脫殼出來的payload.apk,看ReforeceApk中的代碼,就知道他的位置了)
發現dex文件格式是不正確的。說明我們的加固是成功的。
## 五、遇到的問題
1、研究的過程中遇到簽名不正確的地方,開始的時候,直接替換dex文件之后,就直接運行了Apk,但是總是提示簽名不正確。
2、運行的過程中說找不到源程序中的Activity,這個問題其實我在動態加載的那篇文章中說道了,我們需要在脫殼程序中的AndroidManifest.xml中什么一下源程序中的Activiity:
## 六、技術要點
#### 1、對Dex文件格式的了解
#### 2、動態加載技術的深入掌握
#### 3、Application的執行流程的了解
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)