Android之Apk加殼
基於ADT環境開發的的實現,請參考: Android中的Apk的加固(加殼)原理解析和實現
類載入和dex檔案相關的內容,如:Android動態載入Dex機制解析
一、什麼是加殼?
加殼是在二進位制的程式中植入一段程式碼,在執行的時候優先取得程式的控制權,做一些額外的工作。大多數病毒就是基於此原理。是應用加固的一種手法對原始二進位制原文進行加密/隱藏/混淆。
殼最本質的功能就是實現載入器。
- 未加殼前,系統直接執行原dex,即原apk
- 加殼後,系統執行 殼程式碼--> 脫殼得到原dex --> 執行原dex
Apk加殼:就是通過給目標APK加一層保護程式,把需要保護的內容加密、隱藏起來,來防止反編譯的一種方法。
加殼的原理:
所以我們在加殼過程中需要三個關鍵物件:
1、未加密的Apk(即demo.apk)
2、殼程式Apk(即shell.apk,負責解密apk工作)
3、加密工具(即java工程。將demo.apk加密和shell.dex合併,得到新的dex)
二、下面我們來實現如何加殼:
Step1:打包demo工程:demo.apk
Step2(先設定解殼/密):打包解殼工程:shell.apk,解壓獲取:shell.dex
Step3(開始加殼/密):執行java工程,合併shell.dex和demo.apk,得到:classes.dex
step4(修正簽名、執行)
shell_demo.apk就是我們想得到的加殼app!
Step1:打包demo工程:demo.apk
注意:這裡包括後續打包,只能使用同一個簽名。
原始碼:https://github.com/lvxiangan/Shell/tree/master/Demo
功能:獲取當前包名,廣播監聽網路狀態變化,Glide框架顯示網路圖片(網路操作+圖片顯示)等。
關鍵程式碼
1、MyApplication
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i("demo", "apk onCreate:" + this);
}
}
2、AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="demon.demo">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ImageActivity" />
</application></manifest>
Step2:打包解殼工程:shell.apk,解壓獲取:shell.dex
這個shell.apk在經過後面替換dex後,就是我們想要得到的東西。
原始碼:https://github.com/lvxiangan/Shell/tree/master/MyUnshell
工程目錄:
通過解壓shell.Apk的方式獲取到dex檔案, 更名為shell.dex。如圖:
關鍵程式碼
1、ProxyApplication.java
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.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
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.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
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;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
// 建立兩個資料夾payload_odex,payload_lib 私有的,可寫的檔案目錄
File odex = this.getDir("demo_odex", MODE_PRIVATE);
File libs = this.getDir("demo_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odexPath + "/shelldemo.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:" + dexFile.length());
if (!dexFile.exists()) {
// 在payload_odex資料夾內,建立payload.apk
dexFile.createNewFile();
// 讀取程式classes.dex檔案
byte[] dexdata = this.readDexFileFromApk();
// 分離出解殼後的apk檔案已用於動態載入
this.splitPayLoadFromDex(dexdata);
}
// 配置動態載入環境 獲取主執行緒物件 http://blog.csdn.net/myarrow/article/details/14223493
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[]{}, new Object[]{});
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
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);
Log.i("demo", "classloader:" + dLoader);
} 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 (PackageManager.NameNotFoundException e) {
Log.i("demo", "error:" + Log.getStackTraceString(e));
}
//有值的話呼叫該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");
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
* @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 Resources.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 Resources.Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
}
2.RefInvoke.java
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RefInvoke {
/**
* 反射執行類的靜態函式(public)
*
* @param class_name 類名
* @param method_name 函式名
* @param pareTyple 函式的引數型別
* @param pareVaules 呼叫函式時傳入的引數
* @return
*/
public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules) {
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name, pareTyple);
return method.invoke(null, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反射執行類的函式(public)
*
* @param class_name
* @param method_name
* @param obj
* @param pareTyple
* @param pareVaules
* @return
*/
public static Object invokeMethod(String class_name, String method_name, Object obj, Class[] pareTyple, Object[] pareVaules) {
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name, pareTyple);
return method.invoke(obj, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反射得到類的屬性(包括私有和保護)
*
* @param class_name
* @param obj
* @param filedName
* @return
*/
public static Object getFieldOjbect(String class_name, Object obj, String filedName) {
try {
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反射得到類的靜態屬性(包括私有和保護)
*
* @param class_name
* @param filedName
* @return
*/
public static Object getStaticFieldOjbect(String class_name, String filedName) {
try {
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
return field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 設定類的屬性(包括私有和保護)
*
* @param classname
* @param filedName
* @param obj
* @param filedVaule
*/
public static void setFieldOjbect(String classname, String filedName, Object obj, Object filedVaule) {
try {
Class obj_class = Class.forName(classname);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
field.set(obj, filedVaule);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 設定類的靜態屬性(包括私有和保護)
*
* @param class_name
* @param filedName
* @param filedVaule
*/
public static void setStaticOjbect(String class_name, String filedName, Object filedVaule) {
try {
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
field.set(null, filedVaule);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、根據demo的 AndroidManifest.xml ,配置shell工程AndroidManifest.xml
注意:
- 把demo.apk的AndroidManifest.xml中所有:許可權、元件(activity、service、broadcastreceiver) 複製過來,元件必須使用完整的包名。
- 使用meta-data配置 demo.apk 的MyApplication,也要使用完整包名。
注意對比兩個配置檔案的區別。
解殼工程:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="demon.myunshell">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".ProxyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<meta-data
android:name="APPLICATION_CLASS_NAME"
android:value="demon.demo.MyApplication" />
<activity android:name="demon.demo.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="demon.demo.ImageActivity" />
</application>
</manifest>
demo工程:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="demon.demo">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ImageActivity" />
</application>
</manifest>
Step3:執行java工程,合併shell.dex和demo.apk,得到:classes.dex
原始碼:https://github.com/lvxiangan/Shell/tree/master/DexShellTool
這是一個java工程,目錄結構如下:
工程下新建force資料夾,將demo.apk,shell.dex複製到裡面去,執行如下程式碼,生成新的dex檔案,即classes.dex:
加密合併成功後的classes.dex,大小几乎等於demo.apk + shell.dex。
關鍵程式碼:
public class DexShellTool {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File("force/demo.apk"); //需要加殼的程式
System.out.println("apk size:"+payloadSrcFile.length());
File unShellDexFile = new File("force/shell.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();
}
}
}
}
step4:把class.dex放進shell.apk,重新簽名得到:shell_demo.apk
素材下載:https://github.com/lvxiangan/Shell/tree/master/Tools
解壓step2的shell.apk
- 將step3得到的classes.dex替換classes.dex。
- 重新簽名
完成後,如下圖:
注意觀察classes.dex的大小,判斷是否複製成功。
開始重新簽名:
- 新建一個Tools資料夾,將前面的簽名檔案,shell.apk複製進去。
- 簽名命令太長不好記,我們新建sign.bat檔案,新增如下內容,注意使用該命令系統必須配置Java環境變數,可根據自身情況進行修改,方便下次使用:
jarsigner -verbose -keystore DeMon.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar shelldemo.apk shell.apk key
命令說明:
jarsigner -verbose -keystore 簽名檔案 -storepass 密碼 -keypass alias的密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 簽名後的檔案 簽名前的apk alias名稱
-
雙擊執行sign.bat檔案,成功簽名Tools檔案會新增一個shelldemo.apk,會比shell.apk稍大,大概就是生成的簽名檔案的大小。shelldemo.apk就是成功加殼後的apk,可以安裝執行。
正確簽名後的內容如下:
step5:驗證效果
注意對比demo.apk的效果圖,除了標題和包名與不一致外,功能上完全相同,即符合預期。Apk加殼成功!
總結
優點不多述,說說缺點吧:
1、 Apk體積變大,尤其是res檔案成倍增長。
2、解殼過程容易被反編譯,最好用C/C++實現
3、第一次安裝啟動需要等待載入時間較長,使用者體驗不好。
GitHub地址:
https://github.com/lvxiangan/Shell
改進版:https://github.com/lvxiangan/Android-Shell2
1、解決加殼後執行有兩個app的問題、
2、在一個AS工程管理各個模組,打包輸出時記得選擇切換模組
3、在殼程式實現JNI解密
參考:https://blog.csdn.net/DeMonliuhui/article/details/78269234