Android APK加固
标题:Android APK 的当前方法
日期:2020-08-18 20:09:42
类别:
- 安卓
- apk_reverse
标签: - 安卓壳
引言
当地的地方发生竞争,在Android的发展过程中就伴随着逆向和安固的发展。逆向发展可以通过一些非常好的软件,如IDA、JEB等,来原地逆向的速度;应用也会通过各种手段来阻止逆向手术对自己的应用进行。
但是逆向是真的百分百阻止的,所以才能通过其他的手段来提高应用被逆向的难度,让逆向只能时时需要(可以不利用自己的能力)利用手机的时间才能应用逆向成功在实际情况下,只要不明显影响应用运行速度,我们都可以采用这种思想来进行保护。
在这种背景下,膨胀与混淆就应运而生了,这就是我们最开始的一种保护方式。这种方式将代码中的方法名和变量名用非常易于混淆的方式进行命名,如0
,o
,O
,l
,I
,1
等组合命名方式。除了这种混淆方式外,为了提高逆向工作量,会在代码中加入相同或者类似方法名的方法或者增加一些没有必要的父类来达到代码膨胀的目的。
但是这种方式不能阻止逆向工作者的脚步,开发后来者发现可以通过DexClassLoader
这个类来进行DEX文件的动态加载。一般我们下情况称呼这种加固方式为加壳,由于这种是动态加载DEX文件,但是可以成为DEX壳。不过Android主要是由Java代码写的,而Java代码是很容易被逆向分析的,一般我们抽象地将动态加载DEX的代码原生态所以层进行运行,所以层的代码主要为c/c++代码,逆向的难度比Java会高很多,提高了应用的安全性。
随着这种方式的不断涌现,这种方式已经无法阻挡大多数的逆向工作了,人们就需要一种新的方式来和逆向进行对抗。在如此激烈的对抗中,此时就发现了一种利用elf文件格式(Android中.so的共享库为elf文件格式)来进行现有的方法。
在这样中,自己定义一个节,在节中举办我们的一些关键功能代码,通过elf文件格式将这部分的代码进行清洁,然后在elf文件加载初始化函数数组时,将被更新的代码起源这个出来。
加壳的优势:能够在一定程度上保护自己的核心代码算法,提高破解、盗版二次打包的难度,还可以通过代码注入、动态调试、或者内存注入攻击。
加壳或者其他的劣势:从某个时候说会,只要加了都可能影响应用的保护、运行效率。
由于Android手机的电池、CPU等硬件的限制,一般的应用不可能像PC上进行强度非常大的保护。
再生、膨胀
重整
主题思想:用没有意义的字符,如a
,b
,c
或者易于混淆的字符,如0
,o
,O
,l
,I
,1
代替原本的有意义的类名。
参数:将release
下minifyEnabled
的值改为true
,打开重新配置;加上shrinkResources true
,资源压缩。
复制代码隐藏代码
#压缩级别0-7,Android一般为5(对代码迭代优化的次数)
-optimizationpasses 5
#不使用大小写混合类名
-dontusemixedcaseclassnames
#混淆时记录日志
-verbose
#不警告org.greenrobot.greendao.database包及其子包里面未应用的应用
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**
......
#保持jackson包以及其子包的类和类成员不被混淆
-keep class org.codehaus.jackson.** {*;}
#--------重要说明-------
#-keep class 类名 {*;}
#-keepclassmembers class 类名{*;}
#一个*表示保持了该包下的类名不被混淆;
# -keep class org.codehaus.jackson.*
#二个**表示保持该包以及它包含的所有子包下的类名不被混淆
# -keep class org.codehaus.jackson.**
#------------------------
#保持类名、类里面的方法和变量不被混淆
-keep class org.codehaus.jackson.** {*;}
#不混淆类ClassTwoOne的类名以及类里面的public成员和方法
#public 可以换成其他java属性如private、public static 、final等
#还可以使<init>表示构造方法、<methods>表示方法、<fields>表示成员,
#这些前面也可以加public等java属性限定
-keep class com.dev.demo.two.ClassTwoOne {
public *;
}
#不混淆类名,以及里面的构造函数
-keep class com.dev.demo.ClassOne {
public <init>();
}
#不混淆类名,以及参数为int 的构造函数
-keep class com.dev.demo.two.ClassTwoTwo {
public <init>(int);
}
#不混淆类的public修饰的方法,和private修饰的变量
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
public <methods>;
private <fields>;
}
#不混淆内部类,需要用$修饰
#不混淆内部类ClassTwoTwoInner以及里面的全部成员
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}
更多重新配置参考:
https://juejin.cn/post/6844903471095742472
https://www.huaweicloud.com/articles/ae151e2f60923097cefc473bd131addf.html
膨胀
代码繁衍能够在一定程度上增加逆向的难度,但给逆向游戏增加的工作量是比较多的,代码膨胀就增加了总的代码量,让玩家逆向必须分析全部的代码,才能得到一些最终的结果。代码扩展也是最终防御的方法之一,主要思想是写一些垃圾代码扩展代码量,这样在逆向分析时就可能会消耗攻击者大量的时间,从而达到保护APK的目的。
膨胀代码有很多种实现的思想。比如乘法改加法、加法改自加等等,只要把代码量变大,不影响功能的实现就可以了。
这里自己写了一个简单的自动生成代码的工程。
DEX壳
之前Android的主要代码为Java代码,但是在逆向分析中,Java代码很容易被分析出来的。为了解决这个问题,我们就希望在app运行起来后动态加载我们Java代码(.dex文件)。种方法主要利用了DexClassLoader
这个类来实现动态加载。DexClassLoader
类支持动态加载.apk
或者.dex
。
动态加载APK
动态加载APK简单来说,将就是已经编译好的.apk
文件放入到一个.dex
文件中这个。.apk
文件为我们真正的应用程序,以下就称呼这个APK为源APK ;.dex
文件为另外一个工程的.dex
文件,这个工程主要是为了在运行时释放源APK,然后将流程转到源APK执行。
可知上面的原理图,我们需要3个对象。
-
源APK:需要加壳的apk。
-
壳APK:将APK解密还原并执行。
-
清洁工具:将源apk和壳dex进行组合成新的dex并且修改新的dex。
项目实现演示代码
IDE:
Android Studio 4.1.3
安卓版本:4.4+
源APK
-
正常写功能逻辑代码。这里的代码为简单的ctf判断代码。
-
新建类
APP
类并且这个类继承于类Application
,实现onCreate
方法。 -
生成一个发布版本的apk,把这个apk保存起来。
-
修改
MainActivity.java
的父类,使得MainActivity
继承于Activity
。字幕:将显示修改为运行的的英文源APK。
壳APK
Proxy.java
新建一个代{过} {滤}理类叫Proxy
,继承于类Application
。类这个用来释放状语从句:解密原始的APK。
-
attachBaseContext()
替换
Application
中的attachBaseContext
方法。这个方法会在Activity
的onCreate
方法之前执行。方法实现的功能主要有:
-
把壳dex中包含的源apk释放出来。
-
把释放的apk进行。
-
把源apk中的
lib
目录中的文件复制到当前程序(壳)的路径下。 -
创建一个新的
DexClassLoader
,替换到父节点的DexClassLoader
。
DexClassLoader 是
从BaseDexClassLoader继承的,这个时候每个参数都可以自定义,我们一般用来自定义加载的apk/dex/jar 文件。- 代码例子
复制代码隐藏代码
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // the getDir method will create a directory in /data/user/0(uid)/packagename/ // the dx directory holds the file of the source apk File relesaeDir = this.getDir("dx", MODE_PRIVATE); mSouceAPKLibAbsolutePath = this.getDir("lx", MODE_PRIVATE).getAbsolutePath(); mSourceAPKReleaseDir = relesaeDir.getAbsolutePath(); mSourceAPKAbsolutePath = mSourceAPKReleaseDir + "/" + mSouceAPKName; // create the source apk // if the source apk exist, do nothing, otherwise create the source apk file. File sourceApk = new File(mSourceAPKAbsolutePath); if (!sourceApk.exists()){ try{ sourceApk.createNewFile(); } catch (Exception e) { Log.e(TAG, "failed to create file."); } // the source apk file is empty, you need to read source apk file from the dex // file of the shell apk and save it. byte[] shellDexData; // get dex of shell apk. shellDexData = getShellDexFileFromShellApk(); // get the source apk and decrypt it. // copy the libs in the decrypted apk file to the lib directory. getSourceApkFile(shellDexData); } // Configure dynamic load environment Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {}); String packageName = this.getPackageName(); ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages"); WeakReference weakReference = (WeakReference) mPackages.get(packageName); DexClassLoader newDexClassLoader = new DexClassLoader(mSourceAPKAbsolutePath, mSourceAPKReleaseDir, mSouceAPKLibAbsolutePath, (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", weakReference.get(), "mClassLoader")); RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", weakReference.get(), newDexClassLoader); }
-
-
onCreate()
-
加载源apk资源。
-
获取
manifest.xml
中记录的源apk的启动类名。 -
设置ActivityThread信息(
android.app.ActivityThread
->currentActivityThread
)。 -
代码例子
复制代码隐藏代码
@Override public void onCreate() { super.onCreate(); // 源apk启动类 String srcAppClassName = ""; // 原apk所在路径 try { ApplicationInfo applicationInfo = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = applicationInfo.metaData; if (bundle != null && bundle.containsKey(SRC_APP_MAIN_ACTIVITY)) { srcAppClassName = bundle.getString(SRC_APP_MAIN_ACTIVITY);//className 是配置在xml文件中的。 } else { return; } } catch (Exception e) { } //获取ActivityThread类下AppBindData类的成员属性 LoadedApk info; 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"); // 将原来的loadedApkInfo置空 RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null); // 获取壳线程的Application Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication"); ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications"); mAllApplications.remove(oldApplication); // 构造新的Application // 1.更新 2处className 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 = srcAppClassName; appinfo_In_AppBindData.className = srcAppClassName; // 2.注册application Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null }); //替换ActivityThread中的mInitialApplication RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app); //替换之前的 内容提供者为刚刚注册的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); } app.onCreate(); }
-
活动主题功能
它管理应用进程的主线程的执行(相当于普通Java程序的主入口函数),并根据AMS的要求(通过IApplicationThread接口,AMS为Client、ActivityThread.ApplicationThread为Server)负责调度和执行活动、广播和其他操作。
在Android系统中,在默认情况下,一个应用程序内的各个组件(如Activity、ActivityroadcastReceiver、Service)都在同一个进程(Process)里执行,且过程中的【主线程】负责执行。
在Android系统中,如果有特别指定(通过android:process),也可以让特定组件在不同的程序中运行。无论组件在一个主进程中运行,默认情况下,他们都是一个过程中的【线程线程】 】负责执行。
【主线程】既要处理Activity组件的UI事件,又要处理服务后台服务工作,通常会忙不过来。画面的事件。
类结构参考
-
调用
currentActivityThread
方法获取ActivityThread
中的成员sCurrentActivityThread
。复制代码隐藏代码
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});
-
获取
sCurrentActivityThread
中的mBoundApplication
。复制代码隐藏代码
Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication");
-
获取
mBoundApplication
中成员info
。复制代码隐藏代码
Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info");
-
观察
LoadedApk
类,能发现一些属性,下面会提到这个重要的这个。 -
将
info
中的mApplication
属性置空。复制代码隐藏代码
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
-
在
sCurrentActivityThread
下的链表mAllApplications
中移除移除mInitialApplication
。mInitialApplication
存放初始化的应用(当前壳应用),mAllApplications
存放的是所有的应用。把当前的应用,从现有的应用中移除掉,然后再把新构建的加入到里面去。复制代码隐藏代码
Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication"); ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications"); mAllApplications.remove(oldApplication);
-
构造新的应用
- 更新2处
className
。
复制代码隐藏代码
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 = srcAppClassName;appinfo_In_AppBindData.className = srcAppClassName;
- 注册应用程序(用LoadedApk中的makeApplication方法注册)。
复制代码隐藏代码
Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null });
- 替换
mInitialApplication
为刚刚创建³³的app
。
复制代码隐藏代码
RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app);
- 更新ContentProvider。
复制代码隐藏代码
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);}
- 更新2处
-
执行新应用的
onCreate
方法。复制代码隐藏代码
app.onCreate();
RefInvoke.java
Java反射调用的方法。
复制代码隐藏代码
package org.koi.dexloader;import java.lang.reflect.Field;import java.lang.reflect.Method;public class RefInvoke { 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 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; } 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(); } } 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; }}
AndroidManifest.xml
复制代码隐藏代码
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.koi.dexloader"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:name=".Proxy" android:theme="@style/Theme.DexLoader"> <meta-data android:name="APPLICATION_CLASS_NAME" android:value="org.koi.ctf20200802.APP"/> <activity android:name="org.koi.ctf20200802.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
关于资源的问题
出现了,源能够运行起来了,但是在运行的时候肯定会涉及程序相关的资源,比如布局文件等等,我们并没有介绍如何处理资源。
资源有2中大的处理方法。第一种是在壳dex压出源apk中,把apk中的资源保存复制程序下。第二种是替换壳apk中dex文件时,丰满apk中因为本文不重点讨论资源的处理问题,所以采用另一种方法,直接复制替换资源鉴定。
Dex组合修复工具将APK和壳DEX合并,生成一个新的DEX文件,并且最终新的DEX文件头。 加壳步骤
总结dex壳是比较的壳,只是将源APK更新后动态dex文件中,在运行时进行释放。我们也可以用frida框架进行脱壳。 动态加载DEX(Java)我们在上面动态加载APK时是采用了两个工程,一个工程负责加载APK,一个负责的业务流程,业务流程工程核心文件一个dex文件,可以考虑只将dex作为附件,然后进行动态加载dex。 项目实现演示代码
源工程
保存编译之后 删除 洗DEX新建一个Java工程实现一个简单的智能。 复制代码隐藏代码
把得到的文件原汁原味地创建的 把重生后的文件可以通过重新启动动态资产,然后再加载 dex 前进行目录下。 壳工程这里壳工程就在源工程的基础上修改就可以了,不用在新建一个工程。 分别创建
|
免费评分
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)