-
-
整体加固Demo及加壳工具的编写
-
发表于: 1天前 381
-
02_整体加固Demo及加壳工具的编写
一、实验环境准备
首先来准备一下demo,这里写一个小程序,能够体现成功运行的效果即可。
首先创建一个Empty Views Activity的项目

接着画一下界面,这里就使用两个TextView组件,一个里面写的是MyApplication is not Loaded!!,另外一个是MainActivity is not Loaded。展现效果就是启动app之后,这两个textView从not Loaded变成loaded。

接下来是代码的编写
MyApplication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Log.d("[+]","MyApplication's OnCreate is calling..."); } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Log.d("[+]","MyApplication's attachBaseContext is calling..."); }} |
MainActivity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class MainActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); TextView tv_application = (TextView) findViewById(R.id.text_Application); TextView tv_activity = (TextView) findViewById(R.id.text_Activity); if(getApplication() instanceof MyApplication){ tv_application.setText("MyApplication is loaded"); } tv_activity.setText("MainActivity is loaded!!"); }} |
MainActivity当中通过getApplication获取全局唯一的一个Application实例对象,来判断该对象是不是属于MyApplication这个类。最后启动之后的效果如下(未加壳状态):

二、加壳代码的编写
1. 提取相关dex
首先通过bandzip将对应的dex文件提取出来

这里是classes3,只把这个留下来即可,接下来将这个dex文件放到项目当中的assests文件夹当中,准备编写壳程序,对于整体加固来说分成落地加固和不落地加固,**这里使用落地加固,**及直接加载assests目录当中的dex文件。
2. 编写壳程序
1. 壳代码思路
在之前的源码分析中,我们知道了在handleBindApplication当中会创建一个LoadedApk,接着会使用makeApplicationInner来获取一个Application:

1 | 后续使用到的类加载器通过`getClassLoader`获取,而这个获取的就是`LoadedApk`当中的`mClassLoader`。 |
由于app启动时执行的是壳代码的dex的类加载器,想要应用正常执行,就需要将类加载器也就是mClassLoader换成源程序的dex的加载器才能正常执行。可以通过源码查看一下mClassLoader在LoadedApk当中的属性:

可以看到这一个私有非静态成员,那么需要获取LoadedApk才能够访问到这个成员,而且由于是私有成员,那么需要通过反射来在外部访问或者修改这个成员。通过将这个mClassLoader修改成原来的ClassLoader就能够正常往下执行了。
在源码分析当中,简单的分析到app的执行流程如下:attachBaseContext --> ContentProvider.onCreate --> Application.OnCreate --> Activity.OnCreate。这里的attachBaseContext是先执行的,那我们的壳代码就可以放在这里,然后完成对环境的恢复。
- 操作如下:
首先从assets目录中读取加密的dex文件进行解密操作,接着将解密后的dex存放到私有目录当中,接着动态加载解密后的dex文件,将得到的ClassLoader替换掉mClassLoader完成环境的复原。
2. 获取LoadedApk
那么有了思路,就需要来想想该如何获取这个LoadedApk了。有几种方法:
- 通过Context获取:

在ContextImpl当中有一个mPackageInfo的成员,类型是LoadedApk。attachBaseContext传入的参数就是ContextImpl,这里就可以使用反射来获取这个mPackageInfo,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public Object LoadedApk = null;protected void attachBaseContext(Context base) { super.attachBaseContext(base); try{ Context contextImpl = base; while(contextImpl instanceof ContextWrapper){ contextImpl = ((ContextWrapper) contextImpl).getBaseContext(); } Class<?> contextImplClass = contextImpl.getClass(); Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo"); mPackageInfoField.setAccessible(true); LoadedApk = mPackageInfoField.get(contextImpl); } catch (Exception e){ e.printStackTrace(); } // ...} |
- 通过ActivityThread获取

在ActivityThread的源码当中可以看到,ActivityThread有一个mPackages的成员,是一个map,键是包名(String),值是LoadedApk,这意味着我们可以通过获取这个mPackages来获取LoadedApk。这里获取方法也是反射。
获取ActivityThread可以使用静态方法currentActivityThreadMethod,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | protected void attachBaseContext(Context base) { super.attachBaseContext(base); try{ Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); Field mPackagesField = activityThreadClass.getDeclaredField("mPackages"); mPackagesField.setAccessible(true); Map<String,WeakReference<?>> mPackages = (Map<String, WeakReference<?>>) mPackagesField.get(currentActivityThread); String packageName = base.getPackageName(); WeakReference<?> wr = mPackages.get(packageName); if(wr != null){ LoadedApk = wr.get(); }else{ Log.e("[+]","获取LoadedApk失败"); } }catch (Exception e){ Log.e("[+]","获取LoadedApk失败"); e.printStackTrace(); }// ...} |
3. 封装反射
由于比较多地方使用到反射,如果一直都要写一遍反射的话会比较麻烦,这里直接封装一下,定义为Ref.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | package com.example.protectdemo_1;import java.lang.reflect.Field;import java.lang.reflect.Method;public class Ref { public static Object invokeStaticMethod(String class_name,String method_name,Class[] pareType,Object[] pareValues){ try{ Class obj_class = Class.forName(class_name); Method method = obj_class.getMethod(method_name,pareType); return method.invoke(obj_class,pareValues); } catch (Exception e){ e.printStackTrace(); } return null; } public static Object invokeMethod(String class_name, String method_name, Object obj,Class[] pareTyple, Object[] pareValues){ try{ Class obj_class = Class.forName(class_name); Method method = obj_class.getMethod(method_name, pareTyple); return method.invoke(obj,pareValues); }catch (Exception e){ e.printStackTrace(); } return null; } public static Object getFieldObject(String class_name, Object obj, String fileName){ try{ Class obj_class = Class.forName(class_name); Field field = obj_class.getDeclaredField(fileName); field.setAccessible(true); return field.get(obj); }catch(Exception e){ e.printStackTrace(); } return null; } 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; } public static void setFieldObject(String classname, String filename, Object obj, Object fileValue){ try{ Class obj_class = Class.forName(classname); Field field = obj_class.getDeclaredField(filename); field.setAccessible(true); field.set(obj,fileValue); }catch(Exception e){ e.printStackTrace(); } } public static void setStaticObject(String class_name, String filedName, Object filedValue){ try{ Class obj_class = Class.forName(class_name); Field field = obj_class.getDeclaredField(filedName); field.setAccessible(true); field.set(null,filedValue); }catch (Exception e) { e.printStackTrace(); } }} |
使用的时候直接调用:Ref.xxx即可。
4. 壳代码编写
这里使用ActivityThread的方法来获取LoadedApk,并且从文件中加载dex,使用反射替换mClassLoader。来实现环境的修复,首先需要在main文件夹下创建一个assests的文件夹,然后将被保护的dex文件放入文件夹当中(这里实验使用落地加固),如下:

实验代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | protected void attachBaseContext(Context base) { super.attachBaseContext(base); Log.i("[+]","[StubApplication attachBaseContext] start..."); String DexName = "classes.dex"; String DexFilePath = getDir("shell", MODE_PRIVATE).getAbsolutePath() + File.separator + DexName; try{ InputStream ins = base.getAssets().open(DexName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] bytes = new byte[1024]; int index; while((index = ins.read(bytes)) != -1) baos.write(bytes,0,index); byte[] decDex = decrypt(baos.toByteArray()); ins.close(); baos.close(); FileOutputStream fos = new FileOutputStream(new File(DexFilePath)); fos.write(decDex); fos.close(); } catch(IOException e){ e.printStackTrace(); } DexClassLoader dexClassLoader = new DexClassLoader( DexFilePath, base.getCacheDir().getAbsolutePath(), getApplicationInfo().nativeLibraryDir, getClassLoader() ); currentActivityThread = Ref.invokeStaticMethod( "android.app.ActivityThread", "currentActivityThread", null, null ); ArrayMap mPackages = (ArrayMap) Ref.getFieldObject( "android.app.ActivityThread", currentActivityThread, "mPackages" ); WeakReference wr = (WeakReference) mPackages.get(getPackageName()); LoadedApk = wr.get(); Ref.setFieldObject( "android.app.LoadedApk", "mClassLoader", LoadedApk, dexClassLoader ); } private byte[] decrypt(byte[] data){ Log.d("[+]","开始解密..."); /* 解密操作 */ return data; }} |
这里为了方便,就没有写加解密逻辑,可以按照自身情况来编写。运行之后效果如下:

可以发现MainActivity已经成功加载了,但是MyApplication并没有正常加载,接下来就需要处理一下这个问题了。
5. Application问题处理
出现这种问题的原因是通过我们此时加载的Application是壳的,不是源程序的Application。此时需要手动创建Application然后替换一些相关的东西。通过源代码可以看到ActivityThread创建Application的方法:

这里调用的是makeApplicationInner,但是实际上调用的是makeApplication。还有就是需要将下面的mInitialApplication替换成新的Application。接着需要看看makeApplication当中有什么需要修改的:

首先就是这个mApplication需要置零,不然就会直接返回现存的mApplication。

接着就需要修改mAppliactionInfo.className,这里是通过这个get..函数来获取mApplictionInfo当中的className成员。还有一个位置就是这里的add:

这里会创建的Application添加到mAllApplications当中,一开始添加的是壳的Application,所需要将其移除再添加新的Application。这个mAllApplication是ArrayList的类型,可以直接使用remove将对应的项去除掉。
完成这些环境修复之后,需要主动调用OnCreate。这里调用makeApplication时需要注意传入的参数:


我们需要这两个条件当中的代码不执行,所以需要传入的forceDefaultAppClass = false,instrumentation = null。综上所述,我们需要在OnCreate当中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | public void onCreate() { super.onCreate(); // 替换className String className = "com.example.protectdemo_1.MyApplication"; ApplicationInfo applicationInfo = (ApplicationInfo) Ref.getFieldObject( "android.app.LoadedApk", LoadedApk, "mApplicationInfo" ); applicationInfo.className = className; // 清除mApplication Application oldApplication = (Application) Ref.getFieldObject( "android.app.LoadedApk", LoadedApk, "mApplication" ); Ref.setFieldObject( "android.app.LoadedApk", "mApplication", LoadedApk, null ); // 修改mAllApplication ArrayList mAllApplication = (ArrayList)Ref.getFieldObject( "android.app.ActivityThread", currentActivityThread, "mAllApplications" ); mAllApplication.remove(oldApplication); Application realApp = (Application) Ref.invokeMethod( "android.app.LoadedApk", "makeApplication", LoadedApk, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null} ); Ref.setFieldObject( "android.app.ActivityThread", "mInitialApplication", currentActivityThread, realApp ); realApp.onCreate();} |
这样一来,程序就可以正常执行了:

三、自动化加固
说明
这里的自动化加固是指在有壳代码的前提下,通过脚本辅助,使用工具对指定dex文件进行加固。这里使用apktool和
1. 代码优化
className获取方法调整
在上面的Demo当中,className是写死的,如果要写自动化加固的话这样肯定不行,需要动态获取className。这里可以通过下面这个方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | String className = null;try{ ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = applicationInfo.metaData; if(bundle != null && bundle.containsKey(appKey)){ className = bundle.getString(appKey); }}catch(PackageManager.NameNotFoundException e){ Log.e("[+]","[Application OnCreate] NameNotFoundException!!"); e.printStackTrace();}if(className == null){ Log.e("[+]","[Application OnCreate] className is null!!"); return;} |
这段代码的作用是从 Android 应用的 Manifest 文件中的 MetaData 读取一个配置的类名。使用这种方法需要在AndroidManifest.xml当中加入<meta-data />标签:
1 2 3 4 5 | <application> <meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.example.protectdemo_1.MyApplication"</application> |
- 加密函数调整
这里使用一个简单的异或加密来对dex文件进行加密,对应的壳解密代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | private byte[] decrypt(byte[] data){ Log.d("[+]","开始解密..."); /* 解密操作 */ if(data != null){ for(int i = 0; i < data.length; i++){ data[i] ^= 0x53; } } return data;} |
2. 自动化加固思路
首先就是对目标apk进行解包,这里使用apktool的反编译功能,然后对目标dex进行加密,在解包后的文件夹下创建assests文件夹,将加密后的dex文件丢到assests文件夹当中,删除原来的dex文件,接着植入壳代码文件。最后使用apktool回编译,然后签名即可。
知道了思路,现在就可以来着手编写工具了。这里首先需要将**壳代码(.dex)**从之前生成的apk当中取出来。这里还需要使用到tinyxml2.h的库
1. manifest_editor.hpp
通过该类的方法,实现对Mainfest当中的标签进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | #pragma once #include "tinyxml2.h"#include <string>#include <iostream>using namespace tinyxml2;class ManifestEditor{private: const char* PROXY_APP_NAME = "com.example.protectdemo_1.StubApplication"; // 注意:这里要与自己的包名匹配 const char* META_KEY = "APPLICATION_CLASS_NAME";public: void modify(const std::string& xmlPath) { XMLDocument doc; if (doc.LoadFile(xmlPath.c_str()) != XML_SUCCESS) { std::cerr << "无法解析 AndroidManifest.xml" << std::endl; return; } XMLElement* root = doc.RootElement(); if (!root) return; XMLElement* appNode = root->FirstChildElement("application"); if (!appNode)return; // 1. 获取原本的Application Name const char* oldAppName = appNode->Attribute("android:name"); std::string originalAppClass = (oldAppName) ? oldAppName : ""; // 2. 修改Application Name为壳的入口 appNode->SetAttribute("android:name", PROXY_APP_NAME); // 3. 插入Meta-Data保存原来的Application if (!originalAppClass.empty()) { XMLElement* metaData = doc.NewElement("meta-data"); metaData->SetAttribute("android:name", META_KEY); metaData->SetAttribute("android:value", originalAppClass.c_str()); if (appNode->FirstChild()) appNode->InsertFirstChild(metaData); else appNode->InsertEndChild(metaData); } // 4. 保存文件 doc.SaveFile(xmlPath.c_str()); std::cout << "[+] AndroidManifest.xml 修改完成" << std::endl; }}; |
2. 工具类utils.hpp
该代码实现的是拷贝、加密等一系列操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #pragma once#include <iostream>#include <fstream>#include <vector>#include <string>#include <filesystem>#include <algorithm>namespace fs = std::filesystem;class Utils {public: static bool runCommand(const std::string& cmd) { std::cout << "[CMD] 执行命令: " << cmd << std::endl; int ret = system(cmd.c_str()); return ret == 0; } static std::vector<uint8_t> readFile(const std::string& filepath) { std::ifstream file(filepath, std::ios::binary); if(!file.is_open()) { std::cerr << "无法打开文件: " << filepath << std::endl; return {}; } return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); } // 将字节数组写入文件 static bool writeFile(const std::string& path, const std::vector<uint8_t>& data) { std::ofstream file(path, std::ios::binary); if (!file.is_open()) return false; file.write(reinterpret_cast<const char*>(data.data()), data.size()); return true; } // 简单加密 static std::vector<uint8_t> encrypt(std::vector<uint8_t> data) { const uint8_t key = 0x53; for (auto& byte : data) { byte ^= key; } return data; } // 递归拷贝目录 static void copyDir(const std::string& src, const std::string& dst) { try { fs::copy(src, dst, fs::copy_options::recursive | fs::copy_options::overwrite_existing); } catch (fs::filesystem_error& e) { std::cerr << "拷贝失败: " << e.what() << std::endl; } } // 创建目录 static void makeDir(const std::string& dirPath) { if (!fs::exists(dirPath)) { fs::create_directories(dirPath); } } // 删除文件或目录 static void removePath(const std::string& path) { fs::remove_all(path); }}; |
3. main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | #include <iostream>#include "utils.hpp"#include "manifest_editor.hpp"const std::string IN_APK = "test.apk";const std::string OUT_APK = "appShell.apk";const std::string TMP_DIR = "apkDecompile";const std::string SHELL_DEX_SOURCE = "shell_files/classes.dex";const std::string SHELL_LIBS = "shell_files/libs";int main(){ // system("chcp 65001"); std::cout << "==== Android APK 加壳工具 ====" << std::endl; Utils::removePath(TMP_DIR); Utils::removePath(OUT_APK); // 1. 反编译 std::string cmdDecompile = "java -jar apktool.jar d -s " + IN_APK + " -o " + TMP_DIR + " -f"; if (!Utils::runCommand(cmdDecompile)) { std::cerr << "反编译失败, 请检查apktools是否存在!" << "\n"; return -1; } // 2. 加密原始DEX并隐藏 std::cout << "[*] 正在加密原始Dex...\r\n"; std::string srcDexPath = TMP_DIR + "/classes.dex"; // 这里的dex是被保护app当中目标dex的名称 std::string assetsDir = TMP_DIR + "/assets"; std::string encDexPath = assetsDir + "/classes.dex"; // 文件名是壳代码当中的DexName Utils::makeDir(assetsDir); std::vector<uint8_t> dexData = Utils::readFile(srcDexPath); if (dexData.empty()) { std::cerr << "无法读取 classes.dex" << std::endl; return -1; } std::vector<uint8_t> encData = Utils::encrypt(dexData); Utils::writeFile(encDexPath, encData); // 删除原始classes.dex Utils::removePath(srcDexPath); // 3. 植入壳代码(class.dex) std::cout << "正在植入壳代码..." << std::endl; if (!fs::exists(SHELL_DEX_SOURCE)) { std::cerr << "壳文件丢失: " << SHELL_DEX_SOURCE << std::endl; return -1; } fs::copy_file(SHELL_DEX_SOURCE, srcDexPath, fs::copy_options::overwrite_existing); if (fs::exists(SHELL_LIBS)) Utils::copyDir(SHELL_LIBS, TMP_DIR + "/lib"); // 4. 修改AndroidManifest.xml std::cout << "[*] 正在修改 Manifest.xml.." << std::endl; ManifestEditor editor; editor.modify(TMP_DIR + "/AndroidManifest.xml"); // 5. 回编译 std::cout << "[+] 开始回编译..." << std::endl; std::string cmdBuild = "java -jar apktool.jar b " + TMP_DIR + " -o " + OUT_APK; if (!Utils::runCommand(cmdBuild)) { std::cout << "[error] 回编译失败" << std::endl; return -1; } std::cout << "==== 加固完成! 输出文件: " << OUT_APK << " ====" << std::endl; return 0;} |
这里需要将壳代码放在当前目录当中的shell_files的文件夹当中,如果有so层的加密则存放到shell_files/lib当中。
4. 签名
成功生成appShell.apk之后需要先签名才能在手机上安装,这里签名使用keytool和apksigner来进行签名,大致命令如下:
1 2 3 4 5 6 7 8 | # 生成签名文件keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias# 签名java -jar apksigner.jar sign --ks my-release-key.jks --out signed-app.apk appShell.apk# 安装adb install -t signed-app.apk |