在做iOS马甲包,或者加固的时候,我们需要进行代码混淆。目前比较成熟方便的解决方案,大多是采用念茜大神博客里介绍的#define
方式来做。
当然了,博客里给出的方案实现比较简陋,需要手动提取想要混淆的类及方法名。而且似乎因为版本更新等原因,这个比较旧的脚本现在也不能直接拿来用了,所以最好还是当作原理学习的样板吧。不过要是想找个立取可用的工具,可以用stevchen大神的开源工具STCObfuscator。
相较于简陋的”原理样板”来说,STCObfuscator
就很贴心了,先是用runtime
+linkmap
的方式,帮我们自动提取需要混淆的方法名和类名,然后又过滤掉对类名、方法名的硬编码引用。
但是除了类名、方法名的混淆以外,代码里的硬编码,比如最常见的写死了的字符串,也是有必要进行混淆的。
1. 分析实现
硬编码的类型有很多种,凡是在代码里写死了的字符串、数字等等,都能算作是。但对于马甲包或者加固工作来说,影响比较大的还是字符串类型数据。那整个的混淆过程,就可以分成下面这几步:
- 1) 字符串代码块定位
- 2) 字符串代码块内容加密
- 3) 替换原代码块
经过这三步操作以后,我们的代码里,原先的字符串代码块文本内容,就变成了已经加密后的密文。不过这并不影响我们的使用,因为等需要用到这个字符串的时候,我们会通过原生端的解密方法解密出来。
1.1 字符串代码块定位
首先,人工检测是不可能的,这辈子都不可能的。而且对于字符串代码块的自动检测runtime是用不上了,所以最浅显的方案就是直接文本检查。即通过对原代码文本的检查,找出代码中字符串代码块的位置。目前采用的方案,是通过正则表达式,去查找@"
开头,"
结尾的代码块。例如对于下面这段代码,就可以定位出有两个字符串代码块。
1 | - (void)testMethod |
对于这种定位方案,其实没啥好说的,无非就是正则表达式写仔细点,尽量多的去匹配各种神奇的代码写法就好。而唯一需要注意的点,就在于面对带有%@
这种占位符的字符串,如果没有筛选出占位符以外的字符串并混淆的方案,就暂时不要对它进行处理。而现有常见的占位符种类,可以参照苹果的技术文档,大致有这么多种。
1.2 字符串代码块内容加密
因为加密了以后的内容,还是需要解密后使用的,所以就不能用MD5算法这种不可逆的方式来做了。再加上考虑到解密操作对性能的影响,所以目前选用的加密方案,是速度比较快的AES方案。具体的加密模块代码实现,可以在各个论坛找到,在这里就不做太多介绍了。
1.3 替换原代码块
经过上一步的操作,我们已经得到了加密后的密文。再结合第一步中找到的代码块位置,我们就可以把原代码中的字符串代码块内容,替换成无法直接阅读的密文。
不过虽然这时加密的目的是做到了,但编译运行肯定会出错的。所以我们还得整一个解密方法类,跟一个定义了所有密文的定义文件。那到此为止,我们的工作就已经全部完成了,代码中的字符串代码块,也变成了[BAHCKey02b5dc445f4c6f7c3c0a5e50d406cc65 BAHC_Decrypt]
类似的句子。
2. 代码地址
目前的工具已经做好了第一版,github地址点这里。
具体的使用方法可以参照readme文件的介绍。
3. 优化
虽然第一版的工具已经做出来了,但还是存在问题的,主要是下面这两个:
- 字符串代码块识别不够精准
- 反混淆速度太慢
3.1 提高字符串代码块识别精准度
因为目前的识别方式,是使用正则表达式去匹配,所以难免会漏掉一些神奇的代码写法,甚至于也会因为部分字符串内容的原因,导致识别结果出错。
那么该怎样去提高识别精准度呢?答案就是Clang。作为苹果官方编译器LLVM中的一部分,Clang对于OC语法的识别必须是非常精准的。所以,管他三七二十一,实践万岁,我们先来找个Clang的python库尝试跑一把。
libclang是Clang自带的提供给python用的接口库,地址在这里。当然了,想要用的话,还是得把整个Clang目录都下载下来。
好了,现在已经下载下来啦,我们就跑起来吧!想多了,很抱歉,文档奇缺,就只有几个没啥文档说明的例子,所以想要跑起来,那还真是有点难度的。所以咱们还是老老实实一步一步来,先了解下Clang的基本知识吧。
首先是Clang官方网站的标题: Clang: a C language family frontend for LLVM。那这句话里的”前端”(frontend)到底啥意思呢?下面是百度百科的编译器词条中,对于前端的介绍:
前端主要负责解析(parse)输入的源代码,由语法分析器和语意分析器协同工作。语法分析器负责把源代码中的‘单词’(Token)找出来,语意分析器把这些分散的单词按预先定义好的语法组装成有意义的表达式,语句 ,函数等等。例如“a = b + c;”前端语法分析器看到的是“a, =, b , +, c;”,语意分析器按定义的语法,先把他们组装成表达式“b + c”,再组装成“a = b + c”的语句。前端还负责语义(semantic checking)的检查,例如检测参与运算的变量是否是同一类型的,简单的错误处理。最终的结果常常是一个抽象的语法树(abstract syntax tree,或 AST),这样后端可以在此基础上进一步优化,处理。
所以简而言之,Clang的工作,就是把源代码处理成,可被LLVM识别的低级类汇编代码,也就是LLVM中间表达码(LLVM Intermediate Representation)。而Clang内部的工作步骤,则大致分成这么几步:
- 1) 预处理: 把
#include
的头文件中包含的内容,嵌入到目标代码文件之类的工作 - 2) 词法解析: 把预处理得到的代码文件,解析成抽象语法树(AST)
- 3) 静态分析: 对AST进行分析,找出代码中的一部分错误
- 4) 代码生成: 生成LLVM中间表达码
- 5) 优化: 把代码中的部分递归改成循环之类的工作
既然基本知识看完了,那么也就有了大致的实现方案,即通过对AST的读取,拿到字符串代码块的位置和内容。咱们先验证一下方案的可行性,下面是需要解析的原代码:
1 | #import "TestClass.h" |
然后咱们使用Xcode自带的Clang工具,执行下面这行命令来导出AST:
clang -Xclang -ast-dump TestClass.m
运行结果长这样:
1 | TestClass.m:9:9: fatal error: 'TestClass.h' file not found |
仔细看一看执行结果,我们会发现有几行的首部写的是StringLiteral
,而且该行的后面,的确跟着的是我们代码中写的字符串代码块内容,甚至于更棒的是,还标出了在原代码中的准确行列位置。
但是,再仔细对比一下源码,testStatic
、testVariable1
、testVariable2
、testAssemble
的内容都哪儿去了?为什么只剩下了testLog
跟testDefine
的内容。
这是因为,在预处理阶段,Clang就已经去除掉了无用的代码,而testVariable1
等我们发现不见了的代码块,刚好就是因为没有引用到,所以就被摘除掉了。
但是这里还有有个疑点的,那就是对于testStatic
,如果我们在TestClass.h
文件中,声明了extern
属性的话,testStatic
就有可能在其他文件内被引用到了,不应该被从AST中去掉。所以按照这个猜想,我们把TestClass.h
文件也纳入到刚才的代码行中,变成这样的写法:
clang -Xclang -ast-dump TestClass.h TestClass.m
这时候,我们发现命令行的运行结果中,的确是包含了testStatic
代码块的信息。但整个的运行结果,也变成了28万多行之巨。这是因为在预处理时,Clang也把#import <Foundation/Foundation.h>
时引入的Foundation
代码,插入到了TestClass
的代码中。
至此我们面临一个两难的选择: 要么只识别部分字符串代码块,要么就得在二十万多行的内容里去筛查。理所当然,这两种方案我们肯定都难以满意,甚至于难以实用化。那还有没有其他办法,能稍微让我们满意一些呢?试一下这行命令:
clang -Xclang -dump-raw-tokens TestClass.m
运行的结果是这样的:
1 | comment '//' [StartOfLine] Loc=<TestClass.m:1:1> |
我们期待已久的结果终于得到了,只包含本文件内容,没有除去未引用的无用代码。要是libclang的python库中也包含相关的python接口,那就完美了。不过可惜的是,并没有找到相关的python接口,所以我们只能退而求其次,在python中执行bash命令,然后再以文本方式分析命令的运行结果,进而得到最终的字符串代码块位置和内容。
3.2 反混淆速度太慢
反混淆的意思,是指把已经被混淆了的代码恢复原样,这种操作的原理没啥好说的,就只是在代码里找到[BAHCKey02b5dc445f4c6f7c3c0a5e50d406cc65 BAHC_Decrypt]
样式的代码块,替换成对应的解密后的字符串内容而已。
可是目前的第一版实现里,反混淆的速度是比较慢的,具体原因,还得从工作流程上找。现在代码的工作流程大致如下:
- 1) 把宏定义了所有加密后密文的
.h
文件,解析成字典 - 2) 逐个解析代码文件,在代码中查找是否包含字典中的值并替换
这种做法在代码量小的时候问题不大,但在大型工程里,解析后的字典key-value值会有成千上万个,导致挨个拿去在代码文本中查找的操作耗时太长。
当然,改进的方法肯定是有的,最简单的,我们可以先用正则表达式,去匹配出符合[BAHCKey* BAHC_Decrypt]
样式的代码块,然后把这段代码块中的MD5值当作是key,去从字典中取出对应的字符串原文。
这么做肯定是快了许多,但问题在于为什么能变快?用key从字典中取值用到了哈希的原理,这我们都知道。可正则表达式为什么又能这么快呢?这个问题的答案,就得从算法层面去探究了。大致的解释,是因为通过对自动机的使用,使得算法复杂度大幅度降低了。这部分相关的内容,就留到后面再深入研究。
至此,代码优化的方向及解决方案都已经确定了,后面有空的话再做第二版的实现。
4. 附录
libclang python库的简单代码样例:
1 | #!/usr/bin/python |
这段代码的作用,是通过对libclang python库的使用,生成并解析AST。不过因为咱们解决方案的改变,这种方式最终还是没用上。但面对其他需求,还是有可用之处的。