360加固保关键技术浅析

阅读本文需要您有ELF和DEX文件结构等相关背景知识。为避免翻译习惯不同带来歧义,一些术语会使用英文描述。本文所用的APK样本地址点我下载,版本:32.1,文件大小:9,770,467字节。

图 1
图1描述的是360加固保技术方案示意图,序号表示其执行的先后次序。除原DEX外,剩余的组件均为360加固保所添加。更准确点,原DEX和AndroidManifest.xml里也被添加了代码。壳ELF来源于独立的so文件。图中原DEX和辅助DEX之间还有一段数据。
1 城门洞开迎司马
 一般而言,加固后的APK都会加入一些干扰信息,破坏反编译工具的正常运行。最新版本的360加固保是个例外,壳DEX和壳ELF都可以静态反编译。
其原因可能有二:首先,反编译工具的不断完善,能轻松绕过其中的干扰信息;其次,真正有用的数据都已被加密。与其花精力构造无用的反编译陷阱,不如集中力量保护好重要数据。
各种APK加固方案在技术选择上可能不尽相同,但它们都有一个共同的理论基础——被加密的目标数据足够安全。安全到静态反编译无法提取完整的目标数据。如果该基础被攻破,那么整个加固方案都将如流沙上的高楼轰然倒塌。
既然360加固保大门不设防,纵使里面没有太多可用信息,我们还是进入略探其究竟。
1.1 DEX文件静态分析

图 2
解析壳DEX文件,如图2。其中map item个数是16,简单计算可获得壳DEX文件大小为0x3b18,小于当前文件大小0x1d34d4。

图 3
图3所示壳DEX文件被插入的额外数据。看到重复出现的52,很容易让人联想这是用0×52异或简单加密后的数据。
反编译壳DEX文件后可以获得两条信息。
首先在AndroidManifest.xml设置application为com.stub.StubApp,以确保APP启动时加固代码首先获得执行机会;
其次,StubApp根据CPU架构加载相应的so文件,把执行权转移至更为安全的native代码。在StubApp类中声明了上百个native函数,不过只有几个有用,这也算是首次释放烟雾弹,来个下马威。
1.2 ELF文件静态分析
360加固保的壳ELF以独立so文件存在,文件名为libjiagu.so(不同CPU架构对应不同的so文件)。壳ELF比较老实,各种元数据都保持完整,可以轻松反汇编。图4是该文件的部分section headers数据。

图 4
图4所示的.init_arraysection在壳ELF文件偏移0x030bec保存2个函数指针,值为0x5f41和0x5f85(见图5)。显然二者都是Thumb指令函数入口,但objdump.exe把它们当作ARM指令反汇编了(见图6)。不过也难怪出错,壳ELF的确包含了两种指令,这会给动态调试增加难度。主ELF则只使用Thumb指令。

图 5

图 6
再仔细查看图4,还会发现,.bmp和.mips这两个名字和编译/链接器备案的section名字有点差别。.compiler看起来很正统,但实际它们3个section都是一伙的,主ELF就散落在此处。
系统加载ELF文件时并不需要sectionheaders信息,但为何libjiagu.so还保留着?问题先搁一边,下文再解释。
2 改门换庭变新颜
根据安卓APP启动流程,派生于application的com.stub.StubApp最先获得执行。稍加判断CPU架构,便开始加载壳ELF(见图7)。

图 7
上文所述,既然.init_array不为空,那么其指向的函数将最先被执行,比JNI_OnLoad还要早一步。
.init_array至少完成下列操作:
① 初始化一些字符串;
② Dynamic section清零;
③ 把.bmp、.compiler和.mips三者数据整体向后(高地址)移0×1000。这样,一来恢复了各个section原始面貌;二者顺带用“随机数据”覆盖后面的.comment至.shstrtab各个section,自然文件最后的section headers未能幸免。
至此,在JNI_OnLoad执行前,.init_array既恢复了后续需要使用的数据,又擦除了相关section数据。
目的相当明确,破坏门庭,杜绝通过programheaders和section headers登堂入室。       
3 三板斧头挡去路
 .init_array完成后,DVM虚拟机稍作初始化,便调用JNI_OnLoad。大幕开启。

壳ELF毫不客气,一上来就通过反射调用相关接口获取设备的IMEI、model、CPU_ABI、SDK_INT、/proc/version、SERIAL和MAC等等一些关键参数。
接着遍历/proc/self/maps,找到/system/bin/linker后,将该文件再次映射进内存。
例行的反动态调试来了。
为增加难度,先在一些函数间随机跳转随机次数,然后打开/proc/self/status,提取TracerPid,又随机在几个函数间跳转N次后才把TracerPid转换为整数进行判断。若TracerPid值不为零,调用raise(9)自杀。若为零正常,也继续跳转N次后才关闭/proc/self/status文件。
判断是否存在IDA的动态调试端口(00000000:5D8A)的过程依然如此,在打开文件/proc/net/tcp,读取数据,关闭文件各个操作间插入大量的函数跳转。
两个文件名平常使用A5异或后保存,使用时才临时在callstack中恢复真正的名称。
很显然,整个判断过程给动态调试设置了巨大的障碍。
还有,想在open或fopen下断点的童鞋就不要守株待兔了,因为这两个文件没有通过常规系统调用来打开。整个文件读取过程正如迷雾中窜出两只猛虎,瞬间把猎物叼走。
究竟在哪打开文件?请大家自行思考。
即使通过了上面两关,还有第三板斧头接踵而来。通过判断运行时间来识别是否处于调试状态。一旦超时,依然raise(9)自杀。
4 袖里乾坤显真身
走到这里,环境已基本安全,可以释放出主ELF了。
主ELF寄宿在壳ELF的section中,需要借助sectionheaders才能准确定位,这就是壳ELF为何保留section headers的缘故。整个定位过程在.init_array完成,因此之后才可以用“随机”数据覆盖section headers。
主ELF是先被压缩再加密后放置于壳ELF的section中,释放过程先解密,然后再解压缩。

图 8
解密解压缩后的数据见图8,主ELF的头部和programheaders都使用B2做了简单的异或加密。主ELF截掉了Section headers,因为它不像壳ELF那样需要定位section来获取数据。
壳ELF是在java环境通过调用loadLibrary来加载执行的。而主ELF因安全考虑不能通过dlopen方式加载,因此壳ELF只能自己解析主ELF来加载后者。
加载过程比较简单,针对每一个主ELF的重定位符号,遍历主ELF依赖的lib文件,找到实际地址后填充至主ELF的GOT表。
最后,给各个programsegment设置好相应权限,主ELF就算安家落户了。
主ELF也提供类似.init_array功能,供壳ELF在加载完前者后调用做些简单处理。
5 布下八卦迷魂阵
主ELF还是遵循NDK,提供JNI_OnLoad函数作为执行入口。这个函数在主壳ELF模式下不是必须的,其存在的原因应该是为了主ELF可以独立开发调试。然后两个开发小组小小偷懒一下,就不再定义新的内部接口,仍继续沿用JNI_OnLoad作为主壳ELF间接口。
主ELF承担功能繁重,还需要负责与java世界沟通。因此一开始就注册native函数,如图9。回头再比较com.stub.StubApp声明的那堆native接口,现在就简洁多了。

图 9
 5.1 定位壳DEX
原DEX数据保存在壳DEX,因此首要任务是找到壳DEX。
通过遍历maps,根据/dev/ashmem/dalvik-classes.dex,apk@classes.dex,或者.odex特征寻找壳DEX的内存地址。通过匹配apk@classes.dex找到已加载到内存且优化后的dex文件。比较头部magic数据deyn确认为优化后的dex文件。为了确保是自家的壳DEX,360加固保在额外添加的DEX数据头部也加了magic。如图3,3b18地址指向的4个字节即为magic。更进一步,该地址的数据可以用InternalDexesHeader结构来描述,如图10上半部分。

图 10
即使壳ELF在前面已经极力保护执行环境安全,但此时主ELF仍不放心,在释放核心数据前布下迷魂阵——回调壳ELF的一些函数十多万次,尽力摆脱可能存在的跟踪者。
5.2 解析配置数据
参见图1、图3、图10,配置数据就是InternalDexesHeader结构后面的数据,也是用单字节异或加密。
配置数据实际上就是一个键值数组,结构可以描述为KeyValHeader,见图10下半部分。它们可以简单分为两类:
一类是原APK的一些元数据,例如activityName、Apk-md5、CheckSum、签名值和加固时间等。使用这些元数据来防止二次打包。
另一类是360加固保自身的一些元数据和配置数据,例如jiaguVersion(当前版本1.3.7.3),stubAppName,是否上报crash,,是否支持X86,是否直接升级等等。其中fastLevel标识压缩等级是否为快速压缩,在解压缩DEX数据时使用。还有一些如update等配置数据由辅助DEX所使用。
5.3 释放原DEX和辅助DEX
确认数据完整后,将开始释放原DEX和辅助DEX了。
这两个DEX的定位方法和配置数据一样,在各自头部用明文描述了自身大小,这样从InternalDexesHeader开始就可以方便定位原DEX和辅助DEX。
开了两个线程,同时对这两个DEX进行解密和解压缩。两次pthread_create之后又调用pthread_join,强制主线程和子线程间串行化。这样做逻辑上和直接函数调用是一样的,但使用多线程可以充分利用多核来加快释放速度。
这两个DEX的数据都先经过等长解密后再进行解压缩。
加密算法应该是DES(不一定正确,没有进一步深入研究);密钥在本地;DEX和主ELF二者的解压缩使用不同的接口。
6 偷梁换柱龙转凤
 DEX文件已准备好,就待加载到DVM。(本文不讨论ART方式)
通过com/stub/StubApp->getClassLoader-> pathList-> dexElements-> dexFile->loadDex接口加载APK文件/data/app/com.fdsk.bfle-1.apk。
根据DVM分配的mCookie值,用原DEX文件替代apk文件即可。这充分利用了MultiDex技术,具体细节请参考安卓系统相关源码。

 

最后,没有忘记修改DEX的magic,以避免通过”dex”特征遍历内存定位DEX。
至此,主要工作已经完成,主ELF和壳ELF连续退出JNI_OnLoad,执行权回到DVM,继续执行一个的jninative函数StubApp.interface5。该函数主要检查一些配置信息,提取app的packagename。
之后,执行权转移至原APP的main activity,360 加固保在该类添加了一行static代码,如图11。于是继续进入jni native 函数StubApp.interface11。

图 11
7 结语
安卓APP加固,即使加持了各种强悍的保护措施,但囿于安卓体系架构,也难免留下阿克琉斯之踵。限于篇幅,个中细节不展开分析。