Android APK的加固方法
有人的地方就有竞争,在Android的发展过程中就伴随着逆向和安固加固的发展。逆向工作者可以通过一些非常好用的软件,如IDA、JEB等,来加快逆向的速度;应用开发工作者也会通过各种手段来阻止逆向工作者对自己的应用进行逆向。
但是往往情况下逆向是不可能做到百分百阻止的,所以只能通过其他的手段来提高自己应用被逆向时的难度,让逆向工作者需要(可不绕过)花费足够多的时间才能把应用逆向成功。在实际情况下,只要不明显影响应用运行速度,我们都可以采用这种思想来进行保护。
在这种背景下,膨胀与混淆就应运而生了,这就是我们最开始的一种保护方式。这种方式将代码中的方法名和变量名用非常易于混淆的方式进行命名,如0、o、O、l、I、1等组合命名方式。除了这种混淆方式外,为了提高逆向工作量,会在代码中加入相同或者类似方法名的方法或者增加一些没有必要的父类来达到代码膨胀的目的。
但是这种方式不能阻止逆向工作者的脚步,后来开发者发现可以通过DexClassLoader这个类来进行DEX文件的动态加载。我们一般情况下称呼这种加固方式为加壳,由于这种是动态加载DEX文件,所以我们一般可以称为DEX壳。但是Android 主要是由 Java 代码编写的,而 Java 代码是非常容易被逆向分析的,所以渐渐地将动态加载DEX的代码放入 so 层进行运行,so 层的代码主要为c/c++代码,逆向的难度比 Java 会高很多,提高了应用的安全性。
随着这种加固方式的不断普及,这种方式已经不能阻挡大多数的逆向工作了,加固人员就需要一种新的加固方式来和逆向进行对抗。后期不管是加固还是逆向,都将目光放在了 so 层的对抗,此时就发现了一种利用elf文件格式(Android中.so的共享库为elf文件格式)来进行加固的方法。
在 so 中,自己定义一个节,在这个节中存放我们的一些关键功能代码,通过elf文件格式将这部分的代码进行加密,然后在elf文件加载执行初始化函数数组时,将被加密的代码解密出来。
加壳或者其他加固的优势:能在一定程度上保护自己核心代码算法,提高破解、盗版或者二次打包的难度,还可以防范代码注入、动态调试、内存注入攻击。
加壳或者其他加固的劣势:从理论上来说,只要加了保护都可能会影响应用的兼容性、运行效率。
由于受Android手机的电池、CPU等硬件的限制,一般的应用都不可能像PC上进行强度非常大的保护。
1、混淆、膨胀
混淆
主题思想:用没有意义的字符,如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等 #还可以使表示构造方法、表示方法、表示成员, #这些前面也可以加public等java属性限定 -keep class com.dev.demo.two.ClassTwoOne { public *; } #不混淆类名,以及里面的构造函数 -keep class com.dev.demo.ClassOne { public (); } #不混淆类名,以及参数为int 的构造函数 -keep class com.dev.demo.two.ClassTwoTwo { public (int); } #不混淆类的public修饰的方法,和private修饰的变量 -keepclassmembers class com.dev.demo.two.ClassTwoThree { public ; private ; } #不混淆内部类,需要用$修饰 #不混淆内部类ClassTwoTwoInner以及里面的全部成员 -keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}
更多混淆配置参考:
https://juejin.cn/post/6844903471095742472
https://www.huaweicloud.com/articles/ae151e2f60923097cefc473bd131addf.html
膨胀
代码混淆能在一定程度上增加逆向的难度,但是给逆向工作者增加的工作量是比较小的,代码膨胀就能够增加总的代码量,让逆向工作者必须分析全部的代码才能得到最终的一些结果。代码膨胀也是初始防御的方法之一,主要思想是编写一些垃圾代码来扩充代码量,这样在逆向分析时就可能会消耗攻击者大量的时间,从而达到保护APK的目的。
膨胀代码有很多种实现的思想。比如乘法改加法、加法改自加等等,只要把代码量变大,不影响功能的实现就可以了。
这里自己写了一个简单的自动生成代码的工程。
https://gitee.com/koifishly/function_generator
2、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。
项目实现demo代码
IDE:Android Studio 4.1.3
Android版本:4.4+
项目源码:nisosaikou/AndroidDEX壳 - 码云 - 开源中国 (gitee.com)
源APK
1、正常编写功能逻辑代码。这里的代码为简单的ctf 判断代码。
2、新建类APP类并且这个类继承于类Application,实现onCreate方法。
3、生成一个release版本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 mAllApplications = (ArrayList) 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(); }
ActivityThread功能
它管理应用进程的主线程的执行(相当于普通Java程序的main入口函数),并根据AMS的要求(通过IApplicationThread接口,AMS为Client、ActivityThread.ApplicationThread为Server)负责调度和执行activities、broadcasts和其它操作。
在Android系统中,在默认情况下,一个应用程序内的各个组件(如Activity、BroadcastReceiver、Service)都会在同一个进程(Process)里执行,且由此进程的【主线程】负责执行。
在Android系统中,如果有特别指定(通过android:process),也可以让特定组件在不同的进程中运行。无论组件在哪一个进程中运行,默认情况下,他们都由此进程的【主线程】负责执行。
【主线程】既要处理Activity组件的UI事件,又要处理Service后台服务工作,通常会忙不过来。为了解决此问题,主线程可以创建多个子线程来处理后台服务工作,而本身专心处理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 mAllApplications = (ArrayList) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications"); mAllApplications.remove(oldApplication);
构造新的Application
更新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;
注册application(用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); }
执行新app的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
<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>
关于资源的问题
到目前为止,源程序能够运行起来了,但是apk在运行的时候肯定会用到相关的资源,如布局文件等等,我们并没有介绍如何处理资源。
资源有2中大的处理方法。第一种是在壳dex解压出源apk时,把apk中的资源复制到现在程序下。第二种是替换壳apk中dex文件时,顺便用源apk中的资源文件替换到壳apk中。因为本文不重点讨论资源的处理问题,所以采用第二种方法,直接复制替换资源即可。
Dex组合修复工具
将APK和壳DEX文件合并,生成一个新的DEX文件,并且校正新的DEX文件头。
加壳步骤
- src.apk:源APK。
- des.apk:壳APK。
- DexFixed.jar:Dex工具。
- classes.dex:des.apk中的classes.dex。
- res:源APK中的文件夹。
- resources.arsc:源APK中的文件。
1、用DEXFixed.jar工具把src.apk和classes.dex进行合并,生成一个新的Dex,替换到壳APK中。
2、替换壳APK中的classes.dex、res、resources.arsc。
3、apk重新签名。
4、正常运行。
总结
dex壳是比较基础的壳,只是将源APK加密后放入dex文件中,在运行时进行释放。我们只需要在壳程序解密出原始的APK运行后,在内存中把dexdump下来就可以了,我们也可以用frida框架进行脱壳。
动态加载DEX(Java)
我们在上面动态加载APK时是采用了两个工程,一个工程负责加载APK,一个负责业务流程,业务流程工程核心文件就是一个dex文件,可以考虑只将dex文件作为附件,然后进行动态加载dex。
项目实现demo代码
简单来说,这里存放git的链接。
源工程
- 新建一个简单功能的 Android 工程。
- 创建assets文件夹。
保存编译之后apk文件中的.dex文件,把.dex文件保存到assets目录下。dex文件重命名为origin.dex(可以重命名为任意文件名)。
删除MainActivity.java。注意:这里只删除源文件,不要删除Activity。
加密DEX
新建一个Java工程实现一个简单的加密。
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; public class Main { public static void main(String[] args) { if (args.length != 2) { System.out.println("jar : "); return; } String sourceFile = args[0]; String encryptedFile = args[1]; try { FileInputStream fis = new FileInputStream(sourceFile); BufferedInputStream bis = new BufferedInputStream(fis); FileOutputStream fos = new FileOutputStream(encryptedFile); BufferedOutputStream bos = new BufferedOutputStream(fos); byte[] buffer = new byte[10240]; int acount = 0; while((acount = bis.read(buffer)) != -1) { byte[] encryptedData = encrypt(buffer); bos.write(encryptedData,0, acount); } bos.flush(); //关闭的时候只需要关闭最外层的流就行了 bos.close(); bis.close(); } catch (Exception e) { e.printStackTrace(); } } public static byte[] encrypt(byte[] sourceData) { for (int i = 0; i < sourceData.length; i++){ sourceData[i] ^= 273; } return sourceData; } }
把得到的加密文件放入刚刚创建的assets目录下。
把重命名后的文件可以通过加密后再放入assets目录下,然后再加载dex前进行解密。
壳工程
这里壳工程就在源工程的基础上修改就可以了,不需要在新建一个工程。
分别创建ProxyApplication.java和RefInvoke.java。这两个类的代码和上面基本一样,这里就不赘述了,直接看代码。
ProxyApplication.java
package org.koi.ctf20210813; import android.app.Application; import android.content.Context; import android.util.ArrayMap; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import dalvik.system.DexClassLoader; public class P extends Application { private final static String encryptedFileName = "flag"; private final static String package_name = "org.koi.ctf20210813"; private final static String activity_thread = "android.app.ActivityThread"; private final static String current_activity_thread = "currentActivityThread"; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { File cacheDir = getCacheDir(); if (!cacheDir.exists()){ cacheDir.mkdirs(); } File outFile = new File(cacheDir, "out.dex"); InputStream is = getAssets().open(encryptedFileName); FileOutputStream fos = new FileOutputStream(outFile); byte[] buffer = new byte[1024]; int byteCount; while ((byteCount = is.read(buffer)) != -1) { buffer = decrypt(buffer); fos.write(buffer, 0, byteCount); } fos.flush(); is.close(); fos.close(); String file_abs_path = outFile.getAbsolutePath(); Object currentActivityThread = I.invokeStaticMethod(activity_thread, current_activity_thread, new Class[]{}, new Object[]{}); ArrayMap mPackages = (ArrayMap)I.getFieldOjbect(activity_thread, currentActivityThread, "mPackages"); WeakReference weakReference = (WeakReference) mPackages.get(package_name); ClassLoader parent = (ClassLoader)I.getFieldOjbect("android.app.LoadedApk", weakReference.get(), "mClassLoader"); DexClassLoader dLoader = null; File dexOpt = base.getDir("dexOpt", base.MODE_PRIVATE); dLoader = new DexClassLoader(file_abs_path, dexOpt.getAbsolutePath(), null, parent); I.setFieldOjbect("android.app.LoadedApk", "mClassLoader", weakReference.get(), dLoader); } catch (IOException e) { e.printStackTrace(); } } public static byte[] decrypt(byte[] sourceData) { for (int i = 0; i < sourceData.length; i++){ sourceData[i] ^= 273; } return sourceData; } @Override public void onCreate() { super.onCreate(); } }
DexClassLoader加载Dex文件:
DexClassLoader(dexPath, optimizedDirectory, libraryPath, parent) dexPath:目标类所在的APK或者jar包,/.../xxx.jar optimizedDirectory:从APK或者jar解压出来的dex文件存放路径 libraryPath:native库路径,可以为null parent:父类装载器,一般为当前类的装载器、
RefInvoke.java
package org.koi.ctf20210813; import java.lang.reflect.Field; import java.lang.reflect.Method; public class I { 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
确认删除MainActivity.java,然后修改AndroidManifest.xml。
这样在执行时能解密原来dex文件。
APK中的DEX文件中,不包含重要代码。
动态加载DEX(SO)
在上面的基础上,想到可以把ProxyApplication.java和RefInvoke.java中的主要代码移到so中来运行,这就是我们这种壳的主要思路。和上面的实现方式是一样的,只是换到lib中运行而已。
创建一个Android 原生工程,和上面一样,在MainActivity中写一些简单代码。把dex文件加密后放入assets文件夹中。
新建ProxyApplication类,继承Application,把加载Dex这部分代码提取出来放入到一个新的类AttachBaseContent中。
ProxyApplication.java
import android.app.Application; import android.content.Context; public class P extends Application { static { System.loadLibrary("ctf20210814"); } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); attachBase(base); } @Override public void onCreate() { super.onCreate(); } public static native void attachBase(Context base); }
这里新建一个koi.cpp文件,其中有一个Java_org_koi_dexsoshell_AttachBaseContext_onAttach函数,对应Java中AttachBaseContext类下的onAttach方法。
native-lib.cpp
#include #include // only update here #define ENCRYPTED_FILE_NAME "flag" #define DECRYPTED_FILE_NAME "ot.dex" #define PACKAGE_NAME "org.koi.ctf20210814" extern "C" JNIEXPORT void JNICALL Java_org_koi_ctf20210814_P_attachBase(JNIEnv *env, jclass clazz, jobject base) { jclass clz_File = env->FindClass("java/io/File"); jclass clz_Context = env->FindClass("android/content/Context"); jclass clz_AssetManager = env->FindClass("android/content/res/AssetManager"); jclass clz_InputStream = env->FindClass("java/io/InputStream"); jclass clz_FileOutputStream = env->FindClass("java/io/FileOutputStream"); jclass clz_ActivityThread = env->FindClass("android/app/ActivityThread"); jclass clz_ArrayMap = env->FindClass("android/util/ArrayMap"); jclass clz_WeakReference = env->FindClass("java/lang/ref/WeakReference"); jclass clz_LoadedApk = env->FindClass("android/app/LoadedApk"); jclass clz_DexClassLoader = env->FindClass("dalvik/system/DexClassLoader"); jmethodID mid_File_init = env->GetMethodID(clz_File, "", "(Ljava/io/File;Ljava/lang/String;)V"); jmethodID mid_FileOutputStream_init = env->GetMethodID(clz_FileOutputStream, "", "(Ljava/io/File;)V"); jmethodID mid_DexClassLoader_init = env->GetMethodID(clz_DexClassLoader, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V"); jmethodID mid_Context_getCacheDir = env->GetMethodID(clz_Context, "getCacheDir", "()Ljava/io/File;"); jmethodID mid_Context_getAssets = env->GetMethodID(clz_Context, "getAssets", "()Landroid/content/res/AssetManager;"); jmethodID mid_Context_getDir = env->GetMethodID(clz_Context, "getDir", "(Ljava/lang/String;I)Ljava/io/File;"); jmethodID mid_AssetManager_open = env->GetMethodID(clz_AssetManager, "open", "(Ljava/lang/String;)Ljava/io/InputStream;"); jmethodID mid_File_exists = env->GetMethodID(clz_File, "exists", "()Z"); jmethodID mid_File_mkdirs = env->GetMethodID(clz_File, "mkdirs", "()Z"); jmethodID mid_File_getAbsolutePath = env->GetMethodID(clz_File, "getAbsolutePath", "()Ljava/lang/String;"); jmethodID mid_InputStream_read = env->GetMethodID(clz_InputStream, "read", "([B)I"); jmethodID mid_InputStream_close = env->GetMethodID(clz_InputStream, "close", "()V"); jmethodID mid_InputStream_available = env->GetMethodID(clz_InputStream, "available", "()I"); jmethodID mid_FileOutputStream_write = env->GetMethodID(clz_FileOutputStream, "write", "([BII)V"); jmethodID mid_FileOutputStream_flush = env->GetMethodID(clz_FileOutputStream, "flush", "()V"); jmethodID mid_FileOutputStream_close = env->GetMethodID(clz_FileOutputStream, "close", "()V"); jmethodID mid_ActivityThread_currentActivityThread = env->GetStaticMethodID(clz_ActivityThread, "currentActivityThread", "()Landroid/app/ActivityThread;"); jmethodID mid_ArrayMap_get = env->GetMethodID(clz_ArrayMap, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); jmethodID mid_WeakReference_get = env->GetMethodID(clz_WeakReference, "get", "()Ljava/lang/Object;"); jfieldID fid_ActivityThread_mPackages = env->GetFieldID(clz_ActivityThread, "mPackages", "Landroid/util/ArrayMap;"); jfieldID fid_LoadedApk_mClassLoader = env->GetFieldID(clz_LoadedApk, "mClassLoader", "Ljava/lang/ClassLoader;"); try { jobject cacheDir = env->CallObjectMethod(base, mid_Context_getCacheDir); if (!env->CallBooleanMethod(cacheDir, mid_File_exists)) { env->CallBooleanMethod(cacheDir, mid_File_mkdirs); } jstring str = env->NewStringUTF(DECRYPTED_FILE_NAME); jobject outFile = env->NewObject(clz_File, mid_File_init, cacheDir, str); jobject AssetManager = env->CallObjectMethod(base, mid_Context_getAssets); jstring out_file_name = env->NewStringUTF(ENCRYPTED_FILE_NAME); jobject is = env->CallObjectMethod(AssetManager, mid_AssetManager_open, out_file_name); jobject fos = env->NewObject(clz_FileOutputStream, mid_FileOutputStream_init, outFile); jint file_size = env->CallIntMethod(is, mid_InputStream_available); jbyteArray buffer = env->NewByteArray(file_size); env->CallIntMethod(is, mid_InputStream_read, buffer); //read jbyte* p_bt_ary = (jbyte*)env->GetByteArrayElements(buffer, 0); // here you can add decryption function. for (jint i = 0; i < file_size; ++i) { p_bt_ary[i] ^= 273; } env->SetByteArrayRegion(buffer, 0, file_size, p_bt_ary); env->CallVoidMethod(fos, mid_FileOutputStream_write, buffer, 0, file_size); env->DeleteLocalRef(buffer); env->CallVoidMethod(fos, mid_FileOutputStream_flush); env->CallVoidMethod(is, mid_InputStream_close); env->CallVoidMethod(fos, mid_FileOutputStream_close); jstring file_abs_path = (jstring) env->CallObjectMethod(outFile, mid_File_getAbsolutePath); jobject currentActivityThread = env->CallStaticObjectMethod(clz_ActivityThread, mid_ActivityThread_currentActivityThread); jobject mPackages = env->GetObjectField(currentActivityThread, fid_ActivityThread_mPackages); jstring package_name = env->NewStringUTF(PACKAGE_NAME); jobject weakReference = env->CallObjectMethod(mPackages, mid_ArrayMap_get, package_name); jobject loadedApk = env->CallObjectMethod(weakReference, mid_WeakReference_get); jobject parent = env->GetObjectField(loadedApk, fid_LoadedApk_mClassLoader); jstring jstr_dexOpt = env->NewStringUTF("dexOpt"); jobject dexOpt = env->CallObjectMethod(base, mid_Context_getDir, jstr_dexOpt, 0); jstring dexOpt_abs_path = (jstring) env->CallObjectMethod(dexOpt, mid_File_getAbsolutePath); jstring str_null = env->NewStringUTF(""); jobject dLoader = env->NewObject(clz_DexClassLoader, mid_DexClassLoader_init, file_abs_path, dexOpt_abs_path, str_null, parent); env->SetObjectField(loadedApk, fid_LoadedApk_mClassLoader, dLoader); } catch (...) {} }
这个例子中,进行加密解密的操作,可以根据实际情况进行修改。
JNI中有部分代码可以提取到JNI_OnLoad或者initarray中进行处理。
JNI中的所有字符串可以进行一些处理,不直接暴露在源码中。
AndroidManifest.xml
按照上面的方法进行修改。
注意:关闭minifyEnabled。
3、ELF文件壳
在学习这部分内容之前需要熟悉ELF的文件格式。
ELF节加密
主要思想
编写代码时:自定义一个代码节(.mytext)(以后要进行加密,现在没有处理),然后一个初始化函数(.init_array),在这个函数中找到elf文件加载到内存中的地址,然后根据elf文件格式找到.mytext节,对这个节的内容进行解密。
加密:在原始apk编译好后,利用自己写的代码,把目标lib中的.mytext进行加密。
最后进行签名。
代码
创建一个ndk项目。编写一段代码放入自定义的(代码)节.koitext中。
用__attribute__((section(".koitext")))来指定节。
#include #include #define SECTION_NAME ".koitext" #define JNIHIDDEN __attribute__((visibility("hidden"))) // save the result. int fw[40] = {13, 18, 14, 64, 11, 65, 16, 14, 20, 14, 11, 14, 18, 61, 12, 13, 60, 60, 20, 62, 16, 61, 61, 64, 63, 63, 15, 18, 12, 63, 14, 64, 13, 18, 14, 64, 11, 65, 16, 14}; int fs[38]; void str2ints (const char* fw, int* results); char* jstring2charAry(JNIEnv* env, jstring jstr); extern "C" JNIEXPORT __attribute__((section(SECTION_NAME))) jboolean JNICALL Java_org_koi_ctf20210821_MainActivity_checkflag(JNIEnv *env, jobject thiz, jstring flag) { char fg[]="flag{helloboy_ewri346hHeewr34dr}"; str2ints(jstring2charAry(env, flag), fs); for (int i = 0; i < strlen(fg); ++i) { if(fw[i] != fs[i] ) return false; } return true; } __attribute__((section(SECTION_NAME))) void str2ints (const char* fw, int* results) { for (int mX4WyHKgmwSPY1V = 0; mX4WyHKgmwSPY1V < 32; mX4WyHKgmwSPY1V++){results[mX4WyHKgmwSPY1V] = fw[mX4WyHKgmwSPY1V];} for (int _ZKdmdmEjiQ_Ouw = 0; _ZKdmdmEjiQ_Ouw < 32; _ZKdmdmEjiQ_Ouw++){results[_ZKdmdmEjiQ_Ouw] = results[_ZKdmdmEjiQ_Ouw] + 3;} for (int zbTK_I56tB0GevN = 0; zbTK_I56tB0GevN < 32; zbTK_I56tB0GevN++){results[zbTK_I56tB0GevN] = results[zbTK_I56tB0GevN] + 10;} for (int DfeXBWD6dcNPXKo = 0; DfeXBWD6dcNPXKo < 32; DfeXBWD6dcNPXKo++){results[DfeXBWD6dcNPXKo] = results[DfeXBWD6dcNPXKo] - 58;} for (int jqJhXnPQwPYi2G6 = 0; jqJhXnPQwPYi2G6 < 32; jqJhXnPQwPYi2G6++){results[jqJhXnPQwPYi2G6] = results[jqJhXnPQwPYi2G6] - 66;} for (int xA7fCVlKruHZC4Y = 0; xA7fCVlKruHZC4Y < 32; xA7fCVlKruHZC4Y++){results[xA7fCVlKruHZC4Y] = results[xA7fCVlKruHZC4Y] + 66;} for (int sGVbaq_poAxfJ3O = 0; sGVbaq_poAxfJ3O < 32; sGVbaq_poAxfJ3O++){results[sGVbaq_poAxfJ3O] = results[sGVbaq_poAxfJ3O] + 8;} for (int EIGWrEGI6UaAjH8 = 0; EIGWrEGI6UaAjH8 < 32; EIGWrEGI6UaAjH8++){results[EIGWrEGI6UaAjH8] = results[EIGWrEGI6UaAjH8] + 49;} for (int nHJAUmNRoQs5M9k = 0; nHJAUmNRoQs5M9k < 32; nHJAUmNRoQs5M9k++){results[nHJAUmNRoQs5M9k] = results[nHJAUmNRoQs5M9k] + 11;} for (int NzhuxVIobubHcRM = 0; NzhuxVIobubHcRM < 32; NzhuxVIobubHcRM++){results[NzhuxVIobubHcRM] = results[NzhuxVIobubHcRM] - 64;} for (int Wa46hlZr0UFGqFu = 0; Wa46hlZr0UFGqFu < 32; Wa46hlZr0UFGqFu++){results[Wa46hlZr0UFGqFu] = results[Wa46hlZr0UFGqFu] + 4;} } JNIHIDDEN __attribute__((section(SECTION_NAME))) char* jstring2charAry(JNIEnv* env, jstring jstr) { jclass jcls_String = env->FindClass("java/lang/String"); jmethodID jmid_toCharArray = env->GetMethodID(jcls_String, "toCharArray", "()[C"); jmethodID jmid_length = env->GetMethodID(jcls_String, "length", "()I"); jcharArray charArray = (jcharArray)env->CallObjectMethod(jstr, jmid_toCharArray); jint len = env->CallIntMethod(jstr, jmid_length); char* pString = new char[len]; pString[len] = 0; jboolean fals = false; for (int i = 0; i < len; ++i) { pString[i] = env->GetCharArrayElements(charArray, &fals)[i]; } return pString; }
写一个初始化的函数,用来查找elf文件的基址以及给自定义的.koitext解密。
头文件支持:
#include #include #include #include void init_native_Add() __attribute__((constructor)); unsigned long getLibAddr(); // loaded so file #define SO_LIB_FILE_NAME "libctf20210821.so" void init_native_Add(){ char name[15]; unsigned int nblock; unsigned int nsize; unsigned long base; unsigned long text_addr; unsigned int i; Elf32_Ehdr *ehdr; Elf32_Shdr *shdr; base=getLibAddr(); //在/proc/id/maps文件中找到我们的so文件,活动so文件地址 ehdr=(Elf32_Ehdr *)base; text_addr=ehdr->e_shoff+base;//加密节的地址 nblock=ehdr->e_entry >>16;//加密节的大小 nsize=ehdr->e_entry&0xffff;//加密节的大小 printf("nblock = %d", nblock); //修改内存权限 if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ puts("mem privilege change failed"); } //进行解密,是针对加密算法的 for(i=0;i char *addr=(char*)(text_addr+i); *addr=~(*addr); } if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){ puts("mem privilege change failed"); } puts("Decrypt success"); } //获取到SO文件加载到内存中的起始地址,只有找到起始地址才能够进行解密; unsigned long getLibAddr(){ unsigned long ret=0; char name[] = SO_LIB_FILE_NAME; char buf[4096]; char *temp; int pid; FILE *fp; pid=getpid(); sprintf(buf,"/proc/%d/maps",pid); //这个文件中保存了进程映射的模块信息 cap /proc/id/maps 查看 fp=fopen(buf,"r"); if(fp==NULL){ puts("open failed"); goto _error; } while (fgets(buf,sizeof(buf),fp)){ if(strstr(buf,name)){ temp = strtok(buf, "-"); //分割字符串,返回 - 之前的字符 ret = strtoul(temp, NULL, 16); //获取地址 break; } } _error: fclose(fp); return ret; }
效果:
ida会提示elf文件错误。
节表解析错误。
附加加密代码:
#include #include #include typedef uint32_t Elf32_Addr; // Program address typedef uint32_t Elf32_Off; // File offset typedef uint16_t Elf32_Half; typedef uint32_t Elf32_Word; typedef int32_t Elf32_Sword; enum { EI_MAG0 = 0, // File identification index. EI_MAG1 = 1, // File identification index. EI_MAG2 = 2, // File identification index. EI_MAG3 = 3, // File identification index. EI_CLASS = 4, // File class. EI_DATA = 5, // Data encoding. EI_VERSION = 6, // File version. EI_OSABI = 7, // OS/ABI identification. EI_ABIVERSION = 8, // ABI version. EI_PAD = 9, // Start of padding bytes. EI_NIDENT = 16 // Number of bytes in e_ident. }; struct Elf32_Ehdr { unsigned char e_ident[EI_NIDENT]; // ELF Identification bytes Elf32_Half e_type; // Type of file (see ET_* below) Elf32_Half e_machine; // Required architecture for this file (see EM_*) Elf32_Word e_version; // Must be equal to 1 Elf32_Addr e_entry; // Address to jump to in order to start program Elf32_Off e_phoff; // Program header table's file offset, in bytes Elf32_Off e_shoff; // Section header table's file offset, in bytes Elf32_Word e_flags; // Processor-specific flags Elf32_Half e_ehsize; // Size of ELF header, in bytes Elf32_Half e_phentsize; // Size of an entry in the program header table Elf32_Half e_phnum; // Number of entries in the program header table Elf32_Half e_shentsize; // Size of an entry in the section header table Elf32_Half e_shnum; // Number of entries in the section header table Elf32_Half e_shstrndx; // Sect hdr table index of sect name string table unsigned char getFileClass() const { return e_ident[EI_CLASS]; } unsigned char getDataEncoding() const { return e_ident[EI_DATA]; } }; // Program header for ELF32. struct Elf32_Phdr { Elf32_Word p_type; // Type of segment Elf32_Off p_offset; // File offset where segment is located, in bytes Elf32_Addr p_vaddr; // Virtual address of beginning of segment Elf32_Addr p_paddr; // Physical address of beginning of segment (OS-specific) Elf32_Word p_filesz; // Num. of bytes in file image of segment (may be zero) Elf32_Word p_memsz; // Num. of bytes in mem image of segment (may be zero) Elf32_Word p_flags; // Segment flags Elf32_Word p_align; // Segment alignment constraint }; // Section header. struct Elf32_Shdr { Elf32_Word sh_name; // Section name (index into string table) Elf32_Word sh_type; // Section type (SHT_*) Elf32_Word sh_flags; // Section flags (SHF_*) Elf32_Addr sh_addr; // Address where section is to be loaded Elf32_Off sh_offset; // File offset of section data, in bytes Elf32_Word sh_size; // Size of section, in bytes Elf32_Word sh_link; // Section type-specific header table index link Elf32_Word sh_info; // Section type-specific extra information Elf32_Word sh_addralign; // Section address alignment Elf32_Word sh_entsize; // Size of records contained within the section }; long get_file_size(FILE* pf); int main() { char elf_name[64] = "C:\\Users\\koi\\Desktop\\libnative-lib.so"; char want2encrypt_section_name[] = ".mytext"; FILE* pf_elf = fopen(elf_name, "rb"); long sz_file = get_file_size(pf_elf); char* file_buf = new char[sz_file]; fread(file_buf, sz_file, 1, pf_elf); Elf32_Ehdr* ehdr = (Elf32_Ehdr*)(file_buf); // 字符串节头表的位置 Elf32_Shdr* shdrstr = (Elf32_Shdr*)(file_buf + ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shstrndx); char* sh_str = (char*)(file_buf + shdrstr->sh_offset);//偏移到字符串表 Elf32_Shdr* shdr = (Elf32_Shdr*)(file_buf + ehdr->e_shoff); int encrypt_foffset = 0; int encrypt_size = 0; for (int i = 0; i < ehdr->e_shnum; i++, shdr++) { //根据字符串表的名称比较 if (strcmp(sh_str + shdr->sh_name, want2encrypt_section_name) == 0) { encrypt_foffset = shdr->sh_offset; encrypt_size = shdr->sh_size; break; } } char* content = (char*)(file_buf + encrypt_foffset); int block_size = 16; int nblock = encrypt_size / block_size; int nsize = encrypt_foffset / 4096 + (encrypt_foffset % 4096 == 0 ? 0 : 1); printf("base = 0x%x, length = 0x%x", encrypt_foffset, encrypt_size); printf("nblock = %d, nsize = %d", nblock, nsize); //将节的地址和大小写入 ehdr->e_entry = (encrypt_size << 16) + nsize; ehdr->e_shoff = encrypt_foffset; //节的地址 //加密 for (int i = 0; i < encrypt_size; i++) { content[i] = ~content[i]; } strcat(elf_name, "_m"); FILE* m_elf_file = fopen(elf_name, "wb"); fwrite(file_buf, sz_file, 1, m_elf_file); return 0; } long get_file_size(FILE* pf) { long cur_pos = ftell(pf); fseek(pf, 0, SEEK_END); long sz_file = ftell(pf); fseek(pf, cur_pos, SEEK_SET); return sz_file; }
