Obfuscapk是一个python实现的apk混淆工具,使用插件系统构建,被设计为模块化且易于扩展。每个obfuscator都是一个从抽象基类(obfuscator_category.py)继承的插件,都实现了obfuscate方法。
使用新的obfuscator对该工具进行扩展非常简单:在src/obfuscapk/obfuscators目录中添加实现混淆技术的源代码和插件元数据文件(obfuscator-name.obfuscator)即可。本文接下来分析的版本是此时最新的1.1.2版。
在cli.py中处理了命令行参数之后调用main.py中的perform_obfuscation函数,在perform_obfuscation函数中创建一个obfuscation.py中定义的obfuscation对象以存储所有需要的信息。然后将obfuscation对象作为obfuscate方法的参数按顺序传递给所有启用的插件(obfuscator)以进行处理。
obfuscation对象中调用decode_apk函数,其中调用apktool对原始apk文件进行反编译,得到AndroidManifest.xml\resource文件\assets文件\so文件等等信息,对smali文件调用正则表达式匹配得到方法\变量\类等等信息。
接下来我们来看每个obfuscator的实现。NewAlignment,NewSignature,Rebuild分别用来重新对齐,重新签名和重新构建;VirusTotal用来将混淆前和混淆后的apk发送到VirusTotal。它们都不是混淆技术,所以接下来不会涉及。Nop是在smali代码中添加nop指令;DebugRemoval是删除调试信息;RandomManifest是重新排列AndroidManifest.xml文件;Goto是在方法的开头加上goto跳转到方法的最后一行,然后在方法的最后一行加上goto跳转回来;Reorder是将分支指令的条件改成相反的然后重新排列相应的代码,并且还类似于Goto通过goto对指令进行重新排序;MethodOverload利用Java的重载特性,对于一个已经存在的方法创建具有相同名称的新方法并且在原有参数的基础之上添加新的随机参数,然后用随机算术指令填充新方法;Reflection和AdvancedReflection是通过反射的方式调用原来的方法,可以理解成另外一种CallIndirection。这些混淆技术我测试之后发现对反编译的结果影响比较有限或者根本不会造成影响,接下来重点分析的是ArithmeticBranch,AssetEncryption/LibEncryption,ConstStringEncryption(ResStringEncryption),ClassRename,MethodRename,FieldRename和CallIndirection。
插入垃圾代码,垃圾代码由算术计算和依赖于这些计算结果的分支指令组成,这些分支永远不会被执行。例子如下。
如果一个方法使用了两个及以上的寄存器就添加一个形式如(a+b)%b的条件,如果大于等于0继续执行下面的代码,如果小于0(不会发生)跳到method的结尾,method结尾再添加一个跳转回来的goto语句。
虽然看上去效果比较鸡肋,但是可以进一步做得更复杂。
AssetEncryption/LibEncryption都是类似的,这里以AssetEncryption为例。对asset文件进行加密。例子如下。
如果调用了assetManager.open函数打开asset文件就对asset文件进行AES加密,同时把assetManager.open函数替换成自己的解密函数,如果进行了加密并且没有添加存在解密函数的smali文件就添加。
对字符串进行加密。例子如下。
将const-string register, plaintext
中的plaintext
加密成ciphertext
,然后将其替换成下面三行代码(接下来的代码中[]中为变量名)。
const-string/jumbo [register], [ciphertext]
invoke-static {[register]}, Lcom/decryptstringmanager/DecryptString;->decryptString(Ljava/lang/String;)Ljava/lang/String;
move-result-object [register]
将.field (optional) static (optional) string_name:Ljava/lang/String; = plaintext
中的plaintext
加密成ciphertext
,将其替换成.field (optional) static (optional) string_name:Ljava/lang/String;
,然后增加下面四行代码。
const-string/jumbo v0, [ciphertext]
invoke-static {v0}, Lcom/decryptstringmanager/DecryptString;->decryptString(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
sput-object v0, [class_name]->[string_name]:Ljava/lang/String;
如果存在static constructor就把这四行代码添加到static constructor中,否则新建一个static constructor。
同样如果进行了加密并且没有添加存在解密函数的smali文件就添加。
类似的ResStringEncryption可以对资源文件中的字符串加密,这里就不再分析了。
重命名类名。例子如下。
遍历所有smali文件得到类名和smali文件的对应关系。
调用transform_package_name函数重命名包名,具体做法是对.
分割的每部分计算md5取前8位再加上p,并且要修改AndroidManifest.xml中对应的包名。
调用rename_class_declarations函数对类名的定义重命名,对以/
和$
分割的每部分如果不是数字并且不是R类用和前面同样的方法重命名。
对于表示内部类的InnerClass注解也要重命名其中的类名。
rename_class_declarations函数返回重命名前后类名的对应关系rename_transformations。接下来会调用slash_to_dot_notation_for_classes函数对rename_transformations做进一步处理,去掉开头的L
和结尾的;
,并将/
和$
替换成.
,得到dot_rename_transformations。
调用rename_class_usages_in_smali函数替换smali文件中类名的使用。
考虑了以下几种情况:
1.类名能和dot_rename_transformations匹配上
2.类名加上;
之后能和rename_transformations匹配上
3.类名能和rename_transformations匹配上
调用rename_class_usages_in_xml函数对xml文件中的类名进行替换。获取所有layout目录下的xml文件和AndroidManifest.xml文件。
替换时要从最长的到最短的替换,防止发生只替换了一部分的情况。还要替换没有包名的Activity名(AndroidManifest.xml中的String Chunk)。
重命名方法名。例子如下。
读取Obfuscapk\src\obfuscapk\resources目录下的android_class_names_api_27.txt文件得到android系统中的类名,然后读取smali文件中的.super得到apk中用到的父类的类名,两者的交集即为应该忽略的类名。
调用get_methods_to_ignore函数读取smali文件包含的类,检查这个类是否属于应该忽略的类。如果这是一个应该忽略的类,获取它的方法并添加到重命名时要忽略的方法列表methods_to_ignore中。只添加类中的直接方法中除了构造方法,native方法和抽象方法的方法,因为这些方法是不会被重命名的。
调用rename_method_declarations函数对方法的定义重命名,如果是一个枚举类不会重命名,并且只重命名类中的直接方法中除了构造方法,native方法和抽象方法的不在methods_to_ignore中的方法,具体做法是对方法名计算md5取前8位再加上m,最后返回重命名的方法renamed_methods。
调用rename_method_invocations函数对方法的调用重命名,如果调用的是直接方法或者静态方法并且方法在renamed_methods中并且不是在android系统中的类中被调用的,则对此处的调用重命名。
变量重命名。例子如下。
取得所有Landroid或者Ljava开头的SDK类的声明sdk_class。
判断是不是multidex,如果是的话要分别处理每个dex,分别调用rename_field_declarations函数进行对变量的定义重命名,并且每次重命名时都会调用add_random_fields函数随机添加1到4个垃圾变量的定义,具体做法是对变量名计算md5取前8位再加上f,而随机添加的垃圾变量是在重命名之后的基础上再加上8个随机字符得到的。返回重命名的变量renamed_fields。
调用rename_field_references函数对变量的引用重命名。当找到一个变量的引用之后如果该变量在renamed_fields之中并且:1.类名不以Landroid或者Ljava开始或者2.类名在sdk_class中,就对此处的引用重命名。在有SDK类的声明的情况下可以重命名其中的变量。
方法间接调用。例子如下。
判断是不是multidex,如果是的话要分别处理每个dex,分别调用add_call_indirections函数。
add_call_indirections函数中首先调用update_method函数->change_method_call函数将代码中调用原来的方法改成调用新增的方法,并准备好新增的方法的声明,新增的方法中再调用原来的方法。
调用add_method函数将新增的方法的声明添加到# direct methods
之后。
每对一个方法进行这样的混淆都要统计方法的总数,超过数量限制之后break。
Obfuscapk中涉及的混淆技术包括加密,重命名,打乱控制流等绝大部分java层常见的混淆技术,组合在一起使用还是能有比较好的效果的,也能够在此基础之上二次开发定制自己的混淆或者反混淆工具。