整体加固Demo及加壳工具的编写
一、实验环境准备
首先来准备一下demo,这里写一个小程序,能够体现成功运行的效果即可。
首先创建一个Empty Views Activity的项目
接着画一下界面,这里就使用两个TextView组件,一个里面写的是MyApplication is not Loaded!!,另外一个是MainActivity is not Loaded。展现效果就是启动app之后,这两个textView从not Loaded变成loaded。
接下来是代码的编写
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...");
}
}
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:
后续使用到的类加载器通过`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,代码如下:
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有一个mPackages的成员,是一个map,键是包名(String),值是LoadedApk,这意味着我们可以通过获取这个mPackages来获取LoadedApk。这里获取方法也是反射。
获取ActivityThread可以使用静态方法currentActivityThreadMethod,代码如下:
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:
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文件放入文件夹当中(这里实验使用落地加固),如下:
实验代码如下:
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当中添加如下代码:
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. 代码优化
在上面的Demo当中,className是写死的,如果要写自动化加固的话这样肯定不行,需要动态获取className。这里可以通过下面这个方式:
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 />标签:
<application>
<meta-data
android:name="APPLICATION_CLASS_NAME"
android:value="com.example.protectdemo_1.MyApplication"
</application>
这里使用一个简单的异或加密来对dex文件进行加密,对应的壳解密代码如下:
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当中的标签进行修改
#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
该代码实现的是拷贝、加密等一系列操作
#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
#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来进行签名,大致命令如下:
# 生成签名文件
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