跳过内容

iOS 代码质量和构建设置

概述

App 签名

代码签名您的应用可确保用户相信该应用来自已知来源,并且自上次签名以来未被修改。在您的应用能够集成应用服务、安装到非越狱设备上或提交到 App Store 之前,它必须使用 Apple 颁发的证书进行签名。有关如何请求证书和代码签名您的应用的更多信息,请查阅应用分发指南

第三方库

iOS 应用程序经常使用第三方库,这可以加快开发速度,因为开发人员需要编写的代码更少来解决问题。然而,第三方库可能包含漏洞、不兼容的许可或恶意内容。此外,组织和开发人员难以管理应用程序依赖项,包括监控库发布和应用可用的安全补丁。

目前有三种广泛使用的包管理工具:Swift Package ManagerCarthageCocoaPods

  • Swift Package Manager 是开源的,随 Swift 语言一同提供,集成到 Xcode 中(自 Xcode 11 起),并支持 Swift、Objective-C、Objective-C++、C 和 C++ 包。它用 Swift 编写,采用去中心化模式,并使用 Package.swift 文件记录和管理项目依赖项。
  • Carthage 是开源的,可用于 Swift 和 Objective-C 包。它用 Swift 编写,采用去中心化模式,并使用 Cartfile 文件记录和管理项目依赖项。
  • CocoaPods 是开源的,可用于 Swift 和 Objective-C 包。它用 Ruby 编写,为公共和私有包使用集中式包注册表,并使用 Podfile 文件记录和管理项目依赖项。

库分为两类:

  • 不(或不应)打包在实际生产应用程序中的库,例如用于测试的 OHHTTPStubs
  • 打包在实际生产应用程序中的库,例如 Alamofire

这些库可能导致意外的副作用:

  • 库可能包含漏洞,使应用程序变得脆弱。一个很好的例子是 AFNetworking 2.5.1 版本,其中包含一个禁用证书验证的错误。此漏洞可能允许攻击者对使用该库连接到其 API 的应用程序执行中间人 (MITM) 攻击。
  • 库可能不再维护或很少使用,因此没有报告和/或修复漏洞。这可能导致通过该库在应用程序中引入糟糕和/或有漏洞的代码。
  • 库可能使用例如 LGPL2.1 等许可,这要求应用程序作者向使用该应用程序并要求查看其源代码的人提供源代码访问权限。实际上,应用程序应该被允许在修改其源代码后重新分发。这可能会危及应用程序的知识产权 (IP)。

请注意,此问题可能发生在多个层面:当您使用在 webview 中运行 JavaScript 的 webview 时,JavaScript 库也可能存在这些问题。Cordova、React-native 和 Xamarin 应用程序的插件/库也存在同样的问题。

内存损坏错误

iOS 应用程序有多种方式导致内存损坏错误:首先是通用内存损坏错误部分中提到的原生代码问题。其次,Objective-C 和 Swift 中都有各种不安全的操作可以实际包装原生代码,从而产生问题。最后,Swift 和 Objective-C 的实现都可能由于保留不再使用的对象而导致内存泄漏。

了解更多

二进制保护机制

检测二进制保护机制的存在与开发应用程序所使用的语言密切相关。

尽管 Xcode 默认启用所有二进制安全功能,但验证旧应用程序或检查编译器标志配置错误可能仍有意义。以下功能适用:

  • PIE(位置独立可执行文件):
    • PIE 适用于可执行二进制文件(Mach-O 类型 MH_EXECUTE来源
    • 但是,它不适用于库(Mach-O 类型 MH_DYLIB)。
  • 内存管理:
    • 纯 Objective-C、Swift 和混合二进制文件都应启用 ARC (Automatic Reference Counting)。
    • 对于 C/C++ 库,开发人员负责进行适当的手动内存管理。请参阅“内存损坏错误”
  • 栈溢出保护:对于纯 Objective-C 二进制文件,这应该始终启用。由于 Swift 被设计为内存安全,如果库是纯用 Swift 编写的,并且没有启用栈保护,则风险将最小。

了解更多

检测这些保护机制存在的测试在很大程度上取决于用于开发应用程序的语言。例如,现有的检测栈保护存在的技术不适用于纯 Swift 应用程序。

Xcode 项目设置

栈保护(Stack Canary protection)

在 iOS 应用程序中启用栈保护的步骤:

  1. 在 Xcode 中,在“Targets”部分选择您的目标,然后单击“Build Settings”选项卡以查看目标的设置。
  2. 确保在“Other C Flags”部分选中“-fstack-protector-all”选项。
  3. 确保启用了位置独立可执行文件 (PIE) 支持。
PIE 保护

将 iOS 应用程序构建为 PIE 的步骤:

  1. 在 Xcode 中,在“Targets”部分选择您的目标,然后单击“Build Settings”选项卡以查看目标的设置。
  2. 将 iOS 部署目标设置为 iOS 4.3 或更高版本。
  3. 确保“Generate Position-Dependent Code”(“Apple Clang - Code Generation”部分)设置为其默认值(“NO”)。
  4. 确保“Generate Position-Dependent Executable”(“Linking”部分)设置为其默认值(“NO”)。
ARC 保护

swiftc 编译器会自动为 Swift 应用启用 ARC。但是,对于 Objective-C 应用,您需要按照以下步骤确保其启用:

  1. 在 Xcode 中,在“Targets”部分选择您的目标,然后单击“Build Settings”选项卡以查看目标的设置。
  2. 确保“Objective-C Automatic Reference Counting”设置为其默认值(“YES”)。

参见技术问答 QA1788 构建位置独立可执行文件

可调试应用

通过将 `get-task-allow` 键添加到应用授权文件并将其设置为 `true`,可以将应用设置为可调试( 调试)。

虽然调试是开发应用程序时的有用功能,但在发布到 App Store 或企业程序之前必须将其关闭。为此,您需要确定生成应用程序的模式以检查环境中的标志:

  • 选择项目的构建设置
  • 在“Apple LVM - Preprocessing”和“Preprocessor Macros”下,确保未选择“DEBUG”或“DEBUG_MODE”(Objective-C)
  • 确保未选择“Debug executable”选项。
  • 或者在“Swift Compiler - Custom Flags”部分 / “Other Swift Flags”中,确保不存在“-D DEBUG”条目。

调试符号

作为一项良好实践,编译后的二进制文件中应尽可能少地提供解释性信息。调试符号等额外元数据的存在可能会提供有关代码的有价值信息,例如函数名称泄露函数功能的信息。执行二进制文件不需要此元数据,因此在发布版本中安全地丢弃它是可以的,这可以通过使用适当的编译器配置来完成。作为测试人员,您应该检查应用程序随附的所有二进制文件,并确保不存在任何调试符号(至少是那些揭示任何有价值代码信息的符号)。

当 iOS 应用程序被编译时,编译器会为应用程序中的每个二进制文件(主应用程序可执行文件、框架和应用程序扩展)生成一个调试符号列表。这些符号包括类名、全局变量以及方法和函数名称,它们被映射到它们定义的特定文件和行号。应用的调试构建默认将调试符号放置在编译后的二进制文件中,而应用的发布构建则将它们放置在配套的调试符号文件(dSYM)中,以减小分发应用程序的大小。

调试代码和错误日志

为了加快验证并更好地理解错误,开发人员通常会包含调试代码,例如关于其 API 响应以及应用程序进度和/或状态的详细日志语句(使用 NSLogprintlnprintdumpdebugPrint)。此外,可能还有用于“管理功能”的调试代码,供开发人员用于设置应用程序状态或模拟来自 API 的响应。逆向工程师可以轻松使用此信息来跟踪应用程序中发生的情况。因此,应从应用程序的发布版本中删除调试代码。

异常处理

异常通常在应用程序进入异常或错误状态后发生。测试异常处理是为了确保应用程序能够处理异常并进入安全状态,而不会通过其日志机制或 UI 暴露任何敏感信息。

请记住,Objective-C 中的异常处理与 Swift 中的异常处理截然不同。在同时包含旧 Objective-C 代码和 Swift 代码的应用程序中桥接这两种方法可能会出现问题。

Objective-C 中的异常处理

Objective-C 有两种类型的错误:

NSException

NSException 用于处理编程和低级错误(例如,除以 0 和数组越界访问)。NSException 可以通过 raise 抛出,或者使用 @throw 抛出。除非被捕获,否则此异常将调用未处理的异常处理程序,您可以通过它记录语句(日志记录将暂停程序)。如果您使用 @try-@catch-块,@catch 允许您从异常中恢复。

 @try {
    //do work here
 }

@catch (NSException *e) {
    //recover from exception
}

@finally {
    //cleanup

请记住,使用 NSException 存在内存管理陷阱:您需要在finally 块清理 try 块中的分配。请注意,您可以通过在 @catch 块中实例化 NSError 来将 NSException 对象提升为 NSError

NSError

NSError 用于所有其他类型的错误。某些 Cocoa 框架 API 在其失败回调中以对象形式提供错误,以防出现问题;那些不提供的 API 则通过引用传递指向 NSError 对象的指针。提供一个 BOOL 返回类型给接受指向 NSError 对象的指针的方法,以指示成功或失败,是一种很好的做法。如果有返回类型,请务必为错误返回 nil。如果返回 NOnil,它允许您检查错误/失败原因。

Swift 中的异常处理

Swift (2 - 5) 中的异常处理截然不同。try-catch 块不是用来处理 NSException 的。该块用于处理符合 Error (Swift 3) 或 ErrorType (Swift 2) 协议的错误。当 Objective-C 和 Swift 代码在应用程序中结合使用时,这可能会带来挑战。因此,对于用两种语言编写的程序,NSErrorNSException 更可取。此外,Objective-C 中的错误处理是可选的,但 Swift 中必须显式处理 throws。要转换错误抛出,请查看Apple 文档。可能抛出错误的方法使用 throws 关键字。Result 类型表示成功或失败,请参见Result如何在 Swift 5 中使用 ResultSwift 中 Result 类型的强大之处。Swift 中有四种方法可以处理错误

  • 将错误从函数传播到调用该函数的代码。在这种情况下,没有 do-catch;只有 throw 抛出实际错误或 try 执行抛出错误的方法。包含 try 的方法也需要 throws 关键字。
func dosomething(argumentx:TypeX) throws {
    try functionThatThrows(argumentx: argumentx)
}
  • 使用 do-catch 语句处理错误。您可以使用以下模式:
func doTryExample() {
    do {
        try functionThatThrows(number: 203)
    } catch NumberError.lessThanZero {
        // Handle number is less than zero
    } catch let NumberError.tooLarge(delta) {
        // Handle number is too large (with delta value)
    } catch {
        // Handle any other errors
    }
}

enum NumberError: Error {
    case lessThanZero
    case tooLarge(Int)
    case tooSmall(Int)
}

func functionThatThrows(number: Int) throws -> Bool {
    if number < 0 {
        throw NumberError.lessThanZero
    } else if number < 10 {
        throw NumberError.tooSmall(10 - number)
    } else if number > 100 {
        throw NumberError.tooLarge(100 - number)
    } else {
        return true
    }
}
  • 将错误作为可选值处理。
    let x = try? functionThatThrows()
    // In this case the value of x is nil in case of an error.
  • 使用 try! 表达式断言错误不会发生。
  • 将通用错误作为 Result 返回类型处理。
enum ErrorType: Error {
    case typeOne
    case typeTwo
}

func functionWithResult(param: String?) -> Result<String, ErrorType> {
    guard let value = param else {
        return .failure(.typeOne)
    }
    return .success(value)
}

func callResultFunction() {
    let result = functionWithResult(param: "OWASP")

    switch result {
    case let .success(value):
        // Handle success
    case let .failure(error):
        // Handle failure (with error)
    }
}
  • 使用 Result 类型处理网络和 JSON 解码错误。
struct MSTG: Codable {
    var root: String
    var plugins: [String]
    var structure: MSTGStructure
    var title: String
    var language: String
    var description: String
}

struct MSTGStructure: Codable {
    var readme: String
}

enum RequestError: Error {
    case requestError(Error)
    case noData
    case jsonError
}

func getMSTGInfo() {
    guard let url = URL(string: "https://raw.githubusercontent.com/OWASP/mastg/master/book.json") else {
        return
    }

    request(url: url) { result in
        switch result {
        case let .success(data):
            // Handle success with MSTG data
            let mstgTitle = data.title
            let mstgDescription = data.description
        case let .failure(error):
            // Handle failure
            switch error {
            case let .requestError(error):
                // Handle request error (with error)
            case .noData:
                // Handle no data received in response
            case .jsonError:
                // Handle error parsing JSON
            }
        }
    }
}

func request(url: URL, completion: @escaping (Result<MSTG, RequestError>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            return completion(.failure(.requestError(error)))
        } else {
            if let data = data {
                let decoder = JSONDecoder()
                guard let response = try? decoder.decode(MSTG.self, from: data) else {
                    return completion(.failure(.jsonError))
                }
                return completion(.success(response))
            }
        }
    }
    task.resume()
}