2020-10-02

安卓软件加固防护指南:如何最大限度防止自身软件被脱壳工具直接脱壳

作者:好中文的样子 所属分类 - 安全 - 干货 - 广告 - 黑科技

安卓软件开发者比较头疼的一件事是自己的付费软件,被破解者轻易破解。于是多数安卓软件开发者会寻找加固厂商支持,也有部分软件开发者会自行实现软件保护加固方案。这里分享一下如何防止安卓软件加固以后被轻松脱壳。注意:阅读本文的开发者需要先了解安卓App的dex文件是什么用途的,并且需要掌握一定的Java语言基础,以及知道dex文件加固以后有什么变化。建议同时拥有安卓4.4、7.0、9.0、10.0的设备来阅读本文(开发者自备相关安卓系统版本的设备)。

安卓软件加固

在现有的免费或者低价加固方案当中,最常见的加固方案是DEX整体加固并内存或者落地加载。对于这种方案,在安卓7.1以及以下版本当中,很多人选择使用DexCache类里的一个函数:getDex。这个函数如下:

97    Dex getDex() {
98        Dex result = dex;
99        if (result == null) {
100            synchronized (this) {
101                result = dex;
102                if (result == null) {
103                    dex = result = getDexNative();
104                }
105            }
106        }
107        return result;
108    }

源码来自Android 7.0的AOSP系统当中,我们先看如何获取这个Dex类。根据安卓7.0系统的源码,我们可以看见Class.java(java.lang.Class)里面有一个getDex方法,里面就是调用Class的dexCache私有成员的getDex方法。

那么,获取到这个Dex类实例有什么用呢?我们通过查看安卓系统源码,知道这个Dex类具有很重要的脱壳作用,Dex类里面有一个方法,定义如下:

165    public void writeTo(OutputStream out) throws IOException {
166        byte[] buffer = new byte[8192];
167        ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe
168        data.clear();
169        while (data.hasRemaining()) {
170            int count = Math.min(buffer.length, data.remaining());
171            data.get(buffer, 0, count);
172            out.write(buffer, 0, count);
173        }
174    }
175
176    public void writeTo(File dexOut) throws IOException {
177        OutputStream out = new FileOutputStream(dexOut);
178        writeTo(out);
179        out.close();
180    }
220    /**
221     * Returns a copy of the the bytes of this dex.
222     */
223    public byte[] getBytes() {
224        ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe
225        byte[] result = new byte[data.capacity()];
226        data.position(0);
227        data.get(result);
228        return result;
229    }

writeTo方法是干什么的呢?跟脱壳有什么关系呢?我们不难看出,Dex类里面的私有成员data是一个ByteBuffer实例,里面存储了软件原本dex的内容。对于普通dex整体加固,获取这个data并写出到文件,就可以获得加固前的整个dex文件。

这个获取dex方法适用于所有所谓的免费加固,并且对于安卓7.0或者之前的平台,都可以实现获取原dex的效果。我们看看下面这个加固应用加载的流程:

安卓应用加固以后加载的流程

联系我们上面分析的内容,关于dexCache里面getDex函数的调用,实际上我们只需要找到真正应用加载的Class,就能直接调用这个函数获取Dex实例。这里已经有人做出来一个Xposed插件,原理也很简单,开源地址:https://gitee.com/Jamie793/ReflectMaster?_from=gitee_search。这个插件在Activity上显示一个按钮,View附加到Activity上,可以直接调用Activity实例的getClass()函数,获取Dex并脱壳。

那么我们如何防止这种最通用的脱壳方法呢?治标不治本的方法为更改我们的目标平台,让应用只能在安卓8或者更高版本的安卓系统运行(例如在壳或者应用里面调用一些安卓8.0才有的API,这样即使破解者更改了目标平台版本,也会立即崩溃)。除此之外,我们可以直接使用Hook框架来Hook getDex这个函数,使得其返回null(黑科技,不太推荐),判断当前系统SDK为安卓7.1或者更早,就可以尝试hook getDex函数,这样破解者就无法调用这个函数达成dex写出。关于hook框架,这里有一个安卓7.1以及更早版本都兼容的:https://github.com/lianglixin/epic。如何hook getDex函数,可以查看github上的说明,我们只需要hook Class.class里面的getDex函数即可。下面上code:

DeXposedBridge.hookMethod(Class.class.getDeclaredMethod("getDex"), new InterruptMethodHook());

关于InterruptMethodHook,就是beforeHookedMethod里面直接param.setResult(null)即可,这样就能完美解决安卓7.x以及更早版本的Java层脱壳方法。

安卓脱壳效果

到这里,不少朋友问了,都0202年年底了,现在都使用的Android 9免root脱壳(例如SPatch 5.x或更早版本、SandVXposed等软件),这种方法对于SPatch 5.x以及SandVXposed是无效的!Yes,这种防止加固脱壳的方法确实是Too young too simple。我们如今使用的黑科技大多是会用一些非Java的东西(包括Kotlin反射高级语法调用绕过某些钩子),以及在C层下钩子一类的。那么我们应该怎么办呢?这里稍微介绍以下某加固的方法:

插入无效代码

某讯御安全在Dex加固当中插入了一个无效的A001类,这样可以使得反编译smali失败,同时使用了一些动态填写手段(替换方法code item)来使得反编译得到无效代码。

在入口调用的无效函数

使用Code Item替换,进行加固,是一种和Dex2C一样相对新颖的手段,关于Dex2C我们稍后讨论。这里先给大家看看什么是Code Item替换,步骤如下:

  • 1、解析原始dex文件格式,保存所有方法的代码结构体信息。
  • 2、通过传入需要置空指令的方法和类名,检索到其代码结构体信息。
  • 3、通过方法的代码结构体信息获取指令个数和偏移地址,构造空指令集,然后覆盖原始指令。
  • 4、重新计算dex文件的checksum和signature信息,回写到头部信息中。

这里可能部分开发者不懂了,这是啥玩意,是什么鬼?有啥用?怎么用?别着急,我们依然是从Android源码开始。来看安卓5.0以后的ArtMethod.h,这个文件是Art下方法的信息类,关键点如下。

688  // Access flags; low 16 bits are defined by spec.
689  uint32_t access_flags_;
690
691  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
692
693  // Offset to the CodeItem.
694  uint32_t dex_code_item_offset_;
695
696  // Index into method_ids of the dex file associated with this method.
697  uint32_t dex_method_index_;
698
699  /* End of dex file fields. */

明人不说暗话,这里我们直接指出关键是dex_code_item_offset_。对于这个成员的偏移,从安卓5.0到安卓11都是没变化的,有变化的是后面的EntryPoint之类的。而EntryPoint是非关键位置,因为那些地方是compile以后hook框架使用的,我们不看这些玩意。现在我们说说加固是怎么做的:

首先,替换掉dex_code_item_offset_偏移,然后使用jit来主动compile函数,这时候函数的内容就被还原了。然后,进行方法的属性以及签名修复,有的加固会把修复放在compile之前,因为这些加固可能更改了签名之类的。

对于脱壳工具,目前已知的有SPatch(V30以上版本使用FastDumper插件)、FART,开源地址:https://github.com/hanbinglengyue/FART。这种脱壳工具也是利用完全主动的调用方式,强迫方法批量还原。对于这种还原方法,我们只需要简单检查libart.so这个库是否被修改即可,直接dlsym这个脱壳机的DexFile_dumpDexFile函数,如果获取到这个函数,我们简单退出不加载真实code item即可。

接下来介绍一下另一种比较少用的加固方案,就是Java2C(或者叫Dex2C)。这种方案在顶象加固、360加固企业版、梆梆加固企业版、腾讯御安全高级版、网易网盾当中会有。关于较好的开源实现,目前是华为的Open Ark Compiler,开源地址:https://www.openarkcompiler.cn/home。关于这种加固方案,除华为以外,实际上就是简单将Java文件编译成原生库。除了华为的OpenArk编译器,别的加固都是简单把smali字节码使用解释执行的方法在JNI层运行。

华为OpenArk编译器

而对于这种加固的脱壳方案,使用SPatch V60.3或者更高版本,配合SK-Dex2cDumper插件才可完成dex2c的反编译解密工作,支持顶象、爱加密、360企业版、御安全高级版、网易网盾等等所有主流Dex2C加固方案。

开发者较少采用这种方案,最大的原因是经济实用性、兼容性以及性能问题。在使用Dex2C以后,等同于脱裤子放屁,使用JNI层动态解析smali字节码并调用JNIEnv执行,极大影响了运行效率,而且还原程度相比DexProtector是更为彻底的。

安卓Xaramin控制流混淆

目前还有另一种新颖的纯Java层加固方案,即为将部分敏感类采用纯动态反射加载执行以及更改运行流程等(称为反射流以及控制流混淆),目前这种方案适合本身软件体积较小、对性能要求非常低的软件(例如某些写作软件、某些小工具等等)。而纯反射执行目前还原仍不太好,因此可以作为开发者的另一种考虑选择。

麦科技原创,转载请说明出处。