某国产DEX加固原理

Intro

这个加固是来自国内某个公司的作品,虽然在强度不是太高,但思路却不错,很好的平衡了性能和保护效果。目前看来,分析的这个版本只是一个很初级的框架,如果再此之上进行更多的防护和保护,强度会更好。

加固效果

加固之后,其效果是DEX中某些函数被加上ACC_NATIVE标志,原始的DEX代码已经消失,使得反编译、二次打包等功能失效。而加固之后新增的Libt**.so则负责在运行的时候,还原那些被抽取的Dalvik虚拟机代码。

相关结构

了解一下相关的结构有助于分析:

struct DexClassData {
    DexClassDataHeader header;
    DexField*          staticFields;
    DexField*          instanceFields;
    DexMethod*         directMethods;
    DexMethod*         virtualMethods;
};
struct DexMethod {
    u4 methodIdx;    /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff;      /* file offset to a code_item */
};
struct DexCode {
    u2  registersSize;
    u2  insSize;
    u2  outsSize;
    u2  triesSize;
    u4  debugInfoOff;       /* file offset to debug info stream */
    u4  insnsSize;          /* size of the insns array, in u2 units */
    u2  insns[1];
    /* followed by optional u2 padding */
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};

DexClassData描述了DEX文件中的某个类的相关信息,DexMethod则描述的是这个类的函数的信息。DexMethod的methodIdx是method在method_id_item中的Index,accessFlags描述了这个method的Access Control以及其他的信息,code_off是一个偏移,偏移之后的结构是DexCode结构,这个DexCode描述了method具体的信息。更多的信息可以参考Google的官方文档
以上是DEX文件中相关的结构,而DVM运行的时候,会用上述的信息构造Method,DVM使用这个结构来执行:

struct Method {
    ClassObject*    clazz;
    u4              accessFlags;
    u2             methodIndex;
    u2              registersSize;  /* ins + locals */
    u2              outsSize;
    u2              insSize;
    const char*     name;
    DexProto        prototype;
    const char*     shorty;
    const u2*       insns;          /* instructions, in memory-mapped .dex */
    int             jniArgInfo;
    DalvikBridgeFunc nativeFunc;
    bool fastJni;
    bool noRef;
    bool shouldTrace;
    const RegisterMap* registerMap;
    bool            inProfile;
};

如果Method.accessFlags没有ACC_NATIVE标志,则insns指向Dalvik虚拟机代码,反之,表明该Method的实现在Native层,insns应该为空。

加固原理

上面说到,加固的效果是抽取了原始的Dalvik虚拟机代码,并且Method被加上了ACC_NATIVE标志。libt**.so在加载的时候,会获取这些method对应的Method结构,去掉accessFlags中的ACC_NATIVE标志,并且将insns写回这个结构。另外,DVM对insns是有要求的,代码如下:

INLINE const DexCode* dvmGetMethodCode(const Method* meth) {
    if (dvmIsBytecodeMethod(meth)) {
        /*
         * The insns field for a bytecode method actually points at
         * &(DexCode.insns), so we can subtract back to get at the
         * DexCode in front.
         */
        return (const DexCode*)
            (((const u1*) meth->insns) - offsetof(DexCode, insns));
    } else {
        return NULL;
    }
}
INLINE u4 dvmGetMethodInsnsSize(const Method* meth) {
    const DexCode* pCode = dvmGetMethodCode(meth);
    return (pCode == NULL) ? 0 : pCode->insnsSize;
}

可见insns指向的是DexCode中的insns结构,也就是有了Method.insns,就可以得到原始的DexCode结构。

脱壳

进一步分析发现,被这个加固抽取掉的DexCode信息还原封不动的保存在Data section中,而在dex的尾部,新增了一个结构用于描述DexCode和原始函数的关系。libt**.so运行的时候,就是通过这个结构来还原Method中的insns信息。
最后,在smali中加了10来行代码,成功脱掉。

总结

这个加固实现的效果有:

1. 反静态分析
即便是原始的虚拟机代码仍然保存在文件中,但是二者的关系已经被模糊,不经过修改的工具无法得知原始代码的位置。

2. 防止内存Dump
由于针对修复的对象是Method,是DVM内存中的结构体,而原始的odex并没有被修改。

3. 防止二次打包
由于这个加固在DEX尾部添加了一个自定义的结构,baksmali二次打包的时候会导致这些信息丢失,原始函数和虚拟机代码的代码也因此丢失,所以编译之后的程序无法运行。
最后,我分析的这个libt**.so有一处越界的Bug,可能会导致程序崩溃,或者直接篡改任意代码——这有点搞笑……

某国产DEX加固原理》有一个想法

keykernel进行回复取消回复