前言
同事发现了一个crash是来源于公司内某个闭源jar的,而负责此Jar开发的同事已经离职,难以找人修复,于是就想找我帮忙弄下。
想修改class文件,方式很多,如Javassist、ASM等
- Javassist的修改是基于源代码,因此更容易上手,可参考修改jar的.class文件,并重新打包
- ASM则是直接基于java bytecode,复杂性相对高些,但几乎无所不能。若能理解ASM的设计的话,使用也并不复杂,不懂的可参考ASM库的介绍
同事发现了一个crash是来源于公司内某个闭源jar的,而负责此Jar开发的同事已经离职,难以找人修复,于是就想找我帮忙弄下。
想修改class文件,方式很多,如Javassist、ASM等
本系列文章到这里就已经到尾声了,在第二章时,我给大家介绍了很多相关的知识,这些知识是学习本系列文章的基础。接着在第三章中,我介绍了目前流行的相似度识别方案,并针对网上没开源的方案进行简单模拟实现。之后在第四章中,我介绍了一些代码混淆的方案,这些方案只是给大家个引子,希望大家能够举一反三,想出更多更好的方案。
现在我们已经能够把代码混淆得面目全非了,但是怎么才能知道它的混淆效果了呢?一般做法有两种,一是人工分析,二是通过工具自动化分析。
人工分析很难有个衡量标准,所以这里我打算使用工具自动化分析。在第三章中,我介绍了一些相似度分析工具,这里我就用那三个相似度分析工具进行分析,并比较。
接下来使用的混淆工具,都进行了随机混淆处理,也就是说两次分别处理的应用,混淆效果是不一样的,而后面的分析结果,除重打包是基于处理前后的应用外,其它都是基于两次分别处理的应用。
下面是开源中国的测试结果:
JNI(Java Native Interface)意为Java本地接口,我们可以通过JNI来实现Java与C/C++之间互调。
前面我们介绍了重构方法的方案,不过重构后的方法仍然是Java方法,因此分析起来相对也比较容易。
这一章我将介绍的是重构到本地方法中,即需要把字节码转换为本地代码。
实现方案 | 优势 | 劣势 |
---|---|---|
方案一 | 把字节码直接转换为相应的汇编代码 | 字节码是基于栈的,而汇编中直接支持栈的操作,因此实现比较容易且运行效率非常高。 汇编代码不跨平台,因而需要针对每个平台来编写对应的汇编代码。 |
方案二 | 把字节码转换为相应的C/C++代码 跨平台,实现起来比较简单。 | 运行效率相对较低。 |
如果是为了做优化,建议选择方案一,但如果只是为了混淆,那么方案二是个好选择,毕竟编程起来更容易,也更好维护。这里选择的是方案二。
另外,这里选择使用C,因为C编译出来的体积更少。
合并方法指的是把多个方法合并成一个方法,从而达到减少方法数量,增加分析难度的目的。
前面提到过,Android中Dex有65536个方法数量的限制,因而进行方法合并有利于缓解此限制。另外上一章提到的重构方法方案会大量的增加方法,如果把增加的方法进行合并,那么可缓解增加方法的数量。
合并方法时,需要修改合并后的方法签名,而有些方法是对外提供的接口,是不能合并的。如Android中的Activity等四大组件中继承的一些特定方法,插件化框架中对外提供的接口等等。
1、只要被Proguard混淆过的方法,大部分都可以进行合并,而未混淆的代码,则由于可能有各种各样的潜规则(如JNI或其它地方反射调用该方法、作为插件化框架所提供的接口等),而需要通过配置Keep住。
2、没继承关系的方法,皆可进行处理。
3、有继承关系的方法,虽有部分能处理,但经测试量较少,且处理成本非常大,因而不建议处理。
4、重构产生的方法,均可以进行合并处理
在Java开发的过程中,一般是通过javac工具把Java代码编译成字节码。在编译的过程中,每一种语法都会有一一对应的指令组合起来。只要熟悉这一对应关系,就完全能够通过字节码猜出它对应的Java代码。这时,我们可以通过指令等价替换或者增加无意义的花指令的方式,把正常的写法改成javac不会产生的写法,从而加大破解分析的难度。
指令替换意为指令的等价替换,即把一条或多条指令替换为等价的一条或多条指令。
指令替换的方式多种多样,它有可能会增加指令的数量,也有可能会减少指令的数量。有可能会增大或减少方法代码的字节数,有可能会加快或减低代码的执行速度。
下面我讲解几种指令替换方案:
在各种IDE中,都会有重构代码到一个新方法的选项(以下简称为重构方法),如下面是eclipse的重构功能:
重构方法可重复利用代码,优化软件设计,提高代码的可读性,使得功能扩展更容易等等。当然,过度地重构会使得代码变得难以阅读,难以定位出错的原因。
在进行重构之前,我们需要先分析方法中哪些代码是可以重构。可重构的代码有几个特点:
1)不应该包含返回指令(因为此时还未知该代码重构后的返回值是什么)。
2)重构代码外的跳转指令不能跳转到重构代码中,重构代码中的跳转指令只能跳转到重构代码中。
3)代码前后堆栈平衡。
4)最多只能有一个返回值,但可有多个参数。
说起混淆,不得不提的是开源工具proguard了。proguard最大的优势在于它会优化你的代码,并删掉无用的类、方法、属性及代码。其次,proguard还会更改你的类名、方法名、属性名,使得反编译后变得难以看懂。但对于有经验的反编译人员或者一些静态分析工具,就变得无能为力了。
而另一款收费软件dexguard则提供了更为强大的功能,dexguard是proguard的作者针对android推出的加强版,其包含了proguard的全部功能,而且还提供了加密的功能,如常量字符串加密、入口类加密、本地库加密、资源文件加密等。此外,它还提供了隐藏敏感API的功能,这功能的实现就是通过反射调用并加密字符串来实现的。它还针对Android做了很多优化,如整合了编译流程,编译起来比proguard更快、自动去掉android的Log日志、自动分Dex等。
Java的反射调用包括很多方面,如类、属性、方法、注解等,这里我介绍的是针对属性调用与方法调用的反射,下面先介绍反调调用属性。
在bytecode中,获取属性的值使用getstatic和getfield这两条指令,而设置属性的值对应为putstatic和putfield。
在上一章中我给大家介绍了基于代码流的方案,这一章里,我将会介绍基于API序列与基于变量计数的方案。
由于这两种方案目前在网上还未能找到针对Dex的开源实现,所以这里我会简单介绍下其原理实现。
API(Application Programming Interface)一般指的是系统或他人提供的编程接口。
这里我把API定义为所有被调用的方法,在我们编写代码的过程中,一般情况下都会调用各种API,这些API可以分为三类:
1) 系统API
2) 别人提供的SDK或开源库中的API
3) 自己编写的API。
这里所说的相似度分析是针对Android应用来说的,主要是指Dex文件的分析。
通过相似度分析,可以快速地识别一款应用是否来源于另一款应用,如版本更新、重打包之类的。这对于病毒分析、重打包分析等非常之有用。
目前市面上主流的相似度分析方案有3种:基于代码流、基于API序列、基于变量计数。本章将介绍基于代码流的方案。
基于代码流的方案是指针对同一个方法中的多条指令进行连续性识别,Androguard中的androsim.py工具就是基于这种原理。当然,有了前面的知识,我们也可以自己写一个类似的工具。
androsim.py是Androguard中的一个工具,因此需要先安装Androguard。Androguard下载与安装请参见官方文档:http://code.google.com/p/androguard/
前几章中我们分别介绍asm、asmdex以及smali/baksmali这三个开源库,它们各有其特色之处,这一章我们来对比一下它们的优劣,以便更好地选择方案进行开发。
asm修改的是class文件,所以无法直接修改android应用,不过幸运的是,我们可以通过dex2jar进行转换。但这样一来,就相当于同时依赖asm和dex2jar了。这样我们开发过程中就需要对asm和dex2jar都保持怀疑的态度,以免由于asm或dex2jar的Bug而影响到开发的效率。
幸运的是,asm5.0版非常地成熟,到目前还没发现Bug,而dex2jar0.95版有一点Bug,但更新到2.0版后,至今也没发现有严重的Bug。不过dex2jar有个小问题,就是不支持保留行号和调试信息。