https://blog.csdn.net/I123456789T/article/details/91562328
這篇文章介紹了 基本的加固流程,但有一個明顯的問題就是 解密是 java 代碼實現,存在的問題就是key可以被找到,很容易被破解;
一、寫一下基礎流程:
參考文檔:https://developer.android.google.cn/studio/build/multidex.html#keep
看到這張圖其實,還是很好理解的,就是我們把需要加固的apk,外部包裝一層殼,而這個殼的作用是爲了解密源apk的
二、加固仍存在一些問題:
1、解密殼不能加密,解密都是 java 代碼實現,存在的問題就是key可以被找到,很容易被破解;
2、解密之後的apk源程序放在指定目錄的話,還是存在被破解的風險,因爲這種落地方式解密,是很容易獲取解密之後的apk的
3、在解密得到源程序apk,然後再用DexClassLoader進行加載,這裏相當於兩次把apk加載到內存中,第一次是解密的時候,第二次是加載apk的時候,那麼這效率就會大大降低了
三、解決方案:
第一個問題,使用NDK 解決;使用NDK 解密,本文使用的RC4 算法,處理較快,如果安全性高也可以使用AES,demo中都已加入;
第二個問題,加載後可以刪除掉,但每個加載時間會長;
第三個問題暫時無解決;5.0前是可以解決見:Android中內存加載dex https://blog.csdn.net/zzx410527/article/details/51673908
四、主要使用的一個API DexClassLoader
DexClassLoader當然也是一種ClassLoader,但本身屬於顧名思義是用來加載Dex文件的,是安卓系統獨有的一種類加載器。
基礎概念
在此之前可以稍微回顧下ClassLoader的相關基礎:
ClassLoader是用來加載class文件的,它負責將*.class加載爲內在中的Class對象
加載機制爲“雙親委派”,即能交給父類加載器去加載的,絕不自行加載
使用方法
只需要清楚其構造方法的參數意義就可以。
DexClassLoader (String dexPath,
String optimizedDirectory,
String librarySearchPath,
ClassLoader parent)
參數 | 含義 |
dexPath | 包含dex文件的jar包或apk文件路徑 |
optimizedDirectory | 釋放目錄,可以理解爲緩存目錄,必須爲應用私有目錄,不能爲空 |
librarySearchPath | native庫的路徑,可爲空 |
parent | 父類加載器 |
五、 加殼的具體流程:
1.創建一個空的demo進行,
2.然後在項目中添加一個代理module(解密,和系統源碼交互功能)和tools工具加密Java library 的module ;
1、代碼中需要用到幾個類,AES加解密類,Zip壓縮解壓類等工具類
首先我先proxy_core代理module下寫一個代理application ,然後繼承至Application,代碼目錄結構請看:
接着把我們這個代理的application加到我們最常寫的配置文件中AndroidManifest.xml 中,我們是不是每個App都有一個application,然後把它配置到AndroidManifest.xml中,這裏唯一不同的是,不是把我們項目中的那個application寫到AndroidManifest.xml中,而是把我們在代理的寫上。然後把我們app自己用到的application也加上,自己的application寫在meta-data中,另一個meta-data按照下面的寫就行,寫法和位置如下
這個是我們自己項目用到的初始化application,上面的代理只是處理代理操作的。
我們自己的MyApplication裏面目前啥也沒寫,這個使我們項目中用於初始化的,這裏先不寫東西。
這裏開始寫代理了,在ProxyApplication 中:
public class ProxyApplication extends Application {
//定義好的加密後的文件的存放路徑
private String app_name;
private String app_version;
/**
* ActivityThread創建Application之後調用的第一個方法
* 可以在這個方法中進行解密,同時把dex交給Android去加載
* @param base
*/
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//獲取用戶填入的metaData
getMetaData();
//得到當前apk文件
File apkFile = new File(getApplicationInfo().sourceDir);
//把apk解壓 這個目錄中的內容需要root權限才能使用
File versionDir = getDir(app_name+"_" + app_version,MODE_PRIVATE);
File appDir = new File(versionDir,"app");
File dexDir = new File(appDir,"dexDir");
//得到我們需要加載的dex文件
List<File> dexFiles = new ArrayList<>();
//進行解密 (最好做md5文件校驗)
if (!dexDir.exists() || dexDir.list().length == 0){
//把apk解壓到appDir
Zip.unZip(apkFile,appDir);
//獲取目錄下所有的文件
File[] files = appDir.listFiles();
for (File file:files){
String name = file.getName();
if (name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
try{
// 使用 ndk rc4 解密**************************************
Utils.native_rc4_de(file.getAbsolutePath(),file.getAbsolutePath());
dexFiles.add(file);
}catch (Exception e){
e.printStackTrace();
}
}
}
}else {
for (File file:dexDir.listFiles()){
dexFiles.add(file);
}
}
try {
loadDex(dexFiles,versionDir);
}catch (Exception e){
e.printStackTrace();
}
}
private void loadDex(List<File> dexFiles,File versionDir) throws Exception{
//1、獲取pathList
Field pathListField = Utils.findField(getClassLoader(), "pathList");
Object pathList = pathListField.get(getClassLoader());
//2、獲取數組dexElements
Field dexElementsField = Utils.findField(pathList,"dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
//3、反射到初始化makePathElements的方法
Method makeDexElements = Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
ArrayList<IOException> suppressedException = new ArrayList<>();
Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedException);
Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + addElements.length);
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);
//替換classloader中的element數組
dexElementsField.set(pathList,newElements);
}
private void getMetaData(){
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
if (null != metaData){
if (metaData.containsKey("app_name")){
app_name = metaData.getString("app_name");
}
if (metaData.containsKey("app_version")){
app_version = metaData.getString("app_version");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 開始替換application
*/
@Override
public void onCreate() {
super.onCreate();
try {
bindRealApplication();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 讓代碼走入if的第三段中
* @return
*/
@Override
public String getPackageName() {
if (!TextUtils.isEmpty(app_name)){
return "";
}
return super.getPackageName();
}
@Override
public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
if (TextUtils.isEmpty(app_name)){
return super.createPackageContext(packageName, flags);
}
try {
bindRealApplication();
} catch (Exception e) {
e.printStackTrace();
}
return delegate;
}
boolean isBindReal;
Application delegate;
//下面主要是通過反射系統源碼的內容,然後進行處理,把我們的內容加進去處理
private void bindRealApplication() throws Exception{
if (isBindReal){
return;
}
if (TextUtils.isEmpty(app_name)){
return;
}
//得到attchBaseContext(context) 傳入的上下文 ContextImpl
Context baseContext = getBaseContext();
//創建用戶真實的application (MyApplication)
Class<?> delegateClass = null;
delegateClass = Class.forName(app_name);
delegate = (Application) delegateClass.newInstance();
//得到attch()方法
Method attach = Application.class.getDeclaredMethod("attach",Context.class);
attach.setAccessible(true);
attach.invoke(delegate,baseContext);
//獲取ContextImpl ----> ,mOuterContext(app); 通過Application的attachBaseContext回調參數獲取
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
//獲取mOuterContext屬性
Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
mOuterContextField.setAccessible(true);
mOuterContextField.set(baseContext,delegate);
//ActivityThread ----> mAllApplication(ArrayList) ContextImpl的mMainThread屬性
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
Object mMainThread = mMainThreadField.get(baseContext);
//ActivityThread -----> mInitialApplication ContextImpl的mMainThread屬性
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread,delegate);
//ActivityThread ------> mAllApplications(ArrayList) ContextImpl的mMainThread屬性
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList<Application> mApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
mApplications.remove(this);
mApplications.add(delegate);
//LoadedApk -----> mApplicaion ContextImpl的mPackageInfo屬性
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo,delegate);
//修改ApplicationInfo className LoadedApk
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;
delegate.onCreate();
isBindReal = true;
}
}
這裏我換成了 rc4解密;這裏也可以判斷一下,如果目標文件不存在再做這些操作,可以提高性能,但風險增加。最安全的方式是加載完成,直接刪除;
2、下面在proxy_tools中寫一個Main類,和一個main方法,直接運行處理,代碼如下:
public class Main {
public static void main(String[] args) throws Exception{
/**
* 1、製作只包含解密代碼的dex文件
*/
File aarFile = new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
File aarTemp = new File("proxy_tools/temp");
Zip.unZip(aarFile,aarTemp);
File classesDex = new File(aarTemp,"classes.dex");
File classesJar = new File(aarTemp,"classes.jar");
//dx --dex --output out.dex in.jar E:\AndroidSdk\Sdk\build-tools\23.0.3
Process process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
+ " " + classesJar.getAbsolutePath());
process.waitFor();
if (process.exitValue() != 0){
throw new RuntimeException("dex error");
}
process.destroy();
/**
* 2、加密apk中所有的dex文件
*/
File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp = new File("app/build/outputs/apk/debug/temp");
Zip.unZip(apkFile,apkTemp);
//只要dex文件拿出來加密
File[] dexFiles = apkTemp.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
//AES加密
// AES.init(AES.DEFAULT_PWD);
for (File dexFile:dexFiles) {
byte[] bytes = Utils.getBytes(dexFile);
// byte[] encrypt = AES.encrypt(bytes);
byte[] encrypt =RC4.RC4Base(bytes,RC4.ACCESSKEY);
FileOutputStream fos = new FileOutputStream(new File(apkTemp,"secret-" + dexFile.getName()));
fos.write(encrypt);
fos.flush();
fos.close();
dexFile.delete();
}
/**
* 3、把dex放入apk解壓目錄,重新壓成apk文件
*/
classesDex.renameTo(new File(apkTemp,"classes.dex"));
File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp,unSignedApk);
/**
* 4、對其和簽名,最後生成簽名apk
*/
// zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
Process processAlign = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
+" "+alignedApk.getAbsolutePath());
// System.out.println("signedApkprocess : 11111" + " :-----> " +unSignedApk.getAbsolutePath() + "\n" + alignedApk.getAbsolutePath());
processAlign.waitFor( 10,TimeUnit.SECONDS);
// if(process.exitValue()!=0){
// throw new RuntimeException("dex error");
// }
processAlign.destroy();
// apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
// apksigner sign --ks jks文件地址 --ks-key-alias 別名 --ks-pass pass:jsk密碼 --key-pass pass:別名密碼 --out out.apk in.apk
File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
File jks=new File("proxy_tools/proxy1.jks");
Process processsign= Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
+" --ks-key-alias wwy --ks-pass pass:123456 --key-pass pass:123456 --out "
+signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
processsign.waitFor();
if(processsign.exitValue()!=0){
throw new RuntimeException("dex error");
}
processsign.destroy();
System.out.println("excute successful");
}
}
這裏加密 我換成了RC4;
我們在寫好前面的之後,直接運行這個main方法,就可以在我們的app -> build->outputs->apk->debug下面看到生成的幾個apk,分別爲 app-debug.apk, app-unsigned.apk, app-unsigned-aligned.apk, app-signed-aligned.apk,最終 app-signed-aligned.apk 纔是我們最後安裝使用的apk,
使用 java 工具類,需要配置一下環境:
1)、配置電腦的環境變量:
如你的 Android compileSdkVersion 28 請將 D:\AndroidSDK\build-tools\28.0.2 這個路徑加入然後我就把這個路勁配置到用戶變量中 path 中;重新 啓動 Android Studio;
最後會生成簽名後的 apk
上面的代理ProxyApplication被我們配置到Mainfest的application 標籤中,這個位置經常是我們配置項目使用的application的,其實不用擔心,代碼中已經處理過了,當代理application處理完之後,會自動把我們配置的app裏面的項目用到的MyApplication 類替換過來,所以項目在第一次運行完之後,正式運行還是以我們自己的MyApplication爲主;
本文是在https://blog.csdn.net/I123456789T/article/details/91819275 基本上 改進了一些;
1.加密,2.process.waitFor(); 運行問題,見:https://blog.csdn.net/q610098308/article/details/105197814
2.簽名問題見:https://blog.csdn.net/q610098308/article/details/105138228
參數 博客:https://blog.csdn.net/zzx410527/article/details/51673908 不過之個 Android 5.0後就不能再用了。
https://blog.csdn.net/I123456789T/article/details/91819275
Android Apk 加固之Dex文件 完善篇 InMemoryDexClassLoader 之內存加載dex 見