Android App加固原理与技术历程
App为什么会被**入侵
随着黑客技术的普及化平民化,App,这个承载我们移动数字工作和生活的重要工具,不仅是黑客眼中的肥肉,也获得更多网友的关注。百度一下“App**”就有5290万条结果。
一旦App被**,不仅使用者的照片、身份证、手机号、联系住址、邮箱和支付密码等敏感信息会泄露,还可能感染手机的操作系统,进而导致手机被入侵篡改,乃至成为攻击者操控的“僵尸网络”中的一部分。
安卓App的开发除了部分功能采用C/C++编码外,其余主要都是采用Java进行编码开发功能。Java源码最终编译成smali字符码,以classes.dex保存在App的APK中。
Java是一种解释性语言,功能强大,易用性强。初学者能轻松地学习Java,并编写简单的应用程序。而且Java的基本类库(JDK)是开源的,这就使很多Java开发的应用被逆向**的门槛很低。目前市面上有大量的逆向**工具,例如:Dex2Jar、JEB、JD-GUI等等。只要懂代码编程,利用这些工具就可以**市面上那些防御薄弱、存在大量安全漏洞的App。这就很好理解为什么会有如此多人去搜索“App**”了。
之前曾有媒体报道,有网络黑产专门从各种渠道找到App的apk,然后将apk文件逆向**,再植入广告、病毒代码,最后重新打包投入公开市场,当不明真相的网友将带病毒广告的App下载后,会带来巨大经济损失。
加固技术发展历程
传统App加固技术,前后经历了四代技术变更,保护级别每一代都有所提升,但其固有的安全缺陷和兼容性问题始终未能得到解决。而下一代加固技术—虚机源码保护,适用代码类型更广泛,App保护级别更高,兼容性更强,堪称未来级别的保护方案。
第一代加固技术—动态加载
第一代Android加固技术用于保护应用的逻辑不被逆向与分析,最早普遍在恶意软件中使用,其主要基于Java虚拟机提供的动态加载技术。
保护流程
开发阶段
开发阶段中将程序切分成加载(Loader)与关键逻辑(Payload)两部分,并分别打包;
启动流程
运行时加载部分(Loader)会先运行,释放出关键逻辑(Payload),然后java的动态加载技术进行加载,并转交控制权。
核心代码:
备注(multidex组件的加固原理):
Android的DEX文件在设计之初程序普遍较小,所以在DEX文件设计时,只允许包含65535个函数引用。而随着Android应用的发展,大量的应用的代码已经超过了65535的限制,为了解决这个问题,Android5.0之后原生支持加载多个dex,而为了对旧版本的兼容,Android提供了multidex组件。该组件的实现原理与上面介绍的是一致的。
缺陷与对抗
第一代加固技术的缺陷是依赖Java的动态加载机制,而这个机制要求关键逻辑(Payload)部分必须解压,并且释放到文件系统,这就给了攻击机会去获取对应的文件。虽然可以通过关键逻辑(Payload)被加载后,被从文件系统删除,用于防止被复制,但是攻击者可以拦截对应的删除函数,阻止删除。
而关键逻辑(Payload)会被加密后保存,可用于对抗静态分析,但是攻击者可以通过自定义虚拟机,拦截动态加载机制所使用的关键函数,在这个函数内部,复制文件系统中的关键逻辑(Payload)文件。
第二代加固技术—不落地加载
相对第一代加固技术,第二代加固技术在APK修改方面已经完善,能做到对开发的零干扰。开发过程中不需要对应用做特殊处理,只需要在最终发布前进行保护即可。而为了实现这个零干扰的流程,Loader需要处理好Android的组件的生命周期。
保护流程
1)Loader被系统加载。
2)系统初始化Loader内的StubApplication。
3)StubApplication解密并且加载原始的DEX文件(Payload)。
4)StubApplication从原始的DEX文件(Payload)中找到原始的Application对象,创建并初始化。
5)将系统内所有对StubApplication对象的引用使用替换成原始Application,此步骤使用JAVA的反射机制实现。
6)由Android系统进行其他组件的正常生命周期管理。
对开发零干扰的加固后启动流程:
另一方面,不落地加载技术是在第一代加固技术的基础上改进,主要解决第一代技术中Payload必须释放到文件系统(俗称落地)的缺陷,其主要的技术方案有两种:
A.拦截系统IO相关的函数(如read、write),在这些函数中提供透明加解密。具体的流程是:
1)关键逻辑(Payload)以加密的方式存储在APK中。
2)运行时加载部分(Loader)将关键逻辑释(Payload)放到文件系统,此时关键逻辑(Payload)还处于加密状态。
3)加载部分拦截对应的系统IO函数(read,write等)。
4)加载部分(Loader)正常调用Java动态加载机制。由于虚拟机的IO部分被拦截,所以虚拟机读取到已经解密的关键逻辑(Payload)。
透明加解密方案流程:
B.直接调用虚拟机提供的函数进行不落地的加载,具体流程是:
1)关键逻辑(Payload)以加密的方式存储在APK中。
2)运行时加载部分(Loader)将关键逻辑释(Payload)放到内存。
3)加载部分调用虚拟机内部接口进行加载。
不落地加载流程:
关键的系统函数如下:
兼容性
方案A透明加密方案由于其需要拦截系统的IO函数,这部分会使用inline hook或者got hook等技术,其会带来一定的兼容性问题
方案B的不落地加载方案由于其调需要调用系统内部的接口,而这个接口并不导出,各个厂商在实现时又有各自的自定义修改,导致该方案存在兼容性问题。
缺陷与对抗
第二代加固技术在应用启动时要处理大量的加解密加载操作,会造成应用长时间假死(黑屏),用户体验差。
在加固技术实现上没有本质区别,虽然能防止第一代加固技术文件必须落地被复制的缺陷,但是也可以从以下方面进行对抗:
例如内存中的DEX文件头会被清除,用于防止在dump文件中被找到;DEX文件结构被破坏,例如增加了一些错误的数据,提高恢复的成本。
但是Payload被加载之后,在内存中是连续的,利用gdb等调试工具dump内存后可以直接找到Payload,进行简单的处理之后可以恢复出100%的Payload文件。
和第一代加固技术的对抗方法一样,不落地加载也无法对抗自定义虚拟机。只需对上述的关键函数进行拦截然后将对应的内存段写出去,即可恢复Payload。注意,由于IO相关的函数被拦截,所以无法直接调用read/write等函数进行直接的读写,需要使用syscall函数进行绕过。
虽然厂商会自己实现可能上述函数,从而绕过上述函数的拦截。但是Android的类加载器必须能找到对于的结构体才能正常执行,攻击者可以以类加载器做为起点,找到对应的Payload在内存中的位置。
第三代加固技术—指令抽离
由于第二代加固技术仅仅对文件级别进行加密,其带来的问题是内存中的Payload是连续的,可以被攻击者轻易获取。第三代加固技术对这部分进行了改进,将保护级别降到了函数级别。
保护流程
发布阶段
发布阶段将原始DEX内的函数内容(Code Item)清除,单独移除到一个文件中。
运行阶段
运行阶段将函数内容重新恢复到对应的函数体。恢复的时间点有几个方式:
A.加载之后恢复函数内容到DEX壳所在的内存区域
B.加载之后将函数内容恢复到虚拟机内部的结构体上:虚拟机读取DEX文件后内部对每一个函数有一个结构体,这个结构体上有一个指针指向函数内容(CodeItem),可以通过修改这个指针修改对应的函数内容。
C.拦截虚拟机内与查找执行代码相关的函数,返回函数内容。
兼容性
指令抽离技术使用了大量的虚拟内部结构与未被文档的特性,再加上Android复杂的厂商定制,带来大量的兼容性问题。
缺陷与对抗
指令抽离技术的某些方案与虚拟机的JIT性能优化冲突,无法达到最佳的运行性能。依旧使用了java虚拟机进行函数内容的执行。攻击者可以通过自定义Android虚拟机,在解释器的代码上做记录一个函数的内容(CodeItem)。接下来遍历触发所有函数,从而获取到全部的函数内容。最终重新组装成一个完整的DEX文件。目前已经有自动化工具可以指令抽离技术中脱壳。
第三代加固DEX文件脱壳流程:
第四代加固技术:指令转换/VMP
第三代加固技术在函数级别的保护,使用Android虚拟机内的解释器执行代码,带来可能被记录的缺陷,第四代加固技术使用自己的解释器来避免第三代的缺陷。而自定义的解释器无法对Android系统内的其他函数进行直接调用,必须使用JAVA的JNI接口进行调用。
保护流程
主要实现由两种:
A.DEX文件内的函数被标记为native,内容被抽离并转换成一个符合JNI要求的动态库。 动态库内通过JNI和Android系统进行交互。
B.DEX文件内的函数被标记为native,内容被抽离并转换成自定义的指令格式,该格式使用自定义接收器执行,和A一样需要使用JNI和Android系统进行调用。
兼容性
第四代VMP加固技术一般配合第三代加固技术使用,所以第三代的所有兼容性问题,指令转换/VMP加固也存在。
缺陷与对抗
不论使用指令转换/VMP加固的A方案或者B方案,其必须通过虚拟机提供的JNI接口与虚拟机进行交互,攻击者可以直接将指令转换/VMP加固方案当作黑盒,通过自定义的JNI接口对象,对黑盒内部进行探测、记录和分析,进而得到完整DEX程序。
第四代加固DEX文件恢复:
另外,第四代VMP加固技术只实现Java代码保护,没有做到使用VMP技术来保护C/C++等代码,安全保护能力有所欠缺。
下一代加固技术—虚机源码保护
跟第四代的VMP加固技术,虚机源码保护加固是用虚机技术保护所有的代码,包括Java,Kotlin,C/C++,Objective-C,Swift等多种代码,具备极高的兼容性;使App得到更高安全级别的保护,运行更加稳定。
保护流程
虚机源码保护为用户提供一套完整的工具链,首先把用户待保护的核心代码编译成中间的二进制文件,随后生成独特的虚机源码保护执行环境和只能在该环境下执行的运行程序。
虚机源码保护会在App内部隔离出独立的执行环境,该核心代码的运行程序在此独立的执行环境里运行。即便App本身被**,这部分核心代码仍然不可见。
虚机源码保护加固流程:
对抗优势
生成的虚机源码保护拥有独特的可变指令集,极大的提高了指令跟踪、逆向分析的难度。同时,虚机源码保护还提供了反调试能力和监控能力。虚机源码保护可以通过自身的探针感知到环境的变化,实时探测到外界对本环境的调试、注入等非正常执行流程变化,将调试动作引入程序陷阱,并发出警报,进而进行实时更新,提高安全强度。
加固技术发展及其攻防对抗的更迭,伴随着互联网技术发展不断升级,我们深信邪不能胜正,而虚机源码保护加固作为当前领先的加固技术,在未来很长一段时间,能够为App提供足够强度的保护,为企业和开发者的业务发展保驾护航。
————————————————
有加固需求的同学看这里:Android应用加固:https://www.dingxiang-inc.com/business/stee
通过具体分析具体的加固源码进一步分析
0x1:
腾讯云加固:http://www.qcloud.com/product/appup.html
加固示例原版APK: http://pic.hzt360.com/downfile/beijing/elechongNFC.apk
a,首先,看一下原APK和通过腾讯云应用加固后的文件相关变化
加固后的文件列表变化:
新增2个so文件:
libmain.so
libshell.so
修改:
AndroidManifest.xml
classes.dex
b, 用ApkTool反编译加固后的APK, 出现反编译不过去,错误日志如下:
1.通过下面日志能看出来是apktool解析AndroidManifest.xml时出错,注意绿色下划线的name=fasten,这里TX加固是利用android系统解析axml的一个特点来导致apktool反编译时,在解析AndroidManifest.xml时出错。
关于利用AndroidManifest.xml这块的技术点可以参考一下万抽抽大神的文章:http://www.cnblogs.com/wanyuanchun/p/4084292.html
2.下面来分析和修复AndroidManifest.xml
分析前,还是得先了解一下AndroidManifest.xml的二进制格式,可以参考下列文章:
AndroidManifest二进制文件格式分析 http://bbs.pediy.com/showthread.php?t=194206
辅助分析AndroidManifest.xml的二进制格式可以使用下面的:
AXML的010 Editor模板
https://github.com/strazzere/010Editor-stuff/tree/master/Templates
利用axml模版在010Editor解析AndroidManifest.xml能看到,有一个属性结构的name成员的值是25,该值指向是string的索引,同时也是res ID的索引。
属性结构:
String索引:
Res ID索引:
为什么这样做,哈哈哈,我懒,所以直接截图引用万抽抽大神的解释:
嗯,属性结构的name成员的值是即是string索引,又是ResID索引,所以:
Name=25
String[25]=fasten
ResIDs[25]=0x01017FFF
再次引用抽抽大神文章里的一段话:
Android系统在解析AXML的属性的时候,是通过该属性的res id号而非属性名定位的。所谓的AXML就是AndroidManifest.xml对应的二进制文件,APK包中存储的就是AXML。比如属性:
<public type="attr"name="name" id="0x01010003" />
它的属性名为name,id号为0x01010003。
所以fasten这个字符串可以随意改,关键还是ResID的值,TX加固对AndroidManifest.xml处理,是插入一下非法的属性ID (在Android的attr里没有一个ID为0x01017FFF),因为是非法的属性ID,Android是不会去解析,但ApkTool却会去解析,所以导致反编译出错了。
修复方法:
知道怎么回事,修复起来就很简单了,只要把非法的属性ID=0x0101FFFF改成一个合法的属性ID,比如把0x0101FFFF改成name的属性ID=0x01010003,然后再把修改后的AndroidManifest.xml再替换加固后apk里的AndroidManifest.xml,然后用apktook就可以顺利的成功的反编译出来。
附件有我用官网最新版的ApkTool 2.0.0 RC3源码编译,修改了一下,修复非法属性ID无法反编译。如果懒得手动去修改AndroidManifest.xml,可以直接用我这个修改过的apktool进行反编译。
反编译后,看加固修改后的AndroidManifest.xml和原版的AndroidManifest.xml多这三条:
1. <serviceandroid:name="com.tencent.mm.fasten.check.log" />
2. android:fasten="meta-data"
3. <meta-dataandroid:name="@anim/push_top_out2"android:value="meta-data" />
0x2:
a,ApkTool反编译可以成功,那接下来看一下TX加固是怎么对Dex进行加密的
1. 新增了2个smail文件
com\tencent\StubShell\ProxyShell.smali
com\tencent\StubShell\ ShellHelper.smali
2. Smail代码的变化(对指定方法进行加密)
从截图能看到,加固后的dex,通过apktool反编译后的smali代码变化。
(1)
新增静态代码块:
(只要加载此类,就会先执行该代码块,作用是用来动态恢复被加固的方法)
.methodstatic 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
.endmethod
用JEB转成代码如下:
static{
ShellHelper.StartShell("com.boco.nfc.activity",0);
}
(2)
原始方法:
.methodpublic constructor <init>(Landroid/content/Context;)V
改为native属性,并且隐藏字节码:
.methodpublic native constructor <init>(Landroid/content/Context;)V
被加固后的Method数据:
从这里能看到关键是在StartShell函数,这个StartShell函数专门负责在执行时动态恢复被加固的方法,TX加固这种方式没办法直接通过dump来进行脱壳,它机制是需要运行到某个类,加载这个类时才会修复一下该类被加固的方法,但你又不能保证所有类你都能执行到,所以还是得找原始数据来进行修复dex。
publicstatic boolean StartShell(String packageName, int iIndex)
从StartShell函数第二个参数iIndex来看,应该是要修复那个函数的编号。所以,可以猜测肯定会有一份原始的数据供给修复,所以从StartShell函数入手,就能找到修复的原始数据。
StartShell函数会先判断如果没有初始化过则执行InitProxyShell函数,InitProxyShell函数作用其实就是加载libshell.so, 最后,调用libshell.so的load(ShellHelper.strPackageName, iIndex);来进行修复,这里调用具体过程就不说了,哈哈,TX加固还有log可以看,方便大家理思路,大家想了解自己可以去看看,。
从这里能看到,关键是libshell.so的load函数在负责动态修复功能,下面就用IDA把libshell.so分析一下load函数。
(1)看一下libshell.so的JNI_OnLoad函数
主要就是做一些初始化的时,看来没什么,我们直接主题,找load函数。
(2)Load函数在0xC630的偏移
ART模式下的修复就先不看了,有兴趣的朋友自己去看吧, libshell的代码流程再加上有log信息辅助,流程可以很清晰…
这里我大概说一下func_ShellFixDexMethod这个函数处理,详细的可以自己看下附件的libshell.idb吧。
1. 通过/proc/(getpid)/maps 打开自身进程的内存映射,查找classes.dex的内存地址。
2. TX加固会把所有被加固过的Method的原始数据存一份在文件尾部。
定位Method的原始数据存放地址的方法:
原始数据偏移 = DexDataOff + DexDataSize
有多少个Method需要修复 = (DexFileSize – (DexDataOff + DexDataSize))/0x12
每一个Method方法的原始数据是用一个0x12大小的结构来保存的,结构如下:
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;
3. 已经可以知道Method的原始数据,接下来就看怎么修复。关键就是要怎么定位到哪个Method是需要修复的。如果熟悉Dex结构的,应该就比较容易如何修复。
我的修复方法:先通过Class_defs的索引id(TXFixDexData->dwClassDefItem)定位到需要修复的Method所在的类,再取该类的所有Method,把每个Method的DexMethod->methoIdx值等于TXFixDexData->dwMethodIdx,就确定是需要修复的Method, 然后把该Method的DexMethod结构的accessFlags和codeOff修复就OK。
下面修复TX加固的classes.dex的工具, 附件有Bin和Src,代码比较挫,大伙将就看下思路就行了:
最后,把修复完的classes.dex放到apk,再反编译下,能看到被隐藏Method的代码回来了,但是还需要做一些扫尾的事,才能算完全脱壳成功。
1.搜索一下所有smali文件的下面这一句代码,然后全部替换为空:
invoke-static {v0, v1},Lcom/tencent/StubShell/ShellHelper;->StartShell(Ljava/lang/String;I)Z
2.删除掉AndroidManifest.xml这三个地方:
a. <serviceandroid:name="com.tencent.mm.fasten.check.log" />
b. android:fasten="meta-data"
c. <meta-dataandroid:name="@anim/push_top_out2"android:value="meta-data" />
最后再重新打包APK,至此,脱壳完毕!
参考链接
https://segmentfault.com/a/1190000022774292
https://www.52pojie.cn/thread-330022-1-1.html