Android APK加固

标题:Android APK 的当前方法
日期:2020-08-18 20:09:42
类别:

  • 安卓
  • apk_reverse
    标签:
  • 安卓壳

引言

当地的地方发生竞争,在Android的发展过程中就伴随着逆向和安固的发展。逆向发展可以通过一些非常好的软件,如IDA、JEB等,来原地逆向的速度;应用也会通过各种手段来阻止逆向手术对自己的应用进行。

但是逆向是真的百分百阻止的,所以才能通过其他的手段来提高应用被逆向的难度,让逆向只能时时需要(可以不利用自己的能力)利用手机的时间才能应用逆向成功在实际情况下,只要不明显影响应用运行速度,我们都可以采用这种思想来进行保护。

在这种背景下,膨胀混淆就应运而生了,这就是我们最开始的一种保护方式。这种方式将代码中的方法名和变量名用非常易于混淆的方式进行命名,如0oOlI1等组合命名方式。除了这种混淆方式外,为了提高逆向工作量,会在代码中加入相同或者类似方法名的方法或者增加一些没有必要的父类来达到代码膨胀的目的

但是这种方式不能阻止逆向工作者的脚步,开发后来者发现可以通过DexClassLoader这个类来进行DEX文件的动态加载。一般我们下情况称呼这种加固方式为加壳,由于这种是动态加载DEX文件,但是可以成为DEX壳。不过Android主要是由Java代码写的,而Java代码是很容易被逆向分析的,一般我们抽象地将动态加载DEX的代码原生态所以层进行运行,所以层的代码主要为c/c++代码,逆向的难度比Java会高很多,提高了应用的安全性。

随着这种方式的不断涌现,这种方式已经无法阻挡大多数的逆向工作了,人们就需要一种新的方式来和逆向进行对抗。在如此激烈的对抗中,此时就发现了一种利用elf文件格式(Android中.so的共享库为elf文件格式)来进行现有的方法。

在这样中,自己定义一个节,在节中举办我们的一些关键功能代码,通过elf文件格式将这部分的代码进行清洁,然后在elf文件加载初始化函数数组时,将被更新的代码起源这个出来。

加壳的优势:能够在一定程度上保护自己的核心代码算法,提高破解、盗版二次打包的难度,还可以通过代码注入、动态调试、或者内存注入攻击。

加壳或者其他的劣势:从某个时候说会,只要加了都可能影响应用的保护、运行效率。

由于Android手机的电池、CPU等硬件的限制,一般的应用不可能像PC上进行强度非常大的保护。

再生、膨胀

重整

主题思想:用没有意义的字符,如abc或者易于混淆的字符,如0oOlI1代替原本的有意义的类名。

参数:releaseminifyEnabled的值改为true,打开重新配置;加上shrinkResources true,资源压缩。

图像-20210731231222860

复制代码隐藏代码
#压缩级别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的目的。

膨胀代码有很多种实现的思想。比如乘法改加法、加法改自加等等,只要把代码量变大,不影响功能的实现就可以了。

这里自己写了一个简单的自动生成代码的工程。

https://gitee.com/koifishly/function_generator

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+

项目源码:nisosaikou/AndroidDEX壳 - 码云 - 开源中国 (gitee.com)

源APK

  1. 正常写功能逻辑代码。这里的代码为简单的ctf判断代码。

    图像-20210802104640435

  2. 新建类APP类并且这个类继承于类Application,实现onCreate方法。

    图像-20210802105543903

  3. 生成一个发布版本的apk,把这个apk保存起来。

  • 修改MainActivity.java的父类,使得MainActivity继承于Activity。字幕:将显示修改为运行的的英文源APK

    图像-20210331142149979

壳APK

Proxy.java

新建一个代{过} {滤}理类叫Proxy,继承于类Application。类这个用来释放状语从句:解密原始的APK。

  • attachBaseContext()

    替换Application中的attachBaseContext方法。这个方法会在ActivityonCreate方法之前执行。

    方法实现的功能主要有:

    • 把壳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事件,又要处理服务后台服务工作,通常会忙不过来。画面的事件。

类结构参考

图像-20210402145057352

  • 调用currentActivityThread方法获取ActivityThread中的成员sCurrentActivityThread

    复制代码隐藏代码
    Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});

    20210331150011

  • 获取sCurrentActivityThread中的mBoundApplication

    复制代码隐藏代码
    Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication");

  • 获取mBoundApplication中成员info

    复制代码隐藏代码
    Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info");

    图像-20210331155132287

  • 观察LoadedApk类,能发现一些属性,下面会提到这个重要的这个。

    图像-20210331155909576

  • info中的mApplication属性置空。

    复制代码隐藏代码
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
  • sCurrentActivityThread下的链表mAllApplications中移除移除mInitialApplicationmInitialApplication存放初始化的应用(当前壳应用),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 });

    图像-20210331163031229

    • 替换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);}
  • 执行新应用的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文件头。

加壳步骤

  • 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中。

    图像-20210402144136070

  2. 替换壳APK中的classes.dexresresources.arsc

  3. apk签名。

  4. 正常运行。

    图像-20210402144528503

总结

dex壳是比较的壳,只是将源APK更新后动态dex文件中,在运行时进行释放。我们也可以用frida框架进行脱壳。

动态加载DEX(Java)

我们在上面动态加载APK时是采用了两个工程,一个工程负责加载APK,一个负责的业务流程,业务流程工程核心文件一个dex文件,可以考虑只将dex作为附件,然后进行动态加载dex。

项目实现演示代码

简单地说,这里举办git的链接。

源工程

  • 新建一个简单功能的Android工程。
  • 创建assets文件夹。

图像-20210701112829779

保存编译之后apk文件中的.dex文件,把.dex文件保存到资产目录下。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 : <source file> <encrypted file>");            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目录下。

把重生​​后的文件可以通过重新启动动态资产,然后再加载 dex 前进行目录下。

图像-20210813142009676

壳工程

这里壳工程就在源工程的基础上修改就可以了,不用在新建一个工程。

分别创建ProxyApplication.javaRefInvoke.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.jaroptimizedDirectory:从APK或者jar解压出来的dex文件存放路径libraryPath:native库路径,可以为nullparent:父类装载器,一般为当前类的装载器、

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

图像-20210701135922593图像-20210701145257458

这样在执行时能出现dex文件。

APK 中的 DEX 文件中,不包含重要代码。

图像-20210701145105640

动态加载DEX(SO)

在上面的基础上,觉得可以把ProxyApplication.javaRefInvoke.java中的主要代码移到这么中来运行,这就是我们这种壳的主要思路。和上面的实现方式是一样的,只是换到了lib中运行语句。

创建一个Android原生工程,和一样的,在MainActivity中写一些代码。把dex文件后简单的原生资产文件夹中。

新的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

```c++

include <jni.h>#include <string>// 只在这里更新#define ENCRYPTED_FILE_NAME "flag"#define DECRYPTED_FILE_NAME "ot.dex"#define PACKAGE_NAME "org.koi.ctf20210814"extern "C"JNIEXPORT void JNICALLJava_org_koi_ctf2021atta 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(" 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, " "Landroid/util/ArrayMap;"); jfieldID fid_LoadedApk_mClassLoader = env->GetFieldID(clz_LoadedApk, "mClassLoader", "Ljava/lang/ClassLoader;"); 尝试 { 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 是 = 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 缓冲区 = env->NewByteArray(file_size); env->CallIntMethod(is, mid_InputStream_read, buffer); //读取jbytep_bt_ary = (jbyte*)env->GetByteArrayElements(buffer, 0); // 这里你可以添加解密函数。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); } 抓住 (...) {}}

复制代码隐藏代码
> 这个例子中,进行加密解密的操作,可以根据实际情况进行修改。 > > JNI中有部分代码可以提取到`JNI_OnLoad`或者`initarray`中进行处理。 > > JNI中的所有字符串可以进行一些处理,不直接暴露在源码中。 #### `AndroidManifest.xml` 按照上面的方法进行修改。 #### 注意 - 关闭`minifyEnabled`。 # ELF文件壳 在学习这部分内容之前需要**熟悉ELF的文件格式。** ## ELF节加密 ### 主要思想 编写代码时:自定义一个代码节(.mytext)(以后要进行加密,现在没有处理),然后一个初始化函数(.init_array),在这个函数中找到elf文件加载到内存中的地址,然后根据elf文件格式找到`.mytext`节,对这个节的内容进行解密。 加密:在原始apk编译好后,利用自己写的代码,把目标lib中的`.mytext`进行加密。 最后进行签名。 ### 代码 创建一个ndk项目。编写一段代码放入自定义的(代码)节`.koitext`中。 用` __attribute__((section(".koitext")))`来指定节。 ```c++ #include <jni.h>#include <string>#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 JNICALLJava_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;}

写了一个初始化的函数,再找找自己文件的基址以及给自定义的部分.koitext

头文件支持:

```c++

include<sys/types.h>#include<unistd.h>#include <sys/mman.h>#include <elf.h>

复制代码隐藏代码
```c 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\n", 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<nblock;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;}

效果:

图像-20210725235911325

ida会提示elf文件错误。

图像-20210725235943297

节表解析错误。

干净前函数

洗后函数

附加代码:

复制代码隐藏代码
#include <stdio.h>#include <cstdint>#include <string.h>typedef uint32_t Elf32_Addr; // Program addresstypedef uint32_t Elf32_Off;  // File offsettypedef 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\n", encrypt_foffset, encrypt_size);    printf("nblock = %d, nsize = %d\n", 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;}
 

免费评分

 

 

 

posted @   Domefy  阅读(445)  评论(0编辑  收藏  举报
编辑推荐:
· 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)
点击右上角即可分享
微信分享提示