导语:Sirius是Polidea团队推出的一个开源项目,用于保护 iOS 应用程序的开源代码混淆。现在Sirius可以完全访问,并随时可用,它支持最新的苹果产品以及Swift语言。
在上一篇文章中,我介绍了开源代码混淆工具Sirius之所以要开发的思想来源,安全性。今天,我将介绍Sirius工具开发过程在的一些挑战及应对之策。
简单重命名的挑战
在确定选择编译前(从源代码到源代码)方法后,polidea团队发现自己需要一个能够帮助他们正确重命名的工具。这看起来是一项简单的任务,但实际上需要妥善处理大量信息,比如下面这个样本。
struct foo {}protocol SomeProtocol { associatedtype foo func foo(foo: foo) -> foo}protocol AnotherProtocol { func foo(foo: foo) -> foo}class SomeBaseClass<foo> { func foo(foo: foo) -> foo { return foo }}class SomeClass: SomeBaseClass<foo>, SomeProtocol { override func foo(foo: foo) -> foo { return foo }}SomeClass().foo(foo: foo())
虽然其中面临着很多的挑战,但有两个挑战必须指出。第一个是有着多种解释的foo符号名称,它会根据上下文的不同,代表不同的类型名称、关联的类型名称、函数名称、参数名称,泛型类型名称、变量名称或初始化程序调用。“foo”符号名经常会作为样本名称,出现在各种程序和技术文档中。据统计,在各种计算机和通信技术文档中,大约有百分之七的文档出现了这些词语。可是这些文件都没有为它们给出合适的解释。虽然这不是个大问题,但对于初学者,尤其是母语非英语的人来说,这些单词往往会带来不小的迷惑。polidea团队的目标是尽可能多地为符号名称引入熵,因此他们应该将每个foo变量更改为不同的随机名称。要做到这一点,polidea团队就需要一种区分这些用法的方法。如果没有全面的分析,是不可能做到区分效果的。
第二个挑战是SomeProtocol,AnotherProtocol,SomeBaseClass和SomeClass类型中的foo函数之间的关系。由于它们的符号名称相同,因此polidea团队可能怀疑这些函数所表达的含义都是一样的。由Sirius工具的目就是混淆符号名称的含义,所以polidea团队应该在将每个运行范围内的foo名称更改为不同的名称。但前提是修改后的名称不要导致任何编译或运行的错误,为了做到这一点,polidea团队必须保持原有这些foo函数之间的逻辑运行关系。比如SomeClass通过继承SomeBaseClass并符合SomeProtocol来创建这三种类型之间的连接。另一方面,AnotherProtocol从不绑定SomeProtocol和SomeBaseClass。因此,符号重命名应该将AnotherProtocol中的foo函数名称更改为与其他运行范围中的foo函数不同的名称。最具挑战性的部分是,只有在处理完所有源代码后,才能识别这些关系,因为SomeProtocol和SomeBaseClass本身没有任何内容表明它们是连接在一起的。
生成的代码可能类似于以下这种形式:
struct gU9OIWUVMN {}protocol c54MiljGWM { associatedtype YAxsJ4FEM9 func wuxWC0Z9ic(mBeXqpr5Ec: YAxsJ4FEM9) -> YAxsJ4FEM9}protocol RamC43kstx { func oWjESxSMiK(aPZTcAZeLr: gU9OIWUVMN) -> gU9OIWUVMN}class H1eoOLdD2V<gnl5fOVXT6> { func wuxWC0Z9ic(mBeXqpr5Ec: gnl5fOVXT6) -> gnl5fOVXT6 { return mBeXqpr5Ec }}class I6ScLpZfQF: H1eoOLdD2V<gU9OIWUVMN>, c54MiljGWM, RamC43kstx { override func wuxWC0Z9ic(mBeXqpr5Ec: gU9OIWUVMN) -> gU9OIWUVMN { return mBeXqpr5Ec }}I6ScLpZfQF().wuxWC0Z9ic(mBeXqpr5Ec: gU9OIWUVMN())
上面的例子表明,混淆工具必须能够读取源代码,进而解析代码、分析代码、检查代码,然后转换代码,所有这些都需要具备丰富的Swift语言。
不过也有很多可能的方法来代替这个过程,比如使用一组正则表达式和bash命令,通过一个轻量级的自定义解析器,比如antlr,antlr是指可以根据输入自动生成语法树并可视化的显示出来的开源语法分析器。polidea团队为开发者提供了一套完整的Swift工具链(ToolChain)做编译器,用SourceKit或libSyntax做调试工具,能够很方便的实现编译器的fork并直接使用它。
对混淆方案的不断优化
直接使用编译器使polidea团队能够使用Swift AST,它包含了关于应用程序的最多信息。如果需要,polidea团队还可以扩展词法分析器或解析器来处理特殊情况。更重要的是,由于Swift编译器包含Xcode使用的重构引擎,因此它可能有助于在源文件中执行实际更改。
使用fork编译器的主要问题是,其使用的内部库、模块和API都不是公开的。因此,没有版本的更新,没有稳定性保证,没有兼容性保证。这意味着,任何Swift的任何改变都可能导致混淆工具的重写。此外,polidea团队使用的编译器还不是能完全满足polidea团队对Sirius的设计需求,还有些功能Sirius根本用不到。例如,从AST获取有关符号名称的信息有时就是多余的,该功能可能深埋在节点的层次结构中,这些节点捕获的信息对于高效的类型检查或SIL生成至关重要,但Sirius根本用不到。
然而,使用现有的编译器的好处是,其基础结构非常简单。原有的构建系统已经支持定义库以及它们之间的依赖关系。这意味着创建一个使用swiftAST或swiftDriver的swiftObfuscation库非常简单。此外,可以在几十行代码中添加像obfuscator-renamer这样的命令行工具,它们使用hood下的swiftObfuscation。
虽然有关如何与Swift编译器基础结构集成的详细信息超出了本文的范围,但你可以点此了解更多信息。
另一个问题是,Sirius混淆工具必须根据公开的Swift版本进行版本变化。对于每个包含新的Swift版本的Xcode版本,polidea团队必须提供一个单独的混淆器二进制文件,以便语言版本匹配。目前的版本支持Xcode 9.2和Swift 4.0,但polidea团队正在加紧研发它们对Xcode 9.3和9.4的支持(分别为Swift 4.1和4.1.2)。
代码混淆过程
拥有所有工具后,polidea团队按照两个原则设计了混淆过程。首先,所有工具必须易于使用并集成到构建过程中。其次,所有工具必须是模块化的,以实现可扩展性和自定义过程。
为了Sirius工具易于使用,polidea团队的目标是使用尽可能少的参数单独执行单个二进制文件。由于Sirius混淆工具的目标开发者是iOS / macOS开发人员,因此polidea团队决定以项目目录的路径形式提供输入,该目录包含.xcodeproj或.xcworkspace。 Swift源文件的实际路径以及它们导入的框架,以及构建设置(包括架构和SDK版本)都将由polidea团队亲自识别,而不是由开发者识别。单独执行的输出是Xcode项目的副本,其中包含所有已转换的源代码文件和资源(如.storyboard和.xib文件、资产、配置文件等)。还有可能在不创建副本的情况下进行混淆处理,这对CI / CD集成很有用。
生成的界面如下所示:
$ bin/sirius -projectrootpath `<path-to-xcode-project>` -obfuscatedproject `<path-for-obfuscated-project>` [-namemappingstrategy `<name-mapping-strategy>`] [-keepintermediates] [-inplace] [-verbose]
其中方括号中的所有参数都是可选项,并支持提前设置好的默认值。
模块化原则使polidea团队可将整个混淆过程分成许多小的独立步骤,并为每个步骤创建单独的命令行工具。
首先,对Xcode项目进行解析,以搜索执行混淆所需的所有信息。此过程是由一个名为“Files Extractor ”的工具完成的,它是用Ruby编写的,使用的是CocoaPods(CocoaPods是OS X和iOS下的一个第三类库管理工具)下的Xcodeproj,输出的是Files.json格式的JSON文件。然后, Files.json文件被用来解析源代码以搜索应该重命名的所有符号,此时就要用到Symbol Extractor工具,它使用Swift编译器执行分析并遍历AST,输出的是Symbols.json格式的JSON文件。第三步是生成新名称,该过程由名为Name Mapper的工具完成,该工具使用的是Swift编译器基础结构,但它仅用于定义CLI和JSON解析。但它使用的却是Symbols.json文件并以Renames.json格式输出JSON文件。至此,polidea团队已经收集了执行重命名所需的所有信息。最后一步是混淆过程,负责该过程的工具称为Renamer(文件批量重命名工具),它将Files.json和Renames.json作为输入,输出的是经过混淆的代码,它依赖于Swift编译器,以此来对源文件进行实际更改。
可以看出,整个过程是通过调用一个个单独执行的工具完成的。最后,polidea团队还会使用一个简单的验证工具,通过比较混淆前后的符号来验证过程的成功与否。
而这些工具之间的协调沟通则是通过提前定义某种格式的JSON文件完成的,以允许额外的脚本编制和自定义。例如,如果你希望将某个特定符号重命名为某个特殊名称,则在Renamer读取之前更改Renames.json文件中的数据就可以了。
Sirius混淆工具的不断优化
虽然Sirius混淆工具目前还不是一个完美的工具,但它已经非常强大了。它能支持许多Swift语言结构、很少的Xcode项目配置(包括基本的CocoaPods集成)、重命名XIB / Storyboard中的文件以及可能使用的其他配置,例如,利用重命名删除Core Data模型。Core Data是iOS5之后才出现的一个框架,本质上是对SQLite的一个封装,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象,通过CoreData管理应用程序的数据模型,可以极大程度减少需要编写的代码数量!
不过Sirius混淆工具也有很多限制,最明显的是没有DSYM转换,这就造成了你的崩溃报告中将包含混淆的名称;没有多个目标混淆,这就造成了你的单元测试不会在混淆后运行,并且不支持混合Swift / Objective-C项目,另外还有一些polidea团队尚未提供支持的特定Swift语言结构。
不过好消息是Sirius项目是开源的,Polidea非常乐意看到有爱好者对该工具的参与。因此,如果你有兴趣为Sirius混淆工具的任何组件提出意见,请查看专用存储库以获取相关说明、文档和所有其他所需信息。
如果你对技术细节、设计决策、设计中所遇的挑战感兴趣,请查看每个专用存储库中的Documentation文件夹。正如你在以上样本中看到的那样,polidea团队已经花费了大量精力来记录开发过程。
总结
如果你有兴趣为使用Sirius混淆工具来保护你的程序,请检查Github上的存储库以获取可执行文件的链接。如果你发现该工具足以将其集成到你的应用程序开发过程中,但当前版本不包括你的Xcode项目配置、特定的Swift语言构造或任何其他需求,Polidea将很乐意提供咨询和支持。