拒绝“裸奔”!一款基于 SourceKit-LSP 的 Swift 代码混淆工具 (SwiftyShield)

574 阅读11分钟

前言:由 Swift 安全引发的思考

作为 iOS/macOS 开发者,我们往往专注于功能迭代和 UI 交互,却容易忽视发布的“最后一公里”——源码安全。虽然 Swift 是编译型语言,但其二进制文件中依然保留了大量的符号信息。通过 IDA Pro 或 Hopper 等逆向工具,攻击者可以轻易还原出类名、方法名甚至核心业务逻辑。

为了解决这个问题,我开发了 SwiftyShield —— 一款专为 macOS 和 iOS 开发者打造的专业级 Swift 代码混淆工具。

始于颜值,忠于体验:谁说开发工具必须“丑”?

在使用过市面上大量“灰头土脸”的命令行工具或基于 Java/Electron 简单套壳的混淆器后,我决定用 SwiftUI 为 SwiftyShield 打造一套原生、精致的 macOS 体验。

  • 精致的 UI 设计:完全遵循 Apple Human Interface Guidelines,支持完美的深色模式(Dark Mode),与你的 macOS 系统浑然一体。
  • 流畅的交互动画:从代码分析的进度条流转,到混淆完成后的成功动效,每一个交互细节都经过精心打磨。我们希望你在进行枯燥的“加固”工作时,也能感受到操作的愉悦。

核心原理:基于 Apple 官方工具链的深度解析

SwiftyShield 的核心不仅仅是好看,其技术路线基于 Apple 官方的 SourceKit-LSP 和 Xcode Toolchain 构建

这意味着它不是简单地用正则表达式(Regex)去“猜”代码,而是像 Xcode 一样真正“理解”你的代码语法树。它能智能识别哪些符号是公共 API、哪些是模块依赖,从而进行安全、精准的重命名。

下面展示两个最能体现 SwiftyShield 智能程度的核心场景。

场景 1:智能语义分析 —— 搞定“隐形”继承链

这是 SwiftyShield 最硬核的能力之一。

很多初级混淆工具是“文件隔离”的。如果你的控制器 A 遵循了 UITableViewDelegate 但没实现方法,而控制器 B 继承了 A 并实现了 didSelectRowAt,普通工具往往会误判。

因为在控制器 B 的定义中,看不到 UITableViewDelegate 的影子,普通工具会误以为 didSelectRowAt 是一个自定义函数,从而将其重命名,导致 TableView 点击失效

SwiftyShield 通过全项目 AST(抽象语法树)分析,能精准识别出这种“隔代继承”关系:

混淆前 (Before):

ProductListController 继承自 BaseListController,虽然它自己没写 Delegate 声明,但 SwiftyShield 知道它的父类遵循了协议。

Swift

// 文件 A:BaseListController.swift
class BaseListController: UIViewController, UITableViewDelegate {
    // 这里遵循了协议,但没有实现 didSelectRowAt
}

// 文件 B:ProductListController.swift
class ProductListController: BaseListController {
    // ⚠️ 挑战来了:普通工具只看到这是一个普通的函数,可能会误改名
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Item selected")
    }
}

混淆后 (After):

SwiftyShield 成功识别了继承链,保留了系统回调方法名,确保业务逻辑不崩坏。

Swift

// 文件 A
class InogenicMartyressIntroflexIliocaudal: UIViewController, UITableViewDelegate {
    // 类名已混淆
}

// 文件 B
class StrongylonCircumterraneousSemicolon: InogenicMartyressIntroflexIliocaudal {
    // ✅ 成功识别:
    // SwiftyShield 判定该方法属于 System Protocol Requirement,自动豁免混淆
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Item selected")
    }
}

场景 2:深度逻辑混淆 —— 连参数和局部变量也不放过

除了保护继承结构,SwiftyShield 还能深入函数内部。开启临时声明混淆后,连方法的参数标签、参数名以及函数内部的局部变量都会被重写。

混淆前:

参数名 password 和局部变量 combined 直接暴露了意图。

Swift

func generateHash(password: String, salt: String) -> String {
    let combined = password + salt
    return combined.md5()
}

混淆后:

代码变成“天书”,逆向者难以推断数据流向。

Swift

func anomalismSwornUnguentiferous(_ unpersuadableness: String, _ nonillionth: String) -> String {
    let polymerizeSterno = unpersuadableness + nonillionth
    return polymerizeSterno.md5()
}

SwiftyShield 的独特优势

1. 权益跟号走,拒绝“设备焦虑”

很多传统开发软件采用“一机一码”的绑定策略,换台电脑开发就得重置许可,非常麻烦。

SwiftyShield 采用现代化的账号授权体系。所有权益与您的账号绑定,而非特定设备。 无论您是在公司的 iMac 上工作,还是回家用 MacBook Pro 加班,只需登录账号,即可随时同步并使用您的 Pro 权益。

2. 100% 本地化处理,隐私无忧

我们深知源码是开发者的命脉。SwiftyShield 实现了完全离线运行。所有的混淆分析、数据库存储(基于 Realm)都只发生在您的 Mac 本地。没有任何代码会被上传到服务器

3. 实战验证的稳定性

SwiftyShield 已通过 100+ 个热门开源库(如 Alamofire, RxSwift, Lottie)的兼容性测试。工具会自动识别并跳过 Objective-C、SwiftUI、XIB 等文件,确保混合编译项目无缝衔接。

🎬 实战演示

眼见为实,与其看枯燥的文档,不如直接看看 SwiftyShield 如何为你的项目穿上“防弹衣”。

(注:如果视频无法播放,请访问演示链接:观看演示视频)

适用范围与下载

  • 系统要求:macOS 13 Ventura 或更高版本。
  • 安全性:应用已通过 Apple 公证 (Notarized),请放心使用。

👉 官网下载 & 体验www.swiftyshield.com

👉 GitHub:github.com/SwiftyShiel…

(具体使用规则、详细文档,请参考 GitHub 仓库 README)

如果你也是一名追求极致体验的开发者,欢迎下载试用!有任何建议或 Feature Request,欢迎在评论区或 GitHub Issue 中提出。


📅 更新日志:一次 SourceKit 内部崩溃的排查

2026.01.26

今天收到一位用户反馈,他们的工程在插入“垃圾代码”后混淆会崩溃(插入前不会)。经过分析用户发来的崩溃信息和伪“垃圾代码”,我定位到这是一个 SourceKit 内部的 Crash,且无法在 Swift 层面通过 do-catch 捕获。

🐛 问题复现

引发崩溃的代码片段非常有意思:

func function() {
#if false
    do {
        // 模拟一段不需要编译的代码
        let data = "{}".data(using: .utf8)!
        let _ = try JSONDecoder().decode(XXX.self, from: data)
    } catch {
        // ❌ 崩溃触发点:
        // 在 #if false 的 catch 块中,引用任何 Foundation 定义(如 print)
        // 都会直接导致 SourceKit 内部崩溃
        print(error) 
    }
#endif
}

为了解决这个问题,我在一个包含 20w+ 临时变量请求的真实项目中,验证了以下两个隔离方案:

  1. 外挂 XPC 服务:试图将 SourceKit 请求隔离在 XPC Service 中。但在该量级的高并发测试下,XPC 的通信存在不确定性,稳定性未达标(不排除是我实现逻辑的问题),存在风险。

  2. 独立 CLI 进程调用:将 CLI 放在 Bundle 中,通过可执行文件管道交互。

    • 由于每次index都需要传递完整的编译参数(Compiler Arguments),只能将参数写入文件后再通过CLI来index,I/O 成为瓶颈
    • 每运行一个 CLI 命令, 进程开销, 初始化与加载等等都是耗时操作
    • 测试结果:混淆总耗时增加了近 1/4,这是无法接受的性能倒退。

📝 解决方案

鉴于这是 SourceKit 底层机制问题,且上述隔离方案在高性能要求下性价比过低。目前的建议是:

如果开启了“临时变量混淆”,请排查工程中所有不参与编译的代码块(不仅限于 #if false,还包括所有未命中的宏判断分支)。请务必避免在这些分支的 catch 语句块中调用 Foundation 库的 API(如 print 字符串的拼接等),以防止触发 SourceKit 内部崩溃。

在下个版本中,我将在 APP UI 层面增加不支持的代码段示例, 持续收集问题

感谢该用户的反馈, 后续将持续更新使用中出现的问题

📅 更新日志:思路逆转,彻底解决 SourceKit 崩溃问题

2026.01.29

今天上班的时候对之前的 SourceKit 崩溃问题(即 #if false 中引用 Foundation 导致崩溃)有了新的思考。

回顾之前的尝试,无论是 XPC 隔离还是 CLI 管道,本质上都是在“崩溃发生后如何降低影响”的防御性思维里打转。这不仅增加了架构复杂度,还带来了严重的性能损耗。

💡 破局思路: 既然崩溃是因为 SourceKit 分析了“未参与编译的代码”导致的,那为什么不直接在源头上把它“摘除”呢?

🛠️ 技术实现: 我调整了混淆引擎的策略。在获取全局变量混淆信息后,利用已有的 Indexing 数据上下文,精准定位出所有 未参与编译的代码区域(Inactive Regions)。在构建 Indexing 节点的之前,直接过滤剔除可能引发SourceKit崩溃的请求,而且也不影响现有的逻辑,因为未参与编译的代码本来就不应该参与混淆。

这样一来,SourceKit 根本不会去处理那些引发崩溃的危险代码,从根本上杜绝了 SourceKit 内部异常的触发条件。

✅ 结果: 经过多轮测试,该方案不仅完美解决了崩溃问题,且没有引入额外的 I/O 开销。v1.1.4 版本将包含此项核心优化,无需再手动修改代码来规避该问题了!好了,逻辑闭环下班!


📅 更新日志:定制服务之明文字符串加密

2026.02.12

前两天有用户问我:“能不能做字符串的本地加密?”

作为过来人,一听就知道这小子是搞 马甲包(代码混淆 + 字符串加密 + 资源加密三件套)。这个需求当然能做,以前做马甲包的时候我们用的是正则去匹配字符串, 加上各种策略准确率也非常的高, 但是缺陷也有:

  1. 容错率低:遇到复杂的嵌套字符串可能匹配出错。
  2. 无法处理插值:对于 "My Name: (name)" 这种包含变量的字符串, 选择忽略不加密, 原因很简单, 无法定位字符串的精准坐标。

💡 高级方案:基于 Syntax 的精准拆分

为了彻底解决这个问题,我放弃了正则,转而利用编译器层面的 Syntax 信息(包含精准的 Token 坐标和 UTF8 长度)。

核心思路:

利用 Syntax 信息将拼接字符串进行精准拆分,把静态文本剥离出来逐个加密,同时保留变量插值结构。

遇到的挑战(特别是多行字符串):

单行字符串很好处理,因为所有信息都在同一行, 但多行字符串 (Multi-line String)  涉及复杂的跨行缩进规则。既要拆分加密,又不能破坏编译器的缩进检查,一度想要放弃处理多行字符串的插入处理😂。但经过对语法树的深入分析,最终还是解决了多行字符串的拆分与重组问题。

🛡️ 效果演示 (XOR Encryption)

以下演示使用了简单的 异或加密 (XOR)  方案(算法代码由 Gemini 3Pro 生成,Key 为 swiftyshield)。

加密算法实现:

Swift

/// 算法来自 Gemini
extension String {
    fileprivate var key: String { "swiftyshield" }

    func encrypt() -> String {
        let inputData = data(using: .utf8)!
        let keyData = key.data(using: .utf8)!
        let keyBytes = [UInt8](keyData)
        var outputData = Data()
        
        // 遍历输入的每一个字节,和 Key 进行异或运算
        for (index, byte) in inputData.enumerated() {
            outputData.append(byte ^ keyBytes[index % keyBytes.count])
        }
        return outputData.base64EncodedString()
    }
    
    // XOR 的特性:(A ^ B) ^ B = A
    func decrypt() -> String {
        guard let data = Data(base64Encoded: self) else { return self }
        let keyData = key.data(using: .utf8)!
        let keyBytes = [UInt8](keyData)
        var outputData = Data()
        
        for (index, byte) in data.enumerated() {
            outputData.append(byte ^ keyBytes[index % keyBytes.count])
        }
        return String(data: outputData, encoding: .utf8) ?? self
    }
}

加密前 (Before):

包含普通拼接、变量插值以及复杂的多行字符串。

Swift

func stringTruncateTest() {
    let SoraAoi = "蒼井そら"
    let MariaOzawa = "小澤マリア"
    let pureString = "小澤マリア && 蒼井そら"
    
    // 插值字符串
    let concatenateString = "I ❤️ (SoraAoi), but i prefer (MariaOzawa)"
    
    // 多行字符串
    let multiLinesString = """
        today, i watched a film about (SoraAoi),
        but i still felt boring,
        then i watched another about (MariaOzawa),
        and finally i felt exausted
        """
    
    print(pureString)
    print(concatenateString)
    print(multiLinesString)
}

拆分加密后 (After):

所有的静态文本都被加密成了乱码,且原本的插值逻辑、多行结构被完美保留。

Swift

func conductioIndolence() {
    let autophobyRepresenter = "m+XVgs7skOn0hu7t".decrypt()
    let quidditativePedologicalHistorize = "lsfmgMrdkOv3hu/OkPXL".decrypt()
    let procumbentTabellariaSaskatoon = "lsfmgMrdkOv3hu/OkPXLRlJfU4D72Yje5pTo+5f7+g==".decrypt()
    
    // 自动拆分并拼接
    let couponlessFelonsetter = "OleL+9CWy+dJ".decrypt() + "(autophobyRepresenter)" + "X1cLEwBZGkgZFwkCFgVJ".decrypt() + "(quidditativePedologicalHistorize)"
    
    // 多行字符串被精准拆解
    let auroraRoquistMascotImperia = """
            BxgNBw1VUwFJEg0QEB8MAlQYUw4ACQFEEhUGEwBZ
            """.decrypt() + "(autophobyRepresenter)" + """
            X30LEwBZGkgaEQUIH1cPAxgNUwoGFwUKFFtjEhwcHUgARRsFBxQBAxBZEgYGEQQBAVcIBBsMB0g=
            """.decrypt() + "(quidditativePedologicalHistorize)" + """
            X30ICBBZFQEHBAAIClcARhIcHxxJABQFBgQdAxA=
            """.decrypt()
    
    print(procumbentTabellariaSaskatoon)
    print(couponlessFelonsetter)
    print(auroraRoquistMascotImperia)
}

⚠️ 总结与说明

  1. 权衡:字符串加密(包括多语言翻译)对于马甲包是刚需,但对于普通 App 来说,拆分和解密都会带来额外的运行时性能消耗(Runtime Overhead),请根据项目类型权衡使用。
  2. 定制服务:由于每个项目对加密算法(AES, XOR, ChaCha20等)和 Key 的管理方式不同,该功能目前不作为通用功能上线,仅提供定制服务。如有需要马甲包需求的,欢迎联系定制工具。