某加固产品的脱壳分析和修复

0x00 前言

近几年来,随着Android移动领域的迅猛发展,出现越来越多的APP安全问题,为了保护APP防止被逆向破解,恶意篡改打包植入木马病毒, 窃取隐秘数据等问题,随着诞生了APK加固保护技术,本文主要对加固DEX技术的分析。

0x01 准备工作

  • Beyond Compare 可以通过对比看到加固前后的文件变化
  • ApkTool 可反编译和回编译APK
  • 010 Editor 提供了丰富的文件格式模版,可通过该工具分析DEX文件格式
  • IDA Pro 目前最流行的专业反汇编工具,可动态静态调试分析DEX文件和so文件
  • 原始APK文件与通过加固后的APK文件

0x02 分析过程

(1).用Beyond Compare对比工具看一下原始APK和加固后APK文件相关变化:

图1:原始APK与加固后APK的变化对比结果

加固后的文件列表变化:
新增文件列表:
libmain.so
libshell.so
修改文件列表:
AndroidManifest.xml
classes.dex
(2). ApkTool反编译加固后的APK, 出现无法正常反编译的错误,日志如下图2:
通过下面日志(图2)能看出来是apktool解析AndroidManifest.xml时出错,注意绿色下划线的name=fasten,这里是加固利用android系统解析axml的一个特点来导致apktool反编译时,在解析AndroidManifest.xml时出错。
关于利用AndroidManifest.xml这块的技术点传送站:地址

图2:ApkTool反编译加固APK失败的提示信息
(3).下面来分析和修复AndroidManifest.xml
分析前,可以先了解一下AndroidManifest.xml的二进制格式,参考下列文章:
AndroidManifest二进制文件格式分析
辅助分析AndroidManifest.xml的二进制格式可以使用下面的:
AXML的010 Editor模板
利用axml模版在010Editor解析AndroidManifest.xml能看到,有一个属性结构的name成员的值是25, 该值指向是string的索引,同时也是res ID的索引。
属性结构:

图3:利用010editor的模版功能分析AndroidManifest.xml
String索引:

图4:引起ApkTool反编译失败的String索引
Res ID 索引:

图5:引起ApkTool反编译失败的异常ResID
针对AndroidManifest.xml的name属性名相关作用和特点的解释:

图6:引用万抽抽大神的博客里对AndroidManifest.xml的属性名的解释
属性结构的name成员的值是即是string索引,又是ResID索引,所以:

Name=25
String[25]=fasten
ResIDs[25]=0x01017FFF

Android系统在解析AXML的属性的时候,是通过该属性的res id号而非属性名定位的。所谓的AXML就是AndroidManifest.xml对应的二进制文件,APK包中存储的就是AXML。比如属性:

1
<public type="attr" name="name" id="0x01010003" />

它的属性名为name,id号为0x01010003。
所以“faste”这个字符串可以随意改,关键是ResID的值,这里加固是对AndroidManifest.xml处理,插入一下非法的属性ID(在Android的attr里没有一个ID为0x01017FFF),因为是非法的属性ID,Android是不会去解析,但ApkTool却会去解析,所以导致反编译出错了。
关于attr的属性id号的定义,可参考以下文件(AndroidSDK里的文件):
%ANDROID_SDK%\platforms\android-19\data\res\values\public.xml
修复方法:
知道怎么回事,修复起来就很简单了,只要把非法的属性ID=0x0101FFFF改成一个合法的属性ID,比如把0x0101FFFF改成name的属性ID=0x01010003,然后再把修改后的AndroidManifest.xml再替换加固后apk里的AndroidManifest.xml,然后用apktook就可以顺利的成功的反编译出来。
附件有我用官网最新版的ApkTool 2.0.0 RC3源码编译,修改了一下,修复非法属性ID无法反编译。如果懒得手动去修改AndroidManifest.xml,可以直接用我这个修改过的apktool进行反编译。

图7:ApkTool修改版的反编译成功信息
反编译后,看加固修改后的AndroidManifest.xml和原版的AndroidManifest.xml多这三条:

1
2
3
<service android:name="com.tencent.mm.fasten.check.log" />
android:fasten="meta-data"
<meta-data android:name="@anim/push_top_out2" android:value="meta-data" />

0x03 还原修复Dex过程

ApkTool反编译可以成功,那接下来看一下该加固是怎么对Dex进行加密,通过Beyond Compare对比工具得知加固后变化如下:
新增了2个smail文件:

1
2
com\tencent\StubShell\ProxyShell.smali
com\tencent\StubShell\ShellHelper.smali

Smail代码的变化(对指定方法进行抽取代码):

图8:原始APK的类代码和加固后APK被抽取的类代码比较结果
从图8能看到,加固后的dex,通过apktool反编译后的smali代码变化。
(1)
新增静态代码块:
(只要加载此类,就会先执行该代码块,作用是用来动态恢复被加固的方法)

1
2
3
4
5
6
7
8
.method static constructor <clinit>()V
.locals 2
.prologue
const-string v0, "com.boco.nfc.activity"
const/16 v1, 0x0
invoke-static {v0,v1},Lcom/tencent/StubShell/ShellHelper;->StartShell(Ljava/lang/String;I)Z
return-void
.end method

用JEB转成代码如下:

1
2
3
static {
ShellHelper.StartShell("com.boco.nfc.activity", 0);
}

(2)
原始方法:

1
.method public constructor <init>(Landroid/content/Context;)V

改为native属性,并且隐藏字节码:

1
.method public native constructor <init>(Landroid/content/Context;)V

被加固后的Method数据:

图9:加固后DEX的Method结构变化
从这里能看到关键在于StartShell函数,这个StartShell函数专门负责在执行时动态还原被加固的Method结构数据,这种加固方式没办法直接通过dump来进行脱壳,它的机制是需要运行到某个类,在初始化加载这个类时才会修复一下该类被加固的Method结构数据,但又不能保证APP运行后所有类都能触发执行到,所以还是得找原始Method结构数据来进行修复dex。

1
public static boolean StartShell(String packageName, int iIndex)

从StartShell函数第二个参数iIndex来看,是要修复那个Method结构数据的序号。所以,可以肯定会有一份原始的数据供给修复,那么,我们从StartShell函数入手分析,就能找到修复的原始数据。

图10:加固Loader里的还原类函数代码的接口函数
StartShell函数会先判断如果没有初始化过则执行InitProxyShell函数,InitProxyShell函数作用其实就是加载libshell.so, 最后,调用libshell.so的load(ShellHelper.strPackageName, iIndex)函数来进行修复。
从这里能看到,关键是libshell.so的load函数在负责动态修复还原DEX的类方法Method结构数据的功能,下面就用IDA把libshell.so分析一下load函数。
(1) 看一下libshell.so的JNI_OnLoad函数(图11)
主要就是做一些初始化的操作,看来没什么我们想要的东西,我们直接主题,找load函数。

图11:libshell.so的JNI_OnLoad函数
(2) Load函数在0xC630的偏移

图12:libshell.so的Load函数
这里我说一下func_ShellFixDexMethod这个函数处理:
(a).通过/proc/(getpid)/maps 打开自身进程的内存映射,查找classes.dex的内存地址。
(b).该加固会把所有被经过抽取过的类方法Method结构的原始数据存一份在文件尾部。
定位Method的原始数据存放地址的方法:
原始数据偏移 = DexDataOff + DexDataSize
有多少个Method需要修复 = (DexFileSize – (DexDataOff + DexDataSize))/0x12
每一个类方法Method的原始数据是用一个0x12大小的结构来保存的,结构如下:

1
2
3
4
5
6
7
8
typedef struct TXFixDexData
{
DWORD dwClassDefItem; //Class_defs的索引id
DWORD dwMethodIdx; //DexMethod结构里的methodIdx值
DWORD dwaccessFlags; //DexMethod结构里的dwaccessFlags值
DWORD dwDexCodeOff; //DexMethod结构里的codeOff
WORD wProtoIdItem; //proto_ids的索引id
}TXFixDexData;

(c).已经可以知道Method的原始数据,接下来就看怎么修复。关键就是要怎么定位到哪个Method是需要修复的。如果熟悉Dex结构的,应该就比较容易如何修复。
我的修复方法:先通过Class_defs的索引id(TXFixDexData->dwClassDefItem)定位到需要修复的Method所在的类,再取该类的所有Method,把每个Method的DexMethod->methoIdx值等于TXFixDexData->dwMethodIdx,就确定是需要修复的Method, 然后把该Method的DexMethod结构的accessFlags和codeOff修复就OK。
下面是运行对加固的classes.dex的脱壳修复工具:

图13:脱壳修复工具的运行结果
最后,把修复完的classes.dex放到apk,再反编译下,能看到被隐藏Method的代码回来了,但是还需要做一些扫尾的事,才能算完全脱壳成功。
(d).搜索一下所有smali文件的下面这一句代码,然后全部删除:

1
invoke-static {v0, v1}, Lcom/tencent/StubShell/ShellHelper;->StartShell(Ljava/lang/String;I)Z

(e).删除掉AndroidManifest.xml这三个地方:

1
2
3
<service android:name="com.tencent.mm.fasten.check.log" />
android:fasten="meta-data"
<meta-data android:name="@anim/push_top_out2" android:value="meta-data" />

最后再重新打包为APK,至此,该加固的DEX脱壳修复完毕!

0x04 总结

(1).该加固产品的分析思路,先分析原始APK和加固后APK的文件变化,这样能比较清楚了解加固产品对原始APK的有哪些改动,能较快速找到分析的切入点。
(2).该加固产品通过在AndroidManifest.xml插入一个异常非法ResID,导致反编译工具无法正常运作。
(3).该加固产品的加固思路主要在于抽取类代码,在Apk运行调用方法时再进行修复还原,这样的好处是无法直接内存里dump DEX进行脱壳。

相关附件下载地址: http://pan.baidu.com/s/1eQs3fZc