最近在研究安卓平台的加壳技术,以前以为只有原生层的代码才可以加壳,看了看网上的资料,才发现原来Java层也可以加壳,虽然与传统的壳有些区别,但就最终的效果来说,反静态分析的目的还是达到了的。
目前安卓市场上的大多数apk的保护方式都是对Java层进行代码混淆,将关键数据放在原生层,再用传统加壳手段对so文件进行加壳,这种方式的缺陷是Java层的代码安全完全依赖于代码混淆,实际上只要熟悉smali语法,代码混淆的意义就没有想象的那么大,对于那些资深的C/C++逆向工程师来说,逆向smali可比在一堆汇编代码里面按图索骥轻松多了。
传统的PE文件是怎么加壳的
看雪对PE文件的壳解释的很详尽,就不多作阐述了,大致原理是对源PE文件加密或压缩后,将数据嵌入外壳程序里面,外壳程序执行后将源PE文件解密加载,然后跳到源PE文件的入口(OEP)执行。静态分析加壳的程序,就只能看到外壳程序和加密的源PE文件。如果要获得源PE文件(脱壳),就只能运行外壳,让其将源PE文件解密,然后找到OEP,将解密后的PE文件从内存里dump下来,重建输入表,生成脱壳后的PE文件。软件安全工程师可以在壳上采用反调试(Anti-debug)和复杂化控制流等技术来阻止破解者寻找OEP。
可以看出,一个传统的“壳”需要具备以下特点:
1、源程序压缩或加密后内嵌在外壳程序内部;
2、外壳程序先于源程序运行,对源程序解密;
3、解密后的数据必须存放在内存中;
4、源程序加载后要能正常运行;
最为关键的是第3点,需要明白的是,只要投入足够的时间,壳最终是肯定会被破解的,壳的意义实际上在于增加破解成本,尽可能地拖延破解时间,要做到这一点,就必须把与破解者的对抗的关键点放到分析外壳程序的解密逻辑的困难性上,如果破解者有办法绕过分析外壳程序的步骤,直接拿到解密后的源程序,那么这个壳就是无效的。解密后的数据如果留在了文件系统里,破解者就能直接获取到源程序。为什么要单独把这个问题提出来说呢,传统的壳加载源程序,只要解密过后,重定位到OEP就可以了,安卓加载可执行文件却需要使用类加载器DexClassLoader,原型如下:
public DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader loader)
其中第一个参数dexPath是代表的一个路径,也就是说,DexClassLoader只能加载存放在文件系统里的可执行文件。问题就出来了,刚刚说,我们不能把解密后的源程序放在文件系统上,但是要用DexClassLoader 加载安卓可执行文件dex的话就必须先把它存放在文件系统上,这是对Dex文件进行加壳最大的难点。
为了解决这个问题,我想到了两个办法,第一个办法是减少暴露在文件系统里的时间,在DexClassLoader装载完源程序后,立即将其从文件系统中删除,这样破解者就必须动态调试,在源程序被删除之前断下来才能获得源程序。但是这种办法还是很不安全,我们需要一种彻底不在文件系统留下任何痕迹的方法。先回过头来看DexClassLoader,它虽然只提供路径作为参数,但其加载过程最终肯定有一个步骤是将文件转化成输入流,然后映射到内存,也就是说,DexClassLoader 最终还是要从内存中加载源程序。在网上查了资料,最终在http://www.strazzere.com/papers/DexEducation-PracticingSafeDex.pdf里找到了答案,原来安卓4.0以下的版本确实不支持内存加载Dex,4.0以上的版本实际上是提供了一个以byte数组为参数的接口openDexFile来从内存中加载类的,只不过这个接口并没有开放给SDK,因此需要利用Java反射机制来调用。细节等以后再说,本篇文章先讲讲怎么实现第一种方法。
将源APK嵌入外壳Dex中
我们知道Apk是安卓的安装包,本质是一个zip,需要安装后才能运行,APK里面的classes.dex才是真正的可执行文件,为什么要嵌入APK而不是classes.dex呢?因为classes.dex里并不包含资源文件,如果只加载classes.dex的话,运行后是没有任何资源的。
具体内嵌APK的方法参考了这篇博客:http://blog.csdn.net/androidsecurity/article/details/8678399。博主的做法是将APK加密后,添加到外壳的classes.dex尾部,然后加上APK的文件长度,最后修改classes.dex的dex文件头。这部分很简单,博主也提供了源代码,所以就不阐述了。
需要一提的是,如果把内嵌了源APK的外壳classes.dex直接放入外壳APK中,放进手机安装的话会出错,原因是这样破坏了APK签名,需要把外壳APK中的META-INF文件夹删掉,然后用signapk.jar重新签名,我的signapk.jar的下载地址如下:http://www.uzzf.com/soft/60860.html
在外壳中剥离源APK
外壳程序运行后,通过getApplicationInfo().sourceDir就能获取到外壳自身的APK地址,将其以文件形式打开后,输入到一个ZipInputStream流里,然后遍历zip找到classes.dex,将其输入到一个byte数组,利用byte数组的长度-4得到内嵌的源APK文件的长度,最后调用System.arraycopy将源apk拷贝出来,解密后,输出到文件系统里就可以了。读取壳自身apk的代码如下:
1 ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
2 ZipInputStream localZipInputStream = new ZipInputStream(
3 new BufferedInputStream(new FileInputStream(
4 this.getApplicationInfo().sourceDir)));
利用DexClassLoader动态加载源APK
安卓是允许在不安装apk的情况下调用apk的可执行文件的,要实现这个,就得利用DexClassLoader,这是安卓专用的类加载器,它以APK路径作为参数,找到APK中的classes.dex文件,将里面的类加载进来,这样我们就可以调用源APK中的代码了。需要注意的是,直接用DexClassLoader加载进来的安卓组件类是死的,不具有生命周期,跟普通的类没有区别,也就是说我们不能直接像普通调用安卓组件的方式去调用DexClassLoader加载进来的组件。网上找了一会儿,在http://blog.csdn.net/singwhatiwanna/article/details/22597587上找到了一种解决办法,该博主是将源APK中的组件与调用APK的组件的生命周期绑定在一起,相当于将源APK的生命周期模拟了出来,这种调用技术可用于实现APK插件化,但是存在很多限制,最有效的解决方案其实是360提出的Proxy/Delegate框架:http://blogs.360.cn/blog/proxydelegate-application/这种解决方案是将调用APK的Application动态替换为源APK的Application(Proxy与Delegate),将其运用在Dex文件加壳上的话,这样就相当于替换了应用程序运行环境,运行中的外壳就完全变成了源程序,这时候只要调用源程序的Application.onCreate(),源程序就启动了。此外,需要在源程序的Application.onCreate()里加入启动MainActivity的代码,否则MainActivity是不会主动启动的。
未完待续……