IOS-应用安全指南-全-
IOS 应用安全指南(全)
原文:
zh.annas-archive.org/md5/3ced6e3c4931f581966ceace5280fa7e译者:飞龙
前言
有关 iOS 安全模型、越狱、在基础操作系统中寻找代码执行漏洞及其他安全相关特性的文献已经有很多。其他研究则侧重于从法医角度审视 iOS,包括如何从物理设备或备份中提取数据作为刑事调查的一部分。这些信息都很有用,但本书的目标是填补 iOS 文献中最大的空白:应用程序。
公众对编写 iOS 安全应用程序或对 iOS 应用程序进行安全评估的关注较少。因此,iOS 应用程序中的尴尬安全漏洞导致了敏感数据的暴露、身份验证机制的绕过以及用户隐私的滥用(无论是故意的还是偶然的)。人们正在使用 iOS 应用程序处理越来越重要的任务,并将大量敏感信息托付给它们,iOS 应用程序的安全性也需要随之成熟。
因此,我的目标是使本书尽可能接近于 iOS 应用程序安全开发的权威著作。iOS 的变化速度很快,但我尽力使内容尽可能准确,并提供工具帮助你检查和适应未来 API 的变化。
不同版本的 iOS 也有不同的漏洞。由于苹果已“停止支持”某些设备(例如 iPad 1),开发者可能仍希望其应用程序能够在这些设备上运行,因此本书涵盖了 iOS 5.x到 9.0(在撰写时的最新版本)中存在的漏洞,并在适用的情况下,讨论了每个版本特有的风险和缓解措施。
本书的读者
首先,这是一本关于安全的书。如果你是一个开发者或安全专家,正在寻找一本关于 iOS 应用程序如何在保护用户方面失败的常见方式(以及你或客户修补这些漏洞的选项)的指南,那么你来对地方了。
如果你至少有一点 iOS 开发经验,或者对 iOS 应用程序如何在幕后运作有一些了解,那么你能从本书中获得最大的收益。但即便没有这些知识,只要你是一个经验丰富的程序员或渗透测试员,不怕根据需要深入研究苹果的文档,你也应该能够应付。我在第二章中提供了一个关于 Objective-C 及其最常用 API Cocoa Touch 的快速导览,如果你需要一些高层次的基础知识或对语言的回顾,可以从这里开始。
本书内容
自 2008 年以来,我一直在进行各种 iOS 应用程序的安全评审和渗透测试,并积累了大量关于现实世界开发者在编写 iOS 应用程序时遇到的陷阱和错误的知识。本书将这些知识提炼出来,旨在同时吸引那些希望学习安全开发实践的 iOS 开发者和希望学习如何识别 iOS 安全问题的安全专家。
本书结构概述
在 第一部分:iOS 基础知识 中,你将深入了解 iOS 的背景、其安全历史以及基本的应用程序结构。
• 第一章:iOS 安全模型 简要介绍了 iOS 安全模型,让你了解该平台的基本安全保护措施,以及它们能提供什么和不能提供什么。
• 第二章:懒人版 Objective-C 讲解了 Objective-C 与其他编程语言的不同之处,并简要介绍了其术语和设计模式。对于有经验的 Objective-C 程序员来说,这可能并不是什么新知识,但对初学者以及第一次接触 iOS 的人来说,这些内容应该会非常有价值。
• 第三章:iOS 应用结构 概述了 iOS 应用的结构和打包方式,并探讨了可能泄露敏感信息的本地存储机制。
在 第二部分:安全测试 中,你将学习如何设置你的安全测试环境,无论是在开发还是渗透测试中使用。我还会分享一些设置 Xcode 项目的小技巧,帮助你充分利用可用的安全机制。
• 第四章:构建你的测试平台 提供了开始使用工具和配置所需的所有信息,以帮助你审核和测试 iOS 应用程序。这包括关于使用模拟器、配置代理、绕过 TLS 验证以及分析应用行为的信息。
• 第五章:使用 lldb 和工具进行调试 更深入地讲解了如何使用 lldb 和 Xcode 内建工具监控应用程序行为,并将其调整为符合你需求的方式。这将帮助你分析代码中的更复杂问题,同时为你提供一个测试工具,可以进行故障注入等操作。
• 第六章:黑盒测试 深入探讨了你需要的工具和技术,以成功分析没有源代码的应用程序。这包括基本的逆向工程、二进制修改、程序复制以及使用 lldb 的远程实例在设备上进行调试。
在 第三部分:Cocoa API 的安全问题 中,你将了解 Cocoa Touch API 中常见的安全陷阱。
• 第七章:iOS 网络 讨论了 iOS 中网络和传输层安全(TLS)的工作原理,包括认证、证书固定以及 TLS 连接处理中的常见错误。
• 第八章:进程间通信 介绍了进程间通信机制,包括 URL 方案和较新的 Universal Links 机制。
• 第九章:面向 iOS 的 Web 应用 介绍了 Web 应用如何与 iOS 原生应用集成,包括与 Web 视图的配合使用或利用 JavaScript/Cocoa 桥接,如 Cordova。
• 第十章:数据泄露 讨论了敏感数据如何以各种方式不小心泄露到本地存储、其他应用程序或通过网络传播。
• 第十一章:C 语言遗留问题与负担 概述了在 iOS 应用中仍然存在的 C 语言缺陷:栈和堆损坏、格式化字符串缺陷、使用已释放内存以及这些经典缺陷在 Objective-C 中的变体。
• 第十二章:注入攻击 涵盖了诸如 SQL 注入、跨站脚本攻击、XML 注入和谓词注入等攻击方式,特别是它们在 iOS 应用中的表现。
最后,第 IV 部分:保护数据安全 涵盖了与隐私和加密相关的问题。
• 第十三章:加密与认证 讨论了加密最佳实践,包括如何正确使用钥匙串、数据保护 API 和 CommonCrypto 框架提供的其他加密原语。
• 第十四章:移动隐私问题 通过讨论用户隐私问题结束本书,包括收集超出需求的数据对应用程序创建者和用户的影响。
到本书结束时,你应该能够快速抓取一个应用程序,无论是否有源代码,并迅速找出安全漏洞。你还应该能够编写安全可靠的应用程序,在更广泛的世界中使用。
本书遵循的规范
由于 Objective-C 是一种相当冗长的语言,拥有许多极长的类和方法名称,我在源代码列表中对行进行了换行,以最大程度地提高可读性。这可能并不代表你实际希望格式化代码的方式。在某些情况下,换行不可避免地让代码看起来不太美观——如果换行让代码看起来不清晰,可以尝试将其粘贴到 Xcode 中,让 Xcode 自动重新格式化。
正如我在第二章中详细说明的,我更倾向于使用传统的 Objective-C 中缀表示法,而不是点表示法。我还将大括号放在与方法声明同一行,原因类似:我年纪大了。
Objective-C 类和方法名称将以等宽字体显示。C 函数也将以等宽字体显示。为了简洁和清晰,路径 /Users/
关于 Swift 的说明
尽管相对较新的 Swift 语言引起了很多关注,但你会发现我在本书中没有涵盖它。背后有几个原因。
首先,我还没有实际遇到过用 Swift 编写的生产环境应用程序。Objective-C 仍然是 iOS 应用程序中最流行的语言,我们将在未来多年继续处理用它编写的代码。
其次,Swift 确实存在更少的问题。由于它不是基于 C 的,编写更安全的代码更容易,而且它不会引入新的安全漏洞(至少目前没有发现)。
第三,由于 Swift 使用与 Objective-C 相同的 API,您可能遇到的 Cocoa Touch API 中的安全陷阱,在这两种语言中基本上是相同的。本书中讲解的大部分内容都适用于 Objective-C 和 Swift。
此外,Swift 不使用中缀表示法和方括号,这让我既伤心又困惑。
移动安全承诺与威胁
当我第一次开始接触移动应用程序时,我诚实地质疑是否需要一个独立的移动应用程序安全类别。在我看来,移动应用程序在漏洞方面和桌面应用程序是一样的:堆栈和堆溢出、格式字符串漏洞、使用后释放(use-after-free)和其他代码执行问题。尽管在 iOS 中这些问题依然可能发生,但移动设备的安全焦点已扩展到隐私、数据盗窃和恶意进程间通信等方面。
在阅读本书中我所涵盖的 iOS 安全细节时,请记住,用户希望应用程序避免做出某些会危及其安全的行为。即使一个应用程序避免了明显的风险行为,仍然有许多威胁需要考虑,在加强应用程序防御时要特别留意。这一部分讨论了应用程序对用户做出的安全承诺以及可能迫使应用程序违背这些承诺的攻击类型。
移动应用程序不应该能够做的事情
从早期桌面操作系统的设计错误中汲取经验,主要的移动操作系统在设计时就考虑了应用程序隔离。这与桌面应用程序不同,后者用户运行的任何应用程序或多或少都可以访问该用户的所有数据,甚至控制整个机器。
由于在进程隔离和移动操作系统领域的改进,用户的期望也发生了变化。一般来说,移动应用程序(包括您的应用程序)不应该做几件关键的事情。
导致其他应用程序出现不当行为
应用程序不应能够崩溃或干扰其他应用程序。在过去的年代,不仅其他应用程序通常可以读取、修改或销毁数据,它们还可以通过这些数据使整个操作系统崩溃。随着时间的推移,桌面进程隔离有所改进,但主要目标是增加系统稳定性,而不是解决安全或隐私问题。
移动操作系统在此基础上有所改进,但在满足用户互操作性需求的同时,完全的进程隔离仍然不可能。应用程序之间的边界总是会有些许漏洞。开发者需要确保他们的应用程序不会出现不当行为,并采取一切谨慎措施来保护数据,防止恶意应用程序的干扰。
拒绝为用户提供服务
鉴于 iOS 历史上主要用于手机,因此确保应用程序不能做出阻止用户进行紧急呼叫的行为至关重要。在许多地方,这是法律要求,也是防止攻击者(以及用户)篡改底层操作系统的原因。
窃取用户数据
应用程序不应能够读取其他应用程序或基础操作系统的数据并将其传递给第三方。它也不应在未经用户允许的情况下访问敏感的用户数据。操作系统应防止应用程序直接读取其他应用程序的数据存储,但要防止通过其他渠道窃取数据,开发人员需要注意应用程序通过哪些进程间通信(IPC)机制发送或接收数据。
让用户不期望的费用
应用程序不应在未经用户批准的情况下产生费用。在野外发现的许多移动恶意软件利用发送短信的功能,将用户订阅到第三方服务,从而将费用通过用户的手机运营商转嫁给用户。应用内购买应清晰告知用户,并要求明确批准。
本书中的移动安全威胁分类
为了帮助理解移动设备的安全威胁及其防范措施,记住一些攻击类型也是非常有用的。这有助于我们现实地分析威胁,并帮助分析各种攻击及其防御的真正影响。
取证攻击
取证攻击者通常会获取一个设备或其备份,意图提取其中的机密数据。大多数情况下,这涉及到对设备上物理存储的检查。由于手机或平板电脑的盗窃相对容易,且比偷窃其他计算设备更常见,因此更多的关注集中在取证领域。
取证攻击可以由机会主义攻击者或有技能的攻击者对特定个人发起。对于机会主义攻击者,提取信息可能像偷走一部没有 PIN 码保护的手机那样简单;这使得他们可以窃取图片、笔记和手机上正常可以访问的其他数据。它也可能帮助攻击者突破使用手机令牌或短信配合两步验证的服务。
一名技术娴熟的取证攻击者可能是一个不法员工、公司、政府机构、执法人员,或者是一个动机强烈的敲诈者。这类攻击者掌握了执行临时越狱、破解简单密码、并检查设备文件系统中数据的技巧,包括系统级别和应用级别的数据。这能让攻击者不仅获取通过用户界面呈现的数据,还能获取底层缓存信息,其中可能包括截图、按键记录、网页请求中缓存的敏感信息等。
我将在第十章中介绍很多对取证攻击者有用的数据,并在第十三章中讨论一些进一步的防护措施。
代码执行攻击
远程代码执行攻击涉及通过在设备上执行代码来妥协设备或其数据,而无需物理接触设备。这可以通过多种渠道发生:网络、二维码或 NFC、解析恶意构造的文件,甚至是敌对硬件外设。需要注意的是,在设备上获得代码执行权限后,许多用于暴露用户秘密的取证攻击就变得可能。我将在第十一章中讨论几种常见的低级编程缺陷导致的代码执行攻击子类型。
基于 Web 的攻击
基于 Web 的远程代码执行攻击主要使用恶意构造的 HTML 和 JavaScript 来误导用户或窃取数据。远程攻击者要么操作一个恶意网站,要么接管一个合法网站,或者简单地向公共论坛发布恶意构造的内容。
这些攻击可以用来窃取本地数据存储中的数据,例如 HTML5 数据库存储或 localStorage,修改或窃取存储在 SQLite 数据库中的数据,读取会话 cookie,或植入伪造的登录表单以窃取用户凭证。我将在第九章和第十二章中讨论更多关于 Web 应用相关的问题。
基于网络的攻击
基于网络的代码执行攻击试图通过在网络上注入某种类型的可执行代码来控制应用程序或整个系统。这可以是修改进入设备的网络流量,或通过代码执行漏洞利用系统服务或内核。如果漏洞攻击的是具有较高权限的进程,攻击者不仅可以访问特定应用的数据,还可以访问设备存储中的所有数据。攻击者还可以监控设备活动并植入后门以便后续访问。我将在第七章中专门讨论与网络相关的 API。
依赖物理接近的攻击
物理代码执行攻击通常是利用 NFC 或 USB 接口等通信方式对设备进行攻击的漏洞。这些类型的攻击曾用于越狱,但也可以通过短暂的物理交互来妥协设备。这些攻击中的许多是针对操作系统本身的,但我将在第十四章中讨论一些与物理接近相关的问题。
iOS 安全测试员的注意事项
我坚信,渗透测试应该尽可能在源代码的帮助下进行。尽管这并不代表大多数外部攻击者的立场,但它能最大限度地提高在有限时间内发现重要漏洞的能力。现实中的攻击者有足够的时间来分析你的应用程序,且 Objective-C 非常适合逆向工程。只要有时间,他们能弄清楚如何做。然而,大多数渗透测试受到时间和资金的限制,因此通常不应以模拟真实攻击者为目标。
本书涵盖了白盒(即源代码辅助)和黑盒方法,但重点将放在源代码辅助渗透测试上,因为这种方法能更快地发现更多的漏洞,并帮助学习标准的 Cocoa 库。我在本书中描述的许多技术都适用于这两种方法。
话虽如此,iOS 开发人员来自不同的学科,每个人的技能背景都会影响到应用程序中那些未被注意到的安全问题。无论你是在测试别人的应用程序,还是试图在自己的应用程序中发现漏洞,在测试过程中都要考虑不同的开发背景。
一些 iOS 开发人员来自 C 或 C++背景,既然我们都倾向于使用自己熟悉的东西,你会发现他们的代码库通常使用 C/C++ API,而非 Cocoa 等价的 API。如果你知道被测试的应用程序是由前 C/C++程序员创建的,你可能会发现第十一章是一本有用的参考书,因为它讨论了在纯 C/C++代码中常见的问题。
对于一些新程序员来说,Objective-C 实际上是他们的第一门编程语言。他们通常没有学过那么多原生 C API,因此理想情况下,你会发现这类问题较少。此外,还有一些经验丰富的 NeXTStep 程序员,他们已经转到 OS X 或 iOS 开发,拥有一套关于 NeXTStep/Cocoa API 的智慧库,但在移动开发方面经验较少。如果你或你的客户是这种情况,你会发现第三部分的章节最有帮助。
具有 Java 背景的程序员可能会试图将 Java 设计模式强行应用到一个应用程序中,不断地抽象功能。另一方面,被征召来编写移动应用程序的 Web 开发者,可能会试图将尽可能多的代码包装到一个 Web 应用程序中,编写依赖 WebKit 查看应用内容的最小应用程序。你可以查看第九章,其中讲解了一些与 WebKit 相关的陷阱。
拥有我提到的最后几种技能的开发人员不太可能使用低级 API,而这有助于防止经典的 C 语言缺陷。然而,他们在使用这些低级 API 时往往不容易发现错误,因此,如果他们使用了这些 API,你需要特别留意。
当然,这些背景没有哪个一定比其他背景更适合安全开发——高层次和低层次的 API 都可能被滥用。但当你了解现有技能如何影响 iOS 应用的编写时,你就更接近发现并解决安全问题的第一步。
我的背景是渗透测试员,我觉得这和做艺术评论家很相似:我能写代码,但大部分时间都在查看别人写的代码,并告诉他们哪里有问题。就像在艺术界一样,大部分代码其实都挺糟糕的。然而,与艺术界不同的是,代码问题通常可以通过修补程序解决。我的希望是,在读完本书后,你能识别出糟糕的 iOS 代码,并知道如何开始修补漏洞。
第一部分
iOS 基础知识
第一章:1
iOS 安全模型
让我们对苹果的努力表示肯定:苹果在将恶意软件拒之门外方面相当成功(据我所知)。但是,应用审核流程对于开发者来说可能是一个令人沮丧的黑匣子。苹果审核员使用的流程没有公开文档,而且有时很难明确哪些功能是被允许的,哪些是不被允许的。苹果提供了一些不错的指南,^(1) 但有些应用也因一些适用于已通过审核应用的标准而被拒绝。
当然,什么算作恶意行为是由苹果定义的,而不是由用户定义的。苹果通过 App Store 来控制 iOS 平台上可用的功能,这意味着获取某些功能的唯一方式是越狱设备或绕过 App Store 审核流程。一个例子是 Handy Light 应用,它伪装成手电筒应用,但实际上包含了一个隐藏模式,用于启用设备的网络共享功能。^(2)
应用程序审核流程本身无法捕捉所有复杂(或微不足道的)恶意应用,因此需要其他机制来有效防止恶意应用影响更广泛的操作系统环境。在本章中,你将了解 iOS 安全机制的架构;在后续章节中,你将深入学习如何在自己的程序中正确利用这些机制。
让我们快速浏览一下 iOS 实现的基本安全组件,以防止漏洞和保护数据。虽然我将在后续章节中深入探讨这些组件的实际机制,但我首先会给出它们背后的动机及其用途的广泛概述。
安全启动
当你启动一台 iOS 设备时,它会从只读启动 ROM 中读取初始指令,从而引导系统启动。启动 ROM 还包含了苹果证书颁发机构的公钥,接着验证低级引导加载程序(LLB)是否由苹果签名,并启动它。LLB 执行一些基本任务,然后验证第二阶段引导加载程序 iBoot。当 iBoot 启动时,设备可以进入恢复模式或启动内核。当 iBoot 验证内核也由苹果签名后,启动过程正式开始:驱动加载,设备探测,系统守护进程启动。
这一信任链的目的是确保系统的所有组件都由苹果编写、签名并分发——而不是由第三方编写,这些第三方可能包括恶意攻击者和针对越狱设备运行的软件作者。该信任链也用于启动单个应用程序签名的检查;所有应用程序必须直接或间接由苹果签名。
攻击这个信任链是越狱工作原理的核心。越狱作者需要在这个链中的某个地方找到一个漏洞,以禁用对链中后续组件的验证。利用 Boot ROM 漏洞是最理想的,因为这是苹果无法通过软件更新改变的唯一组件。
通过应用沙盒限制访问
苹果的沙盒,历史上被称为 Seatbelt,是一种基于 FreeBSD TrustedBSD 框架的强制访问控制(MAC)机制,主要由 Robert Watson 推动。它使用类似 Lisp 的配置语言来描述程序可以或不能访问哪些资源,包括文件、操作系统服务、网络和内存资源等。
MAC 与传统的访问控制机制,如自主访问控制(DAC)不同,后者允许主体(如用户进程)操控对象(文件、套接字等)的访问控制。DAC 在其最简单、最常见的形式下,通过 UNIX 系统中的用户、组和其他权限进行控制,这些权限可以被授予读取、写入或执行权限^(3)。在 DAC 系统中,如果用户拥有某个对象的所有权,他们可以改变该对象的权限。例如,如果你拥有一个文件,你可以将其设置为世界可读或世界可写,显然这会破坏访问控制。
虽然 MAC 是一个广泛的术语,在沙盒环境中,它指的是应用程序被限制在一个虚拟容器中,容器内有详细的规则指定了主体(如应用程序)可以访问哪些系统资源,例如网络资源、文件读写、创建进程的能力等^(4)。在 OS X 上,你可以控制一些应用程序的沙盒设置,但在 iOS 上,所有第三方应用程序都在单一的限制性策略下运行。
在文件访问方面,进程通常被限制在自己的应用程序包目录中;它们只能读取和写入存储在该目录中的文件。然而,标准策略略显松散。例如,在某些版本的 iOS 中,位于/private/var/mobile/Media/Photos/中的照片可以被第三方应用程序直接访问,尽管它们位于应用程序包目录之外,这使得程序能够在不请求用户许可的情况下偷偷访问照片。对应用程序滥用此类权限的唯一保护措施是苹果的应用审核流程。
这种方法不同于 Android 的实现方式,后者采用更传统的 DAC 模型,其中应用程序被分配自己的用户 ID 和一个由该 ID 所拥有的目录。权限严格通过传统的 UNIX 文件权限进行管理。虽然这两种方法都能实现目标,但 MAC 通常提供更多的灵活性。例如,除了应用目录隔离,MAC 策略还可以用于限制网络访问或限制系统守护进程可以执行的操作。
数据保护和全盘加密
iOS 在提供带有文件系统加密的移动设备方面走在前列,这也是其值得称赞的地方。iOS 提供了全盘加密,并额外提供了数据保护 API,以进一步保护开发者的文件。这两种相关机制使得在设备被盗或遭到破坏时,可以远程擦除设备并保护用户数据。
从历史上看,全盘加密的目的是解决一个问题:静态数据被攻击者窃取。在笔记本或台式机领域,这通常涉及将硬盘从机器中取出并连接到另一台机器,或者启动一个可以读取硬盘文件的操作系统。文件系统加密并不能防止数据从正在运行的设备中被窃取。如果某个应用能够读取磁盘上的文件,那么文件系统加密就没有任何作用,因为内核会透明地为尝试读取文件的任何进程解密文件。换句话说,文件系统加密工作在比通常用来读取文件的调用更低的层次上。能够对系统进行身份验证的攻击者,可以毫不受阻地读取任何可用的文件。
iOS 设备通常设计为始终运行,并且其内部存储不易拆卸。如果攻击者想要在没有身份验证的情况下读取设备上的敏感数据,他们必须完全拆解设备,并将闪存存储连接到自定义接口上直接读取存储。实际上,还有几种更简单的方法可以从设备中获取数据——例如代码执行漏洞、越狱等——因此没有人会真的去做那种繁琐的事情。
但这并不意味着 iOS 的全盘文件系统加密完全无用。它对于正确实现另外两项关键的安全功能是必要的:安全文件删除和远程设备擦除。传统的安全擦除文件的方法不适用于使用固态硬盘(SSD)的 iOS 设备。这种硬件使用的减少磨损机制,取消了覆盖文件以确保实际上覆盖了文件之前物理位置的所有保证。解决这个问题的办法是确保文件用安全存储的密钥进行加密,以便在需要销毁数据时,可以丢弃这些密钥。iOS 中使用的加密密钥层次结构是分层的。通过丢弃一个加密密钥,整个数据类别甚至整个文件系统都可以被销毁。
加密密钥层次结构
iOS 上存储数据的文件系统加密密钥是分层的,其中一些密钥加密其他密钥,从而使苹果能够对数据的可用性进行精细控制。基本的层次结构如图 1-1 所示。

图 1-1:简化版 iOS 加密密钥层次结构
文件密钥 ➎是为每个文件生成的单独密钥,并存储在文件的元数据中。分类密钥 ➍是针对特定数据保护类别的专用密钥,因此具有不同保护级别的文件使用不同的加密密钥。在 iOS 的旧版本中,默认的保护类别是NSFileProtectionNone;从第 5 版开始,默认的保护类别是NSFileProtectionCompleteUntilFirstUserAuthentication,该类别在第十三章中有更详细的描述。文件系统密钥 ➌是一个全局加密密钥,用于加密文件的安全相关元数据,且该元数据在由分类密钥加密后再进行加密。
设备密钥 ➊,也称为 UID 密钥,对于每个设备都是唯一的,且仅能由硬件 AES 引擎访问,而不能由操作系统本身访问。它是系统的主密钥,用来加密文件系统密钥和分类密钥。如果启用,用户密码 ➋会与设备密钥结合,用于加密分类密钥。
当设置了密码时,这种密钥层级还允许开发者指定他们希望如何保护本地存储的数据,包括是否可以在设备锁定时访问数据,数据是否会备份到其他设备,等等。你将在第十三章中学到更多关于如何使用加密和文件保护功能来保护文件免受设备窃贼的侵害,我将在那里更深入地讲解数据保护 API。
钥匙串 API
对于小块的敏感信息,iOS 提供了一个专用的钥匙串 API。这允许开发者将密码、加密密钥和敏感的用户数据存储在一个安全的位置,其他应用无法访问。对钥匙串 API 的调用是通过securityd守护进程进行的,该进程从 SQLite 数据存储中提取数据。程序员可以指定在什么情况下密钥应该对应用可读,类似于数据保护 API。
数据保护 API
数据保护 API 利用文件系统加密、钥匙串以及用户的密码,提供一个额外的保护层,供开发者自行选择。这限制了系统进程在何种情况下可以读取这些文件。这个 API 最常用的场景是使数据在设备锁定时无法访问。
实际生效的数据保护程度很大程度上取决于设备运行的 iOS 版本,因为默认的数据保护类别随着时间的推移有所变化。在新创建的 iOS 应用项目中,默认情况下,所有应用数据都会启用数据保护,直到用户首次解锁设备。数据保护在项目设置中启用,如图 1-2 所示。

图 1-2:为项目添加数据保护权限
本地代码攻击缓解:ASLR、XN 及其他
iOS 实现了两种标准机制来帮助防止代码执行攻击:地址空间布局随机化(ASLR)和 XN 位(即 eXecute Never)。ASLR 在每次执行程序时都会随机化程序可执行文件、程序数据、堆和栈的内存位置;因为共享库需要保持固定位置以供多个进程共享,操作系统每次启动时而不是每次程序调用时随机化共享库的地址。这使得函数和库的具体内存地址难以预测,从而防止了依赖于知道基本 libc 函数内存地址的攻击,如返回到 libc 攻击。我将在第十一章中进一步讨论这些攻击及其工作原理。
XN 位,通常在非 ARM 平台上称为 NX(No-eXecute)位,允许操作系统将某些内存段标记为不可执行,这一操作由 CPU 强制执行。在 iOS 中,默认情况下,这个位被应用于程序的栈和堆。这意味着,如果攻击者能够将恶意代码插入栈或堆中,他们将无法将程序重定向执行他们的攻击代码。图 1-3 展示了进程内存的各个段及其 XN 状态。
只有在用苹果的代码签名权限签名的程序,才能拥有既可写又可执行的内存;这主要用于 Mobile Safari 中的 JavaScript 即时编译器(JIT)。你在自己程序中使用的普通 WebViews 无法访问相同的功能;这是为了防止代码执行攻击。苹果政策的一个不幸后果是,它实际上禁止了第三方 JIT,尤其是阻止了 Chrome 在 iOS 上的表现不如 Safari。Chrome 必须使用内建的 WebViews。

图 1-3:进程的基本内存段
越狱检测
从根本上来说,越狱是任何一种禁用 iOS 代码签名机制的过程,它允许设备运行非苹果直接批准的应用程序。越狱让你能够利用一些有用的开发和测试工具,以及那些永远不会通过 App Store 审核的实用程序。^(5) 越狱功能对于黑箱测试至关重要;我将在第六章进一步探讨黑箱测试。
与普遍的看法相反,越狱并不一定会禁用 iOS 的沙盒。它只是允许你安装沙盒外的应用程序。安装在 mobile 用户的主目录中的应用程序(即通过 App Store 安装的应用)仍然会受到沙盒限制。需要更高权限的第三方 iOS 应用会安装在 /Applications 文件夹中,与苹果的官方应用一起。
越狱检测的历史悠久且充满戏剧性。该过程旨在检测设备是否由于未经签名的第三方程序不太可信而面临更高的安全风险。公平来说,第三方应用库中确实不乏恶意软件和行为异常的程序,但总体来说,越狱检测并不值得花时间去做,因为它无法阻止一个决心坚定的攻击者。
在短短一段时间内,苹果曾推出官方的越狱检测 API,但很快就从后续的 iOS 版本中撤回了。没有了这个 API,开发者们自行实现了许多技巧来尝试检测越狱。尝试越狱检测的最常见技巧大致如下:
• 生成一个新进程,例如使用 fork()、vfork()、popen() 等。这是沙盒明确禁止的操作。当然,在越狱设备上,沙盒依然启用,这使得这种策略几乎没有意义。无论设备是否越狱,这都会导致任何应用商店的应用失败。
• 读取沙盒外的文件路径。开发者常常尝试访问 ssh、bash 的二进制文件、Cydia.app 目录、Cydia 使用的 apt 仓库路径等。这些检查非常容易绕过,工具如 Xcon^(6) 可以帮助最终用户自动绕过这些检查。
• 确保越狱检测逻辑的方法名显而易见,比如 isJailbroken,这样攻击者可以轻松识别并禁用你的越狱检查。
也有一些更隐蔽的技巧。例如,苹果的 iBooks 应用会尝试运行与应用捆绑包一起分发的未签名代码。^(7) 精明的开发者还会尝试使用 _dyld_image_count() 和 _dyld_get_image_name() 来检查已加载的动态库(dylibs)的总数及其名称,^(8) 并使用 _dyld_get_image_header() 检查它们在内存中的位置。^(9) 绕过这些检查通常需要直接修改应用的二进制文件。
如你所见,我通常不看好越狱检测。就像二进制混淆技术和 DRM 一样,越狱检测技术通常只是让你在被绕过时看起来很傻(相信我,我见过一些很傻的混淆技术)。支持者们常常辩称,执行表面上的越狱检测可以减缓盗版者或攻击者的速度。但你的对手的爱好就是破解应用,他们有几周的时间可以用来做这件事——将他们拖延几个小时并不值得。我用混淆二进制文件和一系列测试来绕过越狱检测,最长只花了一天时间——而且我还是这方面的业余爱好者。
应用商店评论有多有效?
在开发应用程序或评估应用面临的威胁时,重要的是要评估流氓应用程序可能最终出现在终端用户设备上的风险。任何恶意的第三方应用程序一旦进入设备,就能够通过 IPC 机制与其他应用程序进行交互,还能窃取个人信息。防御这些应用程序的主要手段是苹果的 App Store 审核过程。
苹果并未公开其测试应用程序是否符合 App Store 接受标准的方法,但显然进行了二进制分析和动态测试。这个过程已将大多数显而易见的恶意软件排除在 App Store 之外,代价是也阻止了苹果不喜欢的任何应用(包括许多类型的通讯应用、色情内容、任何娱乐类应用等等)。
尽管苹果做出了努力,但已经证明,一些中等复杂的攻击者可以通过 App Store 审核提交应用,并保持下载新代码的能力。攻击者有几种不同的方式可以实现这一点。
来自 WebKit 的桥接
有几种通过 JavaScript 访问本地 iOS API 的方法,例如获取用户位置或使用媒体服务,通常是通过基于 WebKit 的桥接来实现。PhoneGap 就是这样的一个典型例子。^(10) 尽管这些桥接可以提供有用的功能和灵活性,但使用它们也意味着大量的应用逻辑最终会在 JavaScript 中实现,而这些逻辑未必一开始就包含在应用程序内。例如,开发者可能实现一个通用的文件打开功能,通过 JavaScript 访问,并在审核过程中避免用它做任何恶意操作。但后来,该开发者可以修改提供给设备的 JavaScript,并尝试访问不应该被访问的设备区域中的数据。
我将在第九章中讨论 JavaScript/本地代码桥接的实现以及一些相关问题。
动态修补
通常,应用程序不能运行未经苹果签发密钥加密签名的本地代码。如果苹果的签名检查逻辑中存在漏洞或缺陷,可能会允许下载和执行本地代码。一个在实际中显著的例子是 Charlie Miller 利用一个漏洞,允许程序分配没有 NX 保护的内存区域(即可读、可写且可执行的内存区域),并且这些区域不需要代码签名。^(11) 这一机制是苹果为支持 Safari 的 JIT 编译器功能而设置的,^(12) 但实现中的一个漏洞使得第三方应用能够进行相同的操作。
这意味着原生代码可以在不需要任何签名的情况下下载并执行。Miller 通过提交一个名为InstaStock的应用程序到 App Store 来演示这一点,该应用声称是一个股票行情查看程序。在应用审查时,该应用并未表现出任何恶意或异常行为;然而,在审查过程完成后,Miller 能够指示程序下载新的未签名代码并顺利执行这些代码。这个问题现已解决,但它确实让你对审查漏洞有所了解。
故意存在漏洞的代码
绕过 App Store 审查的一个有趣方法是故意使你的应用程序容易受到远程攻击。Jekyll^(13)是乔治亚理工大学开发的一个概念验证应用程序,故意在核心应用中引入了缓冲区溢出。恶意代码被包含在应用程序本身中,这样代码会被签名,但应用程序从未调用它。通过审查后,研究人员能够利用缓冲区溢出漏洞改变应用程序的控制流,将恶意代码包含其中,使其能够使用私有的 Apple 框架与蓝牙、短信等进行交互。
嵌入式解释器
尽管苹果对这种做法的政策多年来有所变化,但许多产品(主要是游戏)使用嵌入式 Lua 解释器来执行大部分内部逻辑。目前,使用嵌入式解释器的恶意行为尚未在野外报告,但一个巧妙的应用程序如果使用类似的解释器,可能会动态下载代码并从内存中执行,当然,这种行为不会出现在审查过程中。这将增加新的恶意功能(或有益功能,如果你有这种倾向的话)。
总结思考
最终,应用审查提供了哪些保护呢?嗯,它确实能够筛选掉一些不太复杂的恶意软件。但你可以相当确定地假设,恶意应用程序确实会不时地从审查中漏过。记住这一点,编写防御性的代码;你绝对不能假设操作系统上的其他应用程序是无害的。
第二章:2
懒人版 Objective-C
Objective-C 在其辉煌的历程中既遭遇过嘲笑也获得过赞誉。它通过 NeXTStep 获得了流行,并且受 Smalltalk 设计的启发,Objective-C 是 C 的超集。它最显著的特点是使用中缀表示法和极其冗长的类名。人们往往要么爱它,要么恨它。那些恨它的人是错的。
在本章中,我将介绍 Objective-C 的基础知识,假设你已经熟悉某种语言的编程。然而,需要注意的是,Cocoa 和 Objective-C 在不断变化。我无法在一章中充分覆盖它们的所有细节,但我会提供一些提示,帮助非开发者在查看 Objective-C 代码时能够定位方向。如果你从很少的编程知识开始,可能希望先阅读一本像 Knaster、Malik 和 Dalrymple 合著的 Learn Objective-C on the Mac: For OS X and iOS(Apress, 2012)一书,然后再深入学习。
尽管我很想坚持使用最现代的 Objective-C 编码模式,但如果你在审核现有代码时,可能会遇到大量来自 iOS 初期的陈旧、重复使用的代码。所以为了以防万一,我会讲解一些历史上使用的 Objective-C 构造以及现在被认可的版本。
关键的 iOS 编程术语
有一些术语你需要熟悉,以便理解 Apple 各种 API 的来源。Cocoa 是指在 Objective-C GUI 编程中使用的框架和 API 的总称。Cocoa Touch 是 Cocoa 的超集,包含一些与移动相关的 API,如处理手势和移动 GUI 元素。Foundation 类是构成我们所说的 Cocoa API 的大量 Objective-C 类。Core Foundation 是一个更底层的基于 C 的库,许多 Foundation 类都是基于它的,通常以 CF 而不是 NS 为前缀。
传递消息
理解 Objective-C 的第一个关键是明白该语言围绕 消息传递 的概念设计,而不是 调用。对我来说,思考 Objective-C 为一个对象在拥挤的房间里彼此大声喊叫的语言,而不是一个层级导演对下属发号施令的语言,这样的比喻很有用,尤其是在代理(delegates)的上下文中,这个比喻更为贴切,稍后我会详细讲解。
基本上,发送 Objective-C 消息的样子是这样的:
[Object doThisThingWithValue:myValue];
这就像是说:“嘿,*Object*!请用 *myValue* 的值做这件事。”当传递多个参数时,第一个参数的性质通常由消息名来表示。任何后续的参数都必须是类的一部分,并且在调用时必须明确命名,就像这个例子:
if (pantsColor == @"Black") {
[NSHouseCat sleepOnPerson:person
withRegion:[person lap]
andShedding:YES
retries:INT_MAX];
}
在这个简化的模拟程序中,sleepOnPerson 指定了一个睡觉的地方(person),而 withRegion 通过向 person 发送消息来指定这个人的“膝盖”区域。andShedding 参数接受一个布尔值,retries 则指定此操作将尝试的次数——在本例中,最多可以达到平台上整数的最大值,这个值取决于你是否有一只 64 位猫。
如果你已经编写 Objective-C 一段时间,可能会注意到这个代码的格式看起来与你平时使用的有所不同。这是因为这是一种古老的 Objective-C 代码格式化方法,称为“正确方式”,它通过在参数名称和值之间使用垂直对齐的冒号,使得参数名称和值的配对在视觉上更为明显。
剖析一个 Objective-C 程序
一个 Objective-C 程序的两个主要部分是 接口 和 实现,分别存储在 .h 和 .m 文件中。(这些大致上与 C++ 中的 .h 和 .cpp 文件相类似。)前者定义所有的类和方法,而后者定义程序的实际内容和逻辑。
声明一个接口
接口包含三个主要组件:实例变量(或 ivars)、类方法和实例方法。示例 2-1 是经典的(即被弃用的)Objective-C 1.0 声明接口的方式。
@interface Classname : NSParentClass {
➊ NSSomeType aThing;
int anotherThing;
}
➋ + (type)classMethod:(vartype)myVariable;
➌ - (type)instanceMethod:(vartype)myVariable;
@end
示例 2-1:声明一个接口,古老版本
在主 @interface 块内的 ➊,实例变量是用类(如 NSSomeType)或类型(如 int)声明的,后面跟着它们的名称。在 Objective-C 中,+ 表示声明一个类方法 ➋,而 - 表示实例方法 ➌。与 C 语言类似,方法的返回类型在定义的开始部分用括号指定。
当然,在 Objective-C 中声明接口的现代方式稍有不同。示例 2-2 显示了一个示例。
➊ @interface Kitty : NSObject {
@private NSString *name;
@private NSURL *homepage;
@public NSString *color;
}
@property NSString *name;
@property NSURL *homepage;
➋ @property(readonly) NSString *color;
+ (type)classMethod:(vartype)myVariable;
- (type)instanceMethod:(vartype)myVariable;
示例 2-2:声明一个接口,现代版本
这个新类名为 Kitty,继承自 NSObject ➊。Kitty 有三个不同访问级别的实例变量,并声明了三个属性来匹配这些实例变量。注意,color 被声明为 readonly ➋;这是因为一个 Kitty 对象的颜色不应该发生变化。这意味着当属性被合成时,只会创建一个 getter 方法,而不是同时创建 getter 和 setter 方法。Kitty 还有一对方法:一个类方法和一个实例方法。
你可能已经注意到,示例接口声明在声明实例变量时使用了@private和@public关键字。与其他语言类似,这些关键字定义了实例变量是否只能在声明它的类内部访问(@private),是否可以在声明类及其任何子类中访问(@protected),或者是否可以被任何类访问(@public)。实例变量的默认行为是@protected。
注意
语言的新手通常想知道是否有类似于私有方法的概念。严格来说,Objective-C 中并没有私有方法的概念。然而,你可以通过仅在*@implementation*块中声明方法来实现其功能等效,而不是在*@interface*和*@implementation*中都声明它们。
在实现文件中
就像.c或.cpp文件一样,Objective-C 实现文件包含了 Objective-C 应用程序的核心内容。根据约定,Objective-C 文件使用.m文件,而 Objective-C++文件(混合了 C++和 Objective-C 代码)存储在.mm文件中。列表 2-3 解析了列表 2-2 中Kitty接口的实现文件。
@implementation Kitty
➊ @synthesize name;
@synthesize color;
@synthesize homepage;
+ (type)classMethod:(vartype)myVariable {
// method logic
}
- (type)instanceMethod:(vartype)myVariable {
// method logic
}
@end
Kitty *myKitty = [[Kitty alloc] init];
➋ [myKitty setName:@"Ken"];
➌ myKitty.homepage = [[NSURL alloc] initWithString:@"http://me.ow"];
列表 2-3:一个示例实现
➊处的@synthesize语句创建了属性的 setter 和 getter 方法。稍后,这些 getter 和 setter 方法可以使用 Objective-C 的传统中缀符号表示法➋,其中*propertyName*和*setPropertyName*格式的方法(例如name和setName,分别用于获取和设置值),也可以使用点符号表示法➌,在这种方式下,像homepage这样的属性使用*.property*格式来设置或读取,正如在其他语言中可能出现的那样。
注意
小心使用点符号,或者干脆不要使用它。点符号使得你很难知道你是在处理一个对象还是 C 结构体,实际上你可以用它调用任何方法——不仅仅是 getter 和 setter 方法。点符号在视觉上也不一致。长话短说,在本书中,我将避免使用点符号,以保持一致性和思想上的纯洁性。但尽管我尽力避免,你在现实世界中可能仍然会遇到它。
从技术上讲,对于在接口文件中声明的使用@property的属性(如列表 2-3 中的name、color和homepage),你不需要合成这些属性;Xcode 的较新版本会自动合成这些属性。但是,为了清晰起见或当你想改变实例变量的名称以便与属性名称区分时,你仍然可能希望手动声明它们。手动合成属性的工作原理如下:
@synthesize name = thisCatName;
在这里,属性name是由实例变量thisCatName支持的,因为它是手动合成的。然而,自动属性合成的默认行为类似于这样:
@synthesize name = _name;
这种默认行为可以防止开发人员直接操作实例变量,而不是使用设置器和获取器,这样可能会引起混淆。例如,如果你直接设置一个 ivar,你将绕过设置器/获取器方法中的任何逻辑。自动合成可能是最好的方式,但你在代码中仍然会看到手动合成很长一段时间,因此最好对此有所了解。
使用代码块指定回调
在 Objective-C 代码中,越来越流行的做法是使用代码块,它通常用于 Cocoa 中作为指定回调的一种方式。例如,下面是如何使用NSURLSessionDataTask类的dataTaskWithRequest方法:
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
➊ ^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"Error: %@ %@", error, [error userInfo]);
}];
➊处的^声明了一个代码块,该代码块将在请求完成后执行。注意,未指定此函数的名称,因为它不会从代码中的任何其他地方被调用。一个代码块的声明只需要指定闭包将接受的参数。从那里开始,代码块的其他部分就像普通函数一样。你可以将代码块用于许多其他用途,但首先,了解它们的基本概念应该足够了:以^开头并执行某些操作的东西。
Objective-C 如何管理内存
与其他一些语言不同,Objective-C 没有垃圾回收机制。历史上,Objective-C 使用了引用计数模型,通过retain和release指令来指示何时需要释放对象,从而避免内存泄漏。当你retain一个对象时,你增加了引用计数——也就是希望该对象对其可用的事物的数量。当一段代码不再需要该对象时,它会发送一个release方法。当引用计数达到零时,对象将被释放,如下所示:
➊ NSFish *fish = [[NSFish alloc] init];
NSString *fishName = [fish name];
➋ [fish release];
假设在这段代码运行之前,引用计数为 0。➊之后,引用计数为 1。在➋处,调用了release方法,表示fish对象不再需要(应用程序只需要fish对象的name属性),当fish被释放时,引用计数应该再次为 0。
[[Classname alloc] init]也可以缩写为[Classname new],但new方法在 Objective-C 社区中不太受欢迎,因为它不够明确,并且与除了init之外的其他对象创建方法不一致。例如,你可以用[[NSString alloc] initWithString:@"My string"]来初始化NSString对象,但没有类似的new语法,因此你的代码中会混用这两种方法。并非每个人都反感new,这确实是一个个人喜好的问题,因此你可能会看到这两种写法。但在本书中,我更倾向于使用传统方法。
无论你偏好哪种分配语法,手动 retain/release 的问题在于它可能引发错误:程序员可能会不小心释放已被销毁的对象(导致崩溃)或忘记释放对象(导致内存泄漏)。苹果尝试通过自动引用计数来简化这种情况。
自动引用计数
自动引用计数(ARC) 是现代的 Objective-C 内存管理方法。它通过在适当的时候自动递增和递减引用计数,消除了手动跟踪引用计数的需求。^(1) 本质上,它为你插入了 retain 和 release 方法。ARC 引入了一些新的概念,列举如下:
• 弱引用 和 强引用 有助于防止循环引用(即 强引用循环),在这种情况下,父对象和子对象相互拥有对方,导致它们永远不会被销毁。
• Core Foundation 对象和 Cocoa 对象之间的所有权可以进行桥接。桥接告诉编译器,将 Core Foundation 对象转换为 Cocoa 对象后,应该由 ARC 管理,方法是使用 __bridge 系列关键字。
• @autoreleasepool 替代了之前使用的 NSAutoReleasePool 机制。
在现代使用 ARC 的 Cocoa 应用程序中,内存管理的细节通常不会在安全上下文中发挥作用。以前可被利用的条件,如双重释放,已不再是问题,内存管理相关的崩溃也变得非常少见。但仍然值得注意的是,仍然有其他方式可能引发内存管理问题,因为 Core Foundation 对象仍然存在 CFRetain 和 CFRelease,并且 C 语言的 malloc 和 free 仍然可以使用。我将在第十一章中讨论使用这些低级 API 时可能出现的内存管理问题。
委托与协议
还记得对象如何在“拥挤的房间里互相喊叫”以传递消息吗?委托 是一个能够特别好地展示 Objective-C 消息传递架构的特性。委托对象可以接收在程序执行过程中发送的消息,并通过响应指令来影响程序的行为。
成为代理对象,必须实现 代理协议 中定义的部分或全部方法,这是一种委托者和代理对象之间约定的通信方式。你可以声明自己的协议,但最常用的还是使用核心 API 中的已定义协议。
你编写的委托通常会响应三种基本消息类型之一:should、will 和 did。每当事件即将发生时,调用这些消息,然后让你的委托对象指导程序采取正确的行动。
Should 消息
对象发送 should 消息来请求任何可用委托提供关于是否允许事件发生的意见。这可以看作是最终的反对意见征集。例如,当 shouldSaveApplicationState 消息被触发时,如果你已经实现了一个委托来处理此消息,委托可以执行一些逻辑并说类似这样的话:“不,实际上我们不应该保存应用状态,因为用户选中了一个复选框表示不保存。”这些消息通常期望一个布尔值作为响应。
Will 消息
will 消息给你一个在事件发生之前执行某些操作的机会——有时,甚至可以在事件发生之前踩刹车。这种消息类型更像是说:“嘿,伙计们!只是提醒一下,我将要做这件事情,除非你们需要先做些其他的事情。我对这个想法已经比较坚定,但如果这是个完全不可接受的条件,告诉我,我可以停下。”一个例子是 applicationWillTerminate 消息。
Did 消息
did 消息表示某件事情已经确定决定并且一个事件无论你是否喜欢都将发生。它还表明,如果有任何委托想要执行某些操作,他们应该直接进行。例如 applicationDidEnterBackground。在这种情况下,did 并不是真正表示应用程序已经进入后台,而是反映了决定已经被最终做出。
声明并遵循协议
要声明你的类遵循某个协议,在 @interface 声明中指定该协议,并将其放在尖括号中。要查看实际应用,查看列表 2-4,它展示了一个使用 NSCoding 协议的 @interface 声明示例。这个协议简单地指定了一个类实现两个用于编码或解码数据的方法:encodeWithCoder 用于编码数据,initWithCoder 用于解码数据。
➊ @interface Kitty : NSObject <NSCoding> {
@private NSString *name;
@private NSURL *homepage;
@public NSString *color;
}
@implementation Kitty
➋ - (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (!self) {
return nil;
}
[self setName:[decoder decodeObjectForKey:@"name"]];
[self setHomepage:[decoder decodeObjectForKey:@"homepage"]];
[self setColor:[decoder decodeObjectForKey:@"color"]];
return self;
}
➌ - (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[self name] forKey:@"name"];
[encoder encodeObject:[self author] forKey:@"homepage"];
[encoder encodeObject:[self pageCount] forKey:@"color"];
}
列表 2-4:声明并实现对 NSCoding 协议的遵循
➊ 处的声明指定了 Kitty 类将符合 NSCoding 协议。^(2) 但是,当一个类声明了一个协议时,它也必须遵循该协议,这就是为什么 Kitty 实现了所需的 initWithCoder ➋ 和 encodeWithCoder ➌ 方法。这些特定的方法用于序列化和反序列化对象。
如果内建的消息协议没有满足你的需求,那么你也可以定义自己的协议。查看 Apple 框架头文件中 NSCoding 协议的声明(列表 2-5),看看协议定义是什么样的。
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;
@end
列表 2-5:NSCoding 协议的声明,来自 Frameworks/NSCoding.h
注意,NSCoding 的定义包含了两个方法,任何符合该协议的类必须实现这两个方法:encodeWithCoder 和 initWithCoder。当你定义一个协议时,必须自己指定这些方法。
类别的危险
Objective-C 的分类机制允许你在运行时为现有类实现新的方法,而无需重新编译这些类。分类可以向受影响的类添加或替换方法,并且可以出现在代码库的任何位置。这是一种无需重新实现类就能快速更改类行为的简便方法。
不幸的是,使用分类也是导致严重安全错误的一个简单途径。因为它们可以在代码库的任何地方影响你的类——即使它们仅出现在第三方代码中——关键功能,如 TLS 端点验证,可能会被一个随机的第三方库或一个粗心的开发者完全覆盖。我曾在重要的 iOS 产品中看到过这种情况:开发者在仔细验证 TLS/SSL 在他们的应用中正确工作后,添加了一个覆盖该行为的第三方库,搞砸了他们自己精心设计的代码。
你通常可以通过注意到@implementation指令来识别分类,这些指令声称实现了 Cocoa Touch 中已经存在的类。如果开发者确实在这里创建了一个分类,那么分类的名称会在@implementation指令后面用括号标出(参见列表 2-6)。
@implementation NSURL (CategoryName)
- (BOOL) isPurple; {
if ([self isColor:@"purple"])
return YES;
else
return NO;
}
@end
列表 2-6:实现分类方法
你还可以使用分类来覆盖现有的类方法,这是一种潜在有用但特别危险的方法。这可能导致安全机制被禁用(比如前述的 TLS 验证),也可能导致不可预测的行为。苹果曾说:
如果分类中声明的方法名称与原始类中的方法名称相同,或者与同一类中的另一个分类中的方法名称相同(甚至是父类中的方法),则在运行时无法确定使用哪个方法的实现。
换句话说,多个分类可以定义或覆盖相同的方法,但只有一个会“胜出”并被调用。请注意,一些框架方法可能本身就是通过分类实现的——如果你试图覆盖它们,你的分类可能会被调用,但也有可能不会。
分类还可能意外地覆盖子类的功能,即使你只打算添加一个新方法。例如,如果你在NSObject上定义了一个isPurple方法,那么NSObject的所有子类(也就是说,所有 Cocoa 对象)都会继承这个方法。任何其他定义了相同方法名的类,可能会或可能不会被覆盖。因此,没错,分类非常方便,但要谨慎使用;它们可能会导致严重的混乱以及安全副作用。
方法交换
方法交换 是一种机制,你可以使用它来替换你不拥有的类或实例方法的实现(也就是 Cocoa API 自身提供的方法)。方法交换在功能上类似于类别或子类化,但它通过实际交换方法的实现与一个全新的实现,而不是扩展它,提供了一些额外的能力和灵活性。开发者通常使用这种技术来增强许多不同子类共享使用的方法的功能,这样他们就不必重复代码。
清单 2-7 中的代码使用方法交换(method swizzling)将日志语句添加到任何对 setHidden 方法的调用。这将影响任何 UIView 的子类,包括 UITextView、UITextField 等。
#import <objc/runtime.h>
@implementation UIView(Loghiding)
➊ - (BOOL)swizzled_setHidden {
NSLog(@"We're calling setHidden now!");
➋ BOOL result = [self swizzled_setHidden];
return result;
}
➌ + (void)load {
Method original_setHidden;
Method swizzled_setHidden;
original_setHidden = class_getInstanceMethod(self, @selector(setHidden));
swizzled_setHidden = class_getInstanceMethod(self, @selector(swizzled_
setHidden));
➍ method_exchangeImplementations(original_setHidden, swizzled_setHidden);
}
@end
清单 2-7:交换现有方法的实现和替代方法的实现
在➊处,定义了一个包装方法,该方法只是输出一个 SLog,表明 setHidden 方法正在被调用。但在➋处,swizzle_SetHidden 方法似乎在调用自身。这是因为,在执行任何附加功能后,最好调用原始方法,以防止出现不可预测的行为,比如未能返回调用者期望的值类型。当你在 swizzled_setHidden 内部调用自己时,实际上会调用原始方法,因为原始方法和替换方法已经被交换。
实际的交换发生在 load 类方法 ➌ 中,当 Objective-C 运行时第一次加载该类时会调用此方法。在获取原始方法和交换方法的引用后,在 ➍ 处调用 method_exchangeImplementations 方法,顾名思义,它交换原始实现和交换实现。
实现方法交换有几种不同的策略,但大多数方法都有一定的风险,因为你在更改核心功能。
如果你或你的亲人想实现方法交换,可能需要考虑使用一个经过充分测试的包装包,如 JRSwizzle.^(3) Apple 可能会拒绝看起来以危险方式使用方法交换的应用。
结束语
总体而言,Objective-C 和 Cocoa API 是相当高级的,避免了许多 C 语言中的经典安全问题。尽管仍然存在一些破坏内存管理和对象操作的方法,但在现代代码中,这些方法大多数情况下会导致服务拒绝(Denial of Service,DoS)。如果你是开发者,尽可能依赖 Cocoa,而不是修补 C 或 C++ 代码。
然而,Objective-C 确实包含一些机制,如类别或方法交换,这些机制可能导致意外行为,并且可能广泛影响你的代码库。当你在应用程序评估中看到这些技术时,一定要仔细调查它们,因为它们可能会导致一些严重的安全问题。
第三章:3
iOS 应用程序解剖
为了了解 iOS 应用程序面临的一些问题,了解不同类型的数据是如何存储和操作的非常有用,这些数据存储在应用程序的私人目录中,该目录存储着所有的配置、资源、二进制文件和文档。在这里,你可以发现各种信息泄露问题,并深入探究你正在检查的程序的内部结构。
找出你的应用程序在 iOS 设备上本地存储了哪些数据的最快方式是查看 ~Library/Developer/CoreSimulator/Devices。从 Xcode 6 开始,每个设备类型和操作系统版本的组合都会被分配一个 UUID。你的特定应用程序的数据将存储在该目录下的两个位置。你的应用程序的二进制文件和资源,包括 .nib 用户界面文件和随应用程序附带的图形文件,存储在 <设备 ID>/data/Containers/Bundle/Application/<应用包 ID> 中。你的应用程序存储的更多动态数据则存储在 ~<设备 ID>/data/Containers/Data/Application/<应用包 ID> 中。系统范围的数据,如全局配置,将存储在 <设备 ID> 目录的其余部分。
探索这个目录结构,简化版本见图 3-1,还可以揭示哪些类型的数据由操作系统服务而非你的应用程序处理。

图 3-1:应用程序目录的布局
如果你使用的是越狱设备,你可以通过 SSH 连接到设备并探索目录结构;我将在第六章中讨论越狱以及如何连接测试设备。无论你的设备是否越狱,你都可以使用像 iExplorer 这样的工具^(1),来检查你安装的应用程序的目录结构,如图 3-2 所示。
在本章的其余部分,我将介绍一些 iOS 应用程序常用的目录和数据存储,以及如何通过编程方式与它们交互以及可能从中泄露的哪些数据。

图 3-2:使用 iExplorer 检查应用程序包
处理 plist 文件
在开始检查目录结构之前,你需要了解如何读取你将会发现的一些内容。iOS 将应用程序配置数据存储在 属性列表(plist) 文件中,这些文件使用 Core Foundation 数据类型(如 CFArray、CFString 等)存储信息。从安全角度来看,你需要检查 plist 文件,查看其中是否包含不应以明文存储的内容,比如凭证,并可能对其进行修改,从而改变应用程序的行为。例如,你可以启用一个被禁用的付费功能。
plist 格式有两种类型:二进制和 XML。如以下示例所示,XML 格式易于人类读取。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Test</string>
<key>CFBundleIdentifier</key>
<string>com.dthiel.Test</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Test</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneSimulator</string>
</array>
--snip--
这只是一个包含层级键值对的字典,提供了有关应用的信息——它可以运行的平台、代码签名等等(此处没有签名,因为该应用部署在模拟器应用中)。
但是在从命令行检查文件或编程操作 plist 时,你经常会遇到二进制格式的 plist,它并不是特别适合人类读取(或写入)。你可以使用plutil(1)命令将这些 plist 转换为 XML 格式。
$ plutil -convert xml1 Info.plist -o -
$ plutil -convert xml1 Info.plist -o Info-xml.plist
$ plutil -convert binary1 Info-xml.plist -o Info-bin.plist
第一个命令将二进制的 plist 转换为 XML 格式并输出到标准输出(stdout),你可以将其传递给less(1)或类似的命令。你也可以使用-o *filename*直接输出到文件,就像第二个命令中那样。在第三个命令中,binary1转换类型将 XML 格式的 plist 转换为二进制;但由于格式是可互换的,你实际上不需要这么做。
为了让读取和编辑 plist 文件更无缝,你还可以配置你的文本编辑器,自动将 plist 文件转换格式,这样如果你需要读取或写入文件时,可以在一个熟悉的环境中顺畅地进行。例如,如果你使用 Vim,你可以在你的.vimrc文件中添加如下配置:
" Some quick bindings to edit binary plists
command -bar PlistXML :set binary | :1,$!plutil -convert xml1 /dev/stdin -o -
command -bar Plistbin :1,$!plutil -convert binary1 /dev/stdin -o -
fun ReadPlist()
if getline("'") =~ "^bplist"
:PlistXML
set filetype=xml
endif
endfunction
augroup misc
au BufWinEnter *.plist, call ReadPlist()
augroup end
该配置将使用:PlistXML命令,自动将你编辑的任何二进制 plist 转换为 XML 格式,这样你就可以以人类可读的格式进行更改。在实际写入这些更改之前,配置会再次使用:Plistbin命令将文件转换回二进制格式。请注意,无论文件是二进制格式还是 XML 格式,应用程序都能成功读取它。
你可以在 Xcode 中查看任意格式的 plist,如[图 3-3 所示。使用 Xcode 的优点是你将得到一些额外的帮助和下拉菜单,显示你可能用于各种键的潜在值。然而,了解如何在命令行中处理 plist 文件也很重要,因为这样你就能通过 SSH 会话直接与越狱设备上的 plist 文件交互。

图 3-3:在 Xcode 中查看 plist
请参阅手册页plist(5)和plutil(1),以获取有关查看和编辑 plist 文件的更多信息。如果你在越狱设备上工作,可以使用 Erica Sadun 的 Erica Utilities^(2)(可通过 Cydia 获取)中的plutil命令,直接在本地操作 plist 文件。
设备目录
从 iOS 8 开始,模拟器平台(如 iPhone、iPad 及其变种)存储在以唯一标识符命名的目录中。这些标识符与启动模拟器时在 Xcode 中选择的设备类型相对应,并结合请求的操作系统版本。每个目录中都有一个 plist 文件,描述了该设备。以下是一个示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UDID</key>
<string>DF15DA82-1B06-422F-860D-84DCB6165D3C</string>
<key>deviceType</key>
<string>com.apple.CoreSimulator.SimDeviceType.iPad-2</string>
<key>name</key>
<string>iPad 2</string>
<key>runtime</key>
<string>com.apple.CoreSimulator.SimRuntime.iOS-8-0</string>
<key>state</key>
<integer>3</integer>
</dict>
</plist>
在这个 plist 文件中,哪一个目录对应哪个设备并不立即显现。为了弄清楚这一点,你可以查看Devices目录中的.default_created.plist文件,或者你也可以通过 grep 搜索所有的device.plist文件,如示例 3-1 所示。
$ cd /Users/me/Library/Developer/CoreSimulator/Devices && ls
26E45178-F483-4CDD-A619-9C0780293DD4
78CAAF2B-4C54-4519-A888-0DB84A883723
A2CD467D-E110-4E38-A4D9-5C082618604A
AD45A031-2412-4E83-9613-8944F8BFCE42
676931A8-FDA5-4BDC-85CC-FB9E1B5368B6
989328FA-57FA-430C-A71E-BE0ACF278786
AA9B1492-ADFE-4375-98F1-7DB53FF1EC44
DF15DA82-1B06-422F-860D-84DCB6165D3C
$ for dir in `ls|grep -v default`
do
echo $dir
grep -C1 name $dir/device.plist |tail -1|sed -e 's/<\/*string>//g'
done
26E45178-F483-4CDD-A619-9C0780293DD4
iPhone 5s
676931A8-FDA5-4BDC-85CC-FB9E1B5368B6
iPhone 5
78CAAF2B-4C54-4519-A888-0DB84A883723
iPad Air
989328FA-57FA-430C-A71E-BE0ACF278786
iPhone 4s
A2CD467D-E110-4E38-A4D9-5C082618604A
iPad Retina
AA9B1492-ADFE-4375-98F1-7DB53FF1EC44
Resizable iPad
AD45A031-2412-4E83-9613-8944F8BFCE42
Resizable iPhone
DF15DA82-1B06-422F-860D-84DCB6165D3C
iPad 2
示例 3-1:通过 grep 命令确定标识符与 iOS 设备模型的对应关系
进入你在其上测试应用程序的设备相应目录后,你会看到一个名为data的目录,其中包含所有模拟器文件,包括与应用程序相关的文件。你的应用程序数据被分为三个主要目录,位于data/Containers下:Bundle、Data和Shared。
Bundle 目录
Bundle目录包含一个Applications目录,后者包含每个存储在设备上的应用程序的目录,这些目录由应用程序的 bundle ID 表示。在每个应用程序的目录中,.app文件夹存储着应用程序的核心二进制文件,以及图像资源、本地化信息和包含应用程序核心配置信息的Info.plist文件。Info.plist包含 bundle 标识符和主可执行文件,以及有关应用程序 UI 和应用程序运行所需设备功能的信息。
在文件系统中,这些 plist 文件以 XML 或二进制格式存储,后者为默认格式。你可以通过引用[NSBundle mainBundle]的字典属性以编程方式获取Info.plist中的信息;^(3)这通常用于加载样式或本地化信息。
在Info.plist文件中,可能会引起你关注的一个条目是UIRequiredDeviceCapabilities,它的内容大致如下:
<key>UIRequiredDeviceCapabilities</key>
<dict>
<key>armv7</key>
<true/>
<key>location-services</key>
<true/>
<key>sms</key>
<true/>
</dict>
UIRequiredDeviceCapabilities条目描述了一个应用程序所需的系统资源。虽然这不是一个强制性机制,但它可以给你一些线索,帮助你了解应用程序将进行何种活动。
数据目录
Data目录中最主要的关注点是Applications子目录。Data/Applications目录包含应用程序运行所需的其他数据:首选项、缓存、Cookies 等。这也是你检查大多数数据泄露类型的主要位置。现在,让我们来回顾一下各个子目录以及它们可能包含的数据类型。^(4)
文档和收件箱目录
Documents目录用于存储你的非临时应用程序数据,例如用户创建的内容或允许应用在离线模式下运行的本地信息。如果在应用程序的Info.plist文件中设置了UIFileSharingEnabled,这里的文件将可以通过 iTunes 访问。
其他应用程序要求您的应用程序打开的数据文件存储在您的应用程序的Documents/Inbox目录中。这些文件将通过调用应用程序使用UIDocumentInteractionController类来调用。^(5)
您只能读取或删除存储在收件箱目录中的文件。这些文件来自无法写入您的应用目录的其他应用程序,因此它们由具有更高权限的系统进程放置在此处。您可能需要定期删除这些文件,或者让用户选择删除它们,因为用户无法明确知道此处存储了哪些文档,以及它们是否包含敏感信息。
如果您正在编写一个应用程序,目标是确保敏感信息不会长时间保留在磁盘上,请将文档从收件箱目录复制到可以应用数据保护的单独位置,然后将这些文件从收件箱目录中删除。
还值得记住,在某些情况下,您的应用程序请求打开的任何文件可能会在磁盘上永远存在。如果您尝试打开一个您的程序没有处理的文件类型,那么该文件将被传递给第三方应用程序,而谁知道其他应用程序什么时候会删除它呢?它可能会被无限期地存储。换句话说,您请求其他应用程序打开的任何文件的清理工作超出了您的控制范围,即使您仅仅使用 Quick Look API 预览文件内容。如果让收件箱文件长时间存在是一个问题,考虑让您的应用程序自己查看这些数据(而不是依赖辅助程序),然后确保妥善处理这些文件。
库目录
库目录包含您应用程序的大多数文件,包括由应用程序或特定网络构造缓存的数据。除了缓存目录外,这些文件会通过 iTunes 和 iCloud 进行备份。
应用支持目录
应用支持目录不是用来存储用户创建或接收的文件,而是用来存储将由您的应用程序使用的附加数据文件。例如,可能包括额外购买的可下载内容、配置文件、高分记录等——正如名称所示,这些是支持应用程序运行和操作的内容。这些文件可以在应用程序首次安装时部署,或者可以由您的应用程序稍后下载或创建。
默认情况下,iTunes 会将该目录中的数据备份到您的计算机和 iCloud 中。但是,如果您对这些数据存储在 Apple 云环境中存在隐私或安全担忧,您可以通过为新创建的文件设置NSURLIsExcludedFromBackupKey属性来明确禁止此操作。我将在第十章进一步讨论如何防止数据同步到 iCloud。
请注意,苹果要求应用程序仅将用户数据(包括他们创建的文档、配置文件等)备份到 iCloud,而不是应用程序数据。允许应用程序内容(例如可下载的应用内容)备份到 iCloud 的应用程序可能会被 App Store 拒绝。
缓存和快照目录
Caches 目录的功能类似于网页浏览器的缓存:它用于存储为了性能原因而保留的数据,但不用于存储对应用程序功能至关重要的数据。因此,这个目录不会被 iTunes 备份。
虽然苹果声明你的应用程序负责管理 Caches 目录,但操作系统实际上会操作该目录及其子文件夹 Snapshots 的内容。始终将 Caches 目录的内容视为临时的,并且预计它会在程序启动之间消失。如果系统开始空间不足,iOS 会自动清理这些缓存目录,但在应用程序运行时不会进行此操作。
Caches 目录有时也会在一个子目录中存储网页缓存内容,例如 Caches/com.mycompany.myapp。这是一个敏感数据泄露的地方,因为 iOS 可以将通过 HTTPS 传输的信息缓存很长时间。如果开发者没有特别努力去防止数据被缓存或迅速过期,你通常可以在这里找到一些“好东西”。
最后,当一个应用被放到后台时,操作系统还会自动将该应用的截图存储在 Snapshots 子目录中,这可能会在本地存储中留下敏感信息。这样做的原因是:操作系统需要当前屏幕状态来创建当你将应用带到前台时的“嗖”动画。不幸的是,我经常看到一个副作用:iOS 应用的磁盘上存储了人们的社会安全号码、用户详情等信息。我将在第十章讨论如何缓解这个问题(以及许多其他缓存问题)。
Cookies 目录
Cookies 目录存储由 URL 加载系统设置的 cookies。当你发出一个 NSURLRequest 请求时,任何 cookies 都会根据默认的系统 cookie 策略或你指定的策略进行设置。与 OS X 不同,iOS 上的 cookies 不会在应用之间共享;每个应用都会在这个目录中拥有自己的 cookie 存储。
偏好设置目录
iOS 将应用程序的偏好设置存储在 Preferences 目录中,但不允许应用程序直接写入该目录中的文件。相反,目录中的文件是通过 NSUserDefaults 或 CFPreferences API 创建、读取和操作的。
这些 API 以明文形式存储应用程序的首选项文件;因此,你绝对不应该使用它们来存储敏感的用户信息或凭证。当检查应用程序以查看它存储了哪些信息时,务必检查 Preferences 目录中的 plist 文件。你有时会发现用户名和密码、API 访问密钥或不应由用户更改的安全控制。
已保存的应用程序状态目录
用户期望应用程序能够记住他们在文本字段中输入的内容、启用了哪些设置等。如果用户切换到另一个应用程序,然后稍后恢复原来的应用程序,应用程序可能在这段时间内被操作系统终止。为了使得在程序启动之间 UI 一致,iOS 的最新版本通过状态保存 API 将对象状态信息存储在 Saved Application State 目录中。^(6) 开发者可以标记 UI 中的特定部分,以便在状态保存中包含它们。
如果你不小心存储了应用程序状态的一部分,这里可能会导致数据泄漏。我将在第十章中详细讨论如何避免这种情况。
tmp 目录
如你所料,tmp 目录用于存储临时文件。与 Caches 目录类似,存储在该目录中的文件可能会在应用程序未运行时被操作系统自动删除。这个目录的使用方式与 Caches 目录相似;不同之处在于,Caches 目录用于存放可能需要再次检索或重新创建的文件。例如,如果你从远程服务器下载某些应用数据并希望出于性能原因将其保留,你会将其存储在 Caches 目录中,并在数据丢失时重新下载它。另一方面,tmp 目录用于存放应用程序生成的严格临时文件——换句话说,就是那些如果在你再次访问之前被删除,你也不会在意的文件。此外,像 Caches 目录一样,tmp 目录不会备份到 iTunes 或 iCloud。
共享目录
Shared 目录是一个特殊情况。它用于共享特定应用组的应用程序(在 iOS 8 中引入,以支持扩展),例如那些修改今日视图或键盘行为的应用。苹果要求所有扩展必须有一个容器应用程序,并且该容器应用程序会接收自己的应用 ID。Shared 目录是扩展与其容器应用程序共享数据的方式。例如,应用程序可以通过在初始化 NSUserDefaults 时指定一个套件名称来访问共享的用户默认设置数据库,示例如下:
[[NSUserDefaults alloc] initWithSuiteName:@"com.myorg.mysharedstuff"];
虽然 Shared 目录在撰写时并不常用,但在查找可能存储在首选项或其他私人数据中的敏感信息时,检查该目录是明智的。
总结思考
通过对 iOS 安全模型、Cocoa API 以及 iOS 应用程序布局的基本了解,你现在可以开始进入有趣的部分:剖析应用程序并发现它们的漏洞。在第二部分,我将向你展示如何构建你的测试平台、调试和分析应用程序,以及如何测试那些源代码可用的第三方应用程序。
第二部分
安全测试
第四章:4
构建你的测试平台
在本章中,我将概述你需要的工具,以便回顾你的代码并测试 iOS 应用程序,并展示如何构建一个强大而有用的测试平台。该测试平台将包括一个正确设置的 Xcode 实例、一个互动的网络代理、反向工程工具,以及绕过 iOS 平台安全检查的工具。
我还会讲解你需要在 Xcode 项目中更改的设置,以便更容易地识别和修复 bug。你将学习如何利用 Xcode 的静态分析器和编译器选项,生成高度保护的二进制文件,并进行更深入的 bug 检测。
去掉训练轮
默认的 OS X 安装中有一些行为会阻止你深入系统内部。要让你的操作系统停止隐藏你需要的内容,在终端提示符下输入以下命令:
$ defaults write com.apple.Finder AppleShowAllFiles TRUE
$ defaults write com.apple.Finder ShowPathbar -bool true
$ defaults write com.apple.Finder _FXShowPosixPathInTitle -bool true
$ defaults write NSGlobalDomain AppleShowAllExtensions -bool true
$ chflags nohidden ~/Library/
这些设置允许你在 Finder 中查看所有文件,包括那些由于名称前有点(.)而被隐藏的文件。此外,这些更改将显示更多的路径信息和文件扩展名,最重要的是,它们让你能够看到你特定用户的 Library,这是 iOS 模拟器存储所有数据的地方。
chflags 命令移除苹果公司对某些目录设置的模糊化保护,这些目录被认为对你来说过于复杂,例如 /tmp 或 /usr。我在这里使用该命令,以便在不每次都使用命令行的情况下查看 iOS 模拟器目录的内容。
另一件事:考虑将 \(SIMPATH* 添加到 Finder 的侧边栏,以便轻松访问。使用 *\)SIMPATH 来查看 iOS 模拟器的文件系统非常方便,但默认情况下你无法通过 Finder 访问它。要进行此更改,请在终端中浏览到以下目录:
$ cd ~/Library/Application\ Support
$ open .
然后,在打开的 Finder 窗口中,将 iPhone 模拟器目录拖到侧边栏。一旦你不再依赖训练轮,便可以选择你的测试设备。
建议的测试设备
我最喜欢的测试设备是仅支持 Wi-Fi 的 iPad,因为它价格便宜,且容易越狱,这样就可以测试 iPad、iPhone 和 iPod Touch 应用程序。它缺少基于蜂窝的网络连接,这并不会构成太大障碍,因为大部分时间你都需要截取网络流量。
但这种配置确实有一些小的限制。最显著的是,iPad 没有 GPS 或短信功能,显然也不能打电话。因此,手头有一台实际的 iPhone 作为备用是个不错的主意。
我倾向于为 iOS 测试准备至少两台 iPad:一台越狱设备和一台正常设备。正常设备允许在合法、现实的终端用户环境中进行测试,并且所有平台安全机制仍然保持完好。它还可以正确注册推送通知,而越狱设备过去在这方面经常出现问题。越狱设备则可以让你更深入地检查文件系统布局以及 iOS 的更详细工作原理;它还便于进行黑盒测试,这些测试在仅使用正常设备时是无法实现的。
使用设备测试与使用模拟器测试
与其他一些移动操作系统不同,iOS 开发使用的是 模拟器 而非仿真器。这意味着 iOS 设备没有完全的仿真,因为这需要一个虚拟化的 ARM 环境。相反,Apple 与 Xcode 一起分发的模拟器是为 x64 架构编译的,它们直接在你的开发机器上运行,这使得开发过程显著更快、更简单。(试试在虚拟机中启动 Android 仿真器,你就会更能理解这一特点。)
另一方面,有些事情在 iOS 模拟器中并不像在设备上那样工作。它们的差异如下:
大小写敏感性 除非你故意更改了这一行为,否则 OS X 系统使用的是不区分大小写的 HFS+ 文件系统,而 iOS 使用的是区分大小写的变种。这通常与安全性无关,但在修改程序时可能会导致互操作性问题。
库 在某些情况下,iOS 模拟器的二进制文件链接到 OS X 框架,这些框架的行为可能与 iOS 上的不同。这可能导致稍微不同的行为。
内存和性能 由于应用程序在 iOS 模拟器中原生运行,它们将充分利用你开发机器的资源。在衡量诸如 PBKDF2 轮次(参见第十三章)的影响时,你需要考虑到这一点,或者在真实设备上进行测试。
相机 截至目前,iOS 模拟器并不使用你开发机器的相机。这通常不会是个大问题,但某些应用程序确实包含例如“拍摄我的支票存根或收据”这样的功能,其中处理这些照片数据可能至关重要。
短信和蜂窝网络 你无法在 iOS 模拟器中测试与电话呼叫或 SMS 集成的交互,尽管你可以在技术上模拟某些方面,例如切换“通话中”状态栏。
与旧版本的 iOS 不同,现代版本的 iOS 模拟器实际上模拟了 Keychain API,这意味着你可以管理自己的证书并存储和操作凭证。你可以在 $SIMPATH/Library/Keychains 中找到与此功能相关的文件。
网络和代理设置
大多数时候,测试任何 iOS 应用的第一步是通过代理运行它,这样你可以检查并可能修改从设备到远程端点的流量。我认识的大多数 iOS 安全测试人员都使用 BurpSuite^(1) 来执行这项任务。
绕过 TLS 验证
在通过代理运行待测试应用时,有一个主要的难点:当 iOS 无法验证服务器的证书时,它会坚决拒绝继续 TLS/SSL 连接,这也是它应该这样做的。当然,这是正确的行为,但如果 iOS 无法验证你的代理证书,你的基于代理的测试很快就会中断。
对于 BurpSuite,具体来说,你可以通过将设备或 iOS 模拟器配置为使用 Burp 作为代理,然后在 Mobile Safari 中浏览 burp/cert/ 来获取 CA 证书。这无论是在真实设备还是在 iOS 模拟器中都应该有效。你也可以通过将证书通过邮件发送给自己或通过 web 服务器访问它们来将 CA 证书安装到物理设备上。
对于 iOS 模拟器,一种适用于几乎所有 web 代理的更通用方法是将代理软件的 CA 证书指纹直接添加到 iOS 模拟器的信任存储中。信任存储是一个 SQLite 数据库,因此它比典型的证书包稍微难以编辑。我建议编写脚本来自动化此任务。如果你想看看一个示例来开始,Gotham Digital Science 已经创建了一个 Python 脚本来完成这项工作。你可以在这里找到这个脚本:github.com/GDSSecurity/Add-Trusted-Certificate-to-iOS-Simulator/。
要使用此脚本,你需要获取你想要安装到信任存储中的 CA 证书。首先,配置 Firefox^(2) 使用你的本地代理(127.0.0.1,Burp 的端口是 8080)。然后尝试访问任何 SSL 网站,你应该会看到一个熟悉的证书警告。导航至 添加例外 → 查看 → 详细信息 并点击 PortSwigger CA 条目,如 图 4-1 所示。
点击 导出 并按照提示操作。保存 CA 证书后,打开 Terminal.app 并运行 Python 脚本,将证书添加到存储中,如下所示:
$ python ./add_ca_to_iossim.py ~/Downloads/PortSwiggerCA.pem
不幸的是,在撰写本文时,没有原生的方法可以配置 iOS 模拟器通过 HTTP 代理运行,而不同时将你系统的其他部分也通过该代理。因此,你需要在主机系统的偏好设置中配置代理,如 图 4-2 所示。
如果你将这台机器同时用于测试和其他工作活动,你可以考虑专门配置其他应用程序通过单独的代理进行连接,使用像 FoxyProxy^(3) 这样的工具为浏览器设置代理。

图 4-1:选择 PortSwigger CA 进行导出

图 4-2:配置主机系统通过 Burp 连接
使用 stunnel 绕过 SSL
绕过 SSL 端点验证的一种方法是本地设置一个终结点,然后让你的应用程序使用该终结点。通常可以通过修改包含端点 URL 的 plist 文件,而无需重新编译应用程序,来实现这一点。
如果你想通过明文轻松观察流量(例如,使用 Wireshark),但互联网可访问的端点仅通过 HTTPS 提供,那么这个设置特别有用。首先,下载并安装 stunnel,^(4),它将作为 HTTPS 端点和你的本地机器之间的代理。如果通过 Homebrew 安装,stunnel 的配置文件将在/usr/local/etc/stunnel/stunnel.conf-sample。将此文件移动或复制到/usr/local/etc/stunnel/stunnel.conf,并编辑以反映以下内容:
; SSL client mode
client = yes
; service-level configuration
[https]
accept = 127.0.0.1:80
connect = 10.10.1.50:443
TIMEOUTclose = 0
这只是将 stunnel 设置为客户端模式,指示它在你的回环接口上接受端口 80 的连接,同时通过 SSL 将其转发到远程端点。编辑完该文件后,设置 Burp 以使其使用你的回环监听器作为代理,确保选择支持隐形代理选项,如图 4-3 所示。图 4-4 展示了结果设置。

图 4-3:通过本地 stunnel 端点设置隐形代理

图 4-4:最终的 Burp/stunnel 设置
设备上的证书管理
要在物理 iOS 设备上安装证书,只需将证书发送到与设备关联的帐户,或将其放在公共 Web 服务器上,然后使用 Mobile Safari 导航到该证书。然后,你可以将其导入设备的信任存储中,如图 4-5 所示。你还可以配置设备通过另一台机器托管的网络代理(即 Burp)。只需将代理的 CA 证书(如前所述)安装到设备上,并配置你的代理在网络可访问的 IP 地址上监听,如图 4-6 所示。

图 4-5:证书导入提示

图 4-6:配置 Burp 使用非 localhost 的 IP 地址
设备上的代理设置
配置好证书颁发机构并设置好代理后,进入设置 → 网络 → Wi-Fi,点击当前选择的无线网络右侧的箭头。在这个界面上,你可以输入代理地址和端口(见图 4-7)。

图 4-7:配置设备在内部网络上使用测试代理
请注意,当配置设备使用代理时,只有通过NSURLConnection或NSURLSession发起的连接才会遵循代理设置;其他连接,如NSStream和CFStream(我将在第七章中进一步讨论),则不会受到影响。当然,由于这是一个 HTTP 代理,它仅适用于 HTTP 流量。如果你有一个使用CFStream的应用程序,你可以通过以下代码片段修改代码库,将流量通过与主机操作系统相同的代理路由:
CFDictionaryRef systemProxySettings = CFNetworkCopySystemProxySettings();
CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPProxy, systemProxySettings
);
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyHTTPProxy,
systemProxySettings);
如果你正在使用NSStream,可以通过将NSInputStream和NSOutputStream转换为其 Core Foundation 对应物来完成相同的操作,如下所示:
CFDictionaryRef systemProxySettings = CFNetworkCopySystemProxySettings();
CFReadStreamSetProperty((CFReadStreamRef)readStream, kCFStreamPropertyHTTPProxy, (
CFTypeRef)systemProxySettings);
CFWriteStreamSetProperty((CFWriteStreamRef)writeStream, kCFStreamPropertyHTTPProxy,
(CFTypeRef)systemProxySettings);
如果你正在进行黑盒测试,并且有一个应用程序不遵循系统代理设置进行 HTTP 请求,你可以尝试通过在设备的/etc/hosts文件中添加一行,将名称查找指向你的代理地址,从而将流量通过代理转发,如清单 4-1 所示。
10.50.22.11 myproxy api.testtarget.com
清单 4-1:添加 hosts 文件条目
你还可以配置设备使用由你控制的 DNS 服务器,这不需要越狱你的测试设备。做到这一点的一种方式是使用 Tim Newsham 的 dnsRedir,^(5) 这是一个 Python 脚本,它将为特定域的 DNS 查询提供伪造的答案,同时将其他域的查询传递给另一个 DNS 服务器(默认是 8.8.8.8,但你可以通过-d标志更改此设置)。可以按如下方式使用该脚本:
$ dnsRedir.py 'A:www.evil.com.=1.2.3.4'
这应该使用 IP 地址 1.2.3.4 来回答* www.evil.com *的查询,其中该 IP 地址通常应为你正在通过其代理数据的测试机器的 IP 地址。
对于非 HTTP 流量,事情就有些复杂了。你需要使用 TCP 代理来拦截流量。前面提到的 Tim Newsham 编写了一个程序,可以简化这一过程——它的名字恰如其分,叫做 tcpprox。^(6) 如果你使用清单 4-1 中的hosts文件方法将设备指向你的代理机器,那么你可以让 tcpprox 动态创建 SSL 证书,并将连接代理到远程端点。为此,你需要创建一个证书颁发机构证书并将其安装在设备上,如清单 4-2 所示。
$ ./prox.py -h
Usage: prox.py [opts] addr port
Options:
-h, --help show this help message and exit
-6 Use IPv6
-b BINDADDR Address to bind to
-L LOCPORT Local port to listen on
-s Use SSL for incoming and outgoing connections
--ssl-in Use SSL for incoming connections
--ssl-out Use SSL for outgoing connections
-3 Use SSLv3 protocol
-T Use TLSv1 protocol
-C CERT Cert for SSL
-A AUTOCNAME CName for Auto-generated SSL cert
-1 Handle a single connection
-l LOGFILE Filename to log to
$ ./ca.py -c
$ ./pkcs12.sh ca
(install CA cert on the device)
$ ./prox.py -s -L 8888 -A ssl.testtarget.com ssl.testtarget.com 8888
清单 4-2:创建证书并使用 tcpprox 拦截流量
ca.py脚本创建签名证书,pkcs12.sh脚本生成要安装在设备上的证书,如图 4-5 所示。运行这些脚本并安装证书后,即使是 SSL 连接,你的应用程序也应该通过代理连接到远程端点。完成一些测试后,你可以使用 tcpprox 附带的proxcat.py脚本读取结果,方法如下:
$ ./proxcat.py -x log.txt
一旦你的应用程序通过代理连接,你就可以开始设置 Xcode 环境了。
Xcode 和构建设置
Xcode 包含一个错综复杂的项目配置选项迷宫——几乎没有人真正理解每个选项的作用。本节将更深入地探讨这些选项,讨论你为什么要或不应该使用它们,并向你展示如何让 Xcode 在问题变成真正的麻烦之前帮助你找到错误。
让生活变得艰难
首先,最重要的是:将警告视为错误。clang 生成的大多数警告,作为 Xcode 的编译器前端,值得关注。它们不仅有助于减少代码复杂性和确保语法正确,还能捕捉一些可能难以发现的错误,例如符号问题或格式字符串缺陷。例如,考虑以下代码:
- (void) validate:(NSArray*) someTribbles withValue:(NSInteger) desired {
if (desired > [someTribbles count]) {
[self allocateTribblesWithNumberOfTribbles:desired];
}
}
NSArray的count方法返回一个无符号整数(NSUInteger)。如果你期望从用户输入中得到所需的 tribbles 数量,提交的值可能是-1,这大概意味着用户希望拥有一个反 tribble。因为desired是一个整数,与一个无符号整数进行比较,编译器会把它们都当作无符号整数来处理。因此,这个方法会意外地分配一个荒谬数量的 tribbles,因为-1 在转换为无符号整数时会变成一个极大的数字。我将在第十一章进一步讨论这类整数溢出问题。
你可以通过启用大多数警告并将其视为错误来让 clang 标记这类错误,在这种情况下,构建会失败,并显示一条消息:“不同符号的整数比较:'int'和'NSUInteger'(即'unsigned int')”。
注意
通常,你应该在项目构建配置中启用所有警告,并将警告提升为错误,这样你就能尽早处理开发周期中的 bug。
你可以在项目和目标构建设置中启用这些选项。首先,在警告策略下,将“将警告视为错误”设置为是(图 4-8)。然后,在警告部分,启用所有所需的选项。
请注意,并非所有 clang 支持的构建警告都有一个在 Xcode UI 中暴露的切换开关。要在“困难模式”下开发,你可以像在图 4-9 中那样添加-Wextra或-Weverything标志。并非所有警告都会有用,但最好在禁用它们之前先理解一个选项的意图。
-Weverything,如图 4-9 所示,可能是过度使用,除非你对 clang 的内部结构感到好奇;通常-Wextra就足够了。为了节省一些时间,表 4-1 讨论了两种几乎肯定会妨碍你开发的警告(或者是那些纯粹奇怪的警告)。

图 4-8:将所有警告视为错误

图 4-9:此设置启用了所有警告,包括没有暴露 UI 的选项。
表 4-1: 在 Xcode 中禁用的令人讨厌的警告
| 编译器警告 | 禁用的理由 |
|---|---|
| 隐式合成属性 | 由于属性合成现在是自动的,除非你的开发规范要求显式合成,否则这不算是一个错误。 |
| 未使用的参数/函数/变量等 | 当编写代码时,这些会非常让人烦恼,因为显然你的代码还没有完全实现。考虑仅在非调试构建中启用这些。 |
启用完整的 ASLR
在 iOS 4.3 中,Apple 引入了地址空间布局随机化(ASLR)。ASLR 确保程序及其数据(库、主可执行文件、堆栈和堆、以及内存映射文件)在虚拟地址空间中加载到不那么可预测的位置。这使得利用代码执行漏洞变得更加困难,因为许多漏洞依赖于引用特定库调用的虚拟地址,以及引用堆栈或堆上的数据。
然而,为了使这一切完全生效,应用程序必须构建为位置独立可执行文件(PIE),这指示编译器生成可以无论内存中的位置如何都能正常运行的机器代码。如果没有这个选项,即使重启后,基础可执行文件和堆栈的位置也将保持不变,^(7) 使攻击者的工作变得更容易。
为了确保启用了完整的 ASLR 和 PIE,请检查目标的设置中的 Deployment Target 至少设置为 iOS 版本 4.3。在项目的构建设置中,确保生成位置相关代码(Generate Position-Dependent Code)设置为 No,并且那项奇怪命名的选项“不要创建位置独立可执行文件”(Don’t Create Position Independent Executable)也设置为 No。所以,不要创建位置独立可执行文件。明白了吗?
要进行黑盒测试,或者确保你的应用程序正确启用了 ASLR,你可以在二进制文件上使用 otool,方法如下:
$ unzip MyApp.ipa
$ cd Payload/MyApp.app
$ otool -vh MyApp
MyApp (architecture armv7):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 21 2672 NOUNDEFS DYLDLINK
TWOLEVEL PIE
MyApp (architecture armv7s):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7S 0x00 EXECUTE 21 2672 NOUNDEFS DYLDLINK
TWOLEVEL PIE
在每一行 MH_MAGIC 结束时,如果你的设置正确,你应该看到 PIE 标志,并且该标志会以粗体显示。(注意,这必须在为 iOS 设备编译的二进制文件上执行,在 iOS 模拟器的二进制文件上不起作用。)
Clang 和静态分析
在计算机安全中,静态分析通常指使用工具分析代码库并识别安全漏洞。这可能涉及识别危险的 API,或者包括分析程序中的数据流,识别潜在的不安全处理程序输入。作为构建工具链的一部分,clang 是嵌入静态分析语言的好地方。
从 Xcode 3.2 开始,clang 的静态分析器^(8)已经与 Xcode 集成,提供了一个用户界面来跟踪逻辑错误、编码缺陷以及 API 滥用等问题。虽然 clang 的静态分析器非常方便,但 Xcode 中默认禁用了它的几个重要功能。特别地,经典的危险 C 库函数检查,例如strcpy和strcat,是没有启用的。你可以在你的项目或目标设置中启用这些检查,正如在图 4-10 所示。

图 4-10:在 Xcode 中启用所有 clang 静态分析检查
地址清理器与动态分析
Xcode 的最新版本包含了一个支持地址清理器(ASan)的 clang/llvm 版本。ASan 是一个动态分析工具,类似于 Valgrind,但 ASan 运行得更快,且具有更好的覆盖性。^(9) ASan 可以检测栈溢出、堆溢出、以及使用后释放等错误,帮助你追踪关键的安全漏洞。它确实会带来性能影响(程序执行速度大约会变慢两倍),因此不要在发布版本中启用它,但它在测试、质量保证或模糊测试中应该是完全可用的。
要启用 ASan,请将-fsanitize=address添加到调试版本的编译器标志中(见图 4-11)。在出现任何不安全崩溃时,ASan 应该会向控制台写入额外的调试信息,帮助你确定问题的性质和严重性。结合模糊测试^(10),ASan 对于定位可能具有安全风险的严重问题并评估其可利用性非常有帮助。

图 4-11:设置 ASan 编译器标志
使用 Instruments 监控程序
无论你是在分析别人的应用程序,还是想要改进自己的应用,DTrace 驱动的 Instruments 工具都非常有助于在细粒度层面观察应用的活动。这个工具对于监控网络套接字使用、查找内存分配问题以及观察文件系统交互非常有用。Instruments 还是一个很好的工具,用于发现应用程序在本地存储中存储的对象,从而找出可能泄露敏感信息的地方;我经常用它来做这件事。
激活 Instruments
要在 Xcode 中使用 Instruments 分析应用程序,请按住运行按钮并选择用于性能分析构建选项(见图 4-12)。构建完成后,你将看到一系列预配置的模板,专门用于监控某些资源,例如磁盘读写、内存分配、CPU 使用等。

图 4-12:选择“用于性能分析构建”选项
文件活动模板(如图 4-13 所示)将帮助你监控应用程序的磁盘 I/O 操作。选择模板后,iOS 模拟器应该会自动启动应用程序并开始记录其活动。

图 4-13:选择文件活动分析模板
Instruments 中有几个预设视图用于监控文件活动。一个好的起点是目录 I/O,它会捕捉所有文件创建或删除事件。按你平常的方式测试应用程序,并观察这里的输出。每个事件都会列出其 Objective-C 调用者、底层的 C 函数调用、文件的完整路径,以及如果该事件是重命名操作,文件的新路径。
你可能会注意到这里写入了几种类型的缓存文件(见图 4-14),以及应用程序被要求打开的 cookies 或文档。如果你挂起应用程序,你应该能看到应用程序的截图被写入磁盘,关于这一点,我将在第十章中进行讨论。
若想查看更详细的视图,你可以选择读取/写入视图,如图 4-15 所示。该视图将展示所有文件或套接字的读写操作,并显示读取或写入的数据量统计。

图 4-14:目录 I/O 视图,显示已创建或删除的文件

图 4-15:显示详细文件读写的分析结果
使用 Watchdog 监控文件系统活动
Instruments 应该能够捕捉大部分 iOS 模拟器的活动,但有些文件写入或网络调用可能由其他系统服务执行,从而未被工具捕捉到。手动检查 iOS 模拟器的目录树是个好主意,这可以帮助你更好地了解 iOS 及其应用程序的结构,并捕捉到你可能会错过的应用程序活动。
一种简单的自动化方法是使用 Python 的 watchdog 模块。^(11) Watchdog 将使用 kqueue 或 FSEvents API 来监控目录树中的文件活动,并可以在这些事件发生时记录事件或采取特定的行动。要安装 watchdog,请使用以下命令:
$ pip install watchdog
你可以编写自己的脚本来使用 watchdog 的功能,但你会发现 watchdog 已经包含了一个不错的命令行工具,叫做 watchmedo。如果你打开一个终端窗口并导航到模拟器目录,你应该能够使用 watchmedo 监控 iOS 模拟器目录树下的所有文件更改,如下所示:
$ cd ~/Library/Application\ Support/iPhone\ Simulator/6.1
$ watchmedo log --recursive .
on_modified(self=<watchdog.tricks.LoggerTrick object at 0x103c9b190>, event=<
DirModifiedEvent: src_path=/Users/dthiel/Library/Application Support/iPhone
Simulator/6.1/Library/Preferences>)
on_created(self=<watchdog.tricks.LoggerTrick object at 0x103c9b190>, event=<
FileCreatedEvent: src_path=/Users/dthiel/Library/Application Support/iPhone
Simulator/6.1/Applications/9460475C-B94A-43E8-89C0-285DD036DA7A/Library/Caches
/Snapshots/com.yourcompany.UICatalog/UIApplicationAutomaticSnapshotDefault-
Portrait.png>)
on_modified(self=<watchdog.tricks.LoggerTrick object at 0x103c9b190>, event=<
DirModifiedEvent: src_path=/Users/dthiel/Library/Application Support/iPhone
Simulator/6.1/Applications/9460475C-B94A-43E8-89C0-285DD036DA7A/Library/Caches
/Snapshots>)
on_created(self=<watchdog.tricks.LoggerTrick object at 0x103c9b190>, event=<
DirCreatedEvent: src_path=/Users/dthiel/Library/Application Support/iPhone
Simulator/6.1/Applications/9460475C-B94A-43E8-89C0-285DD036DA7A/Library/Caches
/Snapshots/com.yourcompany.UICatalog>)
on_modified(self=<watchdog.tricks.LoggerTrick object at 0x103c9b190>, event=<
DirModifiedEvent: src_path=/Users/dthiel/Library/Application Support/iPhone
Simulator/6.1/Library/SpringBoard>)
以on_modified开头的条目表示文件已被更改,而以on_created开头的条目表示新文件的创建。你可能还会看到 watchmedo 的其他更改指示符,你可以在 Watchdog 文档中阅读有关它们的更多信息。
结束语
现在你应该已经配置好了用于运行、修改和检查 iOS 应用的构建和测试环境。在第五章中,我们将更深入地探讨如何动态调试和检查应用,以及如何在运行时改变它们的行为。
第五章:5
使用 LLDB 调试和其他工具
调试 iOS 应用被认为是 Xcode 的强项之一。除了 DTrace 的有用分析功能外,Xcode 还提供了一个带有相对易用图形界面的命令行调试器。作为苹果公司逐步放弃 GNU 工具的部分内容,默认的调试器现在是 lldb,^(1) 它为 Objective-C 提供了一级支持。多线程调试得到了很好的支持,你甚至可以从调试器中检查对象。唯一的缺点是,你需要将你通过 gdb 获得的经验知识转移到新的环境中。
调试是一个广泛的主题,关于这一主题有许多书籍可供参考。^(2) 本章涵盖了 Xcode 新手的基础知识,并提供了与安全测试和安全开发相关的技巧。我假设你对 gdb 和调试器有一定的了解。
lldb 中的有用功能
Xcode 内置的调试器界面功能非常强大。它不仅有命令行界面,还可以使用 GUI 查看和与当前线程状态、注释的汇编代码及对象细节进行交互。GUI 中还包括一个中央断点浏览器,你可以在其中查看、启用和禁用断点。
注意
如果你非常熟悉使用 gdb,LLVM 项目有一份常用 gdb 命令到 lldb 命令的映射表;请参见 lldb.llvm.org/lldb-gdb.html。
使用断点
你可以通过 Xcode 的 lldb 界面(见图 5-1)图形化设置断点,也可以通过命令行进行设置。除了在程序访问特定内存地址或 C 函数时设置断点外,你还可以在特定的 Objective-C 方法上设置断点。

图 5-1:Xcode 的 lldb 界面
以下是设置断点的一些方法:
➊ (lldb) breakpoint set --name myfunction --name myotherfunction
➋ (lldb) breakpoint set --name "-[myClass methodCall:]"
➌ (lldb) breakpoint set --selector myObjCSelector:
➍ (lldb) breakpoint set --method myCPlusPlusMethod
➊处的命令会在多个函数上设置一个断点,这个功能可以让你同时启用和禁用一组函数。正如➋所示,你也可以在特定的 Objective-C 实例和类方法上设置断点——这些方法也可以像➊处的 C 函数调用一样进行分组。如果你想在所有调用特定选择器/方法时设置断点,可以使用--selector选项➌,这会在任何调用该名称的选择器时断开,不管它们是在哪个类中实现的。最后,要在特定的 C++方法上设置断点,只需在定义断点时指定--method而不是--name,如➍所示。
实际上,在 lldb 中设置断点的操作如下所示:
(lldb) breakpoint set --name main
Breakpoint 2: where = StatePreservator`main + 34 at main.m:15, address = 0x00002822
(lldb) breakpoint set -S encodeRestorableStateWithCoder:
Breakpoint 2: where = StatePreservator`-[StatePreservatorSecondViewController
encodeRestorableStateWithCoder:] + 44 at StatePreservatorSecondViewController.
m:25, address = 0x00002d5c
设置断点后,lldb 会显示你所设置的断点代码。如果你愿意,可以让它更简洁:像 gdb 一样,lldb 会通过最短的匹配文本来识别关键字。因此,*breakpoint*可以缩写为*break*,甚至缩写为*b*。
在 GUI 中,你可以通过点击行号左侧的空白区域来在特定代码行设置断点(见图 5-2)。再次点击将禁用该断点。或者,你也可以使用 --file *filename.m* --line *66* 语法通过 lldb CLI 在某一行设置断点。

图 5-2:使用鼠标设置特定行的断点。禁用的断点以浅灰色显示。
当你想创建多个断点时,使用命令行中的 -r 标志来设置匹配特定正则表达式的函数断点非常方便,例如:
(lldb) break set -r tableView
Breakpoint 1: 4 locations.
(lldb) break list
Current breakpoints:
1: source regex = "tableView", locations = 4, resolved = 4
1.1: where = DocInteraction`-[DITableViewController tableView:
cellForRowAtIndexPath:] + 695 at DITableViewController.m:225, address = 0
x000032c7, resolved, hit count = 0
1.2: where = DocInteraction`-[DITableViewController tableView:
cellForRowAtIndexPath:] + 1202 at DITableViewController.m:245, address = 0
x000034c2, resolved, hit count = 0
1.3: where = DocInteraction`-[DITableViewController tableView:
cellForRowAtIndexPath:] + 1270 at DITableViewController.m:246, address = 0
x00003506, resolved, hit count = 0
1.4: where = DocInteraction`-[DITableViewController tableView:
cellForRowAtIndexPath:] + 1322 at DITableViewController.m:247, address = 0
x0000353a, resolved, hit count = 0
这将设置一个带有多个位置的单一断点。每个位置可以被启用或禁用,如下所示:
(lldb) break dis 1.4
1 breakpoints disabled.
(lldb) break list
Current breakpoints:
1: source regex = ".*imageView.*", locations = 4, resolved = 3
--snip--
1.4: where = DocInteraction`-[DITableViewController tableView:
cellForRowAtIndexPath:] + 1322 at DITableViewController.m:247, address = 0
x0000353a, unresolved, hit count = 0 Options: disabled
(lldb) break en 1.4
1 breakpoints disabled.
请注意,启用和禁用位置的方式与常规断点相同;只需使用 break disable 和 break enable 并引用正确的数字标识符。
浏览帧和变量
一旦你到达了断点,你可以使用 lldb 来检查程序的状态。你可以通过命令行完成此操作,正如我之前展示的其他 lldb 示例,或者通过可视化的 lldb 浏览器,如图 5-3 所示。

图 5-3:从命令行和 GUI 检查帧变量
除了查看和操作当前帧的变量外,你还可以使用调试导航器浏览程序线程和调用堆栈中的帧,如图 5-4 所示。

图 5-4:使用调试导航器切换帧和线程
类似于使用 gdb,你可以使用 bt(backtrace 的缩写)命令检查当前线程的调用堆栈(参见列表 5-1)。通常,你还可以使用典型的 up、down 和 frame select 命令导航帧。然而,在某些版本的 Xcode 中,存在一个 bug,导致帧立即恢复为在调试导航器中选择的帧。在这种情况下,你必须在调试导航器中手动切换帧,以便单独检查它们。
(lldb) bt
* thread #1: tid = 0x11804c, 0x00002c07 StatePreservator`-
StatePreservatorSecondViewController encodeRestorableStateWithCoder: + 55 at
StatePreservatorSecondViewController.m:25, queue = 'com.apple.main-thread,
stop reason = breakpoint 1.1
frame #0: 0x00002c07 StatePreservator`-StatePreservatorSecondViewController
encodeRestorableStateWithCoder: + 55 at StatePreservatorSecondViewController.m:25
frame #1: 0x000277e7 UIKit`-[UIApplication(StateRestoration)
_saveApplicationPreservationState:] + 1955
frame #2: 0x00027017 UIKit`-[UIApplication(StateRestoration)
_saveApplicationPreservationStateIfSupported] + 434
frame #3: 0x0001b07b UIKit`-[UIApplication _handleApplicationSuspend:eventInfo
:] + 947
frame #4: 0x00023e74 UIKit`-[UIApplication handleEvent:withNewEvent:] + 1469
frame #5: 0x00024beb UIKit`-[UIApplication sendEvent:] + 85
frame #6: 0x00016698 UIKit`_UIApplicationHandleEvent + 9874
frame #7: 0x01beddf9 GraphicsServices`_PurpleEventCallback + 339
frame #8: 0x01bedad0 GraphicsServices`PurpleEventCallback + 46
frame #9: 0x01c07bf5 CoreFoundation`
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
frame #10: 0x01c07962 CoreFoundation`__CFRunLoopDoSource1 + 146
frame #11: 0x01c38bb6 CoreFoundation`__CFRunLoopRun + 2118
frame #12: 0x01c37f44 CoreFoundation`CFRunLoopRunSpecific + 276
frame #13: 0x01c37e1b CoreFoundation`CFRunLoopRunInMode + 123
frame #14: 0x01bec7e3 GraphicsServices`GSEventRunModal + 88
frame #15: 0x01bec668 GraphicsServices`GSEventRun + 104
frame #16: 0x00013ffc UIKit`UIApplicationMain + 1211
frame #17: 0x0000267d StatePreservator`main(argc=1, argv=0xbffff13c) + 141 at
main.m:16
列表 5-1:使用 backtrace 命令获取当前调用堆栈
要检查当前帧的变量,你可以使用 frame variable 命令,如在列表 ?? 中所示。
(lldb) frame variable
(StatePreservatorSecondViewController *const) self = 0x0752d2e0
(SEL) _cmd = "encodeRestorableStateWithCoder:"
(NSCoder *) coder = 0x0d0234e0
列表 5-2:使用 frame variable 命令
这将显示局部栈帧的变量名称和参数,以及它们的类型和内存地址。你还可以在图形调试器中使用上下文菜单打印或编辑变量内容;参见图 5-5。
如果你单独使用 frame select,你还可以看到程序在调用堆栈中的位置,以及相关的周围代码行,如以下示例所示:
(lldb) frame select
frame #0: 0x00002d5c StatePreservator`-StatePreservatorSecondViewController
encodeRestorableStateWithCoder: + 44 at StatePreservatorSecondViewController.m:25
22
23 -(void)encodeRestorableStateWithCoder:(NSCoder *)coder
24 {
-> 25 [coder encodeObject:[_myTextView text] forKey:@"UnsavedText"];
26 [super encodeRestorableStateWithCoder:coder];
27 }
28

图 5-5:变量上下文菜单,显示打印变量内容、设置观察点和查看内存内容的选项
frame select 命令还可以接受一个数字参数,用于指定你想要检查的栈帧,如果你希望查看更上层的调用栈(见示例 5-3)。
(lldb) frame select 4
frame #4: 0x00023e74 UIKit`-[UIApplication handleEvent:withNewEvent:] + 1469
UIKit`-[UIApplication handleEvent:withNewEvent:] + 1469:
-> 0x23e74: xorb %cl, %cl
0x23e76: jmp 0x24808
; -[UIApplication handleEvent:withNewEvent:] + 3921
0x23e7b: movl 16(%ebp), %ebx
0x23e7e: movl %ebx, (%esp)
示例 5-3:检查栈帧时显示的汇编代码
请注意,对于当前项目之外的代码,例如 Cocoa API 的其他部分,通常无法获得源代码;lldb 会显示相关的汇编指令。^(3)
你还可以使用 lldb 的 po(即 print object 的缩写)命令检查对象的值。例如,考虑以下内容:
(lldb) po [self window]
$2 = 0x071848d0 <UIWindow: 0x71848d0; frame = (0 0; 320 480); hidden = YES; layer =
<UIWindowLayer: 0x71849a0>>
使用 po 在主窗口中获取该窗口的地址和属性。
可视化检查对象
如果你使用的是 Xcode 5 或更高版本,你还可以将鼠标悬停在对象上,以检查其内容,如图 5-6 所示。如果你深入查看各个子对象,可以通过点击 i 按钮直接查看其内存内容(如图 5-7 所示),或者使用快速查看“眼睛”按钮查看该对象的内容,这些内容将作为完全渲染的图像、文本或其他任何 OS X 的 Quick Look API 支持的数据类型呈现(见图 5-8)。在我看来,这相当酷。

图 5-6:在断点处检查对象

图 5-7:检查对象在内存中的内容

图 5-8:使用 Quick Look 按钮检查变量当前状态。在此示例中,你查看的是 _statusBar ,它是 UIApplication 委托窗口的属性,Xcode 会将其显示为实际的图像。
操作变量和属性
你不仅可以查看变量和对象的内容,还可以使用 lldb 来执行更多操作。例如,让我们尝试在示例 5-2 中测试 frame variable 命令时所使用的相同代码行。
[coder encodeObject:[_myTextView text] forKey:@"UnsavedText"];
当调试器到达这一行时,假设你想检查 UITextView 的 text 属性的内容并在程序继续执行前修改其值。你可以使用 expr 命令,采用传统的 Objective-C 语法来做到这一点,如下所示:
(lldb) po [_myTextView text]
$0 = 0x08154cb0 Foo
(lldb) expr (void)[_myTextView setText:@"Bar"]
(lldb) po [_myTextView text]
$1 = 0x0806b2e0 Bar
(lldb) cont
当执行恢复时,UI 中该文本框的值应该已经改变。因为 lldb 无法知道以这种方式调用的方法的返回类型,所以你需要使用 (void) 指定类型并配合 expr 命令来进行操作。类似地,如果你调用的是返回 int 的方法,也需要显式地将其转换为该类型。对于简单的赋值操作,例如 myInteger = 666 或类似操作,不同于方法调用,你只需输入 expr 和赋值命令即可。
注意
在 Xcode 的命令行中使用 lldb 时,图形界面会自动补全对象方法名称,并提供简要描述和返回类型。请参见图 5-9 中的示例。

图 5-9:Xcode 中巧妙的 lldb 方法名称自动补全
请记住,您不仅仅限于操作在代码中声明的对象。您还可以操作框架类。
(lldb) expr (void)[[UIPasteboard generalPasteboard] setString:@"my string"]
(lldb) po [[UIPasteboard generalPasteboard] string]
$5 = 0x071c6e50 my string
对于这种交互式操作和查询,我通常发现将断点设置在应用程序代理的 didReceiveMemoryWarning 方法上非常有用,因为这个方法在每个应用程序中都会存在。当我想在 iOS 模拟器中运行程序时检查其状态时,我选择硬件 → 模拟内存警告。完成调整后,我只需通过 cont 继续应用程序。您也可以通过 Xcode 界面上的暂停执行按钮来执行此操作。
断点动作
断点动作 文档不多,但非常有用。它们允许您创建仅在特定条件下触发的断点,并且在这些断点被命中时可以执行复杂的操作。您可以设置它们在执行这些操作后自动恢复执行,或者甚至在某行代码被命中特定次数后才触发。记录信息和使用语音合成展示程序信息是您可以为断点设置的最简单操作,但您还可以查询对象、读取和操作变量等。基本上,断点动作能够做任何您能通过 lldb 命令行做的事情,并且还能做一些额外的操作。
让我们一步步地演示如何创建一个断点动作。
-
通过点击断点区域创建一个断点。
-
按住 CTRL 键点击断点并选择编辑断点。
-
点击添加动作。
-
勾选评估后自动继续框。
-
对于最简单的断点动作类型,只需选择记录信息动作。在这里,您可以打印简单的消息,以及断点名称和命中次数(参见图 5-10)。您可以忽略表达式选项,因为它并不特别直观。
-
添加了简单的日志消息后,您可以点击+按钮添加另一个动作。这次,选择调试器命令。
-
在这里,您可以输入基本的 lldb 表达式——最常见的是使用
po命令来打印对象的描述。示例请参见图 5-11。![image]()
图 5-10:使用断点动作进行简单的日志记录。在此示例中,您将记录一条消息,并使用
%H占位符记录断点被命中的次数。![image]()
图 5-11:除了简单的记录之外,您还可以执行任意 lldb 命令。在这种情况下,您将使用
po命令打印由path方法返回的对象描述。 -
可选地,添加断点条件以指定何时执行您定义的动作(图 5-12)。
![image]()
图 5-12:两个操作和一个断点条件。对于条件,你需要确保路径的长度不为零,然后才执行断点操作,指定返回值
(BOOL)。
尝试按照这些步骤操作,直到你熟悉使用断点操作,然后继续下一个部分,了解如何在安全环境中应用 lldb 的一些具体方法。
使用 lldb 进行安全分析
这些都是有用的技巧,但如何将它们结合起来,找到新的安全问题或测试安全断言呢?让我们看几个示例,看看使用调试器如何帮助你发现更具体的问题。
故障注入
假设你有一个应用程序,它使用自定义二进制网络协议在客户端和远程服务器之间传输数据。这可能会使得使用现成的代理拦截和修改数据变得困难,但你想确定在某些参数中,格式错误的数据是否会导致程序崩溃。你还可以操作数据,以便将来的测试变得更容易。
由于你可以更改数据,你可能想要替换一个随机生成的密钥,换成你选择的密钥。你可以在调试器中执行这一操作,如清单 5-4 所示。这样,数据就会使用你选择的已知密钥进行加密,而不是一个可能无法打印的二进制数据块。以下示例在数据保存到 Keychain 之前修改了应用程序的加密密钥,以便后续的通信使用不同的密钥:
➊ (lldb) frame var
(Class) self = SimpleKeychainWrapper
(SEL) _cmd = "addToKeychain:forService:"
(NSString *) identifier = 0x00005be4 @"com.isecpartners.CryptoKey"
(NSString *) service = 0x00005bf4 @"com.isecpartners.NSCoder+Crypto"
(NSMutableDictionary *) dictionary = 0x08b292f0 6 key/value pairs
(NSMutableData *) item = 0x08b2cee0
(OSStatus) status = 1
➋ (lldb) po item
<9aab766a 260bb165 57675f04 fdb982d3 d73365df 5fd4b05f 3c078f7b b6484b7d>
➌ (lldb) po dictionary
{
acct = <636f6d2e 69736563 70617274 6e657273 2e437279 70746f4b 6579>;
class = genp;
gena = <636f6d2e 69736563 70617274 6e657273 2e437279 70746f4b 6579>;
pdmn = aku;
svce = "com.isecpartners.NSCoder+Crypto";
"v_Data" = <9aab766a 260bb165 57675f04 fdb982d3 d73365df 5fd4b05f 3c078f7b
b6484b7d>;
}
➍ (lldb) expr (void)[dictionary setObject:@"mykey" forKey:(__bridge id)kSecValueData
];
➎ (lldb) po dictionary
{
acct = <636f6d2e 69736563 70617274 6e657273 2e437279 70746f4b 6579>;
class = genp;
gena = <636f6d2e 69736563 70617274 6e657273 2e437279 70746f4b 6579>;
pdmn = aku;
svce = "com.isecpartners.NSCoder+Crypto";
"v_Data" = mykey;
}
清单 5-4:检查和更改内存中的对象值
在➊,代码打印出当前帧的变量,注意到传递给addToKeychain:forService:选择器的参数。这个示例关注的键存储在item参数中,并被添加到字典中。检查这些(➋和➌)会显示键的值。然后,代码使用expr命令 ➍ 修改了 Keychain 字典。在➎,程序验证新的NSString现在是该键的当前值。
数据跟踪
如果你有一个使用主密码加密数据的应用程序,可能有必要在数据被加密之前检查它。默认情况下,数据是否会进入加密例程并不总是显而易见。请参考清单 5-5:
➊ (lldb) frame variable
(CCCryptHelper *const) self = 0x07534b40
➋ (SEL) _cmd = "encrypt:"
➌ (NSString *) data = 0x0000c0ec @"PasswordManager"
(NSData *) encData = 0x07534b40 0 byte
(lldb) frame select
frame #0: 0x00006790 PasswordManager `-CCCryptHelper encrypt: + 48 at CCCryptHelper.m:82
80 - (NSData *)encrypt:(NSString *)data {
-> 81 NSData *encData = [self AES128EncryptData:[data dataUsingEncoding: NS
UTF8StringEncoding]
82 withKey:masterPassword];
清单 5-5:使用 lldb 检查帧变量
如果你在encrypt:选择器 ➋ 设置断点,你可以使用frame variable命令 ➊ 检查局部变量。注意,输出中显示了data和encData。前者 ➌ 是这个示例中感兴趣的部分,因为它是将被加密并由例程返回的数据。这种跟踪技术还可以用于检查和修改将通过网络发送的数据,在它到达加密例程之前。
检查核心框架
lldb 在深入研究 Apple 的 API 的奇怪行为时也非常有用——当你对某个 API 的行为感到困惑时,我建议你使用它。例如,在查看NSURLCache时,我注意到在清单 5-6 中的行为:
(lldb) expr (int)[[NSURLCache sharedURLCache] currentMemoryUsage]
(int) $0 = 158445
(lldb) expr (int)[[NSURLCache sharedURLCache] currentDiskUsage]
(int) $1 = 98304
➊ (lldb) expr (void)[[NSURLCache sharedURLCache] removeAllCachedResponses]
(lldb) expr (int)[[NSURLCache sharedURLCache] currentMemoryUsage]
(int) $3 = 0
(lldb) expr (int)[[NSURLCache sharedURLCache] currentDiskUsage]
➋ (int) $4 = 98304
清单 5-6:NSURLCache API 的某些奇怪行为
在这里,尽管我调用了removeAllCachedResponses方法➊,当前的磁盘使用量仍然是 98304 字节➋。唉,似乎清除缓存是无效的。别担心——你将在第九章看到一些解决方案。与此同时,你可能想自己尝试一些内部机制。这可以帮助你了解 iOS 平台的一些工作原理,并对你的应用程序行为有更深入的理解。
结束语
所有这些调试和检查技巧在调试你自己的应用程序或快速了解一个新代码库时都很有用。然而,你可能并不总是能访问到你正在使用的产品的源代码。在这些情况下,你需要了解一些基本的黑箱测试技巧,我将在第六章中介绍这些技巧。
第六章:6
黑盒测试
虽然白盒测试几乎总是进行安全测试时最好的方式,但有时候你只能在没有源代码或对程序设计的了解的情况下进行测试。在这些情况下,你需要深入了解 iOS 的内部,特别是 Objective-C 和 Mach-O 二进制格式的领域。
iOS 上的黑盒测试是一个快速变化的目标——它依赖于越狱的持续发展,以及强大的第三方工具和调试工具。我尽力将本章中描述的技巧和工具尽可能地做成未来-proof,以为你提供一个坚实的基础。
为了有效地进行 iOS 应用程序的黑盒测试,你首先需要获得一个越狱设备,这样你才能侧载应用程序并安装你的测试工具链。越狱的细节变化太快,我无法在这里记录,但你通常可以从 iPhone Dev Team^(1)或 iClarified.^(2)找到最新信息。
一旦你越狱了你的设备,启动 Cydia,选择开发者模式,然后更新你的软件包列表(在“更改”选项卡下)。
现在你可以在设备上加载一些测试工具,主要来自 Cydia 应用商店。以下是必备工具:
odcctools 包括 otool、lipo 以及其他开发工具。
OpenSSH 你需要这个工具来实际访问设备。务必使用passwd(1)命令立即更改root和mobile账户的密码。
MobileTerminal 这将允许你在设备本地使用命令行,必要时使用。
cURL 你会需要这个工具来通过 HTTP 或 FTP 下载远程文件。
Erica Utilities 这个工具包包括了 Erica Sadun 的一些有用工具。可以在ericasadun.com/ftp/EricaUtilities/查看详细列表。
vbindiff 这是一个二进制差异程序,用来验证二进制文件的变化。
netcat 这是一个通用的网络监听工具。
rsync 你可以安装这个工具来同步整个目录树到设备或者从设备同步。
tcpdump 你可以安装这个工具来捕获网络流量数据包进行分析。
IPA 安装控制台 这个工具将允许你直接安装复制到设备上的.ipa文件。
Cydia Substrate 这个工具用于挂钩和修改应用程序的行为。
现在,让我们来看一下如何将这些测试工具安装到你的设备上。
安装第三方应用
根据你是如何获得应用程序文件的,有几种方法可以将它们侧载到你的设备上。
使用 .app 目录
如果你获得了一个.app目录,你可以进行如下操作:
首先,用tar命令打包你的.app包,并使用scp将压缩包复制到你的测试设备上,如下所示:
$ tar -cvzf archive.tar.gz mybundle.app}
$ scp archive.tar.gz root@dev.ice.i.p:
然后通过ssh连接到你的设备,并将压缩包解压到/Applications目录:
$ cd /Applications
$ tar -xvzf ~/archive.tar.gz
这样应该能将应用程序放置在与苹果官方应用程序相邻的位置。为了使其出现在主屏幕上,您需要重新启动 SpringBoard 或重启设备。要重启 SpringBoard,您可以使用killall命令,像这样:
$ killall -HUP SpringBoard
如果您发现自己经常需要“重新启动 SpringBoard”,可以使用像 Cydia 中的 CCRespring 这样的工具,如 Figure 6-1 所示。

Figure 6-1:CCRespring 在控制中心添加的简单重启按钮
像 CCRespring 这样的工具添加了一个按钮,您可以按下它来重新启动 SpringBoard,这样您就不需要每次都去命令行了。
注意
有些人报告说,仅仅重新启动设备并不会导致应用程序出现在 SpringBoard 上。在这种情况下,您可以选择重新启动设备或作为*mobile*用户运行*uicache*命令。
使用.ipa 包文件
如果您已经获得(或通过其他方式获取)一个.ipa包文件,您可以使用scp将其复制到设备中,并使用installipa命令进行安装,方法如下:
$ installipa ./Wikipedia-iOS.ipa
Analyzing Wikipedia-iOS.ipa...
Installing Wikipedia (v3.3)...
Installed Wikipedia (v3.3) successfully.
$ ls Applications/CC189021-7AD0-498F-ACB6-356C9E521962
Documents Library Wikipedia-iOS.app tmp
解密二进制文件
在您检查二进制文件的内容之前,您需要先解密它们。有几种方法可以实现这一点。最简单的方法是使用预打包的工具,如 Stefan Esser 的 dumpdecrypted。^(3) 这是一个共享库,在执行您的应用程序时动态加载。您可以按如下方式使用它:
$ git clone https://github.com/stefanesser/dumpdecrypted
$ cd dumpdecrypted
$ make
$ scp dumpdecrypted.dylib root@your.dev.ice:
$ ssh root@your.dev.ice
$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Applications/(APP_ID)/
YourApp.app/YourApp
这将输出一个解密后的二进制文件,存放在应用程序的.app包的tmp目录中。
因为已经有很多自动化工具可以用于提取解密二进制文件,其中大多数已经无法使用,最好有一个备份方法。为了更稳健和(理想情况下)更具未来兼容性地解密二进制文件,并帮助您理解一些应用加密和解密的内部工作原理,您可以使用命令行工具和 lldb。^(4)
要创建解密后的二进制文件,您将遵循以下基本步骤:
-
分析二进制文件,确定其加密部分的位置。
-
在 lldb 下运行应用程序。
-
将未加密的部分转储到磁盘。
-
复制原始二进制文件作为目标文件使用。
-
移除目标二进制文件中的
cryptid标志。 -
将未加密的部分移植到目标二进制文件中。
让我们更详细地讨论这个解密过程。
在设备上启动 debugserver
在您获取内存转储之前,您需要将 Apple 的 debugserver 放到设备上。debugserver 位于DeveloperDiskImage.dmg中,深藏在 Xcode 内。通过命令行,您可以挂载磁盘镜像并将 debugserver 提取到本地目录,如 Listing 6-1 所示。
$ hdiutil attach /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/7.1\ \(11D167\)/DeveloperDiskImage.dmg
Checksumming whole disk (Apple_HFS : 0)
..................................................................
disk (Apple_HFS : 0): verified CRC32 $D1221D77
verified CRC32 $B5681BED
/dev/disk6 /Volumes/DeveloperDiskImage
$ cp /Volumes/DeveloperDiskImage/usr/bin/debugserver .
Listing 6-1:从 Developer Disk Image 提取 debugserver
一旦你复制了调试服务器,你需要编辑二进制文件的权限。通常,当 Xcode 本身使用调试服务器时,它会直接启动应用程序;你需要更改它的权限,以便它可以附加到设备上的任意运行程序。首先,使用二进制文件的当前权限生成一个 plist,如下所示:
$ codesign --display --entitlements entitlements.plist debugserver
这应该会生成一个 XML 格式的 plist 文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.backboardd.debugapplications</key>
<true/>
<key>com.apple.backboardd.launchapplications</key>
<true/>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>seatbelt-profiles</key>
<array>
<string>debugserver</string>
</array>
</dict>
</plist>
这个文件需要更新,以包含get-task-allow和task_for_pid-allow权限,并删除seatbelt-profiles权限。这些更新将导致一个像下面这样的 plist:
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</plist>
更新entitlements.plist文件后,你需要使用它来对应用进行签名(从而覆盖二进制文件的现有权限),并将调试服务器复制到设备上,如下所示:
$ codesign -s - --entitlements entitlements.plist -f debugserver
debugserver: replacing existing signature
$ scp debugserver root@de.vi.ce.ip:
现在你终于可以调试应用程序了。确保你想要调试的程序当前在设备上运行,然后启动调试服务器以附加到它,像这样:
$ ssh root@de.vi.ce.ip
$ ./debugserver *:666 --attach=Snapchat
debugserver-310.2 for arm64.
Attaching to process Snapchat...
Listening to port 666 for a connection from *...
这个调试服务器现在正在等待来自另一台运行 lldb 的机器的网络连接。接下来,在你的本地机器上,你可以按以下方式连接到设备:
$ lldb
(lldb) platform select remote-ios
Platform: remote-ios
Connected: no
SDK Path: "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/8.0 (12A4265u)"
SDK Roots: [ 0] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/4.2"
SDK Roots: [ 1] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/4.3"
SDK Roots: [ 2] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/5.0"
SDK Roots: [ 3] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/5.1"
SDK Roots: [ 4] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/6.0"
SDK Roots: [ 5] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/6.1"
SDK Roots: [ 6] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/7.0"
SDK Roots: [ 7] "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.
platform/DeviceSupport/7.1 (11D167)"
SDK Roots: [ 8] "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/5.0.1
(9A405)"
SDK Roots: [ 9] "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/6.0.1
(10A523)"
SDK Roots: [10] "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/7.0.4
(11B554a)"
SDK Roots: [11] "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/8.0
(12A4265u)"
SDK Roots: [12] "/Users/lx/Library/Developer/Xcode/iOS DeviceSupport/8.0
(12A4297e)"
(lldb) process connect connect://de.vi.ce.ip:666
Process 2801 stopped
* thread #1: tid = 0x18b64b, 0x0000000192905cc0 libsystem_kernel.dylib`
mach_msg_trap + 8, stop reason = signal SIGSTOP
frame #0: 0x0000000192905cc0 libsystem_kernel.dylib`mach_msg_trap + 8
libsystem_kernel.dylib`mach_msg_trap + 8:
-> 0x192905cc0: b 0x19290580c
libsystem_kernel.dylib`mach_msg_overwrite_trap:
0x192905cc4: .long 0x0000093a ; unknown opcode
0x192905cc8: ldr w16, 0x192905cd0 ; semaphore_signal_trap
0x192905ccc: b 0x19290580c
在这个例子中,正在运行的程序被中断,此时你可以在本地机器上使用 lldb 对其进行操作。要提取解密后的程序数据,接下来你需要确定二进制文件中加密段所在的部分。
请注意,你可能会发现网络连接不稳定,无法成功完成内存转储。如果是这种情况,你可以使用iproxy命令(usbuxmd 自带的命令)作为 USB 端口和 TCP 端口之间的代理,如下所示:
$ brew install usbmuxd
$ iproxy 1234 1234 &
$ lldb
(lldb) process connect connect://127.0.0.1:1234
这些命令连接到一个网络套接字并使用 lldb,但实际上是通过 USB 端口进行通信的。
定位加密段
要定位加密段,你需要使用 odcctools 和 lldb。首先,运行otool -l *myBinary*并在你喜欢的分页器中查看输出。你可以在设备上或者本地机器上执行此操作。OS X 自带的版本包含一个更新的 otool,可以提供更清晰的输出。以下是一个示例:
$ otool -fh Snapchat
Fat headers
fat_magic 0xcafebabe
nfat_arch 2
architecture 0
cputype 12
cpusubtype 9
capabilities 0x0
offset 16384
size 9136464
align 2¹⁴ (16384)
architecture 1
cputype 12
cpusubtype 11
capabilities 0x0
offset 9158656
size 9169312
align 2¹⁴ (16384)
Snapchat (architecture armv7:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedface 12 9 0x00 2 47 5316 0x00218085
Snapchat (architecture armv7s):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedface 12 11 0x00 2 47 5316 0x00218085
Mach-O 二进制格式允许创建所谓的fat文件,这些文件可以包含针对多个架构编译的程序(这就是 OS X 通用二进制文件的工作方式)。为了简化逆向工程,你需要处理将在目标设备上运行的二进制部分;在我的例子中,我使用的是 iPhone 5s 作为测试设备,因此我需要 armv7s 架构。
确定架构后,你有几个选择。你可以使用lipo(1)命令将二进制文件瘦身,仅包含一个架构(thin标志指定你感兴趣的架构),像这样:
$ lipo -thin armv7 myBinary -output myBinary-thin
但为了本章的目的,我将展示如何处理 fat 二进制文件。首先,你需要使用 otool 来确定二进制文件中 文本 段的基址——这是实际的可执行指令将被加载到内存中的位置——正如在示例 6-2 中所示。
$ otool -arch armv7s -l Snapchat
Snapchat:
Load command 0
cmd LC_SEGMENT
cmdsize 56
segname __PAGEZERO
vmaddr 0x00000000
vmsize 0x00004000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT
cmdsize 736
segname __TEXT
vmaddr 0x00004000
vmsize 0x007a4000
fileoff 0
filesize 8011776
maxprot 0x00000005
initprot 0x00000005
nsects 10
flags 0x0
示例 6-2:查找文本段的基址
你可以看到文本段从 0x00004000 开始。记下这个地址,因为稍后你会用到它。下一步是确定二进制文件加密部分的起始和结束位置。你可以通过 otool 来完成此操作——注意你需要指定 -arch armv7s 命令(或者根据你使用的架构来指定),以确保你查看的是正确的部分。输出应该像示例 6-3 所示。
$ otool -arch armv7s -l Snapchat
--snip--
Load command 9
cmd LC_VERSION_MIN_IPHONEOS
cmdsize 16
version 5.0
sdk 7.1
Load command 10
cmd LC_UNIXTHREAD
cmdsize 84
flavor ARM_THREAD_STATE
count ARM_THREAD_STATE_COUNT
r0 0x00000000 r1 0x00000000 r2 0x00000000 r3 0x00000000
r4 0x00000000 r5 0x00000000 r6 0x00000000 r7 0x00000000
r8 0x00000000 r9 0x00000000 r10 0x00000000 r11 0x00000000
r12 0x00000000 sp 0x00000000 lr 0x00000000 pc 0x0000a300
cpsr 0x00000000
Load command 11
cmd LC_ENCRYPTION_INFO
cmdsize 20
cryptoff 16384
cryptsize 7995392
cryptid 1
示例 6-3:otool 显示二进制文件的加载命令
这里需要关注的值是 cryptoff 和 cryptsize(cryptid 仅表示这是一个加密的二进制文件)。^(5) 它们分别表示应用程序加密段的起始地址和段的大小。它们之间的范围将帮助你在转储内存时使用。这些值是十六进制的——获取十六进制值的一种快速方法是执行以下命令:
$ printf '%x\n' 16384
4000
$ printf '%x\n' 7995392
7a0000
在这种情况下,数字是 0x00004000 和 0x007a0000。也请将这些记下来。现在,回到示例 6-2,已经确定二进制文件中的文本段从 0x00004000 开始。然而,文本段在程序实际运行时可能不会停留在这里,因为 ASLR 会随机移动内存中的某些部分。^(6) 所以,请检查文本段实际加载的位置,使用 lldb 的 image list 命令,如下所示:
(lldb) image list
[ 0] E3BB2396-1EF8-3EA7-BC1D-98F736A0370F 0x000b2000 /var/mobile/Applications/
CCAC51DD-48DB-4798-9D1B-94C5C700191F/Snapchat.app/Snapchat
(0x00000000000b2000)
[ 1] F49F2879-0AA0-36C0-8E55-73071A7E2870 0x2db90000 /Users/lx/Library/Developer/
Xcode/iOS DeviceSupport/7.0.4 (11B554a)/Symbols/System/Library/Frameworks/
AudioToolbox.framework/AudioToolbox
[ 2] 763DDFFB-38AF-3444-B745-01DDE37A5949 0x388ac000 /Users/lx/Library/Developer/
Xcode/iOS DeviceSupport/7.0.4 (11B554a)/Symbols/usr/lib/libresolv.9.dylib
[ 3] 18B3A243-F792-3C39-951C-97AB416ED3E6 0x37fb0000 /Users/lx/Library/Developer/
Xcode/iOS DeviceSupport/7.0.4 (11B554a)/Symbols/usr/lib/libc++.1.dylib
[ 4] BC1A8B9C-9F5D-3B9D-B79E-345D4C3A361A 0x2e7a2000 /Users/lx/Library/Developer/
Xcode/iOS DeviceSupport/7.0.4 (11B554a)/Symbols/System/Library/Frameworks/
CoreLocation.framework/CoreLocation
[ 5] CC733C2C-249E-3161-A9AF-19A44AEB1577 0x2d8c2000 /Users/lx/Library/Developer/
Xcode/iOS DeviceSupport/7.0.4 (11B554a)/Symbols/System/Library/Frameworks/
AddressBook.framework/AddressBook
你可以看到文本段加载到了 0x000b2000。掌握了这个地址后,你终于可以提取二进制文件的可执行部分了。
转储应用程序内存
让我们来看点数学运算来确定最终的偏移量。第一步是将基址与 cryptoff 的值相加;在这种情况下,两者都是 0x00004000,所以起始数字将是 0x00008000。结束数字将是起始数字加上 cryptsize 的值,在这个例子中,cryptsize 位于 0x007a0000。这些数字相加起来比较简单,但如果你遇到无法轻易计算的偏移量,你可以使用 Python 来为你计算,如示例 6-4 所示。
$ python
Python 2.7.10 (default, Dec 14 2015, 19:46:27)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> hex(0x00008000 + 0x007a0000)
'0x7a8000'
示例 6-4:添加起始数字和 cryptsize 的十六进制值
好了,这个例子真的差不多完成了,别担心。从这里,你只需要将你的数字插入以下 lldb 命令中:
(lldb) memory read --force --outfile /tmp/mem.bin --binary 0x00008000 0x007a8000
8011776 bytes written to '/private/tmp/mem.bin'
当然,这并不会给你一个完整、可用的二进制文件——只是一个内存转储。这个映像缺少 Mach-O 头部的元数据。为了解决这个问题,你需要将内存转储移植到一个有效的二进制文件中,为此,你首先需要复制原始二进制文件,并使用scp将其复制到你的开发机上。
然后,你需要将未加密的内存转储内容复制到捐赠二进制文件中,替换掉加密的部分。你可以使用dd命令来完成此操作,并指定seek参数来确定数据开始写入的位置。seek参数应该是vmaddr加上cryptoff的值,在这个例子中是 0x8000。以下是这个例子中的dd命令:
$ dd bs=1 seek=0x8000 conv=notrunc if=/tmp/mem.bin of=Snapchat-decrypted
接下来,你需要将捐赠二进制文件的cryptid值更改为 0,表示未加密的二进制文件。这个操作有几种方法。你可以使用 MachOView^(7)(见图 6-2),它提供了一个易于使用的界面来检查和修改 Mach-O 二进制文件,或者你也可以使用你选择的十六进制编辑器。如果你使用十六进制编辑器,我发现最简单的方法是首先通过搜索2100 0000 1400 0000来找到LC_ENCRYPTION_INFO命令。^(8) 接下来的 16 个数字将是偏移量和大小,后面跟着0100 0000。那个字节就是cryptid,将其更改为0000 0000。
一旦你禁用了cryptid标志,你需要将修改后的二进制文件复制回设备。将修改后的二进制文件放置好后,你可以使用vbindiff验证更改,vbindiff可以通过 Homebrew 安装。vbindiff的输出应该如下所示,见清单 6-5。

图 6-2:使用 MachOView 查看encrypted标志

清单 6-5:使用 vbindiff 验证更改后的cryptid值
➊和➋处的行分别显示了启用和禁用的cryptid位(加粗显示)。现在,如果一切顺利,你就可以开始认真地解剖这个二进制文件了。
从解密二进制文件进行逆向工程
由于 Mach-O 二进制格式结构相对透明,在 iOS 上进行基本的逆向工程是一个相当简单的任务——至少在你获得解密后的二进制文件后。几种工具可以帮助你理解类定义、检查汇编指令,并提供有关二进制文件构建的详细信息。最有用且容易获得的工具是 otool 和 class-dump。你还可以查看 Cycript 和 Hopper,作为逆向特别顽固应用程序的工具。
使用 otool 检查二进制文件
otool 长期以来一直是基本 OS X 工具包的一部分,用于检查 Mach-O 二进制文件。它的当前版本支持 ARM 和 amd64 架构,并可以选择使用 llvm 来反汇编二进制文件。要查看程序的基本内部结构,你可以使用otool -oV命令查看数据段,如清单 6-6 所示。
$ otool -oV MobileMail
MobileMail:
Contents of (__DATA,__objc_classlist) section
000c2870 0xd7be8
isa 0xd7bd4
superclass 0x0
cache 0x0
vtable 0x0
data 0xc303c (struct class_ro_t *)
flags 0x0
instanceStart 80
instanceSize 232
ivarLayout 0x0
name 0xb48ac MailAppController
baseMethods 0xc3064 (struct method_list_t *)
entsize 12
count 122
name 0xa048e toolbarFixedSpaceItem
types 0xb5bb0 @8@0:4
imp 0x40c69
name 0xa04a4 sidebarQuasiSelectTintColor
types 0xb5bb0 @8@0:4
imp 0x40ccd
name 0xa04c0 sidebarMultiselectTintColor
types 0xb5bb0 @8@0:4
imp 0x40d75
name 0xa04dc sidebarTintColor
types 0xb5bb0 @8@0:4
imp 0x130f5
name 0xa04ed updateStyleOfToolbarActivityIndicatorView:
inView:
types 0xb5c34 v16@0:4@8@12
imp 0x18d69
清单 6-6:otool 显示__OBJC段的内容
这让你能够查看类名和方法名,以及 ivars 的信息,前提是这些是在 Objective-C 中实现的,而不是纯 C++。要查看程序的文本段,可以使用 otool -tVq。-q 表示你希望使用 llvm 作为反汇编器,而不是 otool 内置的反汇编器,后者通过 -Q 指定。输出的差异不大,但 llvm 看起来最适合执行此任务,因为它很可能最初就负责将二进制文件汇编起来。它还提供了稍微更易读的输出。清单 6-7 展示了 otool -tVq 的示例输出。
MobileMail:
(__TEXT,__text) section
00003584 0000 movs r0, r0
00003586 e59d b 0x30c4
00003588 1004 asrs r4, r0, #32
--snip--
000035ca 447a add r2, pc
000035cc 6801 ldr r1, [r0]
000035ce 6810 ldr r0, [r2]
000035d0 f0beecf0 blx 0xc1fb4 @ symbol stub for: _objc_msgSend
000035d4 f2417128 movw r1, #5928
000035d8 f2c0010d movt r1, #13
000035dc 4479 add r1, pc
000035de 6809 ldr r1, [r1]
000035e0 f0beece8 blx 0xc1fb4 @ symbol stub for: _objc_msgSend
000035e4 4606 mov r6, r0
清单 6-7:otool 的反汇编输出
在这里,你可以看到方法的实际反汇编,以及一些基本的符号信息。要获取所有符号的转储,可以使用 otool -IV,如 清单 6-8 所示。
$ otool -IV MobileMail
MobileMail:
Indirect symbols for (__TEXT,__symbolstub1) 241 entries
address index name
0x000c1c30 3 _ABAddressBookFindPersonMatchingEmailAddress
0x000c1c34 4 _ABAddressBookRevert
0x000c1c38 5 _ABPersonCopyImageDataAndCropRect
0x000c1c3c 7 _CFAbsoluteTimeGetCurrent
0x000c1c40 8 _CFAbsoluteTimeGetGregorianDate
0x000c1c44 9 _CFArrayAppendValue
0x000c1c48 10 _CFArrayCreateMutable
0x000c1c4c 11 _CFArrayGetCount
0x000c1c50 12 _CFArrayGetFirstIndexOfValue
0x000c1c54 13 _CFArrayGetValueAtIndex
0x000c1c58 14 _CFArrayRemoveValueAtIndex
0x000c1c5c 15 _CFArraySortValues
0x000c1c60 16 _CFDateFormatterCopyProperty
0x000c1c64 17 _CFDateFormatterCreate
清单 6-8:使用 otool 检查符号
使用 class-dump 获取类信息
class-dump^(9) 工具用于从 Objective-C 2.0 二进制文件中提取类信息。最终输出基本上相当于给定二进制文件的头文件。这可以为程序的设计和结构提供极好的洞察,使 class-dump 成为逆向工程中不可或缺的工具。Steve Nygard 最初的 class-dump 仅在 OS X 上运行,但它支持 armv7 架构,因此你可以将文件复制到桌面进行分析。还有一个修改版本,class-dump-z,^(10) 可以在 Linux 和 iOS 上运行。到目前为止,class-dump 似乎更新更为及时且功能更全,因此我建议继续使用它。
你可以将 class-dump 测试应用于任何未加密的 iOS 二进制文件。最简单的方式是将 /Applications 中的内建 Apple 应用复制过来,并对其二进制文件运行 class-dump,如 清单 6-9 所示。
$ class-dump MobileMail
--snip--
@interface MessageHeaderHeader : _AAAccountConfigChangedNotification <
MessageHeaderAddressBookClient, UIActionSheetDelegate>
{
MailMessage *_lastMessage;
id <MessageHeaderDelegate> _delegate;
UIWebBrowserView *_subjectWebView;
DOMHTMLElement *_subjectTextElement;
UILabel *_dateLabel;
unsigned int _markedAsUnread:1;
unsigned int _markedAsFlagged:1;
unsigned int _isOutgoing:1;
UIImageView *_unreadIndicator;
UIImageView *_flaggedIndicator;
WorkingPushButton *_markButton;
id _markUnreadTarget;
SEL _markUnreadAction;
ABPersonIconImageView *_personIconImageView;
SeparatorLayer *_bottomSeparator;
SeparatorLayer *_topSeparator;
float _horizontalInset;
unsigned int _allowUnreadStateToBeShown:1;
}
- (id)initWithFrame:(struct CGRect)fp8;
- (void)dealloc;
清单 6-9:class-dump 显示 MobileMail 的接口详情
令人愉快,不是吗?一旦你有了已解密的二进制文件,大多数 Objective-C 应用程序都会很快变得透明。
使用 Cycript 从运行中的程序提取数据
如果你不想经历解密二进制文件以获取其内部信息的麻烦,可以使用 Cycript^(11) 从正在运行的可执行文件中提取一些信息。使用 Cycript 与运行中的应用程序进行交互有许多技巧,但你可能最感兴趣的是使用 weak_classdump.cy^(12) 来模拟 class-dump 的功能。在 Contacts 应用程序运行时,你可以这样提取 class-dump 信息:
$ curl -OL https://raw.github.com/limneos/weak_classdump/master/
weak_classdump.cy
$ cycript -p Contacts weak_classdump.cy
'Added weak_classdump to "Contacts" (3229)'
$ cycript -p Contacts
cy# weak_classdump_bundle([NSBundle mainBundle],"/tmp/contactsbundle")
"Dumping bundle... Check syslog. Will play lock sound when done."
这将把每个类的头文件写入 /tmp/contactsbundle 目录。
请注意,为了通过 cURL 安全地获取数据,你需要在设备上安装 CA 证书包。如果你使用 MacPorts 且本地已安装 cURL,请执行以下操作:
$ scp /opt/local/share/curl/curl-ca-bundle.crt \
root@de.vi.c.e:/etc/ssl/certificates/ca-certificates.crt
或者,如果你使用 Homebrew 并安装了 OpenSSL 配方,可以使用以下命令:
$ scp /usr/local/etc/openssl/cert.pem \
root@de.vi.c.e:/etc/ssl/certificates/ca-certificates.crt}
使用 Hopper 进行反汇编
在没有源代码的情况下,可能会有一些需要更深入查看程序实际逻辑的情况。虽然 IDA Pro^(13)对于此类情况很有用,但它的价格相当高。我通常使用 Hopper^(14)来进行反汇编、反编译,并在黑箱测试过程中制作流程图。虽然汇编语言和反编译超出了本书的范围,但让我们快速看一下 Hopper 如何展示程序逻辑。查看 Hopper 中的一个基本密码管理器(图 6-3),你会发现一个名为storeSavedKeyFor:的方法,看起来很有前景。

图 6-3:storeSavedKeyFor:函数的反汇编
如果你在这段代码中调用反编译器(if(b)按钮),Hopper 会生成伪代码,帮助你理解程序的实际流程,如图 6-4 所示。

图 6-4:反编译器生成的代码
注意,PearlLogger类正在实例化,并且有一个引用当前正在存储的用户名。var_64显示该用户名被传递给日志功能,可能是传递给 NSLog 功能——这是不好的,原因我将在第十章中进一步解释。然而,你也可以看到该项正在被存储在具有限制性保护属性的钥匙串中(kSecAttrAccessibleWhenUnlockedThisDeviceOnly,在第十三章中有进一步详细说明),这是程序的一个优点。
汇编语言和反编译是广泛的领域,但 Hopper 为你提供了一种非常好的方式,以相对较低的价格开始通过汇编进行逆向工程。如果你想开始培养阅读 ARM 汇编的技能,可以查看 Ray Wenderlich 的教程:www.raywenderlich.com/37181/ios-assembly-tutorial/。
击败证书钉扎
证书钉扎旨在防止恶意的 CA 为你的网站签发伪造(但看起来有效)的证书,从而拦截你的网络端点和应用程序之间的通信。这是一个很好的想法(我将在第七章中讨论如何实现它),但它确实使得黑箱测试变得稍微困难一些。
我和我的同事们经常遇到这个问题,以至于我们编写了一个工具来帮助我们解决它:iOS SSL Killswitch^(15)。Killswitch 工具钩取通过 URL 加载系统的请求,防止验证任何 SSL 证书,确保你可以通过代理运行任何黑箱应用,无论它是否使用证书钉扎。
要安装 Killswitch 工具,复制预编译的 .deb 文件到你的设备,并使用 dpkg 工具进行安装。
# scp ios-ssl-kill-switch.deb root@192.168.1.107
# ssh root@192.168.1.107
(and then, on the test device)
# dpkg -i ios-ssl-kill-switch.deb
# killall -HUP SpringBoard
然后,你可以在设置应用中找到 iOS SSL Killswitch(参见 图 6-5),在这里你可以切换开关。

图 6-5:从设置应用程序中启用 SSL Killswitch 工具
使用 Cydia Substrate 进行 Hook
在越狱设备上(你将在这些设备上执行黑盒测试),你可以使用 Cydia Substrate^(16)(以前称为 Mobile Substrate)来修改基础系统的行为,从而获取有关应用程序活动的更多信息或改变应用程序行为。你的目标可能是禁用某些安全或验证机制(就像 iOS SSL Killswitch 所做的那样),或者仅仅是当某些 API 被使用时通知你,并显示它们传递的参数。Cydia Substrate 钩取被称为 tweaks。
开始开发 Cydia Substrate 修改的最简便方法是使用 Theos 工具包。^(17) 要创建一个新的修改,使用 Theos 中包含的 nic.pl 脚本。请注意,Theos 默认是面向调整 SpringBoard 应用程序的行为,以便自定义用户界面元素。不过,鉴于本书中的目的,你将需要影响所有应用程序,因此你需要指定一个 com.apple.UIKit 的 Bundle 过滤器。这个过滤器将配置 Mobile/Cydia Substrate,在任何链接到 UIKit 框架的应用程序中加载你的修改(也就是说,显示用户界面的应用程序),但不会影响像系统守护进程或命令行工具等其他程序。
首先,你需要获取 Link Identity Editor,ldid,^(18) 这是 Theos 用来生成修改的签名和授权的工具。下面是获取 ldid 的方法:
$ git clone git://git.saurik.com/ldid.git
$ cd ldid
$ git submodule update --init
$ ./make.sh
$ sudo cp ./ldid /usr/local/bin
然后,你可以克隆 Theos 仓库并继续生成一个修改模板,具体步骤如下:
$ git clone git://github.com/DHowett/theos.git ~/git/theos
$ cd /tmp && ~/git/theos/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
[1.] iphone/application
[2.] iphone/library
[3.] iphone/preference_bundle
[4.] iphone/tool
[5.] iphone/tweak
Choose a Template (required): 5
Project Name (required): MyTweak
Package Name [com.yourcompany.mytweak]:
Author/Maintainer Name [dthiel]:
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]: com.apple.
UIKit
Instantiating iphone/tweak in mytweak/...
Done.
这将创建一个 Tweak.xm 文件,默认情况下其所有内容都被注释掉。包括了钩取类方法或实例方法的存根,可以带参数也可以不带参数。
你可以编写的最简单的 Hook 类型就是仅记录方法调用和参数;这里有一个例子,它钩取了 UIPasteboard 的两个类方法:
%hook UIPasteboard
+ (UIPasteboard *)pasteboardWithName:(NSString *)pasteboardName create:(BOOL)create
{
%log;
return %orig;
}
+ (UIPasteboard *)generalPasteboard
{
%log;
return %orig;
}
%end
这段代码使用了 Logos^(19) 指令,如 %hook 和 %log。Logos 是 Theos 的一个组件,旨在使方法钩取代码的编写变得简单。然而,实际上也可以仅使用 C 语言来编写具有相同功能的修改。
你还需要提供完整的方法签名,可以通过 API 文档或框架头文件获得。一旦你对修改进行了满意的自定义,你可以使用 nic.pl 提供的 Makefile 来构建它。
要构建适合安装到越狱设备上的 Debian 包,你还需要安装 dpkg 工具。你可以通过 MacPorts^(20)的port命令或 Homebrew^(21)的brew命令来安装。此示例使用port:
$ sudo port install dpkg
--snip--
$ make
Bootstrapping CydiaSubstrate...
Compiling iPhoneOS CydiaSubstrate stub... default target?
Compiling native CydiaSubstrate stub...
Generating substrate.h header...
Making all for tweak MyTweak...
Preprocessing Tweak.xm...
Compiling Tweak.xm...
Linking tweak MyTweak...
Stripping MyTweak...
Signing MyTweak...
$ make package
Making all for tweak MyTweak...
make[2]: Nothing to be done for `internal-library-compile'.
Making stage for tweak MyTweak...
dpkg-deb: building package `com.yourcompany.mytweak' in `./com.yourcompany.
mytweak_0.0.1-1_iphoneos-arm.deb'.
运行这些命令应该会生成一个可以安装到 iOS 设备上的包。首先,你可以使用scp命令将文件复制到设备,并手动加载它。之后,你可以直接在命令行使用dpkg -i(如下代码所示)或设置自己的 Cydia 仓库。^(22)
$ dpkg -i com.yourcompany.mytweak_0.0.1-1_iphoneos-arm.deb
Selecting previously deselected package com.yourcompany.mytweak.
(Reading database ... 3551 files and directories currently installed.)
Unpacking com.yourcompany.mytweak (from com.yourcompany.mytweak_0.0.1-1_iphoneos-
arm.deb) ...
Setting up com.yourcompany.mytweak (0.0.1-1) ..
当此过程完成后,你可以通过dpkg命令进一步管理该包(使用dpkg -P删除)或通过 Cydia 进行管理,如图 6-6 所示。

图 6-6:你自己的插件在 Cydia 管理界面中的展示
安装一个插件后,如果你查看系统日志,你会看到 Cydia Substrate 动态库在启动所有应用时被加载。你还会看到插件记录的钩取方法调用日志。以下是一个示例日志:
May 2 14:22:08 my-iPad Maps~ipad[249]: MS:Notice: Loading: /Library/
MobileSubstrate/DynamicLibraries/MyTweak.dylib
May 2 14:22:38 lxs-iPad Maps~ipad[249]: +[<UIPasteboard: 0x3ef05408>
generalPasteboard]
当然,插件除了记录之外,还有许多其他功能;参见 iOS SSL Killswitch 工具的Tweak.xm文件,了解如何修改方法行为以及如何设置自己的首选项切换。^(23)
使用 Introspy 自动化钩取
虽然插件对于一次性钩取场景非常有用,但我的同事 Alban Diquet 和 Tom Daniels 使用 Cydia Substrate 框架开发了一个名为 Introspy^(24)的工具,可以帮助自动化黑盒测试中的钩取过程,而无需深入挖掘 iOS 或 Cydia Substrate 的底层。Introspy 直接使用 Cydia Substrate 框架(而不是通过 Theos)来钩取安全敏感的函数调用,并记录它们的参数和返回值,之后可以用来生成报告。要安装 Introspy,下载最新的预编译.deb包,地址为github.com/iSECPartners/Introspy-iOS/releases/,将其复制到你的设备上,并在设备上输入命令dpkg -i *filename*来添加该包。
安装完成后,使用以下命令重新启动设备:
$ killall -HUP SpringBoard
对于任何你想测试的应用程序,如果它已经在运行,进行相同的操作。现在,你可以告诉 Introspy 你想要钩取的应用程序,以及你希望记录的 API 调用(见图 6-7)。测试完成后,如果你正在测试 Apple 内建应用或 Cydia 应用,一个 SQLite 数据库文件会被保存在/var/mobile目录下;如果你正在测试来自 App Store 的应用,则会保存在/User/Applications/

图 6-7:Introspy 设置屏幕。你可以在“应用”选项卡中选择要分析的应用程序。
若要分析这个数据库,你需要使用 Introspy 分析器,^(25),它将生成 Introspy 发现的 HTML 报告(见图 6-8)。

图 6-8:Introspy HTML 报告输出,显示与指定签名匹配的发现列表
如果你将这个数据库复制到你的测试机器上,你可以使用introspy.py生成关于调用的 API 的报告,如下所示:
$ python ./introspy.py --outdir report mydatabase.db
Introspy 的新版还允许通过指定设备的 IP 地址,自动复制和解析数据库。
$ python ./introspy.py -p ios -o outputdir -f device.ip.address
运行 Introspy 将评估调用与潜在问题 API 的签名数据库,帮助你追踪潜在的关注点。为了减少噪音,你可以使用--group和--sub-group标志过滤掉特定的 API 类别或签名类型。安装 Introspy 后,在命令行中输入introspy.py --help获取详细信息。
总结思考
尽管黑盒测试面临一些挑战,但开发社区已经做出了很大的努力,使其变得可行,黑盒测试的某些元素将帮助你,无论你是否拥有源代码。现在你将把主要精力重新集中在白盒测试上;在第七章,我将引导你了解 iOS 中一些最具安全敏感性的 API,包括 IPC 机制、加密功能以及数据如何在应用程序中无意间泄漏的多种方式。
第三部分
Cocoa API 的安全特性
第七章:7
iOS 网络
几乎所有应用都会使用三种 iOS 网络 API 中的一个或多个。按照抽象层级的顺序,它们分别是 URL 加载系统、Foundation NSStream API 和 Core Foundation CFStream API。URL 加载系统用于通过 URL 获取和操作数据,比如网络资源或文件。NSStream 和 CFStream 类则是稍低级的方法,用于处理网络连接,但它们并不直接到达套接字层级。这些类用于非 HTTP 基础的通信,或当你需要更直接控制网络行为时。
在本章中,我将详细讨论 iOS 网络,从高层次的 API 开始。对于大多数用途,应用可以使用高层次的 API,但也有一些情况是这些 API 不能完全满足需求的。然而,使用低层次的 API 时,需要考虑更多的陷阱。
使用 iOS URL 加载系统
URL 加载系统能够处理应用程序需要执行的大多数网络任务。与 URL API 交互的主要方式是构造一个 NSURLRequest 对象,并利用它实例化一个 NSURLConnection 对象,以及一个接收连接响应的代理。当响应完全接收后,代理会收到一个 connection:didReceiveResponse 消息,参数是一个 NSURLResponse 对象。^(1)
但并非每个人都能正确使用 URL 加载系统的功能,因此在本节中,我将首先展示如何发现一个绕过传输层安全性的应用。接着,你将学到如何通过证书验证端点,避免开放重定向的危险,并实现证书固定,限制你的应用信任的证书数量。
正确使用传输层安全性
传输层安全性 (TLS),现代的替代 SSL 的规范,对于几乎所有网络应用的安全至关重要。正确使用 TLS 时,它不仅能确保通过连接传输的数据机密性,还能验证远程端点,确保呈现的证书是由受信任的证书颁发机构签名的。默认情况下,iOS 会做正确的事情™,拒绝连接任何拥有不受信任或无效证书的端点。但在各种应用中,无论是移动端还是其他类型,开发者经常明确禁用 TLS/SSL 端点验证,从而让应用的流量容易被网络攻击者拦截。
在 iOS 中,TLS 可以通过多种方式禁用。过去,开发者通常会使用 NSURLRequest 的未记录的私有类方法 setAllowsAnyHTTPSCertificate 来轻松禁用验证。苹果公司很快开始拒绝使用此方法的应用,就像它对使用私有 API 的应用所做的那样。然而,仍然存在一些混淆方法,可能会让这个 API 在审核过程中悄悄通过,因此需要检查代码库,确保该方法没有被其他名字调用。
还有一种更具灾难性的绕过 TLS 验证的方法。这也很可能会导致你的应用被拒绝,但它说明了类别的重要性。我曾经有一个客户,他们授权了一个本应相当简单的第三方代码,并将其包含在产品中。尽管该项目的其他地方都正确处理了 TLS,但他们更新后的第三方代码没有验证任何 TLS 连接。显然,第三方供应商实现了 NSURLRequest 的一个类别,使用 allowsAnyHTTPSCertificateForHost 方法来避免验证。该类别仅包含指令 return YES;,导致所有 NSURLRequest 安静地忽略错误的证书。这个教训是什么?测试代码,别做假设!另外,你必须审计第三方代码,就像审计你自己代码库中的其他代码一样。错误可能不是你的错,但没人会关心这个。
注意
幸运的是,在 iOS 9 中,意外禁用 TLS 的错误变得更加困难,因为默认情况下,iOS 不允许应用进行非 TLS 连接。相反,开发者需要在应用的 Info.plist 中为通过明文 HTTP 访问的 URL 放置一个特定的例外。然而,这并不能解决故意禁用 TLS 保护的情况。
现在,实际上有一个官方的 API 可以绕过 TLS 验证。你可以使用 NSURLConnection 的委托并实现 NSURLConnectionDelegate 协议。^(2) 委托必须实现 willSendRequestForAuthenticationChallenge 方法,然后可以调用 continueWithoutCredentialForAuthenticationChallenge 方法。这是当前的最新方法;你也可能会看到使用 connection:canAuthenticateAgainstProtectionSpace: 或 connection:didReceiveAuthenticationChallenge: 的旧代码。示例 7-1 显示了如何在实际中看到这样的做法。
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)
challenge {
NSURLProtectionSpace *space = [challenge protectionSpace];
if([[space authenticationMethod] isEqualToString:NS
URLAuthenticationMethodServerTrust]) {
NSURLCredential *cred = [NSURLCredential credentialForTrust:
[space serverTrust]];
[[challenge sender] useCredential:cred forAuthenticationChallenge:
challenge];
}
}
示例 7-1:响应挑战时发送虚拟的 NSURLCredential 对象
这段代码看起来相当无害,特别是因为它在各处都使用了 protection、credential、authentication 和 trust 等词汇。实际上,它所做的是绕过 TLS 端点的验证,使连接容易受到拦截。
当然,我并不是鼓励你在应用程序中实际做任何绕过 TLS 验证的事情。你不应该这样做,如果你这么做,你就是个坏人。这些示例只是展示了你在检查代码时可能会看到的模式。这些模式可能很难发现和理解,但如果你看到绕过 TLS 验证的代码,务必进行修改。
使用 NSURLConnection 的基本认证
HTTP 基本认证并不是一种特别强大的认证机制。它不支持会话管理或密码管理,因此用户不能在不使用单独应用程序的情况下注销或更改密码。但对于某些任务,例如对 API 的认证,这些问题并不那么重要,你仍然可能会在应用程序的代码库中遇到这种机制,或者被要求自己实现它。
你可以使用NSURLSession或NSURLConnection来实现 HTTP 基本认证,但无论是编写应用程序还是检查他人的代码,你都需要注意几个陷阱。
最简单的实现使用NSURLConnection的willSendRequestForAuthenticationChallenge委托方法:
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)
challenge {
NSString *user = @"user";
NSString *pass = @"pass";
if ([[challenge protectionSpace] receivesCredentialSecurely] == YES &&
[[[challenge protectionSpace] host] isEqualToString:@"myhost.com"]) {
NSURLCredential *credential = [NSURLCredential credentialWithUser:user password
:pass persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential
forAuthenticationChallenge:challenge];
}
}
委托对象首先会传递一个NSURLAuthenticationChallenge对象。接着,它创建一个包含用户名和密码的凭证,用户名和密码可以由用户提供,或从钥匙串中获取。最后,挑战的发送者会将凭证和挑战一起返回。
实现 HTTP 基本认证时需要注意两个潜在问题。首先,避免将用户名和密码存储在源代码或共享偏好设置中。你可以使用NSURLCredentialStorage API 自动将用户提供的凭证存储在钥匙串中,使用sharedCredentialStorage,如示例 7-2 所示。
➊ NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:
@"myhost.com" port:443 protocol:@"https" realm:nil authenticationMethod:nil];
➋ NSURLCredential *credential = [NSURLCredential credentialWithUser:user password:
pass persistence:NSURLCredentialPersistencePermanent];
➌ [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
forProtectionSpace:protectionSpace];
示例 7-2:设置保护空间的默认凭证
这会创建一个保护空间➊,其中包括主机、端口、协议,以及可选的 HTTP 认证领域(如果使用 HTTP 基本认证)和认证方法(例如,使用 NTLM 或其他机制)。在➋,示例创建一个凭证,凭证包含最有可能从用户输入中接收到的用户名和密码。然后,在➌,它将凭证设置为该保护空间的默认凭证,并且凭证应该会自动存储在钥匙串中。未来,属于该代码的应用程序可以使用相同的 API 通过defaultCredentialForProtectionSpace方法读取凭证,如示例 7-3 所示。
credentialStorage = [[NSURLCredentialStorage sharedCredentialStorage]
defaultCredentialForProtectionSpace:protectionSpace];
示例 7-3:使用保护空间的默认凭证
需要注意的是,存储在 sharedCredentialStorage 中的凭证会被标记为钥匙串属性 kSecAttrAccessibleWhenUnlocked。如果你需要更严格的保护,你需要自行管理钥匙串存储。我在第十三章中讲解了如何管理钥匙串。
此外,请确保在创建凭证时,注意如何指定 persistence 参数的值。如果你使用 NSURLCredentialStorage 存储在钥匙串中,可以在创建凭证时使用 NSURLCredentialPersistencePermanent 或 NSURLCredentialPersistenceSynchronizable 类型。如果你是用认证做一些更临时的操作,NSURLCredentialPersistenceNone 或 NSURLCredentialPersistenceForSession 类型更为合适。你可以在表 7-1 中找到每种持久性类型的详细含义。
表 7-1: 凭证持久性类型
| 持久性类型 | 含义 |
|---|---|
NSURLCredentialPersistenceNone |
完全不存储凭证。仅在你需要对受保护资源进行一次请求时使用此项。 |
NSURLCredentialPersistenceForSession |
将凭证存储在应用程序的生命周期内。 |
NSURLCredentialPersistencePermanent |
将凭证存储在钥匙串中。 |
NSURLCredentialPersistenceForSession |
将凭证存储在应用程序的生命周期内。若你只在应用运行时需要凭证,可以使用此项。 |
NSURLCredentialPersistencePermanent |
将凭证存储在钥匙串中。当你希望在用户安装应用程序时,凭证能够持续存在时使用此项。 |
NSURLCredentialPersistenceSynchronizable |
将凭证存储在钥匙串中,并允许其同步到其他设备和 iCloud。当你希望用户在设备之间传输凭证并且不担心将凭证发送到像 iCloud 这样的第三方时使用此项。 |
使用 NSURLConnection 实现 TLS 双向认证
执行客户端认证的最佳方法之一是使用客户端证书和私钥;然而,这在 iOS 上有些复杂。基本概念相对简单:实现 willSendRequestForAuthenticationChallenge(以前为 didReceiveAuthenticationChallenge)的代理,检查认证方法是否为 NSURLAuthenticationMethodClientCertificate,检索并加载证书和私钥,构建凭证,并使用凭证进行挑战。不幸的是,Cocoa 没有内置的 API 用于管理证书,因此你需要在 Core Foundation 中进行一些操作,像下面这个基本框架一样:
- (void)connection:(NSURLConnection *) willSendRequestForAuthenticationChallenge:(
NSURLAuthenticationChallenge *)challenge {
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NS
URLAuthenticationMethodClientCertificate]) {
SecIdentityRef identity;
SecTrustRef trust;
➊ extractIdentityAndTrust(somep12Data, &identity, &trust);
SecCertificateRef certificate;
➋ SecIdentityCopyCertificate(identity, &certificate);
➌ const void *certificates[] = { certificate };
➍ CFArrayRef arrayOfCerts = CFArrayCreate(kCFAllocatorDefault, certificates,
1, NULL);
➎ NSURLCredential *cred = [NSURLCredential credentialWithIdentity:identity
certificates:(__bridge NSArray*)arrayOfCerts
persistence:NSURLCredentialPersistenceNone];
➏ [[challenge sender] useCredential:cred
forAuthenticationChallenge:challenge];
}
}
这个示例创建了一个 SecIdentityRef 和 SecTrustRef,以便可以将它们传递给 ➊ 处的 extractIdentityAndTrust 函数。这个函数会从一个 PKCS #12 数据块(文件扩展名为 .p12)中提取身份和信任信息。这些归档文件只是将一组加密对象集中存储在一个地方。
然后,代码将创建一个 SecCertificateRef,并从身份中提取证书 ➋。接着,它构建一个数组,包含在 ➌ 处的唯一证书,并创建一个 CFArrayRef 来存放该证书 ➍。最后,代码创建一个 NSURLCredential,将其身份和仅包含一个元素的证书数组传入 ➎,并将此凭据作为挑战的答案呈现 ➏。
你会注意到在 ➊ 处有一些不明确的描述。这是因为获取实际证书 p12 数据有几种不同的方法。你可以执行一次性引导,通过安全通道获取新生成的证书,或者在本地生成证书,或者从文件系统读取证书,或者从钥匙串中获取证书。获取 somep12Data 中使用的证书信息的一种方式是从文件系统中检索,方法如下:
NSData *myP12Certificate = [NSData dataWithContentsOfFile:path];
CFDataRef somep12Data = (__bridge CFDataRef)myP12Certificate;
存储证书的最佳位置当然是钥匙串;我会在 第十三章进一步讲解。
修改重定向行为
默认情况下,NSURLConnection 会在遇到 HTTP 重定向时跟随它。然而,当发生重定向时,它的行为是比较特殊的。当重定向被触发时,NSURLConnection 会将请求发送到新位置,并携带原始 NSURLHttpRequest 中的 HTTP 头信息。不幸的是,这也意味着你当前的原始域名的 Cookie 会被传递到新位置。因此,如果攻击者能够让你的应用访问一个接受任意 URL 作为重定向目标的页面,那么该攻击者就能窃取你的用户 Cookie,以及你应用可能存储在 HTTP 头中的任何其他敏感数据。这种漏洞被称为 开放重定向。
你可以通过在 iOS 4.3 及更早版本的 NSURLConnectionDelegate 上实现 connect:willSendRequest: redirectResponse^(3),或者在 iOS 5.0 及更高版本的 NSURLConnectionDataDelegate 上实现此方法来修改这一行为。^(4)
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
{
NSURLRequest *newRequest = request;
➊ if (![[[redirectResponse URL] host] isEqual:@"myhost.com"]) {
return newRequest;
}
else {
➋ newRequest = nil;
return newRequest;
}
}
在 ➊ 处,这段代码会检查你要重定向的域名是否与网站的名称相同。如果相同,它会继续正常执行。如果不同,它会将请求修改为 nil ➋。
TLS 证书固定
在过去几年中,关于证书颁发机构(CAs,负责担保我们日常遇到的 TLS 证书的实体)出现了一些令人担忧的发展。除了客户端应用程序信任的签名机构数量庞大外,CAs 还发生了几次显著的安全漏洞事件,包括签名密钥被泄露或颁发过于宽松的证书。这些漏洞使得任何拥有签名密钥的人都可以冒充任何 TLS 服务器,意味着他们可以成功且透明地读取或修改请求及其响应。
为了帮助缓解这些攻击,许多类型的客户端应用程序实现了 证书固定。这个术语可以指代多种不同的技术,但核心思想是通过编程限制应用程序信任的证书数量。您可以将信任限制为单一 CA(即您的公司用于签署服务器证书的 CA),限制为您用来创建自己证书的内部根 CA(即信任链的顶部),或者仅限于一个叶证书(信任链底部的一个特定证书)。
作为 SSL Conservatory 项目的一部分,我的同事 Alban Diquet 开发了一些方便的封装器,使您能够在应用程序中实现证书固定。(了解更多内容,请访问 github.com/iSECPartners/ssl-conservatory。)您可以编写自己的封装器,也可以使用现有的封装器;无论哪种方式,一个好的封装器可以使证书固定变得相当简单。例如,下面是如何通过 Alban 的封装器轻松实现证书固定:
➊ - (NSData*)loadCertificateFromFile:(NSString*)fileName {
NSString *certPath = [[NSString alloc] initWithFormat:@"%@/%@", [[NSBundle
mainBundle] bundlePath], fileName];
NSData *certData = [[NSData alloc] initWithContentsOfFile:certPath];
return certData;
}
- (void)pinThings {
NSMutableDictionary *domainsToPin = [[NSMutableDictionary alloc] init];
➋ NSData *myCertData = [self loadCertificateFromFile:@"myCerts.der"];
if (myCertData == nil) {
NSLog(@"Failed to load the certificates");
return;
}
➌ [domainsToPin setObject:myCertData forKey:@"myhost.com"];
➍ if ([SSLCertificatePinning loadSSLPinsFromDERCertificates:domainsToPin] != YES) {
NSLog(@"Failed to pin the certificates");
return;
}
}
在 ➊ 处,这段代码简单地定义了一个方法,从 DER 格式的文件中加载证书到 NSData 对象,并在 ➋ 处调用此方法。如果加载成功,代码会将 myCertData 放入 NSMutableDictionary ➌ 中,并调用主 SSLCertificatePinning 类的 loadSSLPinsFromDERCertificates 方法 ➍。加载这些固定证书后,应用程序还需要实现一个 NSURLConnection 委托,如 列表 7-4 所示。
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)
challenge {
if([challenge.protectionSpace.authenticationMethod isEqualToString:NS
URLAuthenticationMethodServerTrust]) {
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
NSString *domain = [[challenge protectionSpace] host];
SecTrustResultType trustResult;
SecTrustEvaluate(serverTrust, &trustResult);
if (trustResult == kSecTrustResultUnspecified) {
// Look for a pinned public key in the server's certificate chain
if ([SSLCertificatePinning verifyPinnedCertificateForTrust:serverTrust
andDomain:domain]) {
// Found the certificate; continue connecting
[challenge.sender useCredential:[NSURLCredential credentialForTrust
:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
else {
// Certificate not found; cancel the connection
[[challenge sender] cancelAuthenticationChallenge: challenge];
}
}
else {
// Certificate chain validation failed; cancel the connection
[[challenge sender] cancelAuthenticationChallenge: challenge];
}
}
}
列表 7-4:一个 NSURLConnection 委托,用于处理证书固定逻辑
这段代码简单地评估远程服务器提供的证书链,并将其与应用程序中包含的固定证书进行比较。如果找到固定证书,则连接继续;如果没有找到,则取消认证挑战过程。
按照所示实现您的代理后,所有NSURLConnection的使用应该检查确保它们绑定到您预定义列表中的域名和证书对。如果您感兴趣,可以在github.com/iSECPartners/ssl-conservatory/tree/master/ios找到其余的代码来实现您自己的证书钉扎。涉及的其他逻辑相当复杂,所以我无法在这里展示所有代码。
注意
如果您很急,可以使用 SSL Conservatory 示例代码中的代理类进行子类化。
到目前为止,我展示了围绕NSURLConnection的网络安全问题和解决方案。但从 iOS 7 开始,NSURLSession比传统的NSURLConnection类更为推荐。让我们更详细地看看这个 API。
使用 NSURLSession
NSURLSession类通常更受开发者青睐,因为它专注于使用网络会话,而不是NSURLConnection专注于单个请求。虽然NSURLSession在某种程度上扩大了NSURLConnection的范围,但它还通过允许在单个会话上设置配置,而不是在应用程序中全局设置配置,提供了更多的灵活性。一旦会话被实例化,它们将被分配个别任务来执行,使用NSURLSessionDataTask、NSURLSessionUploadTask和NSURLSessionDownloadTask类。
在本节中,您将探索一些使用NSURLSession的方法,一些潜在的安全陷阱,以及一些NSURLConnection未提供的安全机制。
NSURLSession 配置
NSURLSessionConfiguration类封装了传递给NSURLSession对象的选项,以便您可以为不同类型的请求提供独立的配置。例如,您可以对获取不同敏感级别数据的请求应用不同的缓存和 cookie 策略,而不是让这些策略在整个应用程序中全局应用。要使用NSURLSession的系统策略,您可以使用默认策略[NSURLSessionConfigurationdefaultConfiguration],或者您可以简单地不指定配置策略,直接使用[NSURLSessionsharedSession]来实例化请求对象。
对于那些不应在本地存储留下任何痕迹的安全敏感请求,应使用配置方法ephemeralSessionConfiguration。另一种方法backgroundSessionConfiguration专门用于长时间运行的上传或下载任务。这种类型的会话将交给系统服务来管理完成,即使您的应用被终止或崩溃。
此外,您可以首次指定连接仅使用 TLS 版本 1.2,这有助于防御 BEAST^(5)和 CRIME^(6)等攻击,这些攻击可能允许网络攻击者读取或篡改您的 TLS 连接。
注意
会话配置在实例化 *NSURLSession* 后是只读的;会话期间无法更改策略和配置,且无法更换为不同的配置。
执行 NSURLSession 任务
让我们一起看看创建NSURLSessionConfiguration并为其分配简单任务的典型流程,如示例 7-5 所示。
➊ NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration
ephemeralSessionConfiguration];
➋ [configuration setTLSMinimumSupportedProtocol = kTLSProtocol12];
➌ NSURL *url = [NSURL URLWithString:@"https://www.mycorp.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
➍ NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
➎ NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
➏ // Your completion handler block
}];
➐ [task resume];
示例 7-5:创建一个要求 TLSv1.2 的临时 NSURLConfiguration
在➊处实例化了NSURLSessionConfiguration对象,指定连接应为临时的。这应该能防止缓存数据写入本地存储。然后,在➋处,配置要求使用 TLS 1.2 版本,因为开发者控制着端点并且知道该端点支持该版本。接下来,就像NSURLConnection一样,创建了一个NSURL对象和一个带有该 URL 的NSURLRequest对象 ➌。创建配置和请求后,应用程序可以实例化会话 ➍并为该会话分配任务 ➎。
NSURLSessionDataTask及其相关对象将一个完成处理器块作为参数 ➏。这个块异步处理服务器响应和你因任务收到的数据。或者(或额外),你可以指定一个符合NSURLSessionTaskDelegate协议的自定义代理。你可能希望同时使用completionHandler和代理的原因之一是,完成处理器处理请求结果,而代理则在会话级别而非任务级别管理认证和缓存决策(我将在下一节讨论这个问题)。
最后,在➐处,这段代码通过调用resume方法启动任务,因为所有任务在创建时都会被暂停。
发现 NSURLSession TLS 绕过
NSURLSession 也有一种方法可以避免 TLS 检查。应用程序可以使用didReceiveChallenge代理,并将接收到的挑战的proposedCredential作为凭证传回给会话, 如示例 7-6 所示。
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NS
URLAuthenticationChallenge *)challenge completionHandler:(void (^)(NS
URLSessionAuthChallengeDisposition disposition, NSURLCredential * credential))
completionHandler {
➊ completionHandler(NSURLSessionAuthChallengeUseCredential,
[challenge proposedCredential]);
}
示例 7-6:使用 NSURLSession 绕过服务器验证
这是另一个可能很难发现的绕过方法。查看像➊处那样的代码,其中有一个completionHandler,后面跟着proposedCredential。
使用 NSURLSession 的基本认证
使用NSURLSession进行 HTTP 认证由会话处理,并传递给didReceiveChallenge代理,如示例 7-7 所示。
➊ - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NS
URLAuthenticationChallenge *)challenge completionHandler:(void (^)(NS
URLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSString *user = @"user";
NSString *pass = @"pass";
NSURLProtectionSpace *space = [challenge protectionSpace];
if ([space receivesCredentialSecurely] == YES &&
[[space host] isEqualToString:@"myhost.com"] &&
[[space authenticationMethod] isEqualToString:NS
URLAuthenticationMethodHTTPBasic]) {
➋ NSURLCredential *credential =
[NSURLCredential credentialWithUser:user
password:pass
persistence:NSURLCredentialPersistenceForSession];
➌ completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
}
示例 7-7:一个示例的 didReceiveChallenge 代理
这种方法在 ➊ 处定义了一个代理和一个完成处理程序,在 ➋ 处创建了一个 NSURLCredential,并将该凭据传递给 ➌ 处的完成处理程序。请注意,无论是 NSURLConnection 还是 NSURLSession 方法,一些开发者会忘记确保他们与正确的主机通信或以安全的方式发送凭据。这将导致凭据发送到你应用加载的 每个 URL,而不仅仅是你自己的;清单 7-8 展示了这个错误可能是什么样子的。
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NS
URLAuthenticationChallenge *)challenge completionHandler:(void (^)(NS
URLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSURLCredential *credential =
[NSURLCredential credentialWithUser:user
password:pass
persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
清单 7-8:错误的 HTTP 认证方式
如果你想为某个专用端点使用持久化凭据,可以像使用 NSURLConnection 一样将凭据存储在 sharedCredentialStorage 中。在构建会话时,你可以提前提供这些凭据,而无需担心代理方法,正如在清单 7-9 中所示。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration
defaultSessionConfiguration];
[config setURLCredentialStorage:
[NSURLCredentialStorage sharedCredentialStorage]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:nil
delegateQueue:nil];
清单 7-9:使用 NSURLSessionConfiguration 引用存储的凭据
这只是创建一个 NSURLSessionConfiguration,并指定它应使用共享凭据存储。当你连接到一个在钥匙串中存储了凭据的资源时,这些凭据将被会话使用。
管理存储的 URL 凭据
你已经看过如何使用 sharedCredentialStorage 存储和读取凭据,但 NSURLCredentialStorage API 也允许你使用 removeCredential:forProtectionSpace 方法移除凭据。例如,当用户明确决定从应用中退出或删除帐户时,你可能想要这样做。清单 7-10 展示了一个典型的使用场景。
NSURLProtectionSpace *space = [[NSURLProtectionSpace alloc]
initWithHost:@"myhost.com"
port:443
protocol:@"https"
realm:nil authenticationMethod:nil];
NSURLCredential *credential = [credentialStorage
defaultCredentialForProtectionSpace:space];
[[NSURLCredentialStorage sharedCredentialStorage] removeCredential:credential
forProtectionSpace:space];
清单 7-10:移除默认凭据
这将从你的本地钥匙串中删除凭据。然而,如果凭据的持久化类型为 NSURLCredentialPersistenceSynchronizable,凭据可能已通过 iCloud 同步到其他设备。要从所有设备中删除凭据,请使用 NSURLCredentialStorageRemoveSynchronizableCredentials 选项,正如在清单 7-11 中所示。
NSDictionary *options = [NSDictionary dictionaryWithObjects forKeys:NS
URLCredentialStorageRemoveSynchronizableCredentials, YES];
[[NSURLCredentialStorage sharedCredentialStorage] removeCredential:credential
forProtectionSpace:space
options:options];
清单 7-11:从本地钥匙串和 iCloud 中移除凭据
到此为止,你应该已经理解了 NSURLConnection 和 NSURLSession API 及其基本用法。你可能还会遇到其他网络框架,它们有自己的行为,并需要稍微不同的安全配置。我现在将介绍其中的一些。
第三方网络 API 的风险
在 iOS 应用中,有一些流行的第三方网络 API,主要用于简化各种网络任务,如多部分上传和证书固定。最常用的一个是 AFNetworking,^(7) 其次是现已过时的 ASIHTTPRequest。^(8) 在本节中,我将向你介绍这两个。
AFNetworking 的不当与正确使用
AFNetworking 是一个流行的库,构建于NSOperation和NSHTTPRequest之上。它提供了多种便捷方法来与不同类型的 Web API 交互并执行常见的 HTTP 网络任务。
与其他网络框架一样,一个关键任务是确保 TLS 安全机制没有被禁用。在 AFNetworking 中,TLS 证书验证可以通过几种方式禁用。其一是通过_AFNETWORKING_ALLOW_INVALID_SSL_CERTIFICATES标志,通常在Prefix.pch文件中设置。另一种方式是设置AFHTTPClient的一个属性,如示例 7-12 所示。
NSURL *baseURL = [NSURL URLWithString:@"https://myhost.com"];
AFHTTPClient* client = [AFHTTPClient clientWithBaseURL:baseURL];
[client setAllowsInvalidSSLCertificate:YES];
示例 7-12:通过 setAllowsInvalidSSLCertificate禁用 TLS 验证
你可能看到的最后一种禁用 TLS 验证的方式是通过使用setAllowsInvalidSSLCertificate更改AFHTTPRequestOperationManager的安全策略,如示例 7-13 所示。
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager [securityPolicy setAllowInvalidCertificates:YES]];
示例 7-13:使用 securityPolicy禁用 TLS 验证
你还需要确保你正在检查的代码在生产版本中没有使用AFHTTPRequestOperationLogger类。这个日志记录器在后台使用NSLog将请求的 URL 写入 Apple 系统日志,这使得其他应用程序在某些 iOS 版本中可以看到它们。
AFNetworking 提供的一个特别有用的功能是能够轻松地执行证书固定(certificate pinning)。你只需在项目的.pch文件中设置_AFNETWORKING_PIN_SSL_CERTIFICATES_ #define,并适当设置AFHTTPClient实例的固定模式(defaultSSLPinningMode)属性;可用的模式在表 7-2 中描述。然后将你希望固定的证书放入捆绑包根目录,作为.cer扩展名的文件。
表 7-2: AFNetworking SSL 固定模式
| 模式 | 含义 |
|---|---|
AFSSLPinningModeNone |
不执行证书固定,即使固定已启用。如果需要,可以在调试模式中使用。 |
AFSSLPinningModePublicKey |
固定到证书的公钥。 |
AFSSLPinningModeCertificate |
固定到提供的确切证书(或证书)。如果证书被重新签发,则需要更新应用程序。 |
如 AFNetworking 随附的示例代码所示,你可以检查 URL 来确定它们是否应该被固定。只需评估协议和域名,查看这些域名是否属于你。示例 7-14 展示了一个例子。
if ([[url scheme] isEqualToString:@"https"] &&
[[url host] isEqualToString:@"yourpinneddomain.com"]) {
[self setDefaultSSLPinningMode:AFSSLPinningModePublicKey];
}
else {
[self setDefaultSSLPinningMode:AFSSLPinningModeNone];
}
return self;
}
示例 7-14:确定一个 URL 是否应该被固定
else语句并不是绝对必要的,因为不进行固定是默认设置,但它确实提供了某种清晰度。
请记住,AFNetworking 会固定捆绑包中提供的所有证书,但它不会检查证书的常用名称与网络端点的主机名是否匹配。如果你的应用程序同时绑定到多个具有不同安全标准的网站,这通常会成为一个问题。换句话说,如果你的应用程序同时绑定到 funnyimages.com 和 www.bank.com,那么持有 funnyimages.com 私钥的攻击者就能够拦截你应用程序与 bank.com 之间的通信。
现在你已经对如何使用和滥用 AFNetworking 库有了一个初步了解,让我们继续讨论 ASIHTTPRequest。
不安全的 ASIHTTPRequest 使用
ASIHTTPRequest 是一个已弃用的库,类似于 AFNetworking,但功能稍微不那么完整,并且基于 CFNetwork API。它不应在新项目中使用,但你可能会在现有的代码库中找到它,尤其是在迁移成本过高的情况下。当检查这些代码库时,标准的 SSL 验证绕过方法是 setValidatesSecureCertificate:NO。
你还需要检查项目中的 ASIHTTPRequestConfig.h,以确保没有启用过于冗长的日志记录(见 Listing 7-15)。
// If defined, will use the specified function for debug logging
// Otherwise use NSLog
#ifndef ASI_DEBUG_LOG
#define ASI_DEBUG_LOG NSLog
#endif
// When set to 1, ASIHTTPRequests will print information about what a request is
doing
#ifndef DEBUG_REQUEST_STATUS
#define DEBUG_REQUEST_STATUS 0
#endif
// When set to 1, ASIFormDataRequests will print information about the request body
to the console
#ifndef DEBUG_FORM_DATA_REQUEST
#define DEBUG_FORM_DATA_REQUEST 0
#endif
// When set to 1, ASIHTTPRequests will print information about bandwidth throttling
to the console
#ifndef DEBUG_THROTTLING
#define DEBUG_THROTTLING 0
#endif
// When set to 1, ASIHTTPRequests will print information about persistent
connections to the console
#ifndef DEBUG_PERSISTENT_CONNECTIONS
#define DEBUG_PERSISTENT_CONNECTIONS 0
#endif
// When set to 1, ASIHTTPRequests will print information about HTTP authentication
(Basic, Digest or NTLM) to the console
#ifndef DEBUG_HTTP_AUTHENTICATION
#define DEBUG_HTTP_AUTHENTICATION 0
#endif
Listing 7-15: ASIHTTPRequestConfig.h 中的日志定义
如果你确实希望使用这些日志功能,可能需要将它们包装在 #ifdef DEBUG 条件语句中,如下所示:
#ifndef DEBUG_HTTP_AUTHENTICATION
#ifdef DEBUG
#define DEBUG_HTTP_AUTHENTICATION 1
#else
#define DEBUG_HTTP_AUTHENTICATION 0
#endif
#endif
这个 ASIHTTPRequestConfig.h 文件将日志功能包装在条件语句中,以防止在生产版本中泄露这些信息。
Multipeer Connectivity
iOS 7 引入了 Multipeer Connectivity^(9),它允许附近的设备在最小网络配置下进行通信。Multipeer Connectivity 的通信可以通过 Wi-Fi(点对点或多点网络)或蓝牙个人区域网络(PANs)进行。Bonjour 是浏览和广告可用服务的默认机制。
开发者可以使用 Multipeer Connectivity 来执行点对点文件传输或在设备之间流式传输内容。与任何类型的对等通信一样,验证来自不信任对等方的传入数据至关重要;然而,也有传输安全机制确保数据不被窃听。
Multipeer Connectivity 会话是通过 MCSession 类的 initWithPeer 或 initWithPeer:securityIdentity:encryptionPreference: 类方法创建的。后者方法允许你要求加密,并可以包含证书链来验证设备。
当为encryptionPreference指定值时,可选择的选项有MCEncryptionNone、MCEncryptionRequired和MCEncryptionOptional。请注意,这些选项与0、1或2的值可以互换。因此,尽管0和1的值表现得像布尔值一样,但2的值在功能上等同于完全没有加密。
强烈建议无条件要求加密,因为MCEncryptionOptional容易受到降级攻击。(你可以在 Alban Diquet 的 Black Hat 演讲中找到关于反向工程 Multipeer Connectivity 协议的更多细节^(10))。 列表 7-16 展示了一个典型的调用,创建一个会话并要求加密。
MCPeerID *peerID = [[MCPeerID alloc] initWithDisplayName:@"my device"];
MCSession *session = [[MCSession alloc] initWithPeer:peerID
securityIdentity:nil
encryptionPreference:MCEncryptionRequired];
列表 7-16:创建一个 MCSession
当连接到远程设备时,会调用委托方法session:didReceiveCertificate:fromPeer:certificateHandler:,传入对等方的证书,并允许你指定一个处理方法,根据证书是否成功验证来采取特定行动。
注意
如果你未能创建 *didReceiveCertificate* 委托方法或没有在该委托方法中实现 *certificateHandler* ,则远程端点不会进行验证,这使得连接容易被第三方拦截。
在检查使用 Multipeer Connectivity API 的代码库时,确保所有MCSession实例化时都提供身份并要求传输加密。任何包含敏感信息的会话绝不能仅仅使用initWithPeer实例化。还要确保didReceiveCertificate的委托方法存在并正确实现,并确保当对等方证书验证失败时,certificateHandler能够正确处理。你不希望看到像这样的代码:
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate
fromPeer:(MCPeerID *)peerID
certificateHandler:(void (^)(BOOL accept))certificateHandler
{
certificateHandler(YES);
}
这段代码盲目地将YES布尔值传递给处理器,这是你绝对不应该做的。
你可以自行决定如何实现验证。验证系统往往是定制化的,但你有几种基本选项。你可以让客户端自行生成证书,然后使用首次信任(TOFU),这只会验证所呈现的证书是否与第一次配对时展示的证书相同。你也可以实现一个服务器,当查询时返回用户的公钥证书,从而集中管理身份。选择一个适合你的业务模型和威胁模型的解决方案。
使用 NSStream 进行低级网络编程
NSStream 适用于建立非 HTTP 网络连接,但它也可以通过相对较少的工作用于 HTTP 通信。由于某些无法理解的原因,在 OS X Cocoa 和 iOS Cocoa Touch 之间的过渡中,Apple 删除了允许 NSStream 建立与远程主机网络连接的方法 getStreamsToHost。所以如果你想自己进行流式传输,那太棒了。否则,在技术问答 QA1652 中,^(11) Apple 描述了一个类别,你可以用它定义一个大致等同于 NSStream 的 getStreamsToHostNamed 方法。
另一种选择是使用较低级别的 Core Foundation CFStreamCreatePairWithSocketToHost 函数,并将输入和输出的 CFStream 强制转换为 NSStream,如 列表 7-17 所示。
NSInputStream *inStream;
NSOutputStream *outStream;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"myhost.com", 80, &
readStream, &writeStream);
inStream = (__bridge NSInputStream *)readStream;
outStream = (__bridge NSOutputStream *)writeStream;
列表 7-17:将 CFStreams 转换为 NSStreams
NSStream 只允许用户对连接的特性进行少量控制,如 TCP 端口和 TLS 设置(参见 列表 7-18)。
NSHost *myhost = [NSHost hostWithName:[@"www.conglomco.com"]];
[NSStream getStreamsToHostNamed:myhost
port:443
inputStream:&MyInputStream
outputStream:&MyOutputStream];
➊ [MyInputStream setProperty:NSStreamSocketSecurityLevelTLSv1
forKey:NSStreamSocketSecurityLevelKey];
列表 7-18:使用 NSStream 打开基本的 TLS 连接
这是 NSStream 的典型用法:设置主机、端口和输入输出流。由于你对 TLS 设置没有太多控制,唯一可能出错的设置是 ➊,NSStreamSocketSecurityLevel。你应该将其设置为 NSStreamSocketSecurityLevelTLSv1,以确保你不会使用过时的、已损坏的 SSL/TLS 协议。
使用 CFStream 进行更低级别的网络编程
使用 CFStream 时,开发者在 TLS 会话协商中有不幸过多的控制权。^(12) 参见 表 7-3,了解你应该查找的多个 CFStream 属性。这些控制允许开发者覆盖或禁用验证对等方的规范名称(CN)、忽略过期日期、允许不受信任的根证书,并完全忽略验证证书链。
表 7-3: 恶心的 CFStream TLS 安全常量
| 常量 | 含义 | 默认值 |
|---|---|---|
kCFStreamSSLLevel |
用于加密连接的协议。 | negotiated^a |
kCFStreamSSLAllowsExpiredCertificates |
接受过期的 TLS 证书。 | false |
kCFStreamSSLAllowsExpiredRoots |
接受证书链中包含已过期根证书的证书。 | false |
kCFStreamSSLAllowsAnyRoot |
是否可以使用根证书作为 TLS 端点的证书(换句话说,自签名或未签名证书)。 | false |
kCFStreamSSLValidatesCertificateChain |
是否验证证书链。 | true |
kCFStreamSSLPeerName |
覆盖与证书的 CN 比较的主机名。如果设置为 kCFNull,则不执行验证。 |
hostname |
kCFStreamSSLIsServer |
此流是否作为服务器使用。 | false |
kCFStreamSSLCertificates |
如果kCFStreamSSLIsServer为真,将使用的证书数组。 |
无 |
a. 默认常量是kCFStreamSocketSecurityLevelNegotiatedSSL,它会与服务器协商使用最强的可用方法。
你可能根本不应该使用这些安全常量,但如果你必须使用 TLS CFStream,就按照正确的方法操作。这很简单!前提是你没有在应用内创建网络服务器(这是CFStream在 iOS 应用中相当罕见的用法),你需要遵循两个步骤:
-
将
kCFStreamSSLLevel设置为kCFStreamSocketSecurityLevelTLSv1。 -
不要搞乱其他任何东西。
结束语
你已经了解了许多应用与外界通信的方式,以及这些方式可能会被错误实现的情况。现在让我们把注意力转向与其他应用的通信,以及通过 IPC 传输数据时可能遇到的一些陷阱。
第八章:8
进程间通信
在 iOS 上,进程间通信(IPC)从不同的角度看,可以是令人耳目一新简单,也可以是极为有限的。我个人更倾向于认为它是前者。尽管 Android 拥有灵活的 IPC 机制,如 Intents、Content Providers 和 Binder,但 iOS 的系统则较为简单,基于两种组件:通过 URL 传递消息和应用程序扩展。消息传递帮助其他应用程序和网页通过外部提供的参数调用您的应用程序。应用程序扩展旨在扩展基础系统的功能,提供诸如共享、存储以及改变“今日”屏幕或键盘功能的服务。
在本章中,您将学习如何在 iOS 上实现进程间通信(IPC)的各种方法,了解人们常犯的 IPC 错误,以及如何在不妥协用户安全的前提下,规避系统的一些限制。
URL 方案和 openURL 方法
iOS 提供的官方 IPC 机制是通过 URL 方案,这与桌面系统上的协议处理程序(如 mailto:)类似。
在 iOS 上,开发者可以定义一个 URL 方案,指示他们的应用程序响应该方案,其他应用程序(或网页,尤其重要)可以通过将参数作为 URL 参数传递,来调用该应用程序。
定义 URL 方案
自定义 URL 方案在项目的 Info.plist 文件中描述。要添加新方案,您可以使用 Xcode 的 plist 编辑器,如图 8-1 所示。

图 8-1:在 Xcode plist 编辑器中定义 URL 方案
首先,您需要添加 URL 类型键,这将创建一个名为 Item 0 的子键。URL 标识符的子键将自动创建,应该用反向 DNS 标记的字符串填充,例如 com.mycompany.myapp。然后,您在 Item 0 下创建一个新的子键,即 URL Schemes 键。在 Item 0 下的 URL Schemes 下,输入您希望其他应用程序调用您的应用程序的方案。例如,在这里输入 mynewapp,您的应用程序就会响应 mynewapp:// 的 URL。
您还可以通过使用外部编辑器手动在 plist 文件中定义这些 URL 方案,如示例 8-1 所示。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.funco.myapp</string>
</dict>
</array>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
示例 8-1:在 plist 中显示的 URL 方案
粗体部分表示在图 8-1 中创建 URL 方案后对原始 plist 文件所做的修改。了解这个文件中的内容,以便在检查一个新的陌生代码库时,能快速使用 grep 查找所需的信息。当您寻找自定义 URL 方案时,应该查找 CFBundleURLSchemes 键。
一旦您定义了一个 URL 方案或发现了一个您想要交互的 URL 方案,您就需要实现代码来进行或接收 IPC 调用。幸运的是,这相对简单,但仍然有一些陷阱需要注意。接下来,我们将看看它们。
发送和接收 URL/IPC 请求
要通过 URL 协议发送消息,只需创建一个包含你希望调用的 URL 的 NSString 对象的 NSURL 对象,然后调用 openURL: 方法 [UIApplication sharedApplication]。以下是一个示例:
NSURL *myURL = [NSURL URLWithString:@"someotherapp://somestuff?someparameter=avalue
&otherparameter=anothervalue"];
[[UIApplication sharedApplication] openURL:myURL];
接收应用程序的键和值通过类似于 HTTP URL 的方式传递,使用 ? 表示参数,& 分隔键值对。唯一的例外是,在 ? 前不需要任何文本,因为你并不是在与远程站点进行通信。
接收应用程序随后可以使用标准的 NSURL 对象属性提取 URL 的任何组件,(^(1))如 host(在我的示例中是 somestuff),或者 query(你的键值对)。
验证 URL 和认证发送者
当接收应用程序通过其自定义 URL 协议被调用时,它可以选择验证是否希望首先打开该 URL,使用 application:didFinishLaunchingWithOptions: 方法或 application:will-FinishLaunchingWithOptions: 方法。应用程序通常使用前者,如在 清单 8-2 中所示。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NS
Dictionary *)launchOptions {
if ([launchOptions objectForKey:UIApplicationLaunchOptionsURLKey] != nil) {
NSURL *url = (NSURL *)[launchOptions valueForKey:UI
ApplicationLaunchOptionsURLKey];
if ([url query] != nil) {
NSString *theQuery = [[url query]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if (![self isValidQuery:theQuery]) {
return NO;
}
return YES;
}
}
}
清单 8-2:在 didFinishLaunchingWithOptions 中验证 URL
如果返回 YES,openURL 方法将使用提供的 URL 被调用。在 openURL 方法中,传递的数据(如果有)将被解析,并且 openURL 会根据应用程序的反应做出决策。该方法也是你可以根据调用你应用程序的应用程序做出决策的地方。清单 8-3 显示了一个可能的 openURL 方法的示例。
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:
(id)annotation {
➊ if ([sourceApplication isEqualToString:@"com.apple.mobilesafari"]) {
NSLog(@"Loading app from Safari");
return NO; // We don't want to be called by web pages
}
else {
➋ NSString *theQuery = [[url query]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
➌ NSArray *chunks = [theQuery componentsSeparatedByString:@"&"];
for (NSString* chunk in chunks) {
➍ NSArray *keyval = [chunk componentsSeparatedByString:@"="];
➎ NSString *key = [keyval objectAtIndex:0];
NSString *value = [keyval objectAtIndex:1];
➏ // Do something with your key and value
--snip--
return YES;
}
}
}
清单 8-3:解析由 openURL 接收到的数据
在 ➊,该方法检查源应用程序是否来自标识 Mobile Safari 的捆绑 ID;由于该应用程序仅用于接收来自其他应用程序的输入,因此它返回 NO。如果你的应用程序只希望由特定应用程序打开,你可以将其限制为一个有效的捆绑 ID。
在 ➋,输入内容被解码,以防其中包含 URL 编码字符(如 %20 代表空格)。在 ➌ 和 ➍,单独的键值对被分开,并进一步分解成键值对。在 ➎,获取第一个键值对,并对其进行解析,以告知任何可能编写的逻辑,在 ➏ 处进行处理。
实际查询字符串的解析和验证将取决于你接收到的数据类型。如果你期待的是一个数字值,你也可以使用正则表达式来确保字符串只包含数字。以下是你可能会在 openURL 方法中添加的检查示例:
NSCharacterSet* notNumeric = [[NSCharacterSet decimalDigitCharacterSet] invertedSet
];
if ([value rangeOfCharacterFromSet:notDigits].location != NSNotFound) {
return NO; // We didn't get a numeric value
}
只验证通过基于 URL 的 IPC 接收到的任何参数,确保它们仅包含你预期的数据类型。如果你使用这些参数来构建数据库查询或更改 HTML 内容,务必确保你在清理数据并正确集成内容。我将在 第十二章 中详细讲解。
注意过时的验证代码
请注意,你有时会在某些代码库中看到过时的(但命名更合理的)handleOpenURL 方法;参见示例 8-4 了解示例。
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url
示例 8-4:处理接收到的 URL 的过时方法
在许多情况下,使用handleOpenURL并不理想,因为该方法会盲目地打开任何给定的 URL,且没有提供任何方法来识别该 URL 的来源。当然,验证源应用程序只能提供有限的保障。
发送者验证有多安全?
鉴于我在本节中讨论的内容,你可能会想知道是否能够完全信任sourceApplication参数的值。好问题!虽然发送者检查只是字符串比较,并不直接涉及加密,但 Apple 确保所有提交到 App Store 的应用程序 ID 都是唯一的:先到先得。然而,在越狱设备上,你无法保证这种唯一性,因此不要盲目信任声称来自某个特定应用的 URL。
URL Scheme 劫持
我描述的相对简单的 URL scheme 定义系统有一个潜在的问题。如果另一个应用程序尝试注册你的 URL scheme 会怎么样?对于 Apple 内建的应用程序,其他应用程序无法成功注册重复的 scheme。然而,对于其他应用程序,结果行为是...未定义的。你可以问问 Apple:
如果多个第三方应用注册了相同的 URL scheme,目前没有办法确定哪个应用会处理该 scheme。^(2)
换句话说,你面临两种不愉快的可能性。首先,安装在你应用程序之前的恶意应用可能会注册你的 URL scheme,并在你应用程序安装后仍然保留它。或者,安装在你应用程序之后的恶意应用可能会成功注册你的 URL scheme,实际上劫持了你的应用程序的 URL scheme。这两种情况都可能导致原本应该发送给你应用程序的数据被发送到恶意的第三方应用。你能做什么?等我想明白了再告诉你。
然而,在 iOS 的最新版本中,已经提供了用于应用程序之间传递数据的替代机制,每种机制适用于不同的情况。对于你的应用来说,这些可能比openURL更合适。现在让我们来看一下这些较新的方法。
通用链接
URL 协议劫持是苹果在 iOS 9 中引入通用链接的原因之一。通用链接是一种有效提供 iOS 应用深度链接的方式,并且可以实现网站与移动应用的集成。例如,假设你发布了一款即时通讯应用叫做 HoopChat。如果用户访问一个网站,上面有一个“在 HoopChat 中给我发消息!”的按钮,这个按钮可以链接到一个像 www.hoopchat.com/im/send/?id=356372 的 URL。如果用户点击这个链接并且安装了你的应用,链接将直接在应用中打开,应用可以为用户 ID 为 356372 的人创建一条新消息。如果用户没有安装应用,同样的 URL 会在移动 Safari 中打开,带你到一个基于 Web 的界面来发送消息。
在幕后,这一工作方式是应用具有一个权限,指定它如何处理指向特定域的链接,如 图 8-2 所示。

图 8-2:在 Xcode 中启用与关联域的通用链接
当访问这些域中的一个时,在移动 Safari 中会从 Web 服务器下载一个名为 apple-app-site-association 的文件。这个文件是一个已签名的 JSON 数据块,如 列表 8-5 所示。
{
"applinks": {
"apps": [],
"details": {
➊ "FAFBQM3A4N.com.hoopchat.messenger": {
➋ "paths": [ "*" ]
}
}
}
}
列表 8-5: apple-app-site-association 文件格式
该文件指定了开发者团队 ID、捆绑标识符(在 ➊ 处显示)以及应由应用处理的 URL 路径(与主网站不同)。在这种情况下,如果应用已安装,则所有 URL 应由应用处理,因此文件在 ➋ 处给出了 * 的值。
如前所述,这个二进制文件需要签名;签名密钥实际上是你的生产 SSL 证书的私钥。如果你拥有网站的私钥和公钥,你可以通过命令行签署 JSON 文件,如 列表 8-6 所示。
openssl smime \
-sign \
-nodetach \
➊ -in "unsigned.json" \
➋ -out "apple-app-site-association" \
-outform DER \
➌ -inkey "private-key.pem" \
➍ -signer "certificate.pem"
列表 8-6:签署 apple-app-site-association 文件
这个示例使用 openssl 工具,将未签名的 JSON 文件提供给 ➊ 处,并提供输出文件名 ➋。在 ➌ 和 ➍ 处提供密钥对。如果你的密钥受到密码保护,系统会提示你输入密码,你将获得一个有效的 apple-app-site-association 文件作为输出。然后,这个文件会上传到你网站的根目录,iOS 会通过 HTTPS 获取它,确定是否将 URL 传递给你的应用。在应用内,应用在接收到通用链接后采取什么操作,将取决于你在应用代理的 application:continueUserActivity:restorationHandler: 方法中实现的逻辑。
这种通用链接方式比自定义 URL 处理方案更为优越,原因有几个。首先,通用链接不受 URL 方案劫持的影响;只有你的网站(通过 HTTPS 认证)可以指定哪些 URL 将在你的应用程序中打开,而且这些调用不能被发送到其他的 Bundle ID。其次,无论应用是否安装,链接都应该能正常工作。在 iOS 的早期版本中,如果应用未安装,你会得到一个错误提示,表示该方案无法识别。而通过通用链接,如果应用未安装,你会被重定向到相应的网站。最后,通用链接提供了一些隐私保护,防止应用程序枚举设备上有哪些应用程序(以前应用可以通过canOpenURL方法枚举已安装的应用;而使用通用链接后,不再存在这种机制)。
现在你已经了解了如何控制与你自己的应用程序的交互,让我们看看如何通过UIActivity更深入地将你的应用程序与流行的应用和服务进行集成。
通过 UIActivity 共享数据
在 iOS 6 中,苹果开始允许第三方应用通过一组预定义的方法共享信息,如通过电子邮件发送数据或发布到 Facebook。这个有限的进程间通信(IPC)形式让开发者能够实现最基本的共享功能。你可以通过检查以下UIActivity类型,了解这种方法适用于哪些类型的数据:
• UIActivityTypePostToFacebook
• UIActivityTypePostToTwitter
• UIActivityTypePostToWeibo
• UIActivityTypePostToTencentWeibo
• UIActivityTypePostToFlickr
• UIActivityTypePostToVimeo
• UIActivityTypeMessage
• UIActivityTypeMail
• UIActivityTypePrint
• UIActivityTypeCopyToPasteboard
• UIActivityTypeAssignToContact
• UIActivityTypeSaveToCameraRoll
• UIActivityTypeAddToReadingList
• UIActivityTypeAirDrop
要通过UIActivity进行分享,只需创建一个UIActivityViewController并传递数据,如文本、URL、图片等,具体请参见清单 8-7。
NSString *text = @"Check out this highly adequate iOS security resource";
NSURL *url = [NSURL URLWithString:@"http://nostarch.com/iossecurity/"];
UIActivityViewController *controller = [[UIActivityViewController alloc]
initWithActivityItems:@[text, url]
applicationActivities:nil];
[navigationController presentViewController:controller animated:YES completion:nil
];
清单 8-7:实例化一个 UIActivityViewController
这里,UIActivityViewController被传递了一些文本和一个 URL。如果某些分享方式不适用于该数据,你可以排除它们。例如,如果你希望确保用户只能通过邮件或打印内容,而不能分享到社交网络,你可以告诉UIActivityViewController排除所有其他已知的分享类型,如清单 8-8 所示。
[controller setExcludedActivityTypes:@[UIActivityTypePostToFacebook,
UIActivityTypePostToTwitter
UIActivityTypePostToWeibo
UIActivityTypePostToTencentWeibo
UIActivityTypePostToFlickr
UIActivityTypePostToVimeo
UIActivityTypeMessage
UIActivityTypeCopyToPasteboard
UIActivityTypeAssignToContact
UIActivityTypeSaveToCameraRoll
UIActivityTypeAddToReadingList
UIActivityTypeAirDrop];
清单 8-8:排除某些类型的共享活动
不幸的是,这种排除方法并不方便或彻底,未来版本的 iOS 中新增的任何共享类型都会默认包含在内。如果禁用某些共享 UI 部分很重要,请确保在这些版本正式发布之前,使用最新版本的 iOS 进行测试。
除了 URL 方案和UIActivity方法外,在 iOS 中还有另一种处理 IPC 的方式:通过扩展。
应用扩展
在 iOS 8 及更高版本中,开发者可以编写各种扩展,它们表现得像是专门的进程间通信(IPC)形式。这些扩展允许您向其他应用程序展示数据,或者通过您的应用程序共享数据,或更改系统行为。表 8-1 显示了您可以为其编写的各种扩展点。扩展点定义了扩展将访问操作系统的哪个组件以及如何进行编码。
表 8-1: 扩展点
| 类型 | 功能 |
|---|---|
| 今日 | 操作通知中心今日视图中的小部件 |
| 分享 | 允许通过分享按钮将数据发送到您的应用程序 |
| 动作 | 读取或操作要返回给主机应用程序的数据 |
| 照片 | 提供在“照片”应用中操作照片的方法 |
| 文档提供者 | 允许访问文件库 |
| 键盘 | 提供自定义键盘 |
虽然应用扩展不是独立的应用程序,但它们必须与一个应用程序捆绑在一起,称为容器应用程序。使用扩展的第三方应用程序(称为主机应用程序)可以与捆绑在容器应用程序中的扩展进行通信,但容器应用程序本身不会直接与扩展通信。苹果公司还明确排除了某些功能不允许通过扩展访问,例如使用 HealthKit API、接收 AirDrop 数据或访问摄像头或麦克风。
扩展可以通过多种方式实现,它们也可以被视为独立的应用程序。如图 8-3 所示,扩展作为独立的应用程序在 Xcode 中创建。

图 8-3:将新的扩展目标添加到项目中
然而,在本书中,我们将重点关注从安全角度检查最重要的方面。
检查应用是否实现了扩展
首先,您可以通过在属性列表中搜索NSExtensionPointIdentifier来轻松判断您正在检查的应用程序是否实现了扩展。要搜索该属性,您可以在项目的根目录中执行以下命令:
$ find . -name "*.plist" |xargs grep NSExtensionPointIdentifier
这会在目录中搜索所有.plist文件以查找NSExtensionPointIdentifier。您还可以通过在 Xcode 中检查.plist文件来查找该属性,如图 8-4 所示。

图 8-4:新创建的扩展的Info.plist文件,在 Xcode 中查看
扩展的Info.plist文件将包含正在使用的扩展类型,以及可选的扩展设计来处理的数据类型定义。如果您发现定义了NSExtensionPointIdentifier属性,您应该深入项目,找到该扩展的视图控制器。
限制和验证可共享数据
对于分享和操作扩展,您可以定义一个NSExtensionActivationRule,其中包含一个数据类型字典,限制您的应用程序只能处理这些数据类型(参见图 8-5)。

图 8-5:扩展的激活规则,位于 Xcode 中查看的.plist 文件
这个字典将被评估,以确定您的扩展允许哪些数据类型,以及您将接受这些项的最大数量。但是,应用程序不仅限于接受预定义的数据类型;它们还可以实现自定义NSPredicate来定义它们自己的接受规则。如果是这种情况,您将看到NSExtensionActivationRule以字符串形式表示,而不是数字值。
如果你知道自己正在处理预定义的数据类型,请记住以下预定义的激活规则:
• NSExtensionActivationSupportsAttachmentsWithMaxCount
• NSExtensionActivationSupportsAttachmentsWithMinCount
• NSExtensionActivationSupportsFileWithMaxCount
• NSExtensionActivationSupportsImageWithMaxCount
• NSExtensionActivationSupportsMovieWithMaxCount
• NSExtensionActivationSupportsText
• NSExtensionActivationSupportsWebURLWithMaxCount
• NSExtensionActivationSupportsWebPageWithMaxCount
由于扩展通常会接收未知和任意类型的数据,因此确保扩展在其视图控制器的isContentValid方法中执行正确的验证非常重要,尤其是在分享或操作扩展中。检查应用程序在实现该方法时的逻辑,确定应用程序是否执行了所需的验证。
通常,扩展会检查NSExtensionContext(这是当主机应用调用beginRequestWithExtensionContext时传递的),如示例 8-9 所示。
NSExtensionContext *context = [self extensionContext];
NSArray *items = [context inputItems];
示例 8-9:从NSExtensionContext创建NSExtensionItem数组
这将提供一个NSExtensionItem对象数组,每个对象将包含主机应用程序传递的不同类型数据,例如图像、URL、文本等。在使用这些数据执行操作或允许用户发布数据之前,应该对每个项目进行检查和验证。
防止应用程序与扩展交互
键盘扩展具有独特的特点,它们会读取用户输入的每个按键。不同的第三方键盘可能会有不同程度的按键记录功能,以帮助自动完成或将数据发送到远程 Web 服务进行处理。也有可能会分发恶意键盘,作为纯粹的按键记录器。如果您的应用程序通过键盘接受安全敏感数据,您可能希望阻止在您的应用程序中使用第三方键盘。您可以通过shouldAllowExtensionPointIdentifier代理方法来实现这一点,如示例 8-10 所示。
- (BOOL)application:(UIApplication *)application
shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier {
if ([extensionPointIdentifier isEqualToString:UI
ApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}
清单 8-10: shouldAllowExtensionPointIdentifier 委托方法
这段代码简单地检查extensionPointIdentifier的值,如果它与常量UIApplicationKeyboardExtensionPointIdentifier匹配,则返回NO。请注意,目前只有第三方键盘是可以通过这种方式禁用的扩展。
你已经看到了实现 IPC 的最佳方法,为了结束这一章,我将带你了解一种可能在实际应用中出现的 IPC 方法,但它并不太成功。
一个失败的 IPC 黑客:粘贴板
偶尔会有报告提到有人滥用UIPasteboard机制作为一种 IPC 通道。例如,有些人尝试使用它将用户的数据从应用程序的免费版本转移到“专业”版本,因为新安装的应用程序无法读取旧应用程序的数据。不要这样做!
设计用于与 Twitter 一起工作的 OAuth 库^(3)使用通用粘贴板作为一种机制,将身份验证信息从网页视图传递到应用程序的主部分,如下例所示:
- (void) pasteboardChanged: (NSNotification *) note {
➊ UIPasteboard *pb = [UIPasteboard generalPasteboard];
if ([note.userInfo objectForKey:UIPasteboardChangedTypesAddedKey] == nil)
return;
NSString *copied = pb.string;
if (copied.length != 7 || !copied.oauthtwitter_isNumeric) return;
➋ [self gotPin:copied];
}
在➊读取通用粘贴板的数据后,该库会验证数据并将其发送到➋处的gotPin方法。
但是通用粘贴板是所有应用程序共享的,并且设备上的任何进程都可以读取它。这使得粘贴板成为存储任何类似私密数据的地方特别不安全。我将在第十章中详细讲解粘贴板,但现在,请确保你正在检查的应用程序不会把任何你不希望其他应用程序知道的东西放到粘贴板上。
结语
虽然 iOS 中的 IPC 看似有限,但开发者往往会遇到无法解析外部输入、创建新的数据泄露,甚至可能将数据发送到错误的应用程序的情况。确保分享被适当限制,接收到的数据经过验证,发送的应用程序经过验证,并且不要仅仅相信接收 URL 处理程序就是你预期的那个,从而避免传递未加密的数据。
第九章:9
面向 iOS 的 Web 应用
自从为 iOS 引入第三方开发者 API 以来,Web 已成为 iOS 应用程序的重要组成部分。最初,这些 API 完全是基于 Web 的。虽然这可能让没有 Objective-C 或 Cocoa 经验的人生活更轻松,但它严重限制了非苹果应用的功能,并将它们 relegated 到了二等公民的地位。它们无法访问手机的本地功能,如地理定位,而且只能在浏览器中使用,而无法放置在主屏幕上。
尽管从那时起情况发生了剧烈变化,但从 iOS 集成 Web 应用程序的需求并没有改变。在本章中,你将深入了解本地 iOS 应用程序与 Web 应用程序之间的联系:如何与 Web 应用程序交互,哪些本地 iOS API 可以暴露给 Web 应用程序,以及各种方法的风险。
使用 (及滥用) UIWebViews
开发者使用 Web 视图来渲染和与 Web 内容交互,因为它们简单易实现,并提供类似浏览器的功能。大多数 Web 视图是 UIWebView 类的实例,使用 WebKit 渲染引擎^(1) 来显示 Web 内容。Web 视图常用于将应用程序的部分逻辑抽象化,以便在不同的移动应用平台之间共享,或者仅仅是将更多的逻辑卸载到 Web 应用程序,通常是因为 Web 应用编程方面的内部技术比 iOS 更为熟练。它们还经常用来作为查看第三方 Web 内容的方式,而无需离开应用并启动 Safari。例如,当你在 Facebook 动态中点击一篇文章时,内容会在 Facebook 应用中渲染。
从 iOS 8 开始,引入了 WKWebView 框架。这个框架为开发者提供了额外的灵活性,并可以访问苹果的高性能 Nitro JavaScript 引擎,从而显著提升使用 Web 视图的应用性能。由于你将继续看到 UIWebView,本章将首先介绍 UIWebView,并探讨两个 API 的使用。
使用 UIWebViews
Web 视图将应用程序的一部分逻辑转移到远程 Web API 或应用程序。因此,开发者对 Web 视图的控制程度较本地 iOS 应用程序低,但你可以采取一些控制措施,以让 Web 视图符合你的需求。
通过实现协议 UIWebViewDelegate 的 shouldStartLoadWithRequest 方法^(2),你可以在允许 URL 被打开之前,决定是否打开所有通过 Web 视图打开的 URL。例如,为了限制攻击面,你可以限制所有请求,只允许它们访问 HTTPS URL 或某些特定域名。如果你希望确保你的应用永远不会加载非 HTTPS 的 URL,你可以像 Listing 9-1 示例中那样进行操作。
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)
request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
➊ if ([[url scheme] isEqualToString:@"https"]) {
if ([url host] != nil) {
NSString *goodHost = @"happy.fluffy.bunnies.com";
➋ if ([[url host] isEqualToString:goodHost]) {
return YES;
}
}
}
return NO;
}
列表 9-1:拒绝非 HTTPS URL 和未知主机名
这个示例使用了与正在加载的 NSURLRequest 关联的 NSURL 的两个不同属性。在 ➊,检查 URL 的 scheme 属性,看它是否与指定的 https 协议匹配。在 ➋,将 host 属性与一个白名单域进行比较:happy.fluffy.bunnies.com。这两个限制将应用程序的 Web 视图访问仅限于您的域名,而不是任何可能被攻击者控制的域,并确保请求始终通过 HTTPS 传输,从而保护其内容免受网络攻击者的侵害。
Web 视图看起来是个不错的选择,因为它可以跨平台重用代码库,同时对本地系统保持一定的控制。然而,Web 视图确实存在一些严重的安全隐患。一个限制是无法升级随 UIWebView 一起打包的 WebKit 二进制文件。WebKit 是与 iOS 新版本一起打包的,并且不会从主操作系统中单独更新。这意味着,任何发现的 WebKit 漏洞在发布新版本的 iOS 之前都无法修复。
安全使用 Web 视图的另一个重要方面是妥善处理缓存数据,我将在下一节讨论这个问题。
在 UIWebViews 中执行 JavaScript
Web 视图的 JavaScript 引擎称为 JavaScriptCore,也由 Apple 称为 Nitro。虽然新的 WKWebView 类改进了 JavaScript 支持(请参见 “进入 WKWebView” 页 158),但与现代浏览器中的 JavaScript 引擎相比,UIWebView 中使用的 JavaScriptCore 实现存在一些不足之处。主要的限制是缺少即时编译(JIT)。
UIWebView 的 JavaScript 执行还将总分配限制为 10MB,并且运行时间限制为 10 秒,超过该时间点,执行将立即且无条件地停止。尽管有这些限制,应用程序仍然可以通过将脚本传递给 stringByEvaluatingJavaScriptFromString 来执行有限的 JavaScript,如列表 9-2 所示。
[webView stringByEvaluatingJavaScriptFromString:@"var elem =
document.createElement('script');"
"elem.type = 'text/javascript';"
"elem.text = 'aUselessFunc(name) {"
" alert('Ohai!'+name);"
"};"
"document.getElementById('head').appendChild(elem);"];
[webView stringByEvaluatingJavaScriptFromString:@"aUselessFunc('Mitch');"];
列表 9-2:将 JavaScript 注入到 Web 视图中
stringByEvaluatingJavaScriptFromString 方法接受一个参数,这是一个 JavaScript 代码块,用于插入到视图中。这里,创建了元素 elem,定义了一个简单的函数来生成一个警告框,并将该函数插入到 web 视图中。现在,可以通过后续调用 stringByEvaluatingJavaScriptFromString 来调用新定义的函数。
然而,请注意,允许在应用中动态执行 JavaScript 会使用户面临 JavaScript 注入攻击的风险。因此,应谨慎使用此功能,开发者切勿将不可信的输入反射到动态生成的脚本中。
你将在下一节中了解更多关于 JavaScriptCore 的内容,届时我将讨论如何绕过我之前提到的UIWebView的不足。
JavaScript-Cocoa 桥接的奖励与风险
为了克服UIWebView的限制,开发者使用了各种变通方法,将更多的原生功能暴露给基于网页的应用。例如,Cordova 开发框架通过巧妙(或危险)的网页视图实现,访问 Cocoa API,允许使用相机、加速计、地理定位功能、通讯录等。
在本节中,我将向你介绍一些流行的 JavaScript-Cocoa 桥接,提供它们在实际应用中的使用示例,并讨论它们带来的一些安全风险。
与 JavaScriptCore 进行接口交互
在 iOS 7 之前,[UIWebView stringByEvaluatingJavaScriptFromString:]是应用程序内部调用 JavaScript 的唯一方法。然而,iOS 7 发布了 JavaScriptCore 框架,它完全支持原生 Objective-C 和 JavaScript 运行时之间的桥接通信。该桥接通过新的JSContext全局对象创建,提供了访问 JavaScript 虚拟机以评估代码的能力。Objective-C 运行时还可以通过JSValue对象获取对 JavaScript 值的强引用。
你可以通过两种基本方式使用 JavaScriptCore 与 JavaScript 运行时进行交互:使用内联块或通过JSExport协议直接暴露 Objective-C 对象。我将简要介绍这两种方法的工作原理,然后讨论这种新攻击面带来的安全问题。
直接暴露 Objective-C 块
Objective-C 块的一种用途是提供一个简单的机制,将 Objective-C 代码暴露给 JavaScript。当你将 Objective-C 块暴露给 JavaScript 时,框架会自动将其包装为一个可调用的 JavaScript 函数,这样你就可以直接从 JavaScript 调用 Objective-C 代码。让我们来看一个例子——尽管这是一个假设的例子——在 Listing 9-3 中。
JSContext *context = [[JSContext alloc] init];
➊ context[@"shasum"] = ^(NSString *data, NSString *salt) {
const char *cSalt = [salt cStringUsingEncoding:NSUTF8StringEncoding];
const char *cData = [data cStringUsingEncoding:NSUTF8StringEncoding];
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cSalt, strlen(cSalt), cData, strlen(cData),
digest);
NSMutableString *hash = [NSMutableString stringWithCapacity:
CC_SHA256_DIGEST_LENGTH];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[hash appendFormat:@"%02x", digest[i]];
}
return hash;
};
Listing 9-3: 将 Objective-C 块暴露给 JavaScript
在这里,暴露了一个块(你可以看到它由^操作符在➊处定义),该块接收 JavaScript 中的密码和盐值,并使用 Common-Crypto 框架创建哈希。然后可以直接从 JavaScript 访问此块,生成用户的密码哈希,如 Listing 9-4 所示。
var password = document.getElementById('password');
var salt = document.getElementById('salt');
var pwhash = shasum(password, salt);
Listing 9-4: 调用暴露的 Objective-C 块的 JavaScript
这种技术使你能够利用 Cocoa Touch API,避免重新实现诸如加密或哈希等复杂且容易出错的操作。
Blocks 是将 Objective-C 代码暴露给 JavaScript 的最简单方式,但它们也有一些缺点。例如,所有桥接的对象都是不可变的,因此改变 Objective-C 变量的值不会影响与之映射的 JavaScript 变量。然而,如果你确实需要在两个执行上下文之间共享对象,你也可以使用JSExport协议暴露自定义类。
通过 JSExport 连接 Objective-C 和 JavaScript
JSExport协议允许应用程序将整个 Objective-C 类和实例暴露给 JavaScript,并像操作 JavaScript 对象一样对它们进行操作。此外,它们与 Objective-C 对应对象的引用是强引用,这意味着在一个环境中对对象的修改会反映到另一个环境中。定义继承了JSExport的协议中的变量和方法,向 JavaScriptCore 表明这些元素可以从 JavaScript 访问,如列表 9-5 所示。
@protocol UserExports <JSExport>
//exported variables
@property NSString *name;
@property NSString *address;
//exported functions
- (NSString *) updateUser:(NSDictionary *)info;
@end
列表 9-5:使用白名单方法暴露变量和方法
多亏了JSExport协议声明,JavaScript 可以访问变量name和address以及函数updateUser。苹果使得将这些对象暴露给 JavaScriptCore 变得非常简单,这也意味着开发者很容易不小心暴露出各种不必要的功能。幸运的是,这座桥接遵循完全可选择加入的模式:只有你在协议中实际定义的成员才会被暴露。除非在协议定义中明确列入白名单,否则在类接口中做的任何额外声明都会被隐藏,就像在列表 9-6 中password属性的情况一样。
@interface User : NSObject <UserExports> ➊
// non-exported variable
@property NSString *password;
// non-exported method declaration
- (BOOL) resetPassword;
@end
列表 9-6:在协议定义之外声明的元素在 JavaScript 中不可访问
User接口在➊处继承了UserExports,因此它也继承了JSExport。但是,password属性和resetPassword方法并未在UserExports中声明,因此它们不会暴露给 JavaScript。
现在,JavaScriptCore 了解了你的UserExports协议,它可以在你将其实例添加到JSContext时创建一个合适的包装对象,如下一个列表所示:
➊ JSContext *context = [[JSContext alloc] init];
➋ User *user= [[User alloc] init];
[user setName:@"Winston Furchill"];
[user setValue:24011965];
[user setHiddenName:@"Enigma"];
➌ context[@"user"] = user;
➍ JSValue val = [context evaluateScript:@"user.value"];
➎ JSValue val = [context evaluateScript:@"user.hiddenName"];
NSLog(@"value: %d", [val toInt32]); // => 23011965
NSLog(@"hiddenName: %@", [val toString]); // => undefined
这里,JSContext在➊处被设置,User类的一个实例在➋处被创建,并且给新用户的三个属性赋值。其中一个属性hiddenName只在@implementation中定义,而不是在协议中定义——这与列表 9-6 中password属性的情况相同。在➌处,创建的用户被桥接到JSContext。当代码随后尝试从 JavaScript 访问用户对象的值时,value属性在➍处成功访问,而访问hiddenName的尝试在➎处失败。
注意
在将对象导出到 JavaScriptCore 时要谨慎。如果攻击者利用了脚本注入漏洞,就可以运行任何导出的函数,本质上将脚本注入转变为在用户设备上的本地远程代码执行。
另一个有趣的点是,JavaScriptCore 不允许调用导出的类构造函数。(这是 iOS 中的一个 bug,截至 iOS 8,尚未解决。)因此,即使你将[User class]添加到你的上下文中,也无法使用new来创建新对象。然而,正如我通过一些测试发现的那样,实际上是可以绕过这个限制的。你可以实现一个导出的 Objective-C 块,该块接受一个类名,然后创建并返回一个任意类的实例给 JavaScript,正如我在这里所做的:
self.context[@"newInstance"] = ^(NSString *className) {
Class clazz = NSClassFromString(className);
id inst = [clazz alloc];
return inst;
};
[self.context evaluateScript:@"var u = newInstance('User');"];
JSValue *val = self.context[@"u"];
User *user = [val toObject];
NSLog(@"%@", [user class]); // => User
这种技术绕过了显式导出任何类的需求,并允许你实例化任何类型的对象并将其暴露给 JavaScript-Core。然而,没有成员被列入白名单以供导出,因此没有对类对象的任何方法或变量的强引用。显然,对于绕过 JavaScriptCore 实现的限制,仍然有很大的安全研究空间,因为 Objective-C 运行时是如此动态且强大。
关于 JavaScriptCore 框架的一个常见抱怨是,没有文档说明如何访问UIWebView的JSContext。我接下来会讨论一些可能的解决方法。
在 Web 视图中操作 JavaScript
为什么要在没有访问 Web 视图内JSContext的方式下暴露这种JSContext功能?苹果的意图尚不明确,但开发者只完成了一半的工作,即文档化 JavaScriptCore API。到目前为止,苹果并没有提供官方的方式来操作UIWebView的JSContext,但已有几个人发现了方法来实现这一点。它们大多数都涉及使用valueForKeyPath方法,正如在清单 9-7 中所示。
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext *context = [webView valueForKeyPath:@"documentView.webView.
mainFrame.javaScriptContext"];
context[@"document"][@"cookie"] = @"hello, I'm messing with cookies";
}
清单 9-7:通过 Objective-C 操作 DOM
由于这不是苹果官方认可的方法,因此无法保证此类代码能够通过 App Store 审核,但了解开发者可能会尝试在 JavaScript 与 Objective-C 之间进行通信的方式及其可能带来的问题是很有必要的。
当然,JSContext并不是将 JavaScript 与 Objective-C 连接的唯一方式。我将在下一节中描述另一种流行的桥接方式——Cordova。
使用 Cordova 执行 JavaScript
Cordova(在 Adobe 收购开发公司 Nitobi 之前称为 PhoneGap)是一个 SDK,它以平台无关的方式将原生移动 API 提供给 Web 视图的 JavaScript 执行环境。这使得可以像标准 Web 应用程序一样使用 HTML、CSS 和 JavaScript 开发移动应用程序。这些应用程序随后可以在 Cordova 支持的所有平台上运行。这可以显著减少开发时间,并且无需开发公司聘请特定平台的工程师,但 Cordova 的实现显著增加了应用程序的攻击面。
Cordova 的工作原理
Cordova 通过实现 NSURLProtocol 来桥接 JavaScript 和 Objective-C,以处理任何 JavaScript 发起的 XmlHttpRequest 到 file://!gap_exec。如果原生的 Cordova 库检测到对此 URI 的调用,它将尝试从请求头中提取类、方法、参数和回调信息,正如示例 9-8 所证明的那样。
+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest {
NSURL* theUrl = [theRequest URL];
CDVViewController* viewController = viewControllerForRequest(theRequest);
if ([[theUrl absoluteString] hasPrefix:kCDVAssetsLibraryPrefixs]) {
return YES;
} else if (viewController != nil) {
➊ if ([[theUrl path] isEqualToString:@"/!gap_exec"]) {
➋ NSString* queuedCommandsJSON = [theRequest valueForHTTPHeaderField:@"
cmds"];
NSString* requestId = [theRequest valueForHTTPHeaderField:@"rc"];
if (requestId == nil) {
NSLog(@"!cordova request missing rc header");
return NO;
}
BOOL hasCmds = [queuedCommandsJSON length] > 0;
if (hasCmds) {
SEL sel = @selector(enqueCommandBatch:);
➌ [viewController.commandQueue performSelectorOnMainThread:sel
withObject:queuedCommandsJSON waitUntilDone:NO];
示例 9-8:在 CDVURLProtocol.m^(3) 中检测原生库调用
在➊处,请求的 URL 被检查是否包含路径组件/!gap_exec,在➋处,提取 cmds HTTP 头部的值。然后,Cordova 将这些命令传递到命令队列➌,在那里它们将尽可能地被执行。当这些命令被排队时,Cordova 会在可用的 Cordova 插件映射中查找相关信息,这些插件本质上只是暴露了原生功能的各个部分,并且可以任意扩展。如果某个特定插件被启用,并且请求中的类可以实例化,那么将使用强大的 objc_msgSend 方法调用该方法,并传入提供的参数。
当调用完成时,原生代码通过 [UIWebView stringByEvaluatingJavaScriptFromString] 回调到 JavaScript 运行时,调用在 cordova.js 中定义的 cordova.require('cordova/exec').nativeCallback 方法,并提供原始回调 ID 以及原生代码执行的返回值。
这将前所未有地将大量原生对象控制权导出到 JavaScript 运行时,允许应用程序读取和写入文件,读取和写入钥匙串存储,通过 FTP 将本地文件上传到远程服务器等等。但随着功能的增加,也带来了潜在的风险。
使用 Cordova 的风险
如果你的应用程序包含任何脚本注入漏洞,并且用户能够影响应用程序的导航,攻击者就可能获得远程代码执行的机会。他们只需要注入回调函数,并结合调用来启动与原生代码的通信。例如,攻击者可能会注入一个调用来访问钥匙串中的项,获取所有用户联系人信息的副本,或读取文件并将其传递给他们选择的 JavaScript 函数,如示例 9-9 所示。
<script type="text/javascript">
var exec = cordova.require('cordova/exec');
function callback(msg) {
console.log(msg);
}
exec(callback, callback, "File", "readAsText", ["/private/var/mobile/Library/
Preferences/com.apple.MobileSMS.plist", "UTF-8",
0, 2048]);
</script>
示例 9-9:使用 Cordova 调用 Objective-C 来读取文件内容
这个攻击者提供的 JavaScript 读取了设备的com.apple.MobileSMS.plist,在 iOS 8 中,该文件对设备上的所有应用程序都是可访问的。^(4) 这使得攻击者能够检查用户的联系人信息,并确定设备的所有者。
一项合理的内建安全措施是域名白名单,它可以显著降低脚本注入的风险。^(5) Cordova 的默认安全策略阻止所有网络访问,仅允许与应用配置中 <access> 元素下的白名单域进行交互。白名单确实允许通过通配符([*])访问所有域,但不要懒惰—确保白名单中只有应用程序正常工作所需的域。您可以通过 Xcode 配置此项,通过向 Cordova.plist 中的 ExternalHosts 键添加值,如图 9-1 所示。

图 9-1:使用 ExternalHosts 键在 Cordova 中进行域名白名单配置
除了将本地代码对象暴露给网页视图之外,使用像 Cordova 这样的网页平台封装来实现移动应用程序还有许多其他缺点。主要是每个平台都有其特定的安全模型,基于特定的假设、API 和功能来保护用户并保障本地存储安全。一个平台的安全模型在其他平台上根本行不通。提供一个一刀切的实现,必然会为了易用性而忽视一些平台特有的安全优势。
例如,iOS 通过数据保护 API 提供安全存储(如我在第十三章中描述的),这些 API 需要特定的参数,这些参数不适合跨平台实现。因此,Cordova 不支持这些 API,从而无法精细控制何时对文件数据进行加密存储。为了解决这个问题,您可以启用权限级别的数据保护(参考“DataProtectionClass Entitlement”和第 223 页),这将为应用程序写入磁盘的所有数据应用一个默认的保护级别。
另一个常见问题是缺乏跨平台的类似安全存储元素。这意味着在 iOS 上无法直接访问 Keychain,尽管 Adobe 最终开发了一个开源插件^(6) 来解决这个问题。
这就是 UIWebView 和 JavaScript 桥接的全部内容,但新的应用程序(针对 iOS 8 及更新版本)将越来越多地使用 WKWebView API。我将在接下来的章节中介绍如何处理 WKWebView。
引入 WKWebView
如前所述,iOS 8 引入了一个新的 WebKit 接口来替代 UIWebView。WKWebView 解决了 UIWebView 的几个缺点,包括访问 Nitro JavaScript 引擎,这大大提高了 JavaScript 密集型任务的性能。让我们来看一下应用程序如何创建 WKWebView,以及 WKWebView 如何提升应用程序的安全性。
与 WKWebView 的互动
WKWebView 的实例化方式与 UIWebView 基本相同,如下所示:
CGRect webFrame = CGRectMake(0, 0, width, height);
WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
WKWebView *webView =[[WKWebView alloc] initWithFrame:webFrame
configuration:conf];
NSURL *url = [NSURL URLWithString:@"http://www.nostarch.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
这仅仅分配了一个新的 WKWebView 实例,然后通过 initWithFrame 方法对其进行初始化。
要自定义行为,WKWebView 还可以通过用户提供的 JavaScript 来实例化,如 示例 9-10 中所示。这允许你加载一个第三方网站,同时执行你自己自定义的 JavaScript 脚本。
CGRect webFrame = CGRectMake(0, 0, width, height);
➊ NSString *src = @"alert('Welcome to my WKWebView!')";
➋ WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
➌ WKUserScript *script = [[WKUserScript alloc] initWithSource:src
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
➍ WKUserContentController *controller = [[WKUserContentController alloc] init];
➎ [conf setUserContentController:controller];
➏ [controller addUserScript:script];
➐ WKWebView *webView =[[WKWebView alloc] initWithFrame:webFrame
configuration:conf];
示例 9-10:带自定义 JavaScript 的 WKWebView 实例化
在 ➊ 处,创建了一个由单一 JavaScript 命令组成的简单 NSString。在 ➋ 处,创建了一个配置对象,用于保存稍后创建的 Web 视图的配置参数。在 ➌ 处,创建并初始化了一个 WKUserScript 对象,该对象的 src 包含你希望执行的 JavaScript。然后,在 ➍ 处创建了一个 WKUserContentController,并在 ➎ 处将其设置到配置对象中。最后,在 ➏ 处通过 addUserScript 方法将脚本添加到控制器中,并在 ➐ 处实例化 Web 视图。
注意
与其他注入 JavaScript 的方法一样,务必小心不要在没有严格清理的情况下插入第三方提供的内容。
WKWebView 的安全优势
使用 WKWebView 有几个安全优势。首先,如果你计划加载的页面不需要 JavaScript,可以通过 setJavaScriptEnabled 方法禁用加载 JavaScript;如果远程站点包含恶意脚本,这将防止该脚本执行。你还可以启用 JavaScript,但通过 setJavaScriptCanOpenWindowsAutomatically 方法禁用从 JavaScript 打开新窗口——这样可以防止大多数弹出窗口,这在 Web 视图中非常烦人。
最后,可能最重要的一点是,你实际上可以检测 Web 视图的内容是否通过 HTTPS 加载,从而确保页面的任何部分都没有通过不安全的渠道加载。对于 UIWebView,当 Web 视图加载混合内容时,用户或开发者并不会收到任何提示——而 WKWebView 的 hasOnlySecureContent 方法解决了这个问题。示例 9-11 展示了一种实现相对安全的 WKWebView 的方法。
@interface ViewController ()
@property (strong, nonatomic) WKWebView *webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
➊ WKPreferences *pref = [[WKPreferences alloc] init];
[pref setJavaScriptEnabled:NO];
[pref setJavaScriptCanOpenWindowsAutomatically:NO];
➋ WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init];
[conf setPreferences:pref];
➌ NSURL *myURL = [NSURL URLWithString:@"https://people.mozilla.org/~mkelly/
mixed_test.html"];
➍ _webView = [[WKWebView alloc] initWithFrame:[[self view] frame]
configuration:conf];
[_webView setNavigationDelegate:self];
➎ [_webView loadRequest:[NSURLRequest requestWithURL:myURL]];
[[self view] addSubview:_webView];
}
➏ - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
if (![webView hasOnlySecureContent]) {
NSString *title = @"Ack! Mixed content!";
NSString *message = @"Not all content on this page was loaded securely.";
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
}
}
示例 9-11:一个安全的 WKWebView
这段代码使用了WKWebView提供的几个额外安全机制。在 ➊ 处,实例化了一个 WKPreferences 实例,并设置了 setJavaScriptEnabled 和 setJavaScriptCanOpenWindowsAutomatically 属性。(当然,这些是多余的,你可以选择最适合你需求的属性。)然后,在 ➋ 处实例化了一个 WKWebViewConfiguration 对象,并传入之前创建的 WKPreferences。在 ➌ 处,定义了要加载的 URL;在这个例子中,它只是一个包含混合内容的示例页面。在 ➍ 处,使用之前创建的配置生成了 WKWebView 实例。然后,代码请求在 ➎ 处加载给定的 URL。最后,在 ➏ 处实现了 didFinishNavigation 委托方法,该方法随后调用了网页视图的 hasOnlySecureContent。如果内容是混合的,用户会收到警告。
总结思考
尽管现代版本的 iOS 在允许开发者控制原生代码和网页内容之间的交互方面取得了很大进展,但仍存在一些遗留的黑客方式来桥接这二者,并且这些方式各有其独特性。此时,你应该了解主要的桥接机制,以及在哪里寻找潜在的恶意外部提供数据。
我还简要介绍了在处理网页内容时发生的一些缓存。在 第十章中,你将深入探讨数据如何泄露到本地文件系统,并被攻击者恢复的多种方式。
第十章:10
数据泄露
数据盗窃在移动设备领域是一个严重的隐患,因为包含重要个人和商业数据的设备常常会丢失或被盗。这里需要关注的主要威胁是取证攻击者,因此请特别小心,确保这些数据以无法被物理攻击者或被攻击的设备轻易提取的格式保存。不幸的是,对于哪些 API 实际上会存储敏感数据,存在很多混淆,这是可以理解的,因为很多行为并未文档化。
在本章中,我将探讨数据如何从您的应用程序泄露到设备的隐秘角落——甚至可能不小心同步到像 iCloud 这样的远程服务。您将学习如何在设备或自己的模拟器应用程序目录结构中搜索泄露的数据,以及如何防止这些泄露的发生。
NSLog 和 Apple 系统日志的真相
多年来,开发者一直使用 printf 来输出基本的调试信息。在 iOS 中,NSLog 看起来 是等效的,并且经常被这样使用。然而,NSLog 并不仅仅是将输出写入 Xcode 控制台,正如大多数人认为的那样。它的目的是将错误信息记录到 Apple 系统日志(ASL)设施中。以下是 Apple 的说法:
服务器接收到的消息会保存在数据存储中(受到输入过滤约束)。这个 API 允许客户端创建查询,并在消息数据存储中搜索匹配的消息。^(1)
所以,也许 NSLog 最好被视为 printf 和 syslog 之间的混合体,当调试时,它会在 Xcode 控制台中输出消息,并且在设备上时会将消息发送到全局系统日志。因此,可以推断,NSLog 记录的数据将可以被任何物理接触到设备的人检索,类似于其他缓存的应用数据。
阅读日志不需要特殊的工具。只需将 iOS 设备连接到 Mac,打开 Xcode,选择 窗口 → 设备,然后点击您的设备。设备的系统日志可能最初在控制台中不可见。如果不可见,点击面板左下角的那个小箭头。图 10-1 显示了如何通过设备窗口查看控制台日志的示例。

图 10-1:Xcode 中的设备窗口
苹果的系统日志设施有一个独特之处,使其与传统的 UNIX syslog 设施不同:你可以创建查询来搜索 ASL 中的现有数据。在 iOS 7 之前的版本中,这个功能无论是哪个应用最初提交了数据都能工作,这意味着任何一个应用记录的信息都可以被设备上的其他应用读取。任何应用程序也可以通过编程方式读取 ASL,正如 Oliver Drobnik 在 Cocoanetics 博客中所描述的那样^(2)。事实上,有几个应用程序使用这个 API 充当系统日志查看器。
在 iOS 7 及更高版本中,这一缺陷的影响已大大减轻,因为应用程序只能访问自己的日志。然而,所有应用程序的日志仍然可以通过物理访问设备读取,只要该设备与另一台计算机建立了信任关系(或者攻击者越狱了设备)。
由于在某些情况下日志信息可能会泄漏,你需要格外小心,以确保敏感信息不会出现在系统日志中。例如,我曾见过包含类似清单 10-1 中那种可怕代码的应用程序。
NSLog(@"Sending username \%@ and password \%@", myName, myPass);
清单 10-1:请不要这么做。
如果你将用户名、密码等信息发送到NSLog,实际上是在泄露用户的私密信息,这种行为是不道德的。为了弥补这一点,请停止滥用NSLog;在发布应用给用户之前,将其从代码中去除。
在发布版本中禁用 NSLog
去除NSLog输出的最简单方法是使用一个变参宏(清单 10-2),它会使得NSLog在 Xcode 的 Debug 模式之外变为无操作(no-op)。
#ifdef DEBUG
# define NSLog(...) NSLog(__VA_ARGS__);
#else
# define NSLog(...)
#endif
清单 10-2:在非调试版本中禁用 NSLog
尽管NSLog看起来很糟糕,但包含NSLog的应用程序确实可以进入 App Store。虽然这一情况可能会在某个时刻发生变化,但你不能依赖苹果来检测应用程序是否在记录你不打算公开的信息,也不能依赖苹果阻止其他应用读取这些日志数据。
改用断点操作进行日志记录
另一种选择是使用断点操作进行日志记录,正如我在第五章中提到的那样。在这种情况下,你实际上是通过调试器进行日志记录,而不是程序本身。这在某些情况下更为方便,并且在部署时不会将数据写入系统日志,从而减少了将启用日志记录的代码发布到生产环境中的风险。掌握如何使用这些操作,对未来的调试工作也会有帮助。
断点操作存储在项目中,而不是源代码本身中。它们也是用户特定的,因此你只会看到你关心的断点和日志操作,而不会让团队中的每个人都用日志语句弄乱代码库。但在需要时,Xcode 允许你与其他用户共享你的断点,使它们成为主项目的一部分(见图 10-2)。
你还可以轻松启用或禁用操作,并指定它们在断点被触发一定次数之前不输出。你甚至可以指定复杂的断点条件,定义何时执行相关的操作。
如果你想禁用项目中的所有断点,可以通过几种方式在 Xcode 中实现。你可以打开断点导航器,右键点击工作区图标(图 10-2),或使用快捷键
-Y。

图 10-2:与其他用户共享断点并禁用 Xcode 中的所有断点
虽然 NSLog 会将信息泄漏到磁盘上,供物理攻击者(以及在某些版本的 iOS 中的恶意应用)读取,但数据也可以通过更为瞬时的机制在应用之间泄漏,例如 iOS 粘贴板。让我们现在来看一下它们。
敏感数据如何通过粘贴板泄漏
iOS 粘贴板是一个灵活的机制,用于在应用之间或应用内部共享任意数据。通过粘贴板,你可以在应用之间共享文本数据或序列化对象,并可以选择将这些粘贴板持久化到磁盘上。
无限制的系统粘贴板
系统有两个默认的粘贴板:UIPasteboardNameGeneral 和 UIPasteboardNameFind。前者是几乎所有应用在使用剪切、复制或粘贴菜单项时默认读取和写入的粘贴板,也是你希望在第三方应用之间共享数据时选择的粘贴板。后者是一个特殊的粘贴板,用于存储最后一次在 UISearchBar 中输入的搜索字符串的内容,因此应用可以自动识别用户在其他应用中搜索的内容。
注意
与官方描述的*UIPasteboardNameFind*相反,这个粘贴板在现实中从未被使用过。这个错误已被苹果承认,但尚未修复,也没有更新文档。作为一名安全顾问,我只能希望它会被修复,这样我就可以抱怨它是一个安全漏洞。
重要的是要记住,系统剪贴板 没有 访问控制或限制。如果你的应用程序将某些内容存储在剪贴板上,任何应用都可以访问、删除或篡改这些数据。这种篡改可能来自后台运行的进程,定期轮询剪贴板内容以窃取敏感数据(参见 Osamu Noguchi 的 UIPasteBoardSniffer^(3),展示了这种技术)。因此,你需要特别小心,特别是在 UIPasteboardNameGeneral 和其他剪贴板中存储的内容。
自定义命名剪贴板的风险
自定义命名的剪贴板有时被称为 私有 剪贴板,这其实是一个不恰当的称呼。虽然应用程序可以创建自己的剪贴板供内部使用或与其他特定应用共享,但在 iOS 7 之前的版本中,自定义剪贴板是 公共的,只要名称已知,任何程序都可以使用它们。
自定义剪贴板通过 pasteboardWithName 创建,在 iOS 7 及更高版本中,pasteboardWithName 和 pasteboardWithUniqueName 是特定于应用组内所有应用的。如果应用组外的其他应用尝试创建一个已经在使用的名字的剪贴板,它们将被分配一个完全独立的剪贴板。然而,值得注意的是,两个系统剪贴板仍然可以被任何应用访问。鉴于许多设备无法升级到 iOS 6,更不用说 iOS 7,你应当仔细检查在不同版本的 iOS 中如何使用自定义剪贴板。
使用自定义剪贴板的一件事是,可以通过将 persistent 属性设置为 YES 来标记它在重启后仍然存在。这将导致剪贴板内容被写入 $SIMPATH/Devices/<设备 ID>/data/Library/Caches/com.apple.UIKit.pboard/pasteboardDB,与其他应用程序的剪贴板一起存储。列表 10-3 显示了你可能会在 pasteboardDB 文件中看到的一些数据。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<integer>1</integer>
<dict>
<key>bundle</key>
<string>com.apple.UIKit.pboard</string>
<key>items</key>
<array/>
<key>itemsunderlock</key>
<array/>
<key>name</key>
<string>com.apple.UIKit.pboard.find</string>
<key>persistent</key>
<true/>
</dict>
--snip--
<dict>
<key>bundle</key>
<string>com.apple.UIKit.pboard</string>
<key>items</key>
<array>
<dict>
<key>Apple Web Archive pasteboard type</key>
<data>
bigbase64encodedblob==
</data>
<key>public.text</key>
<data>
aHR0cDovL2J1cnAvY2VydA==
</data>
</dict>
</array>
<key>itemsunderlock</key>
<array/>
<key>name</key>
<string>com.apple.UIKit.pboard.general</string>
<key>persistent</key>
<true/>
</dict>
</array>
</plist>
列表 10-3:com.apple.UIKit.pboard/pasteboardDB 文件的可能内容
base64 编码的二进制数据 bigbase64encodedblob(太大,无法完整包含)和 aHR0cDovL2J1cnAvY2VydA 保存了剪贴板内容,任何能够读取 pasteboardDB 的应用程序都可以访问这些内容。还需要注意的是,剪贴板有不同类型:Apple Web Archive 剪贴板允许将整个网页存储,而 public.text 剪贴板则是一般剪贴板的文本内容。^(4)
剪贴板数据保护策略
为了最小化信息泄露的风险,最好仔细分析你希望通过使用剪贴板实现的行为。以下是一些你可以问自己的问题:
• 我是否希望用户将信息复制到其他应用程序中,还是他们仅需要在我的应用程序内移动数据?
• 剪贴板数据应该保留多久?
• 是否有应用程序中某些地方永远不应复制数据?
• 是否有应用程序部分永远不应接收粘贴的数据?
这些问题的答案将指导你在应用程序中如何处理剪贴板数据。你可以采用几种不同的方式来最小化数据泄露的风险。
切换应用时清空剪贴板
如果你希望用户仅在你的应用程序内进行复制和粘贴,你可以在适当的事件中清空剪贴板,以确保当用户切换应用时数据不会留在剪贴板上。为此,可以在 applicationDidEnterBackground 和 applicationWillTerminate 事件中通过设置 pasteBoard.items = nil 来清空剪贴板。这样做不会阻止后台运行的应用程序读取剪贴板,但会缩短剪贴板上数据的存活时间,并阻止用户将数据粘贴到不应该粘贴的应用程序中。
请记住,清空剪贴板可能会干扰最终用户或其他应用程序出于不同目的使用的数据。你可能希望创建一个标志,指示是否有潜在敏感数据已写入剪贴板,并仅在特定条件下清空剪贴板。
选择性禁止复制/粘贴
即便你确实希望允许用户复制和粘贴,有时也会有特定的地方需要禁止这一选项。例如,你可能想要防止用户粘贴 PIN 码或安全问题的答案(此类数据本不应出现在剪贴板上),但又允许用户从电子邮件中粘贴电子邮件地址。
注意
这并不是说你应该使用安全问题,因为安全问题往往通过使用公开可得的信息作为认证工具,容易导致账户劫持。你将在《键盘记录与自动修正数据库》中详细了解这一点,见第 175 页。
官方的方法是使用 canPerformAction:withSender 响应者方法来允许用户粘贴某些信息,并防止他们粘贴其他信息。^(5) 如图 10-3 所示,在 Xcode 中创建一个新类。

图 10-3:创建 restrictedUITextField 子类
然后,编辑 restrictedUITextField.m 并添加 canPerformAction 方法。
#import "restrictedUITextField.h"
@implementation restrictedUITextField
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
➊ -(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
➋ if (action == @selector(cut:) || action == @selector(copy:))
return NO;
else
return YES;
}
@end
清单 10-4:将 canPerformAction 添加到 restrictedUITextField.m
在➊处,canPerformAction方法接收一个action选择器,可以检查该选择器以查看在➋处请求的操作类型。你可以使用UIResponderStandardEditActions协议中指定的任何方法。如果你希望完全禁用上下文菜单,当然可以在任何情况下都返回NO。
查找并修补 HTTP 缓存泄漏
你还会发现 URL 加载系统的缓存数据被存储在echoone.com/filejuicer/下载 File Juicer,图 10-4 展示了它提供的输出类型。

图 10-4:检查缓存数据库的内容,按文件类型和目录分割,由 File Juicer 提供
File Juicer 根据特定的文件类型将数据拆分为多个目录,这样你可以调查存储的图像、plist 文件、SQLite 数据库或其他二进制文件类型的纯文本转换。
一旦你了解了应用通过缓存数据暴露的数据类型,就可以考虑如何最佳地管理这些数据。
缓存管理
iOS 上的缓存管理有些复杂。有很多配置设置以及看似无穷无尽的方式来影响缓存策略。除此之外,平台尽力缓存并复制它能获取到的所有内容,以尝试改善用户体验。开发者需要判断哪些方法可以实现安全的缓存管理,但很容易让自己产生虚假的安全感。渗透测试者必须知道,哪些看似正确的做法实际上可能将敏感信息泄露到磁盘上。我们来谈谈所有不正确的缓存管理方式。
如我在第五章中提到的,文档化的移除缓存数据的方法[NSURLCache removeAllCachedResponses]仅仅从内存中移除缓存项。这对于安全目的来说基本没用,因为相同的信息仍然会保存在磁盘上,并且不会被移除。也许有更好的方法。
理想情况下,你永远不需要删除缓存,因为删除缓存意味着你一开始就进行了缓存操作。如果响应数据如此敏感,那为什么不干脆不缓存它呢?我们不妨试试看。
限制缓存响应的第一个地方是在NSURLCache配置中,如清单 10-5 所示。这个 API 允许你控制平台为缓存分配的内存和磁盘容量。
NSURLCache *urlCache = [[NSURLCache alloc] init];
[urlCache setDiskCapacity:0];
[NSURLCache setSharedURLCache:urlCache];
清单 10-5:将磁盘缓存存储限制为零字节
这个策略的问题在于,容量管理 API 并不打算作为安全机制。相反,这些配置是为了在内存或磁盘空间不足时为系统提供信息。NSURLCache文档^(6) 指出,只有在必要时,磁盘和内存中的缓存才会被缩减到配置的大小。
所以你不能完全相信配置缓存容量。那么,设置缓存策略为NSURLRequestReloadIgnoringLocalCacheData,强制 URL 加载系统忽略任何缓存响应并重新获取数据怎么样?下面是这种方法的实现:
NSURLRequest* req = [NSURLRequest requestWithURL:aURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:666.0];
[myWebView loadRequest:req];
但是这个策略并没有隐含地阻止响应被缓存;它只是阻止了仅在后续请求中检索缓存响应的 URL 加载系统。任何先前缓存的响应将继续保存在磁盘上,这会导致问题,尤其是如果你的初始应用实现允许缓存的话。没有办法。
正如我所尝试演示的那样,如果你依赖系统默认的 Web 视图缓存管理,你可能只是在实施许多实际上并不能保护用户的预防措施。如果你想可靠地控制应用程序缓存的内容,你需要自己来做。幸运的是,这其实并不困难。
Cocoa Touch API 允许开发者在响应被缓存之前,按请求逐个操作响应,可以使用[NSURLConnection connection:willCacheResponse:]方法。如果你不想缓存数据,可以实现委托方法,如列表 10-6 所示。
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
NSCachedURLResponse *newCachedResponse = cachedResponse;
if ([[[[cachedResponse response] URL] scheme] isEqual:@"https"]) {
newCachedResponse=nil;
}
return newCachedResponse;
}
列表 10-6:防止通过安全连接提供的响应被缓存
该委托的实现只是返回NULL,而不是响应数据的NSCachedURLResponse表示。
类似地,对于使用NSURLSession类获取的数据,你需要实现[NSURLSessionDataDelegate URLSession:dataTask:willCacheResponse:completionHandler:]委托方法。然而,要小心完全依赖这个方法,因为它仅在数据和上传任务中调用。下载任务的缓存行为仍将仅由缓存策略决定,应该像列表 10-6 一样解决。^(7)
总结来说,iOS 上的缓存是不可靠的。要小心,并在长时间使用后再次检查你的应用,确保它没有留下敏感信息。
移除缓存数据的解决方案
删除本地缓存数据的文档方式是使用共享 URL 缓存的removeAllCachedResponses方法,如列表 10-7 所示。
[[NSURLCache sharedURLCache] removeAllCachedResponses];
列表 10-7:删除缓存数据的文档 API
有一个类似的方法,removeCachedResponseForRequest,它可以仅删除特定网站的缓存数据。然而,正如你在第四章中发现的那样,它只会从内存中删除缓存数据,而不会从你真正关心的磁盘缓存中删除数据。如果 Apple 的 bug 跟踪系统不是一个无法逃逸任何光线和信息的无限炙热点的话,我真想提交一个 bug。^(8) 无论如何,你可以通过几种方式绕过这个缓存问题;如果你不幸需要报告一个 bug,那就得自己解决了。
直接不缓存
在大多数情况下,最好完全禁止缓存,而不是事后逐个清理。你可以主动将磁盘和内存的缓存容量设置为零(示例 10-8),或者如果你对内存中的缓存比较放心,也可以仅禁用磁盘缓存。
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[[NSURLCache sharedURLCache] setDiskCapacity:0];
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
// other init code
}
示例 10-8:通过限制允许的存储空间来禁止缓存存储
另外,你可以实现 NSURLConnection 的 willCacheResponse 委托方法,返回 nil,如示例 10-9 所示。
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
NSCachedURLResponse *newCachedResponse=cachedResponse;
➊ if ([cachedResponse response]) {
➋ newCachedResponse=nil;
}
return newCachedResponse;
}
示例 10-9:示例缓存丢弃代码
这只是检查在 ➊ 处是否已发送缓存响应,如果找到了,则在 ➋ 处将其设置为 nil。你还可以通过检查响应的属性,在返回对象到缓存之前有条件地缓存数据,如示例 10-10 所示。
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
NSCachedURLResponse *newCachedResponse=cachedResponse;
➊ if ([[[[cachedResponse response] URL] scheme] isEqual:@"https"]) {
newCachedResponse=nil;
}
return newCachedResponse;
}
示例 10-10:条件缓存丢弃代码
这几乎与示例 10-9 中的代码相同,但它额外检查在 ➊ 处被缓存的响应,以确定其是否通过 HTTPS 传输,并在这种情况下丢弃它。
如果你使用的是 NSURLSession,你还可以使用临时会话,这些会话不会将任何数据存储到磁盘,包括缓存、凭据等。创建一个临时会话非常简单,只需为你的 NSURLSession 实例化一个配置对象,如下所示:
NSURLSessionConfiguration *config = [NSURLSessionConfiguration
ephemeralSessionConfiguration];
你可以在第七章中找到更多关于如何使用 NSURLSession 的信息和示例。
通过服务器禁用缓存
假设你控制着你的应用程序与之通信的服务器,你可以通过 Cache-Control HTTP 头来指示客户端不要缓存请求。这允许你要么禁用整个应用程序的缓存,要么仅对特定请求应用缓存。实现该机制的方式依赖于服务器端的编程语言,但你需要返回的头部是以下内容:
Cache-Control: no-cache, no-store, must-revalidate
遗憾的是,至少在某些版本的 iOS 中(已验证到 6.1 版本),这些头部并没有得到遵守。尽管如此,最好仍然为敏感资源设置这些头部,但不要完全依赖这种方法来解决问题。
使用核选项
之前的方法会阻止数据被缓存,但有时你可能希望缓存数据,然后稍后清理它。这可能是出于性能考虑,或者是因为你在修复应用程序新版本中缓存问题,而该版本已经在本地磁盘上缓存了数据。无论原因是什么,按照文档中的方式清理缓存不起作用,因此你只能像在示例 10-11 中那样强制移除缓存数据。
NSString *cacheDir=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
NSUserDomainMask, YES) objectAtIndex:0];
[[NSFileManager defaultManager] removeItemAtPath:cacheDir
error:nil];
示例 10-11:手动移除缓存数据库
没有保证系统的其他部分在你手动清理缓存数据时不会出问题。然而,这种方法是我发现的唯一可靠方式,用来在缓存数据已经写入磁盘后移除它。
HTTP 本地存储和数据库中的数据泄露
HTML 5 规范允许网站在客户端存储和检索大量数据(比 cookie 能存储的更多)。这些机制有时用于本地缓存数据,使主要基于 Web 的应用程序能够在离线模式下运行。你可以在设备或模拟器上的多个位置找到这些数据库,包括以下位置:
• /Library/Caches/.localstorage*
• /Library/Caches/Databases.db
• /Library/Caches/file__0/.db*
你可以像处理 HTTP 缓存一样,将这些位置提供给 File Juicer,以便访问纯文本数据。对于更大的本地存储和 SQL 数据库,一个明显的应用场景是存储关于通信的结构化信息,例如电子邮件,这样即使用户没有手机信号,也能访问这些通信。这可能会在存储数据库中留下痕迹,如图 10-5 所示。
这种暴露可能是元数据可以接受的风险,但将其存储在加密的 SQLite 存储中可能更好,特别是当存储完整的消息内容时。我将在“数据保护 API”中详细讲解如何实现这一点,见第 219 页。

图 10-5:邮件客户端中留下的电子邮件元数据
键盘记录与自动修正数据库
每个人都熟悉 iOS 的自动完成功能,它是源源不断的娱乐和有趣的打字错误(当试图输入脏话时,尤其令人沮丧)。这个系统的一个方面在媒体中引起了一些关注,即自动完成功能充当了一个意外的键盘记录器,记录用户键入的部分文本,基本上是一个纯文本文件,用来帮助未来的自动补全。一个取证攻击者可以从中检索到这个补全数据库。
这种行为已经在密码字段中被禁用——也就是说,对于设置了setSecureTextEntry:YES的UITextField对象——但应用程序中的许多其他表单也可能包含敏感数据。因此,开发人员必须考虑用户体验和安全性之间常见的权衡。对于某些应用程序,存储任何未加密的数据到磁盘都是不可接受的。其他应用程序处理敏感数据,但涉及大量文本输入,因此禁用自动更正将变得非常麻烦。
但是,处理少量敏感数据的字段,毫无疑问是简单明了的。例如,考虑一下安全问题的答案。对于这些字段,你需要通过将autocorrectionType属性设置为UITextAutocorrectionTypeNo来禁用自动更正功能,适用于UITextField和UITextView对象。这对于UISearchBar对象同样适用(也是个不错的主意),因为搜索内容泄露到磁盘通常是不希望的。查看清单 10-12 了解如何禁用此属性的示例。
UITextField *sensitiveTextField = [[UITextField alloc] initWithFrame:CGRectMake(0,
0, 25, 25)];
[sensitiveTextField setAutocorrectionType:UITextAutocorrectionTypeNo];
清单 10-12:禁用 UITextField 的自动更正功能
当然,注意我说的是,“你将想要禁用这个行为。”你想要禁用,但你做不到。在 iOS 5.1 左右,一个 bug 悄悄出现,即使你禁用了自动更正、自动大写、拼写检查等,磁盘上的单词缓存仍会被更新。目前有两种绕过这种情况的方法,从非常傻到完全荒谬。
傻乎乎的做法(如清单 10-13 所示)是使用UITextView(注意是View,而不是Field),并发送消息setSecureTextEntry:YES。UITextView类实际上并没有正确实现UITextInputTraits协议^(9),因此文本不会像在配置为密码输入的UITextField中那样被圆圈遮挡。然而,它确实可以防止文本写入磁盘。
-(BOOL)twiddleTextView:(UITextView *)textView {
[textView setSecureTextEntry:YES];
}
清单 10-13:设置 SecureTextEntry 属性在 UITextView 上的应用
可笑的方法,适用于UITextView和UITextField对象,显示在清单 10-14 中。
-(BOOL)twiddleTextField:(UITextField *)textField {
[textField setSecureTextEntry:YES];
[textField setSecureTextEntry:NO];
}
清单 10-14:在 UITextField 上摆弄 setSecureTextEntry 功能
是的,真的。只需开启按键记录,然后再关闭它。
这些类的实现方式是,如果你只是简单地切换它,它会忘记重新开启按键记录。遗憾的是,UISearchbar 也没有正确实现协议,因此你无法在搜索框上使用这个技巧。如果防止搜索框数据泄露至关重要,你可能想要用一个适当样式的文本框来替代搜索框。
当然,这个 bug 可能会在未来的操作系统版本中得到修复,所以你应该谨慎,确保你的应用运行在你已经测试过行为的操作系统版本上,然后再执行这种“是/否切换”技巧。清单 10-15 展示了如何做到这一点。
UITextField *sensitiveTextField = [[UITextField alloc] initWithFrame:CGRectMake(0,
0, 25, 25)];
[sensitiveTextField setAutocorrectionType: UITextAutocorrectionTypeNo];
if ([[[UIDevice currentDevice] systemVersion] isEqual: @"8.1.4"]) {
[sensitiveTextField setSecureTextEntry:YES];
[sensitiveTextField setSecureTextEntry:NO];
}
清单 10-15:检查 iOS 版本
为了帮助验证您的应用程序是否泄露了任何意外信息,您还可以检查模拟器或越狱设备上的

图 10-6:使用键盘输入消息内容后的 dynamic-text.dat 内容。请注意,单词的顺序并不反映它们被输入的顺序。
在 iOS 8 及更高版本中,额外的信息被存储在键盘缓存中。这些数据用于帮助快速输入预测系统(QuickType),但它也泄露了关于对话和与设备所有者交流过的人更多的信息。在
误用用户偏好设置
正如我在第三章中简要提到的,用户偏好设置通常包含敏感信息。但实际上,用户默认设置的目的是定义诸如应用程序的 API 应该使用哪个 URL 或其他非敏感的偏好信息。
用户偏好设置通过NSUserDefaults API 或较少使用的CFPreferences API 进行操作,许多开发者显然并不清楚这些数据在设备上的处理方式。这些文件的限制相对宽松,用户的偏好设置可以很容易地被读取和修改,使用常见的工具(如 iExplorer)即可实现。
清单 10-16 显示了来自 iGoat 项目的一个故意糟糕的NSUserDefaults使用方式。^(11)
NSUserDefaults *credentials = [NSUserDefaults standardUserDefaults];
[credentials setObject:self.username.text forKey:@"username"];
[credentials setObject:self.password.text forKey:@"password"];
[credentials synchronize];
清单 10-16:使用NSUserDefaults的最糟糕方式
这本质上是数据泄露的最坏情况:凭证以明文存储在属于应用的 plist 文件中。许多实际应用程序就是以这种方式存储用户凭证的,且许多因此受到了批评。
一个较不常见的问题是,NSUserDefaults 可能被开发者用来存储一些实际上不应该由用户控制的信息。例如,有些应用会交出控制权,允许用户决定是否可以下载和存储文件,或是否需要在使用应用之前输入 PIN 码。为了保护用户,尽可能让服务器来执行这些决策。
在审核应用程序时,请检查每次使用NSUserDefaults或CFPreferences API 的情况,以确保存储的数据是合适的。那里不应存储任何机密信息或用户不希望被修改的信息。
处理快照中的敏感数据
正如我在第三章中所讨论的,iOS 会在将应用发送到后台之前拍摄当前屏幕的快照,这样在重新打开应用时就可以生成动画。这导致潜在的敏感信息散布在磁盘上,有时即使用户并未故意将应用送入后台也会发生。例如,如果某人在输入敏感信息时接了电话,那么该屏幕状态将被写入磁盘,并一直保留,直到被另一个快照覆盖。我见过许多应用程序在这种方式下记录用户的社会安全号码或信用卡号码。
一旦这些快照被写入磁盘,物理攻击者可以使用常见的取证工具轻松地恢复它们。你甚至可以使用模拟器观察文件被写入的过程,如图 10-7 所示。

图 10-7:用户在 Wikipedia 上搜索尴尬材料的快照,已保存到本地存储
只需挂起应用程序并打开UIApplicationAutomaticSnapshot Default-Portrait.png,你可以在应用程序的Library/Caches/Snapshots/com.mycompany.myapp目录下找到该文件。不幸的是,应用程序不能直接手动删除快照。然而,还是有几种其他方法可以防止这些数据泄露。
屏幕清理策略
首先,你可以在实际截取屏幕截图之前改变屏幕状态。你应该在applicationDidEnterBackground委托方法中实现这一点,这是当应用程序即将挂起时系统发送给你的消息,给你几秒钟时间来完成任何任务。
这个委托与applicationWillResignActive或applicationWillTerminate事件不同。前者在应用程序暂时失去焦点时触发(例如,当被来电覆盖时),后者则在应用程序被强制终止或选择退出后台操作时触发。^(12)有关 iOS 应用生命周期中接收到的事件的简化示例,请参见图 10-8。

图 10-8:简化的 iOS 应用生命周期。可以在应用委托中定义处理这些事件的代码。
在完成这些任务后,应该拍摄快照,并且应用程序应该消失,伴随着轻微的“嗖”声动画。但你如何清理用户的屏幕呢?
最简单且最可靠的屏幕内容遮掩方法,也是我主要推荐的方法,就是在所有当前视图上方放置一个包含一些 logo 艺术的启动画面。你可以按照 Listing 10-17 所示来实现。
- (void)applicationDidEnterBackground:(UIApplication *)application {
application = [UIApplication sharedApplication];
self.splash = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen]
bounds]];
[self.splash setImage:[UIImage imageNamed:@"myimage.png"]];
[self.splash setUserInteractionEnabled:NO];
[[application keyWindow] addSubview:splash];
}
Listing 10-17:应用启动画面
在添加了这段代码后,当应用程序进入后台时,你应该将存储在myimage.png中的图像设置为启动画面。或者,你可以设置相关容器对象的hidden属性——例如,UITextField,它的内容可能是敏感的。你也可以用这种方法隐藏整个UIView。这种方法不太好看,但在紧急情况下能轻松完成任务。
一个稍微复杂一点的选项是执行你自己的动画^(13),如 Listing 10-18 所示。这只是做了一个渐变消失的动画,然后再从视图中移除内容。
- (void)fadeMe {
[UIView animateWithDuration:0.2
animations:^{view.alpha = 0.0;}
completion:^(BOOL finished){[view removeFromSuperview];}
];
}
Listing 10-18:动画渐变到透明
我甚至看到有一个应用程序会自动截图当前屏幕状态,并将截图通过模糊算法处理。它看起来很漂亮,但处理所有角落情况非常棘手,而且你需要确保模糊效果足够强烈,以至于攻击者无法还原它。
不管你采用哪种混淆方法,你还需要在applicationDidBecomeActive或applicationWillEnterForeground委托方法中撤销这些更改。例如,为了移除 Listing 10-17 中放置的屏幕上的启动画面,你可以在applicationWillEnterForeground方法中添加类似 Listing 10-19 的内容。
- (void)applicationWillEnterForeground:(UIApplication *)application {
[self.splash removeFromSuperview];
self.splash = nil;
}
Listing 10-19:移除启动画面
在你完成之前,通过反复将应用程序置于不同的状态,同时监控应用程序的Library/Caches/Snapshots/com.mycompany.myapp目录,确保你的清理技术有效。检查保存在该目录中的 PNG 图片,确保窗口的所有部分都被启动画面遮盖。
注意
com.mycompany.myapp目录在每次应用程序挂起时都会重新创建。如果你通过终端监视该目录中的文件创建,你需要重新进入该目录,使用*cd $PWD*或类似的命令才能看到文件。
这些屏幕清理策略为什么有效?
人们常常误解我刚才描述的修复方法,因为他们没有理解 iOS 是如何布局视图和窗口的,所以我制作了一个流程图(图 10-9),展示了你需要知道的一切。

图 10-9:iOS 视图层级
每个在屏幕上显示内容的应用程序都由一个图层支持,默认情况下是CALayer。在图层之上是一个UIWindow类的实例,它管理一个或多个视图,即UIView类的实例。UIView是层次结构的,因此一个视图可以有多个子视图,包括按钮、文本框等等。
iOS 应用程序通常只有一个UIWindow,但多个窗口也是完全可能的。默认情况下,窗口的windowLevel属性值为 0.0,表示该窗口处于UIWindowLevelNormal级别。其他定义的级别包括UIWindowLevelAlert和UIWindowLevelStatusBar,这两个级别优先于UIWindowLevelNormal,意味着它们会出现在其他窗口之上。最明显的场景是弹出警告框,在这种情况下,UIAlertView默认会在所有其他窗口之上创建一个新窗口,但不包括状态栏。
当前接收用户事件的窗口被称为关键窗口,可以通过UIApplication中的keyWindow方法来引用它。
常见的清理错误
不理解 iOS 窗口和视图的开发者经常会错误地清理屏幕。我见过一些应用程序需要经过几次开发迭代才能正确实现。我看到的一个问题是只将关键窗口的rootViewController设置为hidden,如下所示:
UIApplication *application;
application = [UIApplication sharedApplication];
[[[[application] keyWindow] rootViewController] view] setHidden:YES];
这个错误是可以理解的,因为大多数开发者在编写图形界面时习惯使用UIView。虽然代码看起来像是正常工作的,但它仍然会让根视图的任何子视图保持可见。一个改进的方法是隐藏整个关键窗口,如下所示:
UIApplication *application;
application = [UIApplication sharedApplication];
[[[application] keyWindow] setHidden:YES];
但是隐藏关键窗口也不是一种万无一失的选择,因为任何UIAlertView窗口都会出现在其他内容之上并成为关键窗口;实际上,您将只会隐藏警告框。
由于几种隐藏内容的方法容易出错,我几乎总是建议开发者使用启动画面方法。然而,对于某些使用场景,还有一种更简单、万无一失的方法:完全防止挂起。
通过防止挂起来避免快照
如果您的应用程序根本不需要挂起和恢复(也就是说,如果您希望每次启动应用时都有一个全新的开始),那么可以使用 Xcode 的 plist 编辑器将“应用程序在后台不运行”项添加到您的 plist 文件中,并将其值设置为YES,如图 10-10 所示。您还可以在您喜欢的文本编辑器中,将UIApplicationExitsOnSuspend设置为YES,以便在Info.plist文件中实现。
添加该项将使应用程序跳转到applicationWillTerminate事件,而不是停留在applicationDidEnterBackground事件,该事件通常会紧接着屏幕截图的捕捉过程。

图 10-10:将“应用程序在后台不运行”项添加到 plist 文件中
由于状态保存导致的内存泄漏
iOS 6 引入了状态保存的概念,这为在应用程序调用之间保持状态提供了一种方法,即使在此过程中应用程序被终止。当触发状态保存时,调用每个可保存对象的encodeRestorableStateWithCoder委托方法,其中包含如何将各种 UI 元素序列化到磁盘的指令。然后,在应用程序重新启动时调用decodeRestorableStateWithCoder方法。该系统存在一个可能性,即敏感信息可能会从用户界面泄露到磁盘存储中,因为文本字段的内容和其他界面数据将被存储在本地存储中。
当你检查一个新的代码库时,你可以通过在代码库中查找restorationIdentifier,快速判断是否有状态保存发生,而不需要逐个点击所有 Storyboard UI 元素。
如果启用了状态保存功能,你应该在.storyboard文件中找到像这样的结果:
<viewController restorationIdentifier="viewController2" title="Second" id="3"
customClass="StatePreservatorSecondViewController" sceneMemberID=
"viewController">
<view key="view" contentMode="scaleToFill" id="17">
<rect key="frame" x="0.0" y="20" width="320" height="499"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable=
"YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode=
"scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Zl1-tO-jGB">
<textInputTraits key="textInputTraits" autocapitalizationType=
"sentences"/>
</textView>
注意,有一个视图控制器具有restorationIdentifier属性,并且该控制器包含一个子视图,其中有一个textView对象。如果应用程序代理实现了encodeRestorableStateWithCoder方法,它可以指定一个encodeObject方法,以保存UITextView的.text属性以供后续恢复。这个方法可以用来确保在应用程序被终止时,输入框中的文本不会丢失,^(14) 如示例 10-20 所示。
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:_messageBox.text forKey:@"messageBoxContents"];
}
示例 10-20:一个encodeRestorableStateWithCoder方法示例
完成功能测试后,你还可以检查应用程序的Library/Saved Application State/com.company.appname.savedState目录,在那里你会找到命名描述性的data.data文件。这个文件包含了具有restorationIdentifiers的对象的序列化状态。检查这个文件以确定是否有来自用户界面的敏感数据被编码。如果你正在进行黑盒测试,也可以在设备上执行此操作。
安全状态保存
如果一个产品需要状态保存的用户体验和便捷性,但需要将数据安全地存储在磁盘上,你可以在将敏感对象内容传递给encodeObject方法之前对其进行加密。我在第十三章中详细讨论了加密,但这里是如何加密这种特定类型的数据的。
当应用程序安装时,生成一个加密密钥并通过secItemAdd将其存储在钥匙串中。然后,在encodeRestorableStateWithCoder方法中,从钥匙串中读取密钥,并将其作为加密操作的密钥。^(15) 获取生成的数据并使用NSCoder的encodeObject方法进行序列化。最后,在decodeRestorableStateWithCoder方法中,执行相同的操作来恢复应用程序的状态。
你可以使用 SecureNSCoder 项目^(16)来帮助实现该功能。SecureNSCoder 可以为你的应用自动生成一个密钥,存储在钥匙串中,并用它来编码和解码你的程序状态。在接下来的部分中,我将带你通过一个示例项目,展示如何在你的程序中使用这个工具。
首先,将SecureArchiveDelegate和SimpleKeychainWrapper文件包含到你的项目中。然后,在你的视图控制器的.h文件中包含SecureArchiverDelegate.h,如示例 10-21 所示。
#import <UIKit/UIKit.h>
#import "SecureArchiverDelegate.h"
@interface ViewController : UIViewController
// Some simple properties, adding one for the delegate
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) SecureArchiverDelegate *delegate;
@end
示例 10-21:一个基础的 ViewController.h
接下来,按照示例 10-22 中的内容实现initWithCoder方法。
- (id)initWithCoder:(NSKeyedUnarchiver *)coder {
if (self = [super initWithCoder:coder]) {
return self;
}
return nil;
}
示例 10-22:initWithCoder 在 ViewController.m 中
然后实现示例 10-23 中显示的awakeFromNib方法。
- (void)awakeFromNib {
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [UIViewController class];
}
示例 10-23:awakeFromNib 在 ViewController.m 中
最后,按照示例 10-24 中的内容实现两个状态保存方法。
- (void)encodeRestorableStateWithCoder:(NSKeyedArchiver *)coder {
// preserve state
SecureArchiverDelegate *saDelegate = [[SecureArchiverDelegate alloc] init];
[self setDelegate:saDelegate];
[coder setDelegate:[self delegate]];
[coder encodeObject:[[self textField] text] forKey:@"textFieldText"];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSKeyedUnarchiver *)coder {
// restore the preserved state
SecureArchiverDelegate *saDelegate = [[SecureArchiverDelegate alloc] init];
[self setDelegate:saDelegate];
[coder setDelegate:[self delegate]];
[[self textField] setText:[coder decodeObjectForKey:@"textFieldText"]];
[super decodeRestorableStateWithCoder:coder];
}
示例 10-24:在 ViewController.m 中的编码与解码方法
你已经看到了数据如何从设备上的应用程序中泄漏,但备份到 iCloud 的数据怎么样?如果你处理的是敏感数据,实际上我只能推荐一个方法:完全避免将其存储在 iCloud 上。
离开 iCloud 避免泄漏
在 iOS 的最新版本中,应用程序的大部分数据可以同步到用户的 iCloud 账户,并可以在多个设备之间共享。默认情况下,只有三个应用程序目录能够免受 iCloud 的控制。
• AppName.app
• Library/Caches
• /tmp
如果你希望其他任何文件仅保留在设备上,你需要自行负责这些文件的管理。^(17) 使用NSURLIsExcludedFromBackupKey属性来标记这些文件,并使用NSURL作为文件路径,以防止文件备份到 iCloud,如示例 10-25 所示。
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL {
NSError *error = nil;
➊ [URL setResourceValue:[NSNumber numberWithBool:YES]
forKey:NSURLIsExcludedFromBackupKey
error:&error];
return error == nil;
}
示例 10-25:设置文件属性以排除文件备份
你可以使用setResourceValue NSURL方法设置NSURLIsExcludedFromBackupKey,如➊所示。
结束语
移动设备上的数据泄漏是一个广泛且不断变化的领域,构成了在移动应用进行安全审计时发现的大量问题。理想情况下,本章中你所检查的内容将帮助你发现有用的漏洞,并帮助你识别在 iOS 更新版本发布时可能出现的变化。接下来,我将介绍一些基本的 C 语言和内存腐败攻击,这些攻击在 iOS 上通常较少见,但潜在的危害却大得多。
第十一章:11
来自 C 的遗留问题和负担
Objective-C 和 Cocoa 帮助缓解了许多你在 C 或 C++ 中可能遇到的安全问题。然而,Objective-C 仍然是 C 的一种变体,根本上不是一种“安全”的语言,某些 Cocoa API 仍然容易受到你在 C 程序中可能遇到的数据窃取或代码执行攻击的威胁。C 和 C++ 也可以与 Objective-C 自由混合。许多 iOS 应用程序使用大量的 C 和 C++ 代码,无论是因为开发者想要使用熟悉的库,还是试图尽可能保持代码在不同平台之间的可移植性。虽然有一些缓解措施来防止代码执行攻击,如第一章中讨论的内容,但这些措施可能会被更熟练的攻击者绕过。因此,熟悉这些漏洞和攻击是个好主意。
在本章中,你将学习一些需要注意的攻击类型、C 错误如何渗透到 Objective-C 中以及如何修复这些问题。原生 C 代码问题的主题非常广泛,因此本章是这些问题的“精华”,旨在为你提供理解这些缺陷背后的理论以及利用这些缺陷进行攻击的基础知识。
格式字符串
格式字符串攻击^(1)利用了错误使用那些期望 格式字符串(即定义字符串所组成数据类型的字符串)API。在 C 中,最常用的接受格式字符串的函数属于 printf 系列;还有许多其他函数,比如 syslog,也接受格式字符串。在 Objective-C 中,这些方法通常具有 WithFormat 或 AppendingFormat 等后缀,尽管也有一些例外。以下是三者的示例:
• [NSString *WithFormat]
• [NSString stringByAppendingFormat]
• [NSMutableString appendFormat]
• [NSAlert alertWithMessageText]
• [NSException raise:format:]
• NSLog()
攻击者通常利用格式字符串漏洞来做两件事:执行任意代码和读取进程内存。这些漏洞通常源自两个古老的 C 格式字符串操作符:%n 和 %x。很少使用的 %n 操作符用于将目前打印的字符数量存储在栈上的一个整数中。然而,它可以被利用来覆盖内存的部分内容。%x 操作符用于以十六进制打印值,但当没有传入要打印的值时,它会从栈中读取值。
不幸的是,对于我们这些漏洞猎人来说,Apple 在接受格式化字符串的 Cocoa 类中禁用了 %n。但在常规的 C 代码中,%n 格式化字符串是允许的,因此格式化字符串攻击仍然可能导致代码执行^(2)。之所以 %n 会导致代码执行,是因为它会写入堆栈,而格式化字符串本身也存储在堆栈上。利用方式因特定漏洞而异,但主要的结果是,通过构造一个包含 %n 的格式化字符串,并且提供一个要写入的内存地址,你可以将任意整数写入内存的特定位置。结合一些 shell 代码,这种攻击可以像缓冲区溢出攻击一样被利用^(3)。
%x 运算符则在 Objective-C 方法和 C 函数中都活跃且有效。如果攻击者能够将 %x 传递给一个缺少格式化字符串说明符的输入,输入将被解释为格式化字符串,并且堆栈的内容将以十六进制形式显示在原本应该显示字符串的地方。如果攻击者能够查看这个输出,他们可以从进程的内存中收集潜在的敏感信息,如用户名、密码或其他个人数据。
当然,这两种漏洞都依赖于程序未能正确控制用户输入。让我们看看在这种情况下,攻击者可能如何滥用格式化字符串,并且应用程序可以如何防止这种情况发生。
防止经典的 C 格式化字符串攻击
格式化字符串漏洞的典型示例是当程序直接将一个变量传递给 printf,而没有手动指定格式化字符串。如果这个变量的内容是由外部输入提供,并且攻击者能够控制这些输入,那么攻击者可能会在设备上执行代码或窃取其内存中的数据。你可以在 Xcode 中测试一些人为设计的易受攻击的代码:
char *t;
t = "%x%x%x%x%x%x%x%x";
printf(t);
这段代码简单地向 printf 函数传递一个包含一堆 %x 说明符的字符串。在一个真实的程序中,这些值可能来自许多地方,例如用户输入字段或 DNS 查询结果。当代码执行时,你应该看到一串十六进制输出写入控制台。这个输出包含了存储在堆栈上的变量的十六进制值。如果应用程序在堆栈上存储了一个密码或加密密钥,并且解析了一些攻击者提供的数据,攻击者可能导致这些信息泄露到某个他们能够读取的地方。如果你将前面的示例改成包含 %n 说明符,行为会有所不同。以下是它的表现:
char *t;
t = "%n%n%n%n%n";
printf(t);
在 Xcode 中运行此示例时,应该会导致 Xcode 跳转到 lldb 并出现 EXC_BAD_ACCESS 错误。每当你看到这个消息时,表示程序正在尝试读取或写入不应该访问的内存。当然,在精心设计的攻击中,你不会看到这样的错误;代码将直接执行。
但是,你可以通过控制用户输入轻松防止攻击者劫持字符串。在这种情况下,只需将 printf 改为指定自己的格式字符串,如下所示:
char *t;
t = "%n%n%n%n%n";
printf("%s", t);
在 Xcode 中运行这个,你应该能看到字面上的 %n%n%n%n%n 被无害地写入控制台。当然,这些示例都是纯 C,但了解它们的工作原理将帮助你探索带有 Objective-C 扩展的格式字符串攻击。
防止 Objective-C 格式字符串攻击
类似于纯 C 语言,你可以将任何 printf 格式操作符传递给多个不同的 Objective-C API。你可以通过将虚假的格式字符串传递给 NSLog 在 Xcode 中轻松测试这一点:
NSString *userText = @"%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x";
NSLog(userText);
就像之前的 %x 示例一样,这将把内存内容以十六进制形式打印到控制台。我在实际的 iOS 应用中遇到过的一个格式字符串漏洞是,代码将用户提供的输入传递给一个“格式化”函数,该函数进行一些处理并返回一个 NSString 对象,如 列表 11-1 所示。
NSString *myStuff = @"Here is my stuff.";
NSString *unformattedStuff = @"Evil things %x%x%x%x%x";
➊ myStuff = [myStuff stringByAppendingFormat:[UtilityClass formatStuff:
unformattedStuff.text]];
列表 11-1:完全错误的将数据传递给格式字符串的方法
这个示例假设在 ➊ 处存储在 myStuff 中的 NSString 是安全的;毕竟,unformattedStuff.text 的内容已经被“格式化”了。但是除非 formatStuff 方法有某种特殊的方式来清理输入文件,否则结果字符串可能包含格式字符串说明符。如果发生这种情况,你仍然会面临格式字符串问题,且结果字符串将包含来自栈的值。
NSString 对象并不是天生安全的,不能免受格式字符串攻击。正确的方式是使用 %@ 说明符来输出传递给需要格式字符串的方法的 NSString,如 列表 11-2 所示。
NSString myStuff = @"Here is my stuff.";
myStuff = [myStuff stringByAppendingFormat:@"%@", [UtilityClass formatStuff:
unformattedStuff.text]];
列表 11-2:正确使用期望格式字符串的方法
在 %@ 说明符前,无论 unformattedStuff.text 中包含多少 %x 和 %n 操作符,myStuff 应该会输出为一个无害的字符串。
%x 和 %n 说明符对攻击者来说是最有用的,但即使它们不存在,攻击者仍然可以在尝试读取无法访问的内存时引发不良行为,例如崩溃,即使是使用基本的 %s 说明符。现在我已经讲解了格式字符串攻击的原理以及如何防止它们,接下来我会展示一些其他执行恶意代码的方法。
缓冲区溢出与栈
缓冲区溢出长期以来困扰着 C 语言的世界,允许来自不信任来源的构造输入崩溃程序或在易受攻击程序的进程内执行第三方代码。尽管缓冲区溢出从 1970 年代就已知,但它们被首次广泛利用是在莫里斯蠕虫中,其中包括对 UNIX finger 守护进程的缓冲区溢出攻击。
缓冲区溢出从覆盖内存的部分开始。进程的基本内存布局包括程序代码、程序运行所需的任何数据、堆栈和堆,如图 11-1 所示。

图 11-1:进程内存的布局
代码段(通常称为文本段)是程序的实际可执行文件被加载到内存的地方。数据段包含程序的全局变量和静态局部变量。堆是程序运行时动态分配内存的地方,存储大部分非可执行程序数据。栈是存储局部变量的地方,以及函数的地址,重要的是,它还包含指向下一条程序执行指令的地址。
溢出有两种基本类型:一种是覆盖程序堆栈的部分内容,另一种是覆盖堆区的部分内容。现在让我们来看一下缓冲区溢出漏洞。
一个 strcpy 缓冲区溢出
栈基缓冲区溢出的经典例子见清单 11-3。
#include <string.h>
uid_t check_user(char *provided_uname, char *provided_pw) {
char password[32];
char username[32];
strcpy(password, provided_pw);
strcpy(username, provided_uname);
struct *passwd pw = getpwnam(username);
if (0 != strcmp(crypt(password), pw->pw_passwd))
return -1;
return pw->uid;
}
清单 11-3:容易发生溢出的代码
username和password都分配了 32 个字节。在大多数情况下,程序应正常运行,并将用户提供的密码与存储的密码进行比较,因为用户名和密码通常不超过 32 个字符。然而,当输入的值超过 32 个字符时,额外的字符会开始覆盖堆栈上相邻变量的内存,如图 11-2 所示。这意味着攻击者可以覆盖函数的返回地址,指定下一个要执行的是攻击者在当前输入或内存的其他地方放置的恶意代码块。

图 11-2:溢出前后的内存布局
由于这个例子硬编码了字符限制,并且没有检查输入是否在限制范围内,攻击者控制的输入可能会比接收数据结构允许的长度还要长。数据会溢出缓冲区的边界,覆盖内存的部分内容,从而可能允许代码执行。
防止缓冲区溢出
有几种方法可以防止缓冲区溢出,而且大多数方法都相当简单。
在使用输入之前检查其大小
最简单的解决方法是在将输入加载到数据结构之前进行合理性检查。例如,像清单 11-3 中的脆弱程序通常通过自己计算传入数据的大小来防御缓冲区溢出,而不是信任外部提供的大小是正确的。这种修复可以像用if语句替换清单 11-3 中的strcpy函数那样简单:
if (strnlen(provided_pw, 32) < strnlen(password, 32))
strcpy(password, provided_pw);
使用 sizeof 检查提供的密码的大小应该确保任何超过缓冲区大小的数据被拒绝。理想情况下,当然你根本不应使用静态大小的缓冲区——像 NSString 或 std::string 这样的高级类及其相关方法应该能为你处理这些问题。
使用更安全的字符串 API
另一种能够保护你免受缓冲区溢出的编码最佳实践是避免使用“已知不安全”的 API,例如 strcpy 和 strcat 系列。这些函数会将数据复制到目标缓冲区,而不检查目标是否能够处理如此多的数据,这也是上一节中为什么添加大小检查如此重要的原因。示例 11-3 展示了 strcpy 的一种错误用法;这里有一个更简单的例子:
void copythings(char *things) {
char buf[32];
strcpy(buf, things);
}
在这种简单且明显的缓冲区溢出漏洞中,buf 缓冲区只有 32 字节长,而参数 things 被复制到其中。但这段代码在尝试将 things 复制到 buf 之前并没有检查 things 缓冲区的大小。如果任何调用此函数的缓冲区大于 32 字节,结果将是缓冲区溢出。
更安全的字符串复制和拼接方式是使用 strlcpy 和 strlcat 函数,^(4) 它们将目标缓冲区的大小作为参数,示例如下:
void copythings(char *things) {
char buf[32];
length = strlcpy(buf, things, sizeof(buf));
}
在这里,strlcpy 函数只会复制源字符串中的 31 字节,加上一个空字符终止符。这样可能导致字符串被截断,但至少不会导致静态大小缓冲区溢出。strl 系列并不是所有平台都支持,但在基于 BSD 的系统上可用,包括 iOS。
除了这些类型的溢出,执行整数操作时也可能发生错误,这可能导致服务拒绝或代码执行。
整数溢出与堆
整数溢出 是由执行一个计算,得到的值超出了平台上整数的最大值。当你了解 C 语言(因此也包括 Objective-C)时,你会知道有两种类型的整数:有符号整数和无符号整数。有符号整数可以是正数或负数,而无符号整数总是正数。如果你尝试执行一个超出任一类型整数值的计算,就会发生错误。无符号整数将会绕过整数的最大值,从零重新开始。如果整数是有符号的,它将从一个负数开始,这是无符号整数的最小值。以下是一个例子:
NSInteger foo = 9223372036854775807;
NSLog(@"%li", (long)foo);
foo++;
NSLog(@"%li", (long)foo);
这从一个有符号整数 foo 开始,使用 iOS 上有符号整数的最大大小。当该数字递增时,控制台输出应会回绕到一个负数,-9223372036854775808。
如果你像下面的例子中那样使用无符号整数,你会看到整数溢出,控制台上的输出将是 0:
NSUInteger foo = 18446744073709551615;
NSLog(@"%lu", (unsigned long)foo);
foo++;
NSLog(@"%lu", (unsigned long)foo);
当缓冲区溢出会覆盖栈时,整数溢出则会让攻击者访问堆内存,我接下来会向你展示这一点是如何发生的。
一个 malloc 整数溢出
整数溢出通常会在计算传递给malloc()调用所需空间时发生,导致分配的空间远远不足以容纳要存储的值。当数据被加载到新分配的空间中时,无法容纳的数据将被写入分配空间的末尾,进入堆内存。这就会导致堆溢出:如果攻击者向malloc()提供恶意构造的数据并覆盖堆中的正确指针,代码执行就可能发生。
整数溢出漏洞通常呈现以下形式:
#define GOAT_NAME_LEN 32
typedef struct Goat {
int leg_count; // usually 4
bool has_goatee;
char name[GOAT_NAME_LEN];
struct Goat* parent1;
struct Goat* parent2;
size_t kid_count;
struct Goat** kids;
} Goat;
int ReadInt(int socket) {
int result;
read(socket, &result, sizeof(result));
return result;
}
void ReadGoat(Goat* goat, int socket) {
read(socket, goat, sizeof(Goat));
}
Goat* ReadGoats(int* count, int socket) {
➊ *count = ReadInt(socket);
➋ Goat* goats = malloc(*count * sizeof(Goat));
➌ for (int i = 0; i < *count; ++i) {
ReadGoat(&goats[i], socket);
}
return goats;
}
这段代码创建了一个类型为Goat的对象,以及ReadGoats函数,该函数接受一个套接字和需要从该套接字读取的山羊数量。在➊处,ReadInt函数从套接字本身读取将要处理的山羊数量。
如果这个数字足够大,在➋处的malloc()操作将导致一个非常大的大小,以至于整数会环绕到负数。当count的值合适时,攻击者可以使得malloc()尝试分配零字节或一个非常小的数值。当在➌处的循环执行时,它将从对应于非常大count值的套接字读取山羊的数量。由于goats很小,这可能会溢出分配的内存,允许数据写入堆内存。
防止整数溢出
防止整数溢出的方式有几种,但基本的思路是在操作整数之前检查它们的值。我建议采纳苹果编程指南中的基本结构。^(5) 这是一个例子:
if (n > 0 && m > 0 && INT_MAX/n >= m) {
size_t bytes = n * m;
foo = malloc(bytes);
}
在计算bytes的值之前,这个if语句检查n和m是否大于 0,并将其中一个因子除以最大值,以确保结果大于另一个因子。如果两个条件都成立,那么你就知道bytes能够适应一个整数,使用它来分配内存应该是安全的。
总结思考
本章列出的 C 语言编码缺陷远不全面,但了解这些缺陷有助于你开始在 iOS 应用中识别与 C 相关的问题。还有很多其他资源可以帮助你提高 C 语言安全技能。如果你对深入了解 C 语言的复杂性以及可能出现的问题感兴趣,我建议你阅读 Peter van der Linden 的《Expert C Programming: Deep C Secrets》(Prentice Hall, 1994)。
现在我已经把 C 语言的一些“脏衣服”晾晒出来了,我们回到 Cocoa 领域,看看那些主要来源于 Web 应用安全领域的现代攻击:注入攻击。
第十二章:12
注入攻击
在本章中,我将讨论各种注入攻击,其中许多攻击同时适用于 iOS 客户端应用程序及其远程端点或 API。虽然对所有潜在的服务器端缺陷进行彻底检查超出了本书的范围,但本章将为你提供一个思路,了解 iOS 应用程序及其配套的端点或 Web 应用程序如何协作来防止安全漏洞。
注入攻击是 Web 应用程序的常见问题,但客户端注入攻击则较为罕见,开发人员和安全工程师往往未予以注意。客户端注入攻击发生在远程提供的数据被设备上运行的程序解析时。最著名的例子包括跨站脚本攻击、SQL 注入、谓词注入和 XML 注入。
客户端跨站脚本攻击
跨站脚本攻击(XSS) 是一种通常出现在 web 应用程序中的问题,但 JavaScript 也可以被注入到 iOS 应用程序使用的内容中。一款著名的存在 XSS 漏洞的应用是 Skype 移动应用程序。正如安全研究员 Phil Purviance 在他的 Superevr 博客中所描述的,当时该应用使用了 UIWebView 来渲染内容。^(1) 在显示远程用户的全名时没有进行消毒,这使得攻击者能够通过将恶意脚本嵌入到用户名中,进而将脚本注入到远程用户的应用程序中。在这种情况下,攻击者可以窃取设备上的敏感数据(如通讯录内容)。这种攻击还可以用来插入一个虚假的登录页面,将凭据提交给攻击者控制的域名。
如果你的应用使用了 UIWebView,为了避免 XSS 漏洞,特别需要注意不要从服务器或其他外部源获取未经消毒的用户提供的数据,并将其集成到用户界面中。你可以通过两部分的方法来最有效地做到这一点,既使用 输入消毒,又使用 输出编码。
输入消毒
输入消毒涉及从外部输入中去除潜在的有害字符,可以采用 黑名单 或 白名单 方法。
黑名单恶意输入
在黑名单中,你会尝试列出所有可能导致安全问题的字符,并将这个列表交给你的应用程序。然后,你编写应用程序,要么删除不接受的字符,要么当这些字符出现时抛出错误。
黑名单是一种脆弱的方法,且很少有效。你需要知道每一种可能导致问题的数据形式,包括每种字符编码、每个 JavaScript 事件处理程序或 SQL 特殊字符等等。例如,你可能仅仅将 < 和 > 添加到黑名单中,指望通过 <script> 标签来防止 XSS 攻击,但你忽略了可以仅通过双引号、括号和等号等字符来完成的攻击。
一般来说,如果您的应用程序或您正在测试的应用程序依赖于黑名单字符,请调查该黑名单是否掩盖了潜在的缺陷。这类过滤器很容易被绕过,而依赖这种技术的应用程序可能也缺乏有效的输出编码,这点我将在 “输出编码” 中于第 201 页讨论。
允许输入的白名单
在白名单方法中,您需要明确地定义哪些字符对特定用户输入是可接受的。白名单优于黑名单,因为全面指定应该允许哪些字符比推测哪些字符可能是坏的要容易得多。在白名单方法中,您可能会定义电话号码字段应允许的字符:0 到 9,以及可能的破折号和括号。这不仅能排除几乎所有恶意输入,还能保持数据库中的数据清洁。
找到平衡
在黑名单或白名单的输入清理中,可能会出现过度热衷的情况。一些程序和网站实际上不允许某些输入中的合法字符(最显著的是用户密码)。您可能遇到过拒绝接受包含特殊字符的密码的应用程序或网站(例如!、<、>、' 或 ;)。这通常是程序员在后端处理数据时表现出极端不胜任的迹象。
例如,如果一个应用程序去除撇号或分号,开发者可能没有使用参数化的 SQL 语句,而是依赖于去除“坏”的特殊字符来防止 SQL 注入。但这种针对可疑坏字符的黑名单仅仅是降低了用户密码的复杂性,并且不太可能以任何全面的方式解决 SQL 注入问题。
为了确保输入清理正确工作,它还需要尽可能接近数据处理或存储之前的那一刻。例如,当一个 iOS 应用程序与远程 API 通信时,应用程序当然可以尝试去除有害字符或将输入限制在某个字符范围内。这是可以的,但它仅仅会提高用户的可用性。用户可以立即看到他们的输入不会被接受,而不需要等到填写完所有表单数据并尝试提交时才发现问题。
您的典型用户可能会欣赏这种副作用,但这里有一个问题:用户控制设备,并最终控制程序的行为。如果您的用户界面不允许某些值作为输入,攻击者所需要做的就是通过代理路由设备的流量,正如我在 “网络和代理设置” 中于第 43 页描述的那样。用户可以在数据离开应用程序但尚未到达服务器时修改数据,并将有害字符重新添加回去。
为了应对这种可能性,永远不要信任移动应用提供正确的数据。在客户端-服务器应用程序中,始终确保消毒工作在服务器端完成。
在输入消毒得当的情况下,你应该继续进行输出编码。
输出编码
输出编码,有时也称为 HTML 实体编码,是将用户输入中的字符替换为其 HTML 表示的过程。对于任何可能未被信任的数据,这个过程是必要的,这些数据可能最终会在 WebView 中呈现。例如,字符 < 和 > 会分别转换为 < 和 >。当数据显示给用户时,这些字符应该在 UI 中显示为 < 和 >,但由于它们已被编码,HTML 引擎不会将它们处理为元字符,后者可能在 <script> 标签中使用。
输出编码是交付包含第三方输入的 HTML 给客户端之前的最后一道且最强有力的防线。即使你在输入消毒过程中完全忽略了潜在的有害元字符,只要你对输出进行编码,就不必担心你发送的数据是否会被浏览器执行,而只是显示出来。
显示不可信的数据
与输入消毒一样,输出编码通常应该在服务器端执行,而不是在客户端。但如果你必须展示来自你无法控制的域的数据,并且这些数据不可信,那么你需要在显示内容给用户之前进行 HTML 实体编码。
Google Toolbox for Mac 包含两个 NSString 类的类别方法,你可以用来在客户端编码 HTML 实体:gtm_string-ByEscapingForHTML 和 gtm_stringByEscapingForAsciiHTML。^(2) 在你的项目中包括 Google 的 NSString 类别,使得你可以简单地调用任何 NSString 对象的方法,返回一个编码后的表示:
NSString *escaped;
escaped = [@"Meet & greet" gtm_stringByEscapingForHTML];
在进行转义之后,escaped 应该包含 NSString Meet & greet,它应该可以安全地在 HTML 中渲染。
不要过度编码
与输入消毒一样,输出编码也需要小心,避免过度处理。一些应用程序在将接收到的字符发送到服务器或存储到数据库之前,会先进行实体编码,然后又重新编码已经编码的数据。你可能在移动应用程序或 Web 应用程序中看到过这种情况。
例如,我曾经看到一个应用程序显示一个横幅,邀请我参加“Meet & greet”。在底层的 HTML 源代码中,这些数据将如下所示:
Meet &amp; greet
原始输入已经被编码(为 &),在浏览器中会正确显示为 &。如果再次进行编码,它就会显示为 &。这样并不会引发安全问题,但可能导致数据变得混乱,难以处理。只需记住,这项技术被称为 输出编码,原因在于它需要在输出之前进行。
SQL 注入
客户端 SQL 注入是由于解析外部提供的数据,将有效的 SQL 注入到格式错误的 SQL 语句中。动态构建的语句,如果使用了未经清理的外部输入,就容易受到 SQL 注入攻击。恶意输入会包含 SQL 元字符和语句,破坏原始查询的意图。
例如,假设一个用户将简单的状态信息发布到网站上。然后该信息被下载并添加到本地数据存储中。如果发布原始内容的用户具有基本的安全知识和恶意意图,用户可能会将 SQL 嵌入到信息中,当 SQL 引擎解析时就会执行这些恶意 SQL。此类恶意 SQL 可能会破坏或修改数据存储中的现有数据。
在 iOS 上,最常用的 SQL API 是 SQLite。清单 12-1 显示了一个格式错误的、动态构建的 SQLite SQL 语句示例。
NSString *uid = [myHTTPConnection getUID];
NSString *statement = [NSString StringWithFormat:@"SELECT username FROM users where
uid = '%@'",uid];
const char *sql = [statement UTF8String];
清单 12-1:一个易受 SQL 注入攻击的未参数化 SQL 语句
这里的问题是 uid 的值来自用户提供的输入,并且直接插入到 SQL 语句中,采用了格式化字符串的方式。任何用户提供的 SQL 都会在最终执行时成为该语句的一部分。
为了防止 SQL 注入,只需使用参数化语句来避免首先动态构建 SQL 语句。与其动态构建语句并将其传递给 SQL 解析器,不如使用参数化语句,这样 SQL 语句就会独立于参数进行评估和编译。在执行时,参数会被传递给已编译的语句。
使用参数化语句,正确的查询构造方式是将 ? 作为占位符,用于表示提供的参数,如 清单 12-2 所示,而不是像 清单 12-1 那样动态构建 SQL。
static sqlite3_stmt *selectUid = nil;
➊ const char *sql = "SELECT username FROM users where uid = ?";
➋ sqlite3_prepare_v2(db, sql, -1, &selectUid, NULL);
➌ sqlite3_bind_int(selectUid, 1, uid);
int status = sqlite3_step(selectUid);
清单 12-2:一个正确参数化的 SQL 语句
SQL 语句在 ➊ 处使用 ? 占位符构建。然后,代码使用 sqlite3_prepare_v2 在 ➋ 处编译 SQL 语句,最后通过 sqlite3_bind_int 在 ➌ 处绑定用户提供的 uid。由于 SQL 语句已经构建完成,uid 参数中提供的任何额外 SQL 都不会被添加到 SQL 中,它仅通过值传递。
除了防止 SQL 注入,使用参数化的预处理语句在大多数情况下还会提高应用程序的性能。即使某个语句没有接受来自不信任源的输入,您也应当为所有 SQL 语句使用它们。
谓词注入
谓词 让你使用类似 SQL 的基本查询语言,在数据之间执行逻辑比较。在基本的 NSPredicate 中,值是通过格式化字符串进行比较或过滤的。
➊ NSMutableArray *fruit = [NSMutableArray arrayWithObjects:@"Grape", @"Peach",
@"orange", @"grapefruit", nil];
➋ NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS[c] 'Grape'"];
➌ NSArray *grapethings = [fruit filteredArrayUsingPredicate:pred];
NSLog(@"%@", grapethings);
在➊,创建了一个包含各种水果类型的数组;这个数组将作为数据源,用来与表达式进行比较。在➋创建谓词时,生成了一个查询,检查字符串"Grape"是否包含在谓词正在比较的项中。([c]使得这个比较不区分大小写。)当在➌实例化一个新数组以包含这个比较的结果时,fruit数组的filteredArrayUsingPredicate方法被用来传入谓词。结果,grapethings数组现在应该包含"Grape"和"grapefruit"。
到目前为止,一切顺利!但是在使用外部提供的数据构建谓词查询时,可能会出现一些问题。首先,考虑使用 SQL 的LIKE操作符构建谓词的情况,如下所示。
NSPredicate *pred;
pred = [NSPredicate predicateWithFormat:@"pin LIKE %@", [self.pin text]];
这个示例评估一个 PIN 码,可能是我应用程序的二级身份验证方式。但LIKE操作符执行了这个评估,这意味着用户输入简单的通配符字符([*])会导致谓词评估为真,从而有效地绕过了 PIN 保护。
对于熟悉 SQL 注入的人来说,这个结果可能是显而易见的(因为 SQL 也有LIKE操作符),但请考虑更微妙的情况,比如你正在检查使用谓词MATCHES操作符的代码,如下所示:
NSPredicate *pred;
pred = [NSPredicate predicateWithFormat:@"pin MATCHES %@", [self.pin text]];
这段代码与LIKE示例存在相同的问题,但与仅接受通配符不同,MATCHES期望一个正则表达式。因此,使用.*作为你的 PIN 码就足以绕过验证。
为了防止谓词注入攻击,检查你代码中所有NSPredicate的使用,确保所用的操作符对应用程序而言是合理的。还应该限制用户提供的数据中可以传递给谓词的字符,以确保像通配符这样的字符不会被插入。或者,干脆不要在安全敏感操作中使用谓词。
XML 注入
XML 注入发生在恶意 XML 被 XML 解析器实例解析时。通常,这种类型的攻击被用来迫使应用程序通过网络加载外部资源或消耗系统资源。在 iOS 环境中,最常用的 XML 解析器是 Foundation 的NSXMLParser类。
通过 XML 外部实体注入
XML 解析器的一个基本功能是处理 XML 实体。你可以把这些当作快捷方式或委婉说法。例如,假设你有这样一个简单的字符串:
<!ENTITY myEntity "This is some text that I don't want to have to spell out
repeatedly">
然后,你可以在 XML 文档的其他部分引用这个实体,解析器会在该占位符处插入实体的内容。要引用你定义的实体,只需使用以下语法:
<explanation>&myEntity;</explanation>
NSXMLParser实例有多个可配置的参数,这些参数可以在实例化后进行设置。如果shouldResolveExternalEntities在NSXMLParser实例上设置为YES,则解析器将遵循文档类型定义(DTD),该定义可以从外部 URL 获取实体。(这就是这些被称为外部实体的原因。)当解析的 XML 中遇到已定义的实体时,URL 将被请求,并且查询结果将用于填充 XML,如下例所示:
NSURL *testURL = [NSURL URLWithString:@"http://api.nostarch.com"];
NSXMLParser *testParser = [[NSXMLParser alloc] initWithContentsOfURL:testURL];
[testParser setShouldResolveExternalEntities:YES];
在这里,实例化了一个 XML 解析器,它从传递给initWithContentsOfURL参数的NSURL读取数据。但如果远程服务器决定返回大量数据,或只是简单地挂起,客户端应用程序可能会崩溃或响应挂起。
然而,请记住,外部实体也可以引用本地文件,这意味着文件的内容可能会被包含在解析的 XML 中。如果该 XML 被存储并在以后发送到服务器或其他第三方,则该文件的内容将与其余的 XML 一起披露。为了避免这种情况,确保任何传递给 XML 解析器的 URL 或文件名都经过彻底清理,理想的做法是使用白名单方法,就像我在"允许输入的白名单"中讨论的跨站脚本一样,详见第 12 页。
请注意,在 iOS 7.0 和 7.1 中,XML 解析器的默认行为是解析外部实体(与解析器的预期行为相反),并且使用setShouldResolveExternalEntities:NO实际上不起作用。^(3) 不幸的是,除了使用替代的 XML 解析器外,没有办法修复 iOS 旧版本中的 XML 解析器安全问题。该问题在 iOS 8 中已得到解决。
注意
与某些人所声称的相反, *NSXMLParser* 并不 易受递归实体攻击的影响,递归实体攻击是一种拒绝服务攻击,通常被称为十亿笑声攻击。易受攻击的解析器会解析递归实体(引用其他实体的实体),并消耗大量系统资源。然而,如果递归实体声明传递给 *NSXMLParser*,会抛出一个 *NSXMLParserEntityRefLoopError** 错误。
然而,滥用官方外部实体并不是 iOS 代码中 XML 注入的唯一需要注意的因素。一些应用程序会集成第三方 XML 库,这些库带来了自己的一系列问题。
关于替代 XML 库的问题
你可能会在各种 iOS 项目中遇到替代的 XML 库,这些库通常因其比 NSXMLParser 更好的性能特性以及对 XPath 等功能的支持而被选用。(Ray Wenderlich 在他的博客上提供了一篇关于选择 XML 解析器的好教程。^(4)) 在查看使用替代 XML 库的代码时,首先确保通过该库的标准方法禁用外部实体扩展。然后,确认任何集成外部输入的 XPath 查询首先对输入进行清理,就像防止跨站脚本攻击时一样。XPath 查询还应该以类似 SQL 查询的方式进行参数化(见 第 203 页的“SQL 注入”部分),但具体方法可能会根据涉及的第三方库有所不同。
结束思考
最终,本章大多数攻击的处理方法都归结为将所有外部输入视为敌对:去除潜在的恶意内容,并尽可能地对其进行编码或处理,以防止代码执行。明确允许的每个参数的内容是个好主意,尤其是从 UI 或远程用户操作的来源获取的内容,并在程序中强制执行这一点。
现在,我将不再讨论防范恶意数据攻击,而是转向使用适当的加密技术保护良好数据。
第四部分
数据安全保障
第十三章:13
加密与身份验证
尽管苹果的加密 API 相当强大,但许多开发者不知道如何有效使用它们。你可以控制两个主要的内建加密组件:钥匙串和数据保护 API。这些组件共享一些相同的加密密钥,并具有类似的保护属性,本章将介绍它们。我还将探讨低级加密原语及其(有限的)使用场景。
使用钥匙串
钥匙串用于存储少量敏感数据,包括密码、个人数据等。钥匙串本身使用设备密钥进行加密,并结合用户密码(如果有的话)。钥匙串的 API 包括四个主要操作:SecItemAdd、SecItemUpdate、SecItemCopyMatching和SecItemDelete。这些操作分别用于将项目添加到钥匙串、更新现有项目、检索项目和从钥匙串中删除项目。
话虽如此,我真的希望我永远不再看到 GenericKeychain^(1)示例代码。每个人似乎都将他们的钥匙串代码建立在这个基础上(这是合理的),但这段代码早于任何现代的钥匙串保护措施,这些保护措施实际上可以防止物理攻击者从你的设备上窃取机密数据。在本节中,你将了解这些保护措施以及如何利用它们。
用户备份中的钥匙串
当用户执行设备的完整备份时,他们有两个与安全相关的选项:未加密和加密。未加密备份只能还原到其原始设备。加密备份允许用户选择一个密码来加密备份数据。这使得备份可以还原到任何设备(除了标记为ThisDeviceOnly的项目),并且会备份钥匙串的完整内容。如果不希望将钥匙串项目存储在备份中,可以使用钥匙串的数据保护属性。
钥匙串保护属性
钥匙串保护属性指定何时可以将钥匙串数据存储在内存中,并由操作系统或应用程序请求。将密码或个人数据等项目添加到钥匙串时,指定保护属性非常重要,因为这明确说明了数据应该何时可用。未指定保护属性应视为一个错误。
在首次将项目存储到钥匙串时,通过使用SecItemAdd方法来指定属性。你需要传递预定义值集中的一个(见表 13-1)作为kSecAttrAccessible的值。
可以通过此属性指定三种主要的访问类型:
始终可访问 无论手机是否锁定,密钥始终可用。
解锁时可访问 当设备解锁时,密钥是可访问的;否则,访问尝试将失败。
第一次解锁后可访问 密钥在设备启动并第一次解锁后可访问。
对于三种主要的密钥链保护类型,每种类型都有一个额外的后缀为 ThisDeviceOnly 的对应项。这意味着密钥链项将不会备份到 iCloud,只会在使用加密备份时备份到 iTunes,并且不能恢复到其他设备上。
表 13-1: 密钥链保护属性及其相关含义
| 密钥链保护属性 | 含义 |
|---|---|
kSecAttrAccessibleAfterFirstUnlock |
设备启动后,密钥在用户第一次输入密码之前不可访问。 |
kSecAttrAccessibleAlways |
密钥始终可访问,只要设备已启动。请注意,在 iOS 9 中此项已被弃用,因为它相较于 kSecAttrAccessibleAfterFirstUnlock 并没有实际优势。 |
kSecAttrAccessibleAlwaysThisDeviceOnly |
密钥始终可访问,但无法移植到其他 iOS 设备上。 |
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly |
这与前一个密钥相同,但该密钥仅保留在此设备上。 |
kSecAttrAccessibleWhenUnlocked |
每当设备解锁(即用户输入密码后),密钥可访问。 |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
这与前一个密钥相同,但该密钥仅保留在此设备上(除非是完整的加密备份)。 |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly |
这与前一个密钥相同,但此密钥仅对设置了密码的用户可用,如果取消设置密码,该密钥将从设备中移除,并且不会包含在任何备份中。 |
在密钥链保护首次引入时,默认值为 kSecAttrAccessibleAlways,这会产生明显的安全问题。在这种情况下,可访问 应理解为“对物理攻击者可用”:如果有人偷了你的设备,他们将能够读取密钥链中的内容。通常,这通过执行临时越狱并提取密钥来完成;改用 kSecAttrAccessibleAfterFirstUnlock 通常可以防止这种情况,因为通常需要重启设备才能执行越狱。然而,代码执行攻击(例如有人利用 Wi-Fi 驱动程序中的漏洞)可以在设备仍然运行时获得访问权限。在这种情况下,需要使用 kSecAttrAccessibleWhenUnlocked 来防止密钥泄露,这意味着攻击者需要确定用户的密码才能提取秘密。
注意
不幸的是,在 iOS 上暴力破解四位数 PIN 码的速度快得惊人。不仅可以通过临时越狱实现此操作,^(2) 而且我的同事们已经成功制造了可爱的机器人,在不到一天的时间内通过物理暴力破解 PIN 码。^(3)
当前,默认属性是kSecAttrAccessibleWhenUnlocked,这是一个相对限制性的默认值。然而,苹果的公开文档对于默认属性应该是什么存在分歧,因此,为了保险起见,你应该在所有钥匙串项目中显式设置该属性。对于你自己的代码,考虑在合适的情况下使用kSecAttrAccessibleWhenUnlockedThisDeviceOnly;在检查第三方源代码时,确保使用限制性的保护属性。
在 iOS 8 中,添加了kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly保护属性。开发者长期以来一直要求提供一个需要用户设置密码的 API。这个新属性并不能直接实现这一要求,但开发者可以利用它来根据是否设置了密码来做出决策。当你尝试向钥匙串中添加指定kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly属性的项目时,如果用户没有设置密码,操作将失败。你可以利用这个失败作为一个决策点,决定是否回退到另一个钥匙串保护属性、提醒用户,或者只是将不太敏感的数据存储在本地。
如果用户设置了密码,添加操作将成功;然而,如果用户决定禁用密码,解密该项目所用的类密钥将会被丢弃,从而阻止应用程序解密该项目。
基本钥匙串使用
有几种类别的钥匙串项目,如表 13-2 所列。除非你在处理证书,否则通常可以使用kSecClassGenericPassword来存储大多数敏感数据,接下来我们将看看该类别中一些有用的方法。
表 13-2:钥匙串项目类别
| 项目类别 | 含义 |
|---|---|
kSecClassGenericPassword |
一个普通的密码 |
kSecClassInternetPassword |
专用于互联网服务的密码 |
kSecClassCertificate |
一个加密证书 |
kSecClassKey |
一个加密密钥 |
kSecClassIdentity |
一个密钥对,包括公钥证书和私钥 |
列表 13-1 展示了如何使用钥匙串通过SecItemAdd添加基本密码项目的示例。它设置了一个字典来保存钥匙串查询,该查询包含了识别密码的适当键值对,设置了密码策略,并指定了密码本身。
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSData *passwordData = [@"mypassword" dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)
kSecClass];
[dict setObject:@"Conglomco login" forKey:(__bridge id)kSecAttrLabel];
[dict setObject:@"This is your password for the Conglomco service." forKey:
(__bridge id)kSecAttrDescription];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)
kSecAttrService];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData];
[dict setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)
kSecAttrAccessible];
OSStatus error = SecItemAdd((__bridge CFDictionaryRef)dict, NULL);
if (error == errSecSuccess) {
NSLog(@"Yay");
}
列表 13-1:向钥匙串添加项目
在这里,kSecClassGenericPassword类别被设置为钥匙串项目,另外还包括一个用户可读的标签、长描述、账户(用户名)以及服务标识符(用于防止重复)。代码还设置了密码和访问控制属性。
SecItemUpdate的工作方式类似。列表 13-2 展示了SecItemUpdate的使用示例,其中更新了存储在kSecValueData中的用户密码。
NSString *newPassword = @"";
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)
kSecClass];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)
kSecAttrService];
NSDictionary *updatedAttribute = [NSDictionary dictionaryWithObject:[newPassword
dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
OSStatus error = SecItemUpdate((__bridge CFDictionaryRef)dict, (__bridge
CFDictionaryRef)updatedAttribute);
列表 13-2:使用 SecItemUpdate 更新钥匙串项目
在使用SecItemUpdate更新钥匙串条目时,你必须设置两个字典。一个字典应指定基本的钥匙串标识信息(至少包括类、账户和服务信息),另一个字典应包含要更新的属性。
SecItemCopyMatching可以用来查询钥匙串,查找符合给定条件的一个或多个条目。通常,你会使用类、账户和服务属性来构建一个搜索字典,这些属性在创建或更新钥匙串条目时使用。然后,你将实例化一个NSDictionary,该字典将保存搜索结果,并执行实际的SecItemCopyMatching调用,传入搜索字典和结果字典的引用。可以在列表 13-3 中找到示例。
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)
kSecClass];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)
kSecAttrService];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
NSDictionary *result = nil;
OSStatus error = SecItemCopyMatching((__bridge CFDictionaryRef)dict, (void *)&
result);
NSLog(@"Yay %@", result);
列表 13-3:使用SecItemCopyMatching查询钥匙串
在result字典中获取到钥匙串数据后,你可以使用这些信息执行诸如向远程服务认证或解密数据等安全敏感的任务。请注意,如果你构建的查询没有包含账户和服务属性(这两个属性唯一标识钥匙串条目),你可能会得到一个包含多个钥匙串条目的返回字典。这个字典可以通过kSecMatchLimit来限制(即将其设置为 1),但如果你正在尝试搜索像密码这样的单个数据,这可能会导致不可预测的行为。
你现在可能已经猜到,SecItemDelete调用的样子是什么——可以在列表 13-4 中看到实际代码。
NSMutableDictionary *searchDictionary = [NSMutableDictionary dictionary];
[searchDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:
(__bridge id)kSecClass];
[searchDictionary setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[searchDictionary setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)
kSecAttrService];
OSStatus error = SecItemDelete((__bridge CFDictionaryRef)searchDictionary);
列表 13-4:使用SecItemDelete删除钥匙串条目
请注意,如果你没有唯一地标识你的钥匙串条目,所有你的应用程序可以访问的匹配项都会被删除。
钥匙串封装器
在使用钥匙串时,你可能最终会写许多封装函数,以使其更方便,因为大多数应用程序只使用钥匙串 API 功能的一个子集。事实上,第三方已经提供了许多预写的钥匙串封装器;我倾向于使用 Lockbox^(4),因为它简单且功能强大。Lockbox 提供了一组类方法,用于存储字符串、日期、数组和集合。你可以查看在列表 13-5 中存储秘密字符串的过程。
#import "Lockbox.h"
NSString *keyname = @"KeyForMyApp";
NSString *secret = @"secretstring";
[Lockbox setString:secret
forKey:keyname
accessibility:kSecAttrAccessibleWhenUnlocked];
列表 13-5:使用 Lockbox 设置钥匙串条目
键名将自动以你的应用程序的包 ID 为前缀,并且这个值将作为账户和服务键的值。检索钥匙串中的数据的过程如列表 13-6 所示。
NSString *result = [Lockbox stringForKey:secret];
列表 13-6:使用 Lockbox 从钥匙串中检索字符串
无论你选择或编写哪个封装器,请确保它具有设置kSecAttrAccessible属性的能力,因为许多现有的示例代码忽略了这个功能。
共享钥匙串
iOS 具有通过使用 Keychain 访问组在同一开发者的多个应用程序之间共享 Keychain 数据的能力。例如,如果你有一个“买家”应用和一个“卖家”应用,用于在线市场,你可以让用户在这两个应用之间共享相同的用户名和密码。不幸的是,这种机制被广泛误解,导致人们做出一些可怕的事情,比如使用命名的剪贴板共享应该专属于 Keychain 的项目。
注意
要使用 Keychain 访问组,你的应用程序必须共享相同的包种子 ID。这只能在创建新的 App ID 时指定。^(5)
为了让你的应用程序利用访问组,你需要创建一个 Entitlements 属性列表(参见 图 13-1),其中包含一个名为 keychain-access-groups 的数组,每个共享的 Keychain 项目都有一个字符串条目。

图 13-1:定义一个由你的包种子 ID 和公司前缀组成的 Keychain 访问组,后跟 Keychain 项目的通用名称。
Keychain 项目将由包种子 ID、你的反向 DNS 标注开发者标识符以及两个应用程序都能引用该权限的符号名称组成(参见 列表 13-7)。
[dict setObject:@"DTHIELISEC.securitySuite" forKey:(id)kSecAttrAccessGroup];
列表 13-7:设置 Keychain 项目的访问组
在这里,DTHIELISEC是包种子 ID。你的包 ID 也将是一个 10 个字符的字母数字字符串。在通过 SecItemAdd 函数创建 Keychain 项目时,你需要将新权限的值作为 kSecAttrAccessGroup 键的值传入。请注意,一个 Keychain 项目只能有一个 Keychain 访问组。
注意
从技术上讲,如果你创建了一个 Keychain 访问组,但在创建 Keychain 项目时没有指定该组,则会使用*keychain-access-groups*数组中的第一个字符串作为默认权限。因此,如果你只使用一个访问组,在执行*SecItemAdd*时无需指定该组——但最好还是指定。
iCloud 同步
iOS 7 引入了一种机制,允许 Keychain 项目与 iCloud 同步,使用户能够在多个设备间共享 Keychain 项目。默认情况下,应用程序创建的 Keychain 项目并未启用此功能,但可以通过将 kSecAttrSynchronizable 设置为 true 来启用。
[query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable];
因为此项现在可能会在多个钥匙串之间同步,所以对该项的更新(包括删除)将传播到所有其他位置。确保你的应用能够处理系统删除或更改钥匙串项的情况。还需注意,使用此选项时不能指定不兼容的 kSecAttrAccessible 属性。例如,指定 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 是不行的,因为 ThisDeviceOnly 指定该项永远不应备份,无论是备份到 iCloud,还是备份到笔记本或桌面电脑,或任何其他同步提供商。
数据保护 API
作为额外的保护层,苹果引入了数据保护 API(与微软的 Data Protection API 不同),该 API 允许开发者指定何时可以访问文件解密密钥。它允许你控制对文件本身的访问,类似于钥匙串项的 kSecAttrAccessible 属性。数据保护 API 使用用户的密码和类密钥结合来加密每个受保护文件的特定密钥,并在这些文件不应被访问时(即设备锁定时)将类密钥丢弃在内存中。当启用了 PIN 时,密码设置界面会显示数据保护已启用,如 图 13-2 所示。

图 13-2:启用数据保护的密码设置
保护级别
开发者可以通过数据保护 API 请求多个保护级别,这些级别大致类似于在钥匙串项上设置的kSecAttrAccessible属性。现在让我们来探讨这些级别。
CompleteUntilFirstUserAuthentication 保护级别
CompleteUntilFirstUserAuthentication 是 iOS 5 及更高版本的默认文件保护属性。除非明确指定了其他属性,否则它将应用于所有适用的文件。其功能上类似于 FileProtectionComplete,但文件在用户首次解锁设备(重启后)后始终可用。如果有人获得了在运行设备上执行远程代码的权限,或者发生了沙盒绕过,那么它的保护作用就不大,但它确实能防止一些需要重启才能进行的攻击。
完全保护级别
Complete 是可用的最安全的文件保护类,如果你能使用它的话。完全保护确保在短暂延迟后,锁定设备会将类密钥从内存中丢弃,使文件内容无法读取。
此保护级别通过 NSFileManager 的 NSFileProtectionComplete 属性以及 NSData 对象的 NSDataWritingFileProtectionComplete 标志来表达。对于 NSData 对象,你可以从设置 NSDataWritingFileProtectionComplete 标志开始,如 示例 13-8 所示。
NSData *data = [request responseData];
if (data) {
NSError *error = nil;
NSString *downloadFilePath = [NSString stringWithFormat:@"%@mydoc.pdf", NS
TemporaryDirectory()];
[data writeToFile:downloadFilePath options:NSDataWritingFileProtectionComplete
error:&error];
示例 13-8:在 NSData 对象上设置 NSDataWritingFileProtectionComplete 标志
一旦您在 NSData 对象上设置了 NSDataWritingFileProtectionComplete,您可以使用 NSFileManager 来设置 NSFileProtectionComplete 标志。
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NS
UserDomainMask, YES);
NSString *applicationDocumentsDirectory = [searchPaths lastObject];
NSString *filePath = [applicationDocumentsDirectory stringByAppendingPathComponent:
@"mySensitivedata.txt"];
NSError *error = nil;
NSDictionary *attr =
[NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attr
ofItemAtPath:filePath
error:&error];
清单 13-9:使用 NSFileManager 设置 NSFileProtectionComplete 标志
您还可以为您创建的 SQLite 数据库添加文件保护属性,使用长得奇怪的 SQLITE_OPEN_READWRITE_SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN 参数,如 清单 13-10 所示。
NSString *databasePath = [documentsDirectory stringByAppendingPathComponent:@"
MyNewDB.sqlite"];
sqlite3_open_v2([databasePath UTF8String], &handle, SQLITE_OPEN_CREATE|
SQLITE_OPEN_READWRITE_SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN,NULL);
清单 13-10:为 SQLite 数据库设置保护属性
在尝试使用完整保护之前,请先考虑一下您的应用程序如何工作。如果您的进程需要持续的读/写访问文件,那么这种保护模式将不适用。
完整保护级别(除非打开)
CompleteUnlessOpen 保护级别稍微复杂一些。使用 NSFileManager 时,您会设置 NSFileProtectionCompleteUnlessOpen 标志,操作 NSData 存储时则设置 NSDataWritingFileProtectionCompleteUnlessOpen。它并不像名字所示那样在文件被应用程序打开时禁用文件保护。CompleteUnlessOpen 实际上确保即使设备被锁定,已打开的文件仍可以写入,并且允许新的文件写入磁盘。任何已经具有此类保护的文件,在设备锁定时无法打开,除非它们之前已经被打开。
其工作原理是通过生成一对密钥,并使用它来计算和派生共享密钥,从而包裹文件密钥。图 13-3 展示了这一过程。

图 13-3:密钥生成和包装。请注意,文件私钥 ➌ 是暂时的,在包裹的文件密钥存储在文件元数据中后会被丢弃。
让我们一步一步地走过这个文件保护过程:
-
与所有文件一样,会生成文件密钥 ➊ 用于加密文件内容。
-
生成另一个密钥对^(6)来生成文件公钥 ➋ 和文件私钥 ➌。
-
文件私钥 ➌ 和受保护除非打开类公钥 ➍ 用于计算共享密钥 ➎。
-
该共享密钥 ➏ 的 SHA-1 哈希用于加密文件密钥。
-
这个加密文件密钥 ➐ 被存储在文件的元数据中,以及文件公钥一起。
-
系统会丢弃文件私钥。
-
关闭文件时,未加密的文件密钥会从内存中删除。
-
当文件需要重新打开时,受保护除非打开类私钥和文件公钥将用于计算共享密钥。
-
该密钥的 SHA-1 哈希值随后用于解密文件密钥。
这一过程的结果是,您仍然可以在设备锁定时写入数据,而无需担心攻击者能够读取这些数据。
数据保护类权限
如果你的应用程序在后台或设备锁定时不需要读取或写入任何文件,可以为项目添加一个值为NSFileProtectionComplete的授权。这将确保所有写入磁盘的可保护数据文件仅在设备解锁时可访问,这等同于在每个适用的文件上设置kSecAttrAccessibleWhenUnlocked。
注意
尽管这将影响通过 *NSFileManager*、*NSData*、SQLite 和 Core Data 文件管理的文件,但其他类型的文件(例如,plist 文件、缓存等)将不会受到保护。*
在 Xcode 5 及以后的版本中,数据保护授权在新项目中默认启用;然而,旧项目不会自动迁移。启用授权本身相当简单——只需按照图 13-4 所示切换开关。

图 13-4:在 Xcode 5 中启用数据保护授权。
请注意,在 iOS 7 之前安装的应用程序默认未启用数据保护。它们要么需要更新,要么过去曾明确请求过数据保护属性。
检查受保护数据的可用性
对于在前台执行所有操作的应用程序,数据保护应该能透明地工作。对于需要在设备锁定时在后台运行的应用程序,你的应用程序需要在使用受保护数据之前确定其可用性。可以通过三种不同的机制来实现。
实现委托方法
为了在数据可用性变化时接收通知并采取相应的措施,你的应用程序应该实现applicationProtectedDataWillBecomeUnavailable和applicationProtectedDataDidBecomeAvailable委托方法,正如清单 13-11 所示。
- (void)applicationProtectedDataWillBecomeUnavailable:
(UIApplication *)application {
[self [theBodies hide]];
}
- (void)applicationProtectedDataDidBecomeAvailable:
(UIApplication *)application {
[self [theBodies exhume]];
}
清单 13-11:检测数据保护可用性变化的委托方法
使用这些委托方法来确保需要受保护数据文件的任务能够优雅地清理,并在文件重新激活时通知你。
使用通知中心
NSNotificationCenter API 本质上提供了一种应用内广播机制,应用的一部分可以监听事件通知,而这些事件通知可以从代码的其他地方调用。要使用通知中心检测这些状态变化,你可以注册接收UIApplicationProtectedDataWillBecomeUnavailable和UIApplicationProtectedDataDidBecomeAvailable通知,正如清单 13-12 所示。
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:
(NSDictionary*)launchOptions {
➊ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
➋ [nc addObserver:self
selector:@selector(dataGoingAway:)
name:UIApplicationProtectedDataWillBecomeUnavailable
object:nil];
}
➌ - (void)dataGoingAway {
[self [theBodies hide]];
}
清单 13-12:使用通知中心检测数据可用性变化
在 ➊,实例化了默认的通知中心实例,然后添加了一个观察者 ➋,该观察者指定了当 name: 所指定的事件发生时要调用的选择器。然后你可以简单地在同一类中实现该选择器 ➌,并在接收到事件时执行任何你想要的逻辑。
使用 UIApplication 检测数据保护
你还可以轻松检测是否启用了数据保护,如 Listing 13-13 所示。
if ([[UIApplication sharedApplication] isProtectedDataAvailable]) {
[self [theBodies hide]];
}
Listing 13-13: 使用 protectedDataAvailable 属性
只需检查 UIApplication 实例方法 isProtectedDataAvailable 的布尔结果。
使用 CommonCrypto 进行加密
首先要说明的是:你(可能)不是加密专家。^(7) 我也不是加密专家。很容易认为自己理解加密算法的细节,或者从网上复制粘贴加密代码,但如果你自己尝试进行加密,一般都会出错。
话虽如此,你应该了解 CommonCrypto 框架,即使仅仅是为了能够分辨其他开发者是否在尝试扮演加密专家。这个框架提供了一些低级的加密和解密原语,但唯一可以玩弄的就是 CCCrypt。Listing 13-14 展示了一个可能的使用示例。
CCCrypt(CCOperation op, CCAlgorithm alg, CCOptions options,
const void *key, size_t keyLength, const void *iv, const void *dataIn,
size_t dataInLength, void *dataOut, size_t dataOutAvailable,
size_t *dataOutMoved);
Listing 13-14: CCCrypt 方法签名
CCCrypt 方法有 11 个参数:控制算法、密钥长度、初始化向量、操作模式等等。每一个都可能成为加密错误的源头。根据我的经验,开发者在使用 CCCrypt 时常常会遇到几个常见的陷阱,我将在这里描述。不要犯同样的错误!
应避免的破坏性算法
CCCrypt 支持已知的糟糕加密算法,例如 DES,如果你使用其中之一,你的应用几乎肯定会受到加密攻击和暴力破解的威胁。即使你使用的是更现代的 AES,CCCrypt 也会让你通过 CCOptions 将默认的密码块链接(CBC)模式切换为电子密码本(ECB)模式,这也是一个糟糕的选择。使用 ECB 模式会导致相同的明文块加密成相同的密文块。^(8) 这是一个问题,因为如果攻击者知道一块加密数据,他们就能推测出其他数据块的内容。通常可以通过盐值或初始化向量来解决这个问题,但它们也可能存在问题。
破坏性的初始化向量
AES 的 CBC 模式规范要求向算法提供一个非机密的初始化向量(IV)。IV 帮助随机化加密,即使相同的明文被多次加密,也能产生不同的密文。这样,你就不需要每次生成新的密钥来防止相同数据块的泄露。
然而,重要的是永远不要在相同的密钥下重用 IV,否则两个以相同字节开始的明文消息将会有相同的密文开头,这将向攻击者泄露加密消息的信息。因此,为每次加密操作使用随机 IV 是非常重要的。
你还应该始终确保调用 AES CBC 模式加密函数时,不传递空的初始化向量。如果传递了空 IV,那么多个消息集将使用相同的密钥和 IV 进行加密,导致我刚才描述的情况。
如你所见,使用静态 IV 或空 IV 的结果是相同的:包含相同数据的小块密文将显得相同。一个可能出现问题的例子是密码管理器,在其中存储加密密钥;如果攻击者能够读取这些数据并确定一些密文相同,他们就会知道多个网站使用了相同的密码。
损坏的熵
在最坏的情况下,你可能会遇到使用rand来尝试获取随机字节的代码(rand在密码学上不安全,不适用于加密操作)。官方的 Cocoa 获取熵的方式是通过SecRandomCopyBytes。
uint8_t randomdata[16];
int result = SecRandomCopyBytes(kSecRandomDefault, 16, (uint8_t*)randomdata);
这段代码实际上充当了/dev/random的封装器,从内核的内建熵池中读取熵。请注意,kSecRandomDefault常量在 OS X 上不可用,因此,如果你正在编写可移植的代码,只需将第一个参数指定为NULL。 (在 iOS 下,这相当于使用kSecRandomDefault。)
低质量密钥
人们常常错误地将用户提供的密码用作加密密钥,特别是在移动设备上,这会导致一个相当弱、低熵的加密密钥。有时,这甚至和一个四位数的 PIN 一样糟糕。当使用用户输入来确定加密密钥时,应该使用像 PBKDF2 这样的密钥派生算法。CommonCrypto 框架通过CCKeyDerivationPBKDF提供了这个功能。
PBKDF2 是一种密钥派生函数,它使用一个密码短语以及重复的哈希算法迭代来生成合适的加密密钥。重复的迭代故意让该过程需要更长的时间完成,从而使离线暴力破解密码短语的攻击变得不可行。CCKeyDerivationPBKDF支持以下迭代算法:
• kCCPRFHmacAlgSHA1
• kCCPRFHmacAlgSHA224
• kCCPRFHmacAlgSHA256
• kCCPRFHmacAlgSHA384
• kCCPRFHmacAlgSHA512
如果可能的话,你应该使用至少 SHA-256 或更高版本。SHA-1 应该被视为过时,因为近年来已经取得进展,能够加速破解 SHA-1 哈希。
执行哈希操作
在某些情况下,你可能需要执行哈希操作,以判断两个数据块是否匹配,而无需比较整个内容。这通常用于验证文件是否与“已知的良好”版本相符,或验证敏感信息而不存储信息本身。要对字符串执行简单的哈希操作,可以使用 CC_SHA 系列方法,如下所示:
char secret[] = "swordfish";
size_t length = sizeof(secret);
unsigned char hash[CC_SHA256_DIGEST_LENGTH];
➊ CC_SHA256(data, length, hash);
这段代码简单地定义了一个密钥和它的长度,并创建了一个 char 类型的 hash 来保存哈希操作的结果。在 ➊ 处,调用 CC_SHA_256 会将 data 中的数据进行哈希计算,并将结果存储在 hash 中。
你可能已经习惯了使用 OpenSSL 来进行哈希运算。iOS 没有包含 OpenSSL,但它包括了一些兼容层,用来支持 OpenSSL 依赖的哈希代码。这些在CommonDigest.h中定义,如列表 13-15 所示。
#ifdef COMMON_DIGEST_FOR_OPENSSL
--snip--
#define SHA_DIGEST_LENGTH CC_SHA1_DIGEST_LENGTH
#define SHA_CTX CC_SHA1_CTX
#define SHA1_Init CC_SHA1_Init
#define SHA1_Update CC_SHA1_Update
#define SHA1_Final CC_SHA1_Final
列表 13-15:OpenSSL 兼容的 CommonCrypto 哈希函数挂钩
因此,只要你定义了 COMMON_DIGEST_FOR_OPENSSL,OpenSSL 风格的哈希操作应该能够透明地工作。你可以在列表 13-16 中看到一个示例。
#define COMMON_DIGEST_FOR_OPENSSL
#include <CommonCrypto/CommonDigest.h>
SHA_CTX ctx;
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1_Init(&ctx);
memset(hash, 0, sizeof(hash));
SHA1_Update(&ctx, "Secret chunk", 12);
SHA1_Update(&ctx, "Other secret chunk", 18);
SHA1_Final(hash, &ctx);
列表 13-16:OpenSSL 风格的分块 SHA 哈希
列表 13-16 使用了 SHA1_Update 和 SHA1_Final,这更适合用于哈希大文件,在这种情况下,分块读取文件可以减少整体内存使用。
通过 HMAC 确保消息的真实性
确保加密后的消息数据没有被篡改或损坏,并且是由持有密钥的方生成的,这一点非常重要。你可以使用带有密钥的哈希消息认证码(HMAC)作为机制,来保证消息的真实性和完整性。在 iOS 应用中,你可以使用 HMAC 来验证应用间发送的消息的真实性,或者让远程服务器验证请求是否由正确的应用生成。(只需要确保密钥的生成和存储方式能确保它是设备唯一的并且得到妥善保护。)
要计算 HMAC,你只需要一个密钥和一些数据,传递给 CCHmac 函数,如列表 13-17 所示。
#include <CommonCrypto/CommonDigest.h>
#include <CommonCrypto/CommonHMAC.h>
➊ NSData *key = [@"key for the hash" dataUsingEncoding:NSUTF8StringEncoding];
➋ NSData *data = [@"data to be hashed" dataUsingEncoding:NSUTF8StringEncoding];
➌ NSMutableData *hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
➍ CCHmac(kCCHmacAlgSHA256, [key bytes], [key length], [data bytes], [data length],
[hash mutableBytes]);
列表 13-17:计算 HMAC
注意,列表 13-17 被简化了,只展示了基本的机制;将静态密钥嵌入源代码中并不是推荐的做法。在大多数情况下,这个密钥应该是动态生成的,并存储在钥匙串中。操作过程相对简单。在 ➊ 处,哈希的密钥以 UTF-8 编码的字符串形式传入(这部分应存储在钥匙串中)。在 ➋ 处,将要哈希的数据作为 UTF-8 字符串传入。然后创建一个 NSMutableData 对象 ➌ 用来存储哈希结果,最后将三个组件一起传递给 CCHmac 函数,在 ➍ 处进行处理。
包装 CommonCrypto 与 RNCryptor
如果你需要使用 CommonCrypto 提供的加密功能,RNCryptor 是一个不错的框架。^(9) 它封装了 CommonCrypto,并帮助执行最常用的功能:通过用户提供的密钥使用 AES 加密数据。RNCryptor 还通过提供合理的默认设置来帮助你。说明中给出的基本示例应该足以应对大多数使用场景。有关基本用法,请参见 清单 13-18。
NSData *data = [@"Data" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSData *encryptedData = [RNEncryptor encryptData:data
withSettings:kRNCryptorAES256Settings
password:aPassword
error:&error];
清单 13-18:使用 RNCryptor 加密
只需将数据传递给 encryptData 方法,并提供一个常量,指定你想要使用的加密设置、一个密钥(可以从钥匙串中获取或由用户输入)以及一个 NSError 对象来存储结果。
解密数据(清单 13-19)几乎是加密的反向操作,只是你不需要提供 kRNCryptorAES256Settings 常量,因为该常量已从加密数据的头部读取。
NSData *decryptedData = [RNDecryptor decryptData:encryptedData
withPassword:aPassword
error:&error];
清单 13-19:解密 RNCryptor 加密的数据
在加密流或大量数据的同时保持内存使用合理要稍微复杂一些(请参见 github.com/rnapier/RNCryptor 获取当前示例),但这里展示的示例涵盖了你可能遇到的最常见用例。
注意
RNCryptor 的旧版本存在一个漏洞^(10) ,攻击者可以通过修改密文来篡改解密后的部分数据,因此请确保你的代码使用的是最新版本的 RNCryptor。
本地认证:使用 TouchID
在 iOS 8 中,苹果开放了本地认证 API,使第三方应用可以将指纹识别器作为身份验证工具。本地认证 API 通过实例化 LAContext 类并传入一个认证策略来评估;目前,只有一种策略可用,即通过生物特征识别身份。清单 13-20 详细展示了这个过程。请注意,使用此 API 并不会让开发者访问指纹数据,它只是提供一个来自读卡器硬件的成功或失败结果。
➊ LAContext *context = [[LAContext alloc] init];
➋ NSError *error = nil;
➌ NSString *reason = @"We use this to verify your identity";
➍ if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&error]) {
➎ [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:reason
reply:^(BOOL success, NSError *error) {
if (success) {
➏ NSLog(@"Hooray, that's your finger!");
} else {
➐ NSLog(@"Couldn't read your fingerprint. Falling back to PIN or
something.");
}
}];
} else {
// Something went wrong. Maybe the policy can't be evaluated because the
// device doesn't have a fingerprint reader.
➑ NSLog(@"Error: %@ %@", error, [error userInfo]);
}
清单 13-20:通过指纹认证用户
首先,代码创建一个 LAContext 对象 ➊ 和一个 NSError 对象 ➋ 来保存操作结果。还需要为用户展示指纹界面时提供一个理由 ➌。创建这些之后,代码会检查是否可以在 ➍ 处评估 LAPolicyDeviceOwnerAuthenticationWithBiometrics 策略。
如果可以进行评估,则会评估该策略 ➎;评估的原因和一个处理评估结果的块也会传递给evaluatePolicy方法。如果指纹验证成功,您可以允许应用程序继续执行当前操作 ➏。如果指纹无效,那么根据您编写应用程序的方式,它可能会回退到其他身份验证方法,或者身份验证可能完全失败 ➐。
如果在 ➍ 调用canEvaluatePolicy失败,则执行会进入 ➑。这种情况最有可能发生在用户的设备不支持指纹识别器、指纹功能被禁用,或者没有注册指纹时。
指纹有多安全?
与大多数其他生物识别身份验证形式一样,指纹身份验证是一种不完美的方法。它很方便,但由于你随处留下指纹,因此很容易重建一个模具,有效地模拟你的手指。在美国,执法部门有法律权利使用指纹解锁设备,而不能强迫某人透露他们的密码。
开发人员可以做几件事来解决这些不足之处。最明显的是提供一个选项,让用户可以使用 PIN 码代替 TouchID,或者可能在 TouchID 之外同时使用 PIN 码。另一个有助于减轻指纹克隆攻击的方法是实施一个类似于苹果用于处理锁屏的系统:在三次失败尝试后,恢复使用 PIN 码或要求用户输入密码。由于成功获取克隆指纹是一个不可靠的过程,这可能有助于防止成功的欺诈指纹认证。
结束语
加密和身份验证功能并不总是最简单易用的,但考虑到用户数据隐私的重要性,无论从法律还是声誉角度来看,正确部署这些功能至关重要。本章应当已经让你对可能遇到或需要部署的策略有了合理的了解。然而,保护用户隐私是一个比加密更广泛的话题——你将在下一章中关注这一点。
第十四章:14
移动隐私问题
人们往往随身携带具有定位功能的移动设备,并在这些设备上存储大量个人数据,这使得隐私成为移动安全中的一个持续关注点。现代 iOS 设备允许应用程序(经请求)读取用户的位置信息、使用麦克风、读取联系人、访问 M7 运动处理器等。负责任地使用这些 API 对用户来说非常重要,也有助于减少责任风险,并增加应用被顺利接受进入 App Store 的可能性。
我在第十章中讨论了很多与隐私相关的内容;这主要是关于意外的数据泄露。在本章中,我将讨论在故意收集和监控用户数据时,影响用户和应用作者的隐私问题,以及一些潜在陷阱的应对措施。
唯一设备标识符的危险
iOS 的 唯一设备标识符 (UDID) 是一个警示性的例子。在 iOS 的大多数历史时期,UDID 被用来唯一标识一台 iOS 设备,许多应用程序随后使用该标识符来追踪用户活动或将用户 ID 与特定硬件关联。许多公司将这些标识符用作远程服务的访问令牌,结果证明这是一个极其糟糕的想法。
由于许多组织拥有设备的 UDID,而 UDID 又不被认为是敏感信息,因此那些确实将 UDID 用作身份验证工具的公司,突然间面临一个局面——数千个第三方拥有了其用户的凭证。软件开发者还普遍认为 UDID 是不可变的,但早已有工具能够伪造 UDID,无论是全局伪造还是针对特定应用。
来自苹果的解决方案
由于这些问题,苹果现在拒绝新提交的使用 uniqueIdentifier API 的应用,并建议开发者改用 identifierForVendor API。该 API 返回一个 NSUUID 类的实例。identifierForVendor 机制应当为同一 iOS 设备上的同一供应商开发的所有应用返回相同的 UUID,且该 UUID 将通过 iTunes 进行备份和恢复。然而,它并非不可变的,用户可以重置它。
在 App Store 中,使用 uniqueIdentifier 的旧版应用会返回以 FFFFFFFF 开头的字符串,后跟通常由 identifierForVendor 返回的字符串。同样,使用 gethostuuid 的应用现在会被拒绝上架 App Store,现有应用在调用此功能时会返回 identifierForVendor 值。
使用NET_RT_IFLIST sysctl 或SIOCGIFCONF ioctl 读取设备 MAC 地址的应用程序现在会收到02:00:00:00:00:00。当然,使用 MAC 地址作为任何类型的令牌或认证器一直是个糟糕的主意;MAC 地址会在你连接的每个网络中泄露,而且它们很容易被更改。这种不具体的返回值适当地惩罚了那些采取这种方法的开发人员。
为了进行广告和追踪,苹果引入了ASIdentifierManager类的advertisingIdentifier属性。该属性返回一个 NSUUID,所有应用程序开发者都可以使用,但像uniqueIdentifier一样,这个 NSUUID 可以被擦除或更改(如图 14-1 所示)。

图 14-1:指示 advertisingIdentifier 仅应用于有限目的的用户界面
这个系统与原始的uniqueIdentifier API 的区别在于,advertisingIdentifier是显式的。
• 仅用于广告和追踪;
• 不是不可变的;并且
• 受用户偏好设置的影响。
advertisingIdentifier的这些方面表面上赋予了用户对广告商使用该机制的跟踪控制权。苹果表示,应用程序必须检查advertisingTrackingEnabled的值,如果设置为NO,该标识符只能用于“频率限制、转化事件、估算唯一用户数量、安全和欺诈检测以及调试。”^(1) 不幸的是,这个列表几乎可以涵盖广告商希望对advertisingIdentifier做的任何事情。
你可以通过列表 14-1 中的方式来确定advertisingTrackingEnabled的值。
➊ BOOL limittracking = [[ASIdentifierManager sharedManager]
advertisingTrackingEnabled];
➋ NSUUID *advid = [[ASIdentifierManager sharedManager] advertisingIdentifier];
列表 14-1:确定是否启用了有限广告追踪并获取 advertisingIdentifier
在➊调用advertisingTrackingEnabled时,它会读取用户的广告追踪 ID 的偏好设置,然后在➋读取advertisingIdentifier本身。
处理唯一标识符的规则
在处理任何类型的唯一标识符时,有几个基本规则需要遵循。首先,永远不要假设标识符是不可变的。任何设备提供的标识符都可以被持有设备的人更改。其次,永远不要假设设备和标识符之间有 1:1 的关系。标识符可以从一个设备转移到另一个设备,因此不能依赖它们唯一地标识单个设备。因为标识符可能会更改、并非唯一且可能被广泛分发,所以你也不应该使用它们来认证用户。最后,尽可能保持标识符的匿名性。它们可能对追踪用户行为的总体趋势有用,但除非有充分的理由,否则不要将标识符与用户身份绑定。
移动 Safari 和“请勿追踪”头部
从 iOS 6 开始,Mobile Safari 包含启用 Do Not Track 机制的选项,^(2) 该选项通知远程服务器用户希望选择退出某些方的跟踪。该选项通过 HTTP_DNT 头部表示。当设置为 1 时,表示用户同意仅被当前访问的网站跟踪;当设置为 0 时,表示用户不希望任何方进行跟踪。用户可以在 Safari 设置中启用此模式(图 14-2)。

图 14-2:启用 Do Not Track 的用户界面
至少可以合理假设,用户希望保护其活动细节,免受第三方广告商或分析公司的侵犯。这是由 HTTP_DNT 值为 1 指定的行为,即 iOS 默认发送的头部。
但是,跟踪的定义各不相同。Do Not Track 机制本身的规范指出了以下内容:
工作组尚未就跟踪的定义和 DNT 的范围达成共识。因此,一个站点实际上无法自信地说是否在进行跟踪,更不用说描述跟踪状态资源中的详细内容了。^(3)
根据规范,网站可以使用 storeSiteSpecificTrackingException JavaScript API 提示用户选择参与特定的跟踪场景,但在本文撰写时,这一功能尚未广泛实现。
Cookie 接受政策
iOS 上的 cookies 通过 NSHTTPCookieStorage API 进行管理。sharedHTTPCookieStorage 方法返回 cookie 存储,但尽管该方法的名称如此,iOS 的 cookie 存储是针对每个应用特定的。cookies 实际存储在应用程序主包目录下的一个数据库中。
注意
名称 *sharedHTTPCookieStorage* 源自 OS X,操作系统使用一个全局 cookie 存储库,所有应用都可以共享。
URL 加载系统使用的 cookies 会根据系统范围内共享的 cookieAcceptPolicy 来接受,任何应用都可以指定此政策。此政策可以设置为以下任意一种:
**NSHTTPCookieAcceptPolicyAlways** 接受并存储所有接收到的 cookie。这是默认设置。
**NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain** 仅接受第一方 cookies。
**NSHTTPCookieAcceptPolicyNever** 永不接受 cookies。
请注意,在运行任何低于 iOS 7 的设备上,cookie 接受政策是应用之间共享的,这可能会导致您的应用出现问题。在这些设备上,当另一个正在运行的应用改变其接受政策时,您的应用的政策也会随之改变。例如,依赖第三方 cookie 进行广告收入的应用,可能会反复将其 cookie 政策设置为NSHTTPCookieAcceptPolicyAlways,并在此过程中将您的政策更改为相同的政策。
幸运的是,你可以使用像 didFinishLaunchingWithOptions 这样的事件来指定首选的 cookieAcceptPolicy,并在程序运行时监控 cookie 接受政策的变化,如列表 14-2 所示。
➊ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector
(cookieNotificationHandler:)
name:NSHTTPCookieManagerAcceptPolicyChangedNotification object:nil];
- (void) cookieNotificationHandler:(NSNotification *)notification {
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage
sharedHTTPCookieStorage];
➋ [cookieStorage setCookieAcceptPolicy:NS
HTTPCookieAcceptPolicyOnlyFromMainDocumentDomain];
}
列表 14-2:注册接收当 cookie 接受政策变化时的通知
列表 14-2 在 ➊ 注册一个 NSNotificationCenter,它监听 NSHTTPCookieManagerAcceptPolicyChangedNotification。如果政策发生变化,在 ➊ 中标识的选择器 cookieNotificationHandler 将被调用。在 cookieNotificationHandler 中,你将在 ➋ 设置政策为 NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain。
在 iOS 7 及更高版本中,cookie 管理政策的变化只影响正在运行的应用程序。应用程序还可以通过 NSURLSession 为不同的 HTTP 会话指定不同的 cookie 管理政策。有关更多信息,请参见 “使用 NSURLSession”,见第 117 页。
监控位置和运动
移动平台的最有用的功能之一是能够根据用户当前的物理位置和运动方式提供相关的信息和功能。iOS 设备主要通过 Wi-Fi 和 GPS 确定位置,并通过 M7 运动处理器监控身体运动。
获取位置和运动数据是有风险的。不过,在这一部分,我将讨论获取这两种数据是如何运作的,以及为什么你在存储此类信息时需要小心。
地理定位如何工作
Wi-Fi 地理定位扫描可用的无线接入点,并查询一个包含接入点及其 GPS 坐标的数据库。这些数据库由第三方构建,第三方有效地进行“驾车探测”整个城市,并记录每个发现的接入点的坐标。当然,在某些情况下,这可能会导致不准确的结果。例如,如果有人携带网络设备旅行或重新安置设备,位置数据可能一段时间内没有更新。
GPS 能够提供更具体的导航信息,以及运动信息,以追踪用户的移动。这需要能够与 GPS 卫星进行联系,但并非总是可行,因此 GPS 经常作为备用,或者在需要高精度时使用。GPS 还被用于确定速度或海拔等信息。
存储位置数据的风险
在移动隐私方面,极少有问题像通过定位数据追踪用户一样引发如此多的负面报道。虽然地理位置数据对于各种位置感知服务非常有用,但当定位数据被记录和存储一段时间后,许多问题会随之而来。最明显的就是隐私问题:用户可能会反对他们的定位数据被长期存储并与其他个人信息关联。^(4) 除了公关问题外,一些欧洲国家有严格的隐私和数据保护法律,必须考虑到这些法律。
一个不太明显的问题是,存储与特定用户关联的定位数据可能会使你在法律上处于不利地位。当你将定位数据与可以将其链接到特定个人的数据存储在一起时,这些数据可能会被执法机关或诉讼方传唤。这种情况经常出现在离婚案件中,律师们试图通过展示某一方在关系中的身体出入来证明不忠;长期以来,使用电子追踪的收费公路管理部门不得不应对这些调查。
限制定位精度
由于精确的历史定位数据会引发隐私和法律责任的担忧,因此重要的是只使用满足目的的最低精度。例如,如果你的应用程序旨在确定用户所在的城市或社区,以便进行晚餐预订,你只需要获得用户大约一公里范围内的位置。如果你的目的是找到离用户最近的 ATM 机,你将需要使用更窄的范围。以下是通过 Core Location API 提供的定位精度常量:
• kCLLocationAccuracyBestForNavigation
• kCLLocationAccuracyBest
• kCLLocationAccuracyNearestTenMeters
• kCLLocationAccuracyHundredMeters
• kCLLocationAccuracyKilometer
• kCLLocationAccuracyThreeKilometers
将定位精度限制在必要的最小程度不仅是出于隐私和法律原因的最佳实践,而且还可以减少电池消耗。这是因为较低精度的方法使用相对快速的 Wi-Fi 检测机制,并且更新频率较低,而最高精度设置通常会使用 GPS 并频繁更新。
如果确实需要随着时间存储用户的多次定位数据,确保有适当的程序来最终修剪这些数据。例如,如果你只需要参考一个月的定位数据,确保旧数据已被妥善清理或删除。如果你使用定位数据进行分析,而这些分析不需要与特定用户关联,应该省略或删除任何唯一标识用户的数据。
请求定位数据
定位数据的请求是通过CLLocationManager来完成的,该工具指定了精度常量,并且可以设置当应用程序处于后台时是否需要定位数据。示例 14-3 展示了一个调用示例。
➊ [self setLocationManager:[[CLLocationManager alloc] init]];
➋ [[self locationManager] setDelegate:self];
➌ [[self locationManager] setDesiredAccuracy:kCLLocationAccuracyHundredMeters];
if ([[self locationManager] respondsToSelector:
@selector(requestWhenInUseAuthorization)]) {
➍ [[UIApplication sharedApplication] sendAction:
@selector(requestWhenInUseAuthorization)
to:[self locationManager]
from:self
forEvent:nil];
➎ [[self locationManager] startUpdatingLocation];
}
清单 14-3:请求位置数据权限
在这里,首先分配一个 CLLocationManager ➊,并将其代理设置为 self ➋。然后设置约 100 米的期望精度 ➌。接着在 ➍ 发送权限请求,这会导致向用户显示授权提示。最后,在 ➎,请求管理器开始监控用户的位置。
请注意,从 iOS 8 开始,位置管理器实际上不会启动,除非你提供为什么需要位置数据的描述。这个描述在你的 Info.plist 文件中指定,如果你只需要在应用程序使用时访问位置数据,则使用 NSLocationWhenInUseUsageDescription,如果你还需要从后台获取位置信息,则使用 NSLocationAlwaysUsageDescription。将其中之一添加到你的 plist 文件中,并提供简明但具体的理由,以便在提示用户授予位置数据权限时显示给他们。
管理健康和运动信息
应用程序可以处理的最敏感信息之一就是用户的健康信息。在 iOS 上,可以使用 HealthKit API 和设备的 M7 运动处理器提供的 API(如果有的话)来检索这些数据。你将简要了解如何读取和写入这些数据,以及如何请求应用程序正常运行所需的最小权限。
注意
从 iOS 9 开始,HealthKit 仅在 iPhone 上可用,iPad 上不可用。
从 HealthKit 读取和写入数据
可以请求 HealthKit 信息用于读取或同时用于读取和写入(Apple 称之为共享,有些令人困惑)。根据只请求绝对必要的权限的原则,如果可能的话,请请求只读访问权限。清单 14-4 显示了如何请求特定健康数据的权限。
if ([HKHealthStore isHealthDataAvailable]) {
HKHealthStore *healthStore = [[HKHealthStore alloc] init];
➊ HKObjectType *heartRate = [HKObjectType quantityTypeForIdentifier:
HKQuantityTypeIdentifierHeartRate];
➋ HKObjectType *dob = [HKObjectType characteristicTypeForIdentifier:
HKCharacteristicTypeIdentifierDateOfBirth];
➌ [healthStore requestAuthorizationToShareTypes:
[NSSet setWithObject:heartRate]
readTypes:[NSSet setWithObject:dob]
➍ completion:^(BOOL success, NSError *error) {
if (!success) {
// Failure and sadness
} else {
// We succeeded!
}
}];
}
清单 14-4:请求健康数据权限
在 ➊ 和 ➋,你指定了两种你希望访问的数据类型,即心率和出生日期。 在 ➌,你请求授权访问这些数据,并提供一个 completion 块来处理成功或失败。请注意,requestAuthorizationToShareTypes 正在请求读写访问权限,推测这是因为该应用程序旨在跟踪和记录用户的心率。readTypes 参数指定你希望监控用户的心率,但不进行写入操作。在这种情况下,你请求用户的出生日期(这通常不太可能改变)来推断他们的年龄。最后,为了让你能够分发该应用程序,你需要在 Xcode 中启用 HealthKit 权限,如 图 14-3 所示。
虽然 HealthKit 展示了如何记录步数,但还有更详细的方法可以获取运动数据,以帮助猜测用户到底在进行哪种活动。这些更详细的数据可以通过 M7 运动追踪器来获取。

图 14-3:在 Xcode 中启用 HealthKit 权限
M7 运动处理器
iPhone 5s 引入了 M7 运动追踪处理器,可以记录关于微小运动的详细信息,同时减少了过去所造成的电池消耗。健身应用可以利用这些数据来确定用户当前进行的运动类型以及他们走了多少步。监控睡眠质量的应用也可以利用这些数据,基于细微的运动判断用户的睡眠深度。显然,能够判断用户何时入睡以及他们在使用手机之外的活动是一项重大责任。Apple 详细说明了 M7 在跟踪用户时的程度如下:
M7 知道你何时在走路、跑步,甚至是开车。例如,当你停车并继续步行时,地图会自动从驾驶模式切换到步行的逐向导航。由于 M7 能够识别你是否在移动的车辆中,iPhone 5s 不会要求你连接经过的 Wi-Fi 网络。如果你的手机长时间未移动,例如当你在睡觉时,M7 会减少网络请求以节省电池。^(5)
M7 处理器的使用方式与基本的地理位置权限相似。但 M7 有一个其他地理位置数据访问中不存在的特殊情况:应用在被授权访问位置信息之前,仍然可以访问已经记录的数据。如果你打算使用这些历史数据,请在权限提示中告知用户,并最好让他们选择是否使用或忽略这些数据。
请求数据收集权限
当尝试访问敏感数据,如用户的联系人、日历、提醒事项、麦克风或运动数据时,用户将会收到一个提示,要求授予或拒绝此访问权限。为了确保用户了解你需要这些访问权限的原因,请定义一段文字,在访问提示中向用户展示。确保这些解释简洁而描述性,如图 14-4 所示。

图 14-4:向用户发送请求
你可以通过 Xcode 在应用的 Info.plist 文件中设置这些消息,如图 14-5 所示。

图 14-5:描述在 Info.plist 中请求不同类型访问权限
此外,确保你的应用能够优雅地处理拒绝这些权限的情况。与 Android 不同,Android 的权限授予是全有或全无的,而 Apple 期望 iOS 应用能够处理某些权限被授予而其他权限被拒绝的情况。
使用 iBeacons 进行定位追踪
Apple 的 iBeacons 旨在衡量您与硬件的距离,并在您处于有效范围内时执行某些操作。例如,应用程序可以使用信标来跟踪您在商场或商店中的移动,或指示刚刚停在您旁边的汽车是您请求的 Uber 车。iBeacon 功能是 Core Location API 的一部分,该 API 在兼容设备上使用低功耗蓝牙(BTLE)来管理接近度监控。
在本节中,我将首先讨论一些应用程序如何检测 iBeacons,以及 iOS 设备如何成为 iBeacons。最后,我将讨论使用 iBeacons 时需要考虑的隐私问题。
监测 iBeacons
通过实例化 Core Location CLLocationManager 并将 CLBeaconRegion 传递给管理器的 startMonitoringForRegion 方法来实现对 iBeacons 的监控,参考 示例 14-5。
➊ NSUUID *myUUID = [[NSUUID alloc] initWithUUIDString:
@"CE7B5250-C6DD-4522-A4EC-7108BCF3F7A4"];
NSString *myName = @"My test beacon";
CLLocationManager *myManager = [[CLLocationManager alloc] init];
[myManager setDelegate:self];
➋ CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:myUUID
➌ identifier:myName];
[myManager startMonitoringForRegion:region];
示例 14-5:启动对特定区域(由 UUID 定义)的监控
在 ➊ 生成的 NSUUID 被分配给 CLBeaconRegion ➋,并将用于唯一标识该信标。标识符 ➌ 将指定信标的符号名称。请注意,您可以注册以监控多个具有相同 CLLocationManager 的区域。
注意
您可以在终端中使用 *uuidgen(1)* 命令生成一个唯一的 UUID,作为信标标识符。
您还需要实现一个 locationManager 委托方法,如 示例 14-6,以处理位置更新。
- (void)locationManager:(CLLocationManager *)manager
didEnterRegion:(CLRegion *)region {
if ([[region identifier] isEqualToString:myName]) {
[self startRangingBeaconsInRegion:region];
}
}
示例 14-6:一个示例的 locationManager 委托方法
每当运行您的应用程序的设备进入 iBeacon 注册区域时,此方法将被调用;然后,您的应用可以在进入该区域后执行相应的逻辑。应用程序一旦收到设备已进入信标范围的通知,就可以开始进行范围测量,即测量设备与信标之间的距离。
在应用程序开始对信标进行范围测量后,locationManager:didRangeBeacons:inRegion 委托方法(示例 14-7)将定期被调用,允许应用程序根据信标的接近度做出决策。
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:
(NSArray *)beacons inRegion:(CLBeaconRegion *)region
{
CLBeacon *beacon = [beacons objectAtIndex:0];
switch ([beacon proximity]) {
case CLProximityImmediate:
//
break;
case CLProximityNear:
//
break;
case CLProximityFar:
//
break;
case CLProximityUnknown:
//
break;
}
}
示例 14-7: locationManager 回调用于检查信标
有四个常量表示接近度:CLProximityImmediate、CLProximityNear、CLProximityFar 和 CLProximityUnknown。请参见 表 14-1 了解这些值的含义。
表 14-1: 区域接近度(范围)测量
| 项目类 | 含义 |
|---|---|
CLProximityUnknown |
范围未确定。 |
CLProximityImmediate |
设备紧挨信标。 |
CLProximityNear |
设备位于信标的几米范围内。 |
CLProximityFar |
设备在范围内,但接近区域的边缘。 |
将 iOS 设备变成 iBeacon
BTLE iOS 设备也可以充当 iBeacons,向外界广播它们的存在,这可以用来检测和衡量 iOS 设备之间的距离。通过 CoreBluetooth 框架来完成这一操作,使用CBPeripheralManager实例并给它一个包含选择的 UUID 的CLBeaconRegion(Listing 14-8)。
➊ NSUUID *myUUID = [[NSUUID alloc] initWithUUIDString:
@"CE7B5250-C6DD-4522-A4EC-7108BCF3F7A4"];
➋ NSString *myName = @"My test beacon";
➌ CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:myUUID
identifier:myName];
➍ NSDictionary *peripheralData = [region peripheralDataWithMeasuredPower:nil];
➎ CBPeripheralManager *manager = [[CBPeripheralManager alloc] initWithDelegate:self
queue:nil];
➏ [manager startAdvertising:peripheralData];
Listing 14-8: 将你的应用程序变成 iBeacon
代码在➊处生成 UUID,在➋处生成符号名称,然后在➌处定义一个区域。在➍处,peripheralDataWithMeasuredPower方法返回一个字典,其中包含广告信标所需的信息(nil参数仅告诉代码使用设备的默认信号强度参数)。在➎处,实例化CBPeripheralManager,最后将peripheralData ➏传递给管理器,以便它可以开始广播。
现在你已经了解了如何管理 iBeacons,让我们看看实现它们的一些隐私影响。
iBeacon 注意事项
显然,iBeacons 提供了关于用户位置的极为详细的信息。信标不必只是简单的发射器;它们也可以是可编程设备或其他 iOS 设备,能够记录位置更新并将其发送到中央服务器。用户可能会反对长期追踪这些数据,因此,像其他地理定位数据一样,避免将任何信标日志保存超过必要的时间。另外,不要将时间和信标信息关联在一起,以便它们可以长期与特定用户相关联。
你的应用应该谨慎地将其安装的设备转变为一个信标。成为信标使设备变得可被发现,因此务必以一种能够传达这一事实的方式告知用户你的意图。如果可能,限制蓝牙广告的时间窗口,在必要的数据交换完成后停止广告。
现在你已经了解了应用如何收集用户信息的多种方式,让我们看看一些政策指南,这些指南将决定应用如何处理个人数据。
建立隐私政策
为了自身的保护,始终在你的应用中明确声明隐私政策。如果你的应用是为儿童设计的,那么隐私政策不仅是 App Store 的要求,也是法律上的要求,依据《儿童在线隐私保护法》(COPPA)。
我不是律师,所以当然不能为你提供具体的法律建议关于你的政策应如何实施。然而,我建议你在隐私政策中包括以下内容:
• 你的应用收集的信息,以及这些信息是否具有身份识别性(即,是否可以追溯到特定用户)
• 信息收集的机制
• 收集每种数据的原因
• 这些数据是如何处理和存储的
• 数据的保留政策(即,数据存储多久)
• 你收集的信息是否以及如何与第三方共享
• 用户如何更改数据收集设置(如果需要)
• 用于保护用户数据的安全机制
• 隐私政策更改的历史记录
电子前沿基金会(EFF)提供了一个很好的模板,用于制定有效且富有信息性的隐私政策,你可以在www.eff.org/policy找到。
请注意,苹果公司对如何在应用程序中实施隐私政策以及如何提供该政策有一些特定要求。特别是,所有提供自动续订或免费订阅的应用程序,以及被分类为“为儿童设计”的应用程序,都必须包含隐私政策的 URL。如果应用程序设置为“为儿童设计”,则该政策需要根据应用程序内的每种本地化进行本地化。^(6)
结束语
鉴于关于美国及海外大规模政府监控的披露,消费者对公司收集并关联其个人信息和习惯的关注和担忧可能会增加。同时也越来越清楚,收集的用户信息越多,公司暴露的风险就越大。那些掌握用户最详细信息的公司,也是最容易受到政府干预的目标,无论是通过传票、监控,还是通过情报机构的黑客攻击。
总结来说,始终清晰地定义你的意图,并尽量减少收集的数据,以限制暴露风险,并与消费者建立和维持信任。
第十五章:INDEX
A
地址清理器(ASan),55
地址空间布局随机化(ASLR),8,53–54,87
advertisingIdentifier,235
advertisingTrackingEnabled,235
AES 算法,226–227
AFNetworking,122–124
alloc,19
.app 目录,78–79
苹果系统日志(ASL),161–164
应用程序结构,27–38
Bundle 目录,33–34
Data 目录,34–37
设备目录,32–33
Documents 目录,34–35
Shared 目录,37
applicationDidEnterBackground,20,167,179–180,183
应用扩展,140–144
extensionPointIdentifier,144
扩展点,140
NSExtensionActivationRule,142
NSExtensionContext,143
NSExtensionItem,143
shouldAllowExtensionPointIdentifier,143
第三方键盘,144
Application Support 目录,35
applicationWillResignActive,180
applicationWillTerminate,20,167,183
应用审查,3–4,10–12
躲避,11–12
应用商店,3–4
审查过程,10–12
绕过,11–12
ARC(自动引用计数),19
ASan(地址清理器),55
ASIHTTPRequest,122,124–125
ASL(苹果系统日志),161–164
ASLR(地址空间布局随机化),8,53–54,87
身份验证
生物识别,231–232
指纹认证的安全性,232
HTTP 基本身份验证,110–111,119–121
本地身份验证 API,231–232
TouchID,231–232
LAContext,231–232
自动修正,175–177
自动引用计数(ARC),19
autoreleasepool,19
B
回溯(bt)命令,65–66
BEAST 攻击,117
生物识别,231–232
黑盒测试,77
黑名单,200
块,Objective-C
声明,18
向 JavaScript 暴露,150–151
蓝牙低功耗 (BTLE), 244
蓝牙个人区域网 (PAN), 125
Bonjour, 125
启动 ROM, 4
断点, 62
操作, 70–72, 164
条件, 72
启用/禁用, 64
设置, 62–64
暴力破解, PIN 码, 214
bt (回溯) 命令, 65–66
BTLE (蓝牙低功耗), 244
缓冲区溢出, 12, 193–196
示例, 194–195
防止, 195–196
Bundle 目录, 33–34
bundle ID, 33, 138
bundle seed ID, 218–219
BurpSuite, 43–47
C
CA (证书授权机构), 114–115
CA 证书, 44
证书管理, 47
证书钉扎, 114–117, 124
打败, 96–97
缓存管理, 170–171
移除缓存数据, 171–174
Caches 目录, 35–36
缓存, 36
CALayer, 182–183
规范名称 (CN), 128–129
canPerformAction, 168–169
类别, Objective-C, 22–23
CBPeripheralManager, 246
CCCrypt, 186, 226
CCHmac, 229
CCRespring, 79
证书授权机构 (CA), 114–115
certificateHandler, 126
CFArrayRef, 112–113
CFBundleURLSchemes, 133
CFDataRef, 113
CFPreferences, 36, 178
CFStream, 48, 107, 128–129
chflags 命令, 42
clang, 51–53
class-dump 命令, 90, 92–93
CLBeaconRegion, 244, 246
CLLocationManager, 240, 244
CN (规范名称), 128–129
Cocoa, 14
Cocoa Touch, 14
代码段, 193–195
codesign 命令, 82
CommonCrypto, 151, 230, 230
CCCrypt, 226
CompleteUnlessOpen, 222–223
CompleteUntilFirstUserAuthentication, 220
cookieAcceptPolicy, 237
cookies, 36
接受策略, 237–238
被盗,114
Cookies 目录,36
禁用复制/粘贴,168–169
Cordova,150,154–157
Cordova.plist,156
CoreBluetooth,246
Core Data,204,223
CRIME 攻击,118
跨站脚本攻击(XSS),199–200
输入 sanitization,200–201
输出编码,201–202
cryptid,81,86–90
cryptoff,86–90
cryptsize,86–90
cURL,78
证书,93–94
Cycript,90,93–94
Cydia,31,77
Cydia Substrate,78,97–100
D
DAC(自主访问控制),4–5
Data 目录,34–37
数据泄漏,161–188
Apple 系统日志,161–164
自动更正,175–177
断点操作,164
缓存管理,170–174
dynamic-text.dat,177
短暂会话,173
HTTP 缓存,169–174
本地存储,174
iCloud,161
避免,188
键盘记录器,177
NSLog,161–164
禁用,163
NSURLSession,173
粘贴板,164–169
canPerformAction,168
禁用复制/粘贴,168–169
pasteboardDB 文件,165–167
pasteboardWithUniqueName,165–167
保护数据,167–169
UISearchBar,165
擦除,167
快照,178–184
applicationDidEnterBackground,179–180
防止挂起,183–184
屏幕模糊,179–183
状态保存,184–187
restorationIdentifier,184–185
用户偏好设置,178
数据保护 API,7–8,219–225
类密钥,220
CompleteUnlessOpen,222–223
CompleteUntilFirstUserAuthentication,220
DataProtectionClass,223
数据保护授权,223–224
委托方法,224
检测,225
FileProtectionComplete,220–221
isProtectedDataAvailable,225
保护级别,220–223
DataProtectionClass 权限, 157
数据段, 193–195
dataTaskWithRequest, 18
数据盗窃, 161
dd 命令, 88
调试, 61–75
断点, 62
动作, 70–72
条件, 72
启用/禁用, 64
设置, 62–64
调试导航器, 65
debugserver, 81–84
连接到, 83
安装, 81–82
故障注入, 72–73
帧和变量, 64–68
lldb, 62–75
回溯(bt)命令, 65–66
expr 命令, 69
frame select 命令, 66–67
frame variable 命令, 66
image list, 87
print object 命令, 67–68
对象检查, 68
跟踪数据, 74
变量和属性, 69–70
调试导航器, 65
debugserver, 81–84
连接到, 83
安装, 81–82
decodeRestorableStateWithCoder, 184
解密二进制文件, 80–90
.default_created.plist, 32
Default-Portrait.png, 179
defaults 命令, 42
委托, 20
DES 算法, 226
反序列化, 21
开发者团队 ID, 138
设备目录, 32–33
设备密钥, 7
device.plist, 32
didFinishNavigation, 159–160
did 消息, 20
didReceiveCertificate, 126
反汇编, 使用 Hopper, 94–96
自主访问控制(DAC), 4–5
Documents 目录, 34–35
不追踪, 236–237
dpkg 命令, 96, 99–101
DTrace, 55, 61
dumpdecrypted 命令, 80
_dyld_get_image_name, 10
dylibs, 10
动态分析, 55
动态打补丁, 11–12
E
模拟器, 参见 仿真器
encodewithcoder, 21–22
加密段, 84–90
加密, 211–230
AES, CCB 模式, 226–227
坏算法, 226
CommonCrypto, 225, 230
CCCrypt, 226
Curve25519, 222
数据保护 API,5,7–8,219–225
类密钥,220
CompleteUnlessOpen,222–223
CompleteUntilFirstUserAuthentication,220
DataProtectionClass,223
数据保护权限,223–224
委托方法,224
检测,225
FileProtectionComplete,220–221
FileProtectionCompleteUnlessOpen,222
isProtectedDataAvailable,225
保护级别,220–223
DES 算法,226
设备密钥,7
磁盘加密,5–7
椭圆曲线 Diffie-Hellman 算法,222
熵,227
文件密钥,7
完整磁盘加密,5–7
哈希,228–230
HMAC(哈希消息认证码),229–230
初始化向量(IV),226–227
钥匙串,6–7,113,186,211–219
API,7
备份,212
iCloud 同步,219
项目类,214
密钥层次结构,6–7
kSecAttrAccessGroup,218–219
保护属性,212–214
SecItemAdd,219
共享钥匙串,218–219
使用,214–217
包装器,217–218
密钥派生,227–228
密钥质量,227–228
锁箱,217
OpenSSL,228–229
RNCryptor,230
SecRandomCopyBytes,227
TLS(传输层安全性),127–129
权限,218,223
entitlements.plist,81–82
熵,227
Erica 工具,31,78
/etc/hosts,49
EXC_BAD_ACCESS,191
永久执行(XN),8–9
expr命令,69
extensionPointIdentifier,144
extractIdentityAndTrust,112–113
F
故障注入,72–73
File Juicer,169,174
文件密钥,7
FileProtectionComplete,220–221
文件系统监控,58–59
Finder,42
指纹认证的安全性,232
法医攻击者,161
格式化字符串攻击,190–193
NSString, 192–193
防止, 191–193
Foundation 类, 14
帧和变量, 68
frame select 命令, 66–67
frame variable 命令, 66
全盘加密, 5–7
模糊测试, 55
G
垃圾回收, 18
gdb, 62
地理定位, 238
精度, 239
CLLocationManager, 240
风险, 238–239
get-task-allow, 82
Google Toolbox for Mac, 202
GPS, 238
H
handleOpenURL, 136
哈希, 228–230
哈希消息认证码 (HMAC), 229
hasOnlySecureContent, 159–160
HealthKit, 240–241
堆, 8, 53–54, 193
隐藏文件, 41–42
HMAC (哈希消息认证码), 229
Homebrew, 46, 88, 94, 99
钩子
使用 Cydia Substrate, 97–100
使用 Introspy, 100–103
Hopper, 94–96
HTML 实体, 201
编码, 见 输出编码
HTTP 基本认证, 110–111, 119–121
HTTP 本地存储, 174
HTTP 重定向, 113–114
I
iBeacons, 244–247
CBPeripheralManager, 246
CLBeaconRegion, 244–246
CLLocationManager, 244
startMonitoringForRegion, 244
iBoot, 4
iCloud, 35, 111, 161, 212, 219
避免, 187
IDA Pro, 94
identifierForVendor, 234
iExplorer, 28–29
iGoat, 178
image list, 87
实现,声明, 16–17
Info.plist, 33
init, 19
初始化向量 (IV), 226–227
initWithCoder, 21–22
initWithContentsOfURL, 206
注入攻击, 199–207
跨站脚本攻击 (XSS), 199–202
输入清理, 200–201
输出编码, 200–202
显示不可信数据, 202
谓词注入, 204–205
SQL 注入, 203–204
参数化 SQL, 203–204
SQLite,203–204
XML 注入,207
XML 外部实体,205–206
XPath,207
输入消毒,200–201
installipa 命令,80
InstaStock,12
Instruments,55–57
整数溢出,196–198
示例,197–198
防止,198
接口,声明,15–16
进程间通信,参见 IPC(进程间通信)
Introspy,100–103
针对 iOS 的 Web 应用,147–160
IPA 安装控制台,78
.ipa 包,80
IPC(进程间通信),131–145
应用扩展,131,140
extensionPointIdentifier,144
扩展点,140
isContentValid,143
NSExtensionActivationRule,142
NSExtensionContext,143
NSExtensionItem,143
shouldAllowExtensionPointIdentifier,143
第三方键盘,143–144
canOpenURL,138
handleOpenURL,136
isContentValid,143
openURL,132–137
sourceApplication,136
UIActivity,139–140
UIPasteboard,144
通用链接,137–138
URL 方案,132–133
CFBundleURLSchemes,133
定义,132–133
劫持,136–137
验证 URL 和发送者,134
iproxy 命令,84
isContentValid,143
IV(初始化向量),226–227
ivars,15–17,91
J
越狱检测,9–10
无用,9
越狱,4,9–10,77
JavaScript,11
在 Cordova 中执行,154–157
在UIWebView中执行,149–150
stringByEvaluatingJavaScriptFromString,149–150
JavaScript–Cocoa 桥接,150–157
JavaScriptCore,150–154
区块,150–151
JSContext,152–154
JSExport,151–152
Jekyll,12
即时(JIT)编译器,8–9,149
JRSwizzle,25
JSContext,152–154
JSExport,151–152
K
kCFStreamSSLLevel,129
Keychain API, 6–7, 113, 186, 211
备份, 212
iCloud 同步, 219
kSecAttrAccessGroup, 218–219
保护属性, 212–214
SecItemAdd, 214–215, 219
SecItemCopyMatching, 216
SecItemDelete, 216
SecItemUpdate, 215
共享 Keychain, 218–219
用法, 214–217
包装器, 217–218
密钥派生, 227–228
键盘记录, 175–177
killall 命令, 79, 101
kSecAttrAccessGroup, 218–219
kSecAttrAccessible, 220
kSecAttrSynchronizable, 219
L
LAContext, 231–232
ldid 命令, 97
LDID(链接标识编辑器), 97
从 C 继承的遗留问题, 189–198
缓冲区溢出, 193–196
示例, 194–195
预防, 195–196
格式化字符串攻击, 190–193
NSString, 192–193
预防, 191–193
整数溢出, 196–198
示例, 197–198
预防, 198
libc, 8
Library 目录, 35–37
Application Support 目录, 35
Caches 目录, 35–36, 187
Snapshots 目录, 36
Cookies 目录, 36
Preferences 目录, 36
Saved Application State 目录, 37
LIKE 操作符, 205
链接标识编辑器 (LDID), 97
lipo 命令, 78, 85
lldb, 62–81, 83–84, 191
backtrace(bt)命令, 65–66
断点, 62
操作, 70–72, 164
条件, 72
启用/禁用, 64
设置, 62–64
expr 命令, 69
frame select 命令, 66–67
frame variable 命令, 66
image list, 87
print object 命令, 67–68
llvm, 90
本地身份验证 API, 231–232
Logos, 98
回环接口, 46–47
Lua, 12
M
M7 处理器, 242
MAC(强制访问控制), 4–5
MAC 地址, 234
Mach-O 二进制格式, 77, 85
MachOView, 88
MacPorts, 94
malloc, 197–198
强制访问控制(MAC), 4–5
MATCHES 操作符, 205
MCEncryptionNone, 126
MCEncryptionOptional, 126
MCEncryptionRequired, 126
MCSession, 126
消息传递, 13–15
方法交换, 23–25
Mobile Safari, 44
MobileTerminal, 78
多点连接, 125–127
certificateHandler, 126
didReceiveCertificate, 126
加密, 125–127
N
netcat 命令, 78
网络, 107–129
AFNetworking, 122–124
证书钉扎, 123–124
ASIHTTPRequest, 122, 124–125
backgroundSessionConfiguration, 117
CFStream, 48, 107, 128–129
ephemeralSessionConfiguration, 117
多点连接, 125–127
certificateHandler, 126
didReceiveCertificate, 126
加密, 125–127
NSInputStream, 49
NSOutputStream, 49
NSStream, 48, 107, 127–128
NSURLSession, 122
URL 加载系统, 107–122
HTTP 基本认证, 110–111, 119–121
HTTP 重定向, 113–114
NSURLConnection, 48, 108
NSURLConnectionDelegate, 109
NSURLCredential, 120
NSURLCredentialStorage, 110–111
NSURLRequest, 108
NSURLResponse, 108
NSURLSession, 48, 117
NSURLSessionConfiguration, 120–121
NSURLSessionTaskDelegate, 119
sharedCredentialStorage, 120–122
存储的 URL 凭证, 121–122
通知中心, 224–225
NSCoder, 185–187
NSCoding, 21–22
NSData, 113
NSExtensionContext, 143
NSExtensionItem, 143
NSFileManager, 221–223
NSHTTPCookieStorage, 237
NSHTTPRequest, 122
NSInputStream, 49
NSLog,95,161–164,192
禁用,163
NSNotificationCenter,224–225
NSOperation,122
NSOutputStream,49
NSPredicate,204–205
NSStream,48,107,127–128
NSString,192–193,195,202
NSURAuthenticationMethodClientCertificate,112
NSURL,188
NSURLCache,74–75,150
NSURLConnection,48,108,117
NSURLConnectionDelegate,109,114
NSURLCredential,113,120
NSURLCredentialStorage,110–111,121
NSURLIsExcludedFromBackupKey,35,187–188
NSURLProtectionSpace,109–111,122
NSURLProtocol,155
NSURLRequest,108,148–149
NSURLResponse,108
NSURLSession,48,117–122
NSURLSessionConfiguration,117–119
NSURLSessionDataTask,18
NSURLSessionTaskDelegate,119
NSUserDefaults,36,37,178
NSUUID,234
NSXMLParser,205–206
O
Objective-C,13–25
blocks
声明,18
暴露给 JavaScript,150–151
类别,22–23
代码结构,15–17
令人愉快的,13
垃圾回收,18
实现,声明,16–17
ivars,15–16
消息传递,14–15
私有方法,缺失的部分,16
属性合成,17
引用计数,18–19
odcctools,78,84
OpenSSH,78
OpenSSL,94,228–229
openssl 命令,138
openURL,132–137
otool,53,78,84–86
检查二进制文件,90–92
输出编码,200–202
P
p12 文件,113
参数化 SQL,203–204
pasteboardDB 文件,165–167
剪贴板,164–169
canPerformAction,168
禁用复制/粘贴,168–169
pasteboardDB 文件,165–167
pasteboardWithUniqueName,165–167
UISearchBar,165
pasteboardWithUniqueName,165–167
PhoneGap,11,150
物理攻击者,161
PIE(位置无关可执行文件),53–54
移除,87
plist 文件,29–31
转换,30–31
XML,29–30
plutil 命令,30–31
popen,10
位置无关可执行文件(PIE),53–54
移除,87
谓词注入,204–205
LIKE 运算符,205
MATCHES 运算符,205
通配符,204–205
谓词,205
predicateWithFormat,204–205
Preferences 目录,36
printf 命令,87,190–192
print object 命令,67–68
隐私问题,233–248
advertisingTrackingEnabled,235
蓝牙低功耗(BTLE),244
cookies,237–238
不追踪,236–237
地理定位,238–240
精度,239
CLLocationManager,240
locationManager,244
风险,238–239
GPS,238
HealthKit,240–241
iBeacons,244–247
CBPeripheralManager,246
CLBeaconRegion,244–246
CLLocationManager,244
startMonitoringForRegion,244
M7 处理器,242
MAC 地址,234
麦克风,233
隐私政策,247–248
接近跟踪,244–247
请求权限,243
唯一设备标识符(UDID),233–235
advertisingIdentifier,235
identifierForVendor,234
NSUUID,234
uniqueIdentifier,234
私有方法,16
属性合成,17
协议,20–22
声明,21–22
接近跟踪,244–247
代理设置,43–50
Q
Quick Look,35,68
QuickType,177
R
引用计数模型,18–19
retain 和 release,18–19
引用,强引用和弱引用,19
release, 18–19
远程设备擦除, 5, 6
removeAllCachedResponses, 75
重启, 79, 101
restorationIdentifier, 184–185
retain, 18–19
return-to-libc 攻击, 8
RNCryptor, 186, 230
rootViewController, 183
rsync 命令, 78
S
安全字符串 API, 195
沙盒, 4–5
已保存应用程序状态 目录, 37
Seatbelt, 4–5
SecCertificateRef, 112–113
SecIdentityRef, 112–113
SecItemAdd, 186, 212, 215, 219
SecItemCopyMatching, 216
SecItemDelete, 216
SecItemUpdate, 215
SecRandomCopyBytes, 227
SecTrustRef, 112–113
安全启动, 4
SecureNSCoder, 186–187
securityd, 7
序列化, 21
setAllowsAnyHTTPSCertificate, 108
setJavaScriptCanOpenWindowsAutomatically, 159
setJavaScriptEnabled, 159–160
setResourceValue, 188
setSecureTextEntry, 175–177
setShouldResolveExternalEntities, 206
共享 目录, 37
sharedCredentialStorage, 120–122
sharedHTTPCookieStorage, 237
共享钥匙串, 218–219
shouldAllowExtensionPointIdentifier, 143
应该消息, 20
shouldSaveApplicationState, 20
shouldStartLoadWithRequest, 148
边加载, 77–80
有符号整数, 196
符号位, 51, 196
模拟器, 43–46
相机, 43
大小写敏感, 43
安装证书, 44
钥匙串, 43
PBKDF2, 43
代理, 44–46
信任库, 44
SpringBoard, 79
SQL 注入, 201, 203–204
参数化 SQL, 203–204
SQLite, 203–204
SSH, 28, 82
SSL, 见 TLS(传输层安全性)
SSL Conservatory, 115–117
SSL 终止开关, 96–97
堆栈, 8, 53–54, 190, 193
startMonitoringForRegion,244
状态保持,184–187
漏洞,184–185
restorationIdentifier,184–185
安全,185–187
静态分析,54
std::string,195
strcat,195
strcpy,194,195
stringByEvaluatingJavaScriptFromString,149–150
strlcat,195–196
strlcpy,195–196
强引用,19
stunnel,46
子类化,23
synthesize,17
syslog,162,190
T
task_for_pid-allow,82
tcpdump 命令,78
tcpprox,49–50
TCP 代理,49–50
测试设备,42
文本段,85–86
Theos,97–98
精简二进制文件,85
ThisDeviceOnly,212
TLS(传输层安全性),108–119,127–129
BEAST 攻击,118
绕过验证,44–47,119
证书钉扎,114–117,123–124
CRIME 攻击,118
相互认证,112–113
setAllowsAnyHTTPSCertificate,108
验证,类别绕过,22
tmp 目录,37,80,187
今日屏幕,131
TOFU(首次使用即信任),127
TouchID,231–232
LAContext,231–232
传输层安全性,见 TLS(传输层安全性)
Tribbles,51
首次使用即信任(TOFU),127
调整,Cydia Substrate,97
U
UDID(唯一设备标识符),233–235
advertisingIdentifier,235
identifierForVendor,234
NSUUID,234
uniqueIdentifier,234
UIActivity,139–140
UIAlertView,183
UI 层,182–183
UIPasteBoard,144,164–169
UIPasteboardNameFind,165
UIPasteboardNameGeneral,165
UIRequiredDeviceCapabilities,34
UIResponderStandardEditActions,169
UISearchBar,165,175
UITextField,175
UITextView,175
UIView,182–183
UIWebView, 200, 201
UIWindow, 182–183
唯一设备标识符 (UDID), 233–235
advertisingIdentifier, 235
identifierForVendor, 234
NSUUID, 234
uniqueIdentifier, 234
uniqueIdentifier, 234
通用链接, 137–138
无符号整数, 196
URL 加载系统, 107
凭证持久性类型, 111
HTTP 基本认证, 110–111
HTTP 重定向, 113–114
NSURLConnection, 108
NSURLConnectionDelegate, 109
NSURLCredential, 120
NSURLCredentialStorage, 110–111
NSURLRequest, 108
NSURLResponse, 108
NSURLSession, 117–122
NSURLSessionConfiguration, 117–119
NSURLSessionTaskDelegate, 119
sharedCredentialStorage, 120–122
存储的 URL 凭证, 121–122
URL 协议, 132–133
CFBundleURLSchemes, 133
定义, 132–133
劫持, 136–137
验证 URL 和发送者, 134
USB,TCP 代理, 84
usbmuxd 命令, 84
用户偏好设置, 178
UUID, 27
uuidgen, 244
V
Valgrind, 55
vbindiff 命令, 78, 88
vfork, 10
.vimrc 文件, 30
vmaddr, 88
W
wardriving, 238
警告策略, 51
看门狗, 58–59
watchmedo 命令, 58–59
weak_classdump, 93
弱引用, 19
web 应用, 147–160
WebViews, 9, 147–160
Cordova, 154–157
风险, 156
XmlHttpRequest, 155
JavaScript, 149
在 Cordova 中执行, 154–157
在 UIWebView 中执行, 149–150
stringByEvaluatingJavaScriptFromString, 149–150
JavaScript–Cocoa 桥接, 150–157
JavaScriptCore, 149–154
blocks, 150–151
JSContext, 152–154
JSExport, 151–152
即时编译 (JIT) 编译器, 149
Nitro, 148, 149
NSURLRequest, 148–149
UIWebView, 147–150
WebKit, 11, 147–148
WKWebView, 参见 WKWebView
白名单, 149, 152, 200–201
will 消息, 20
willSendRequestForAuthenticationChallenge, 112
Wireshark, 46
WKPreferences, 160
WKWebView, 148, 158–160
addUserScript, 159
的好处, 159–160
didFinishNavigation, 159–160
hasOnlySecureContent, 159–160
setJavaScriptCanOpenWindowsAutomatically, 159
setJavaScriptEnabled, 159–160
WKPreferences, 160
WKUserScript, 159
WKWebViewConfiguration, 160
X
xcodebuild, 190
Xcode 设置, 50–53, 55
警告, 51–53
Xcon, 10
XML 注入, 207
NSXMLParser, 205–206
XML 外部实体, 205–206
XPath, 207
XN(eXecute Never), 8–9
XPath, 207
XSS(跨站脚本攻击), 199–200
输入数据清理, 200–201
输出编码, 201–202
xxd 命令, 88
iOS 应用安全中使用的字体包括 New Baskerville、Futura、The Sans Mono Condensed 和 Dogma。该书采用 LATEX 2[ε] 包 nostarch 由 Boris Veytsman 进行排版((2008/06/06 v1.3 为 No Starch Press 排版书籍))。
第十六章
更新信息
访问 www.nostarch.com/iossecurity 获取更新、勘误和其他信息。
更多实用书籍请访问
NO STARCH PRESS

汽车黑客手册
作者 CRAIG SMITH
2016 年春季,352 页,$49.95
ISBN 978-1-59327-703-1

黑帽 Python
黑客与渗透测试的 Python 编程
作者 JUSTIN SEITZ
2014 年 12 月,192 页,$34.95
ISBN 978-1-59327-590-7

游戏黑客
为在线游戏开发自主机器人
作者 NICK CANO
2016 年春季,384 页,$44.95
ISBN 978-1-59327-669-0

Rootkit 与 Bootkit
逆向分析现代恶意软件与下一代威胁
作者 ALEX MATROSOV, EUGENE
RODIONOV, 以及 SERGEY BRATUS
2016 年春季,304 页,$49.95
ISBN 978-1-59327-716-1

安卓安全内部解析
安卓安全架构深入指南
作者 NIKOLAY ELENKOV
2014 年 10 月,432 页,$49.95
ISBN 978-1-59327-581-5

IDA PRO 书籍,第 2 版
世界上最流行的反汇编器非官方指南
作者 CHRIS EAGLE
2011 年 7 月,672 页,$69.95
ISBN 978-1-59327-289-0
电话:
800.420.7240 或 415.863.9900
电子邮件:
SALES@NOSTARCH.COM
网站:
第十七章

第十八章:脚注
第一章:iOS 安全模型
developer.apple.com/appstore/resources/approval/guidelines.html2.www.macworld.com/article/1152835/iphone_flashlight_tethering.html3. 这个描述当然是略微简化的;还有一些粘性位、setuid 位等等。由于 iOS 并未将 DAC 作为其主要访问控制机制,因此在本书中我不会深入探讨这些话题。4. 你可以在media.blackhat.com/bh-us-11/DaiZovi/BH_US_11_DaiZovi_iOS_Security_WP.pdf上找到 iOS 默认沙盒策略的一个很好的总结。5. 然而,似乎大多数越狱用户的动机是为了执行数字化等同于给汽车装上旋转轮毂盖的操作。6.theiphonewiki.com/wiki/XCon7.www.cultofmac.com/82097/ibooks-1-2-1-tries-to-run-jailbreak-code-to-detect-jailbroken-iphones/8.theiphonewiki.com/wiki/Bypassing_Jailbreak_Detection9.stackoverflow.com/questions/4165138/detect-udid-spoofing-on-the-iphone-at-runtime/10.phonegap.com/11.arstechnica.com/apple/2011/11/safari-charlie-discovers-security-flaw-in-ios-gets-booted-from-dev-program/12.reverse.put.as/wp-content/uploads/2011/06/syscan11_breaking_ios_code_signing.pdf13.www.cc.gatech.edu/~klu38/publications/security13.pdf
第二章:懒人版 Objective-C
developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html2.developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/Reference/Reference.html3.github.com/rentzsch/jrswizzle/
第三章:iOS 应用程序架构
www.macroplant.com/iexplorer/2. Erica Utilities 提供了许多其他用于处理越狱设备的实用工具;你可以在ericasadun.com/ftp/EricaUtilities/上查看完整列表。3.developer.apple.com/library/Mac/documentation/Cocoa/Reference/Foundation/Classes/NSBundle_Class/Reference/Reference.html4. 请注意,并非所有在此目录树中可以存在的目录都会在每个应用程序中存在;有些目录只有在应用程序使用某些 API 时才会动态创建。5.developer.apple.com/library/ios/#documentation/FileManagement/Conceptual/DocumentInteraction_TopicsForIOS6.developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/iPhoneAppProgrammingGuide.pdf(第 69 页)
第四章:构建你的测试平台
www.portswigger.net2. 我通常认为 Chrome 是更安全的日常浏览器,但 Firefox 的自包含特性确实让你更方便地调整代理设置。3.getfoxyproxy.org4.www.stunnel.org/5.github.com/iSECPartners/dnsRedir/6.github.com/iSECPartners/tcpprox/7.www.trailofbits.com/resources/ios4_security_evaluation_paper.pdf8.clang-analyzer.llvm.org/9.clang.llvm.org/docs/AddressSanitizer.html10.blog.chromium.org/2012/04/fuzzing-for-security.html11.pypi.python.org/pypi/watchdog/
第五章:使用 lldb 和其他工具进行调试
lldb.llvm.org/2. 对于 Xcode 调试的详细资源,我推荐《iOS 7 编程:突破极限》;请参见iosptl.com/。3. 如果你想深入了解 iOS 和 ARM 的汇编,可以查看 Ray Wenderlich 的教程,链接是www.raywenderlich.com/37181/ios-assembly-tutorial/。
第六章:黑盒测试
blog.iphone-dev.org/2. iclarified.com/3. github.com/stefanesser/dumpdecrypted4. 传统上,这是通过 GNU 调试器 gdb 完成的。然而,自版本 4 以来,gdb 就没有包含在 Xcode 中,而 Cydia 中的大多数版本也已损坏。使用 lldb 的方法在可预见的未来应该是可行的……我想。5. developer.apple.com/library/mac/#documentation/DeveloperTools/Conceptual/MachORuntime/Reference/reference.html6. 除非你禁用 PIE。你可以使用 removePIE 工具来实现;请参阅 github.com/peterfillmore/removePIE/。7. sourceforge.net/projects/machoview/8. 这就是它在xxd(1)中的显示方式,这是我通常用来进行快速且简便编辑的工具。你的编辑器可能有所不同。如果不确定,先使用 MachOView 检查,然后再开发你可能需要的脚本。9. stevenygard.com/projects/class-dump/10. code.google.com/p/networkpx/wiki/class_dump_z11. www.cycript.org/12. github.com/limneos/weak_classdump/13. www.hex-rays.com/products/ida/14. www.hopperapp.com/15. github.com/iSECPartners/ios-ssl-kill-switch/16. iphonedevwiki.net/index.php/MobileSubstrate17. iphonedevwiki.net/index.php/Theos/Getting_Started18. gitweb.saurik.com/ldid.git19. iphonedevwiki.net/index.php/Logos20. www.macports.org/21. brew.sh/22. www.saurik.com/id/7/23. github.com/iSECPartners/ios-ssl-kill-switch/blob/master/Tweak.xm24. github.com/iSECPartners/Introspy-iOS/25. github.com/iSECPartners/Introspy-Analyzer/
第七章:iOS 网络
developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.pdf2.developer.apple.com/library/mac/#documentation/Foundation/Reference/NSURLConnectionDelegate_Protocol3.developer.apple.com/library/ios/#documentation/cocoa/conceptual/URLLoadingSystem/Articles/RequestChanges.html4. https://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSURLConnectionDataDelegate_protocol/Reference/Reference.html#//apple_ref/occ/intfm/NSURLConnectionDataDelegate/connection:willSendRequest:redirectResponse:5.bug665814.bugzilla.mozilla.org/attachment.cgi?id=5408396.docs.google.com/presentation/d/11eBmGiHbYcHR9gL5nDyZChu_-lCa2GizeuOfaLU2HOU/edit?pli=1#slide=id.g1d134dff_1_2227.github.com/AFNetworking/AFNetworking8.github.com/pokeb/asi-http-request9.developer.apple.com/library/prerelease/ios/documentation/MultipeerConnectivity/Reference/MultipeerConnectivityFramework/index.html10.nabla-c0d3.github.io/blog/2014/08/20/multipeer-connectivity-follow-up/11.developer.apple.com/library/ios/#qa/qa2009/qa1652.html12.developer.apple.com/library/mac/#documentation/CoreFoundation/Reference/CFSocketStreamRef/Reference/reference.html
第八章:进程间通信
developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/index.html#//apple_ref/doc/uid/20000301-SW212.developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/iPhoneAppProgrammingGuide.pdf(第 99 页)3.github.com/bengottlieb/Twitter-OAuth-iPhone
第九章:面向 iOS 的 Web 应用程序
www.webkit.org/2.developer.apple.com/library/ios/documentation/uikit/reference/UIWebViewDelegate_Protocol/Reference/Reference.html3.github.com/apache/cordova-ios/blob/master/CordovaLib/Classes/Public/CDVURLProtocol.m4.www.andreas-kurtz.de/2014/09/malicious-apps-ios8.html5.docs.phonegap.com/en/1.9.0/guide_whitelist_index.md.html6.github.com/shazron/KeychainPlugin
第十章:数据泄露
developer.apple.com/library/ios/#documentation/System/Conceptual/ManPages_iPhoneOS/man3/asl.3.html2.www.cocoanetics.com/2011/03/accessing-the-ios-system-log/3.github.com/Atrac613/UIPasteboardSniffer-iOS4.developer.apple.com/library/mac/documentation/Cocoa/Reference/WebKit/Classes/WebArchive_Class/Reference/Reference.html5.developer.apple.com/library/ios/#documentation/uikit/reference/UIResponder_Class/Reference/Reference.html6.developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Classes/NSURLCache_Class/Reference/Reference.html#//apple_ref/occ/instm/NSURLCache/setDiskCapacity:7.developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html#//apple_ref/doc/uid/20001843-BAJEAIEE8. 对我来说不报错并不符合我的个性,但苹果的错误跟踪系统 RADAR 令人震惊、侮辱性地无用,任何理智的人都不该使用它。相反,我建议访问fixradarorgtfo.com/并提交这个单独的 RADAR 错误:“修复 Radar 或滚蛋(rdar://10993759 的重复项)。”9.developer.apple.com/library/ios/#documentation/uikit/reference/UITextInputTraits_Protocol/Reference/UITextInputTraits.html10. en 前缀对于不同的地区会有所不同,但这是英文设备上的表现形式。11.www.owasp.org/index.php/OWASP_iGoat_Project12. 关于这些事件触发的更多详细信息,请访问www.cocoanetics.com/2010/07/understanding-ios-4-backgrounding-and-delegate-messaging/。13. http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIView_Class/UIView/UIView.html#//apple_ref/occ/instp/UIView/alpha14. 查看一个使用 Storyboard 创建应用并进行状态保存的好例子,地址是www.techotopia.com/index.php/An_iOS_6_iPhone_State_Preservation_and_Restoration_Tutorial。15. 使用 CCCrypt 或者理想的 RNCryptor:github.com/rnapier/RNCryptor16. 可在github.com/iSECPartners/SecureNSCoder获取 17.developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/iPhoneAppProgrammingGuide.pdf(第 112 页)
第十一章:C 语言遗留问题和负担
- 格式字符串攻击 这个术语由 Tim Newsham 的同名论文推广;详见
www.thenewsh.com/~newsham/format-string-attacks.pdf。2. 是的,%n确实有效。Xcode 可能会对此提出警告,但通过xcodebuild命令行工具进行的手动构建没有问题。3. 你可以在 Scut 的论文中找到更多关于利用格式字符串执行代码的细节;详见crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf。4.sudo的维护者 Todd C. Miller 在www.sudo.ws/todd/papers/strlcpy.html进一步讨论了这些函数的优点。5.developer.apple.com/library/ios/documentation/Security/Conceptual/SecureCodingGuide/Articles/BufferOverflows.html
第十二章:注入攻击
superevr.com/blog/2011/xss-in-skype-for-ios/2. 你可以在code.google.com/p/google-toolbox-for-mac/下载适用于 Mac 的 Google Toolbox。3.support.apple.com/kb/HT64414.www.raywenderlich.com/553/how-to-chose-the-best-xml-parser-for-your-iphone-project
第十三章:加密与认证
developer.apple.com/library/ios/#samplecode/GenericKeychain/Introduction/Intro.html2.www.trailofbits.com/resources/ios4_security_evaluation_slides.pdf3.github.com/iSECPartners/R2B24.github.com/granoff/Lockbox5.useyourloaf.com/blog/2010/4/3/keychain-group-access.html6. iOS 使用 D. J. Bernstein 的 Curve25519 算法生成公钥和私钥,这是一种椭圆曲线 Diffie-Hellman 算法 (cr.yp.to/ecdh.html). 7. 如果你实际上是一个密码学家,请忽略这一点。8. 我经常看到这一点。没有人应该从安全默认模式切换到 ECB 模式,但我每个月或两个月就会遇到这个问题。9.github.com/rnapier/RNCryptor10.robnapier.net/blog/rncryptor-hmac-vulnerability-827
第十四章:移动隐私问题
developer.apple.com/library/ios/#documentation/AdSupport/Reference/ASIdentifierManager_Ref/ASIdentifierManager.html2.www.w3.org/TR/tracking-dnt/3.www.w3.org/2011/tracking-protection/drafts/tracking-dnt.html4.www.pskl.us/wp/wp-content/uploads/2010/09/iPhone-Applications-Privacy-Issues.pdf5.www.apple.com/iphone-5s/features/6.developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/8_AddingNewApps/AddingNewApps.html





浙公网安备 33010602011771号