从最小化指针使用到编译时的强类型检查,Swift是用于安全开发的出色语言。 但这意味着完全忘记安全性是很诱人的。 仍然存在漏洞,Swift也诱使尚未了解安全性的新开发人员。
本教程是一个安全的编码指南,将解决Swift 4中的更改以及Xcode 9中可用的新工具选项,这些选项将帮助您缓解安全漏洞。
指针和溢出
许多安全漏洞都与C及其指针的使用有关。 这是因为指针使您可以访问原始内存位置,从而更容易读取和写入错误的区域。 这是攻击者恶意更改程序的一种主要方法。
Swift大多不再使用指针,但仍然允许您与C进行接口。许多API(包括Apple的整个Core Foundation API)完全基于C,因此将指针的用法引入Swift很容易。
幸运的是,Apple适当地命名了指针类型: UnsafePointer<T>
, UnsafeRawPointer<T>
, UnsafeBufferPointer<T>
和UnsafeRawBufferPointer
。 有时,您所连接的API会返回这些类型,并且使用它们时的主要规则是不存储或返回指针供以后使用 。 例如:
let myString = "Hello World!"
var unsafePointer : UnsafePointer<CChar>? = nil
myString.withCString { myStringPointer in
unsafePointer = myStringPointer
}
//sometime later...
print(unsafePointer?.pointee)
因为我们在闭包之外访问了指针,所以我们不确定指针是否仍指向预期的内存内容。 在此示例中,使用指针的安全方法是将其与print语句一起保留在闭包中。
指向字符串和数组的指针也没有边界检查。 这意味着在数组上使用不安全的指针很容易,但是意外地超出了数组的边界访问- 缓冲区溢出 。
var numbers = [1, 2, 3, 4, 5]
numbers.withUnsafeMutableBufferPointer { buffer in
//ok
buffer[0] = 5
print(buffer[0])
//bad
buffer[5] = 0
print(buffer[5])
}
好消息是,Swift 4尝试使应用程序崩溃,而不是继续进行所谓的未定义行为 。 我们不知道buffer[5]
指向什么! 但是,Swift无法解决所有问题。 在以下代码之后设置一个断点,并查看变量a
和c
。 它们将设置为999
。
func getAddress(pointer:UnsafeMutablePointer<Int>) -> UnsafeMutablePointer<Int>
{
return pointer
}
var a = 111
var b = 222
var c = 333
let pointer : UnsafeMutablePointer<Int> = getAddress(pointer: &b)
pointer.successor().initialize(to: 999)
pointer.predecessor().initialize(to: 999)
这说明了堆栈溢出,因为在没有显式分配的情况下,变量通常存储在堆栈中。
在下一个示例中,我们分配的容量仅为单个Int8
。 分配存储在堆上,因此下一行将使堆溢出 。 在这个例子中,唯一的Xcode警告您与该控制台的说明gets
不安全。
let buffer = UnsafeMutablePointer<Int8>.allocate(capacity:1)
gets(buffer)
那么,避免溢出的最佳方法是什么? 与C进行接口对输入进行边界检查以确保其在范围内时,这非常重要。
您可能会认为很难记住并找到所有不同的案例。 因此,为了帮助您,Xcode附带了一个非常有用的工具,称为Address Sanitizer。
在Xcode 9中对Address Sanitizer进行了改进。它是一种工具,可以帮助您捕获无效的内存访问,例如我们刚刚看到的示例。 如果要使用Unsafe*
类型,则最好使用Address Sanitizer工具。 默认情况下未启用它,因此要启用它,请转到产品>方案>编辑方案>诊断 ,然后检查Address Sanitizer 。 在Xcode 9中有一个新的子选项, return之后检测堆栈的使用 。 此新选项从我们的第一个示例中检测范围后使用和返回后使用漏洞。
有时会忽略整数溢出 。 这是因为整数溢出仅在用作缓冲区的索引或大小时,或者溢出的意外值更改了关键安全代码的流程时,才是安全漏洞。 Swift 4在编译时会捕获最明显的整数溢出,例如,当数字明显大于整数的最大值时。
例如,以下内容将不会编译。
var someInteger : CInt = 2147483647
someInteger += 1
但是很多时候数字会在运行时动态到达,例如当用户在UITextField
输入信息时。 Undefined Behavior Sanitizer是Xcode 9中的新工具,它可以检测有符号整数溢出和其他类型不匹配的错误。 要启用它,请转到“ 产品”>“方案”>“编辑方案”>“诊断” ,然后打开“ Undefined Behavior Sanitizer” 。 然后在Build Settings> Undefined Behavior Sanitizer中 ,将Enable Extra Integer Checks设置为Yes 。
关于未定义的行为,还有另一件事值得一提。 即使纯Swift隐藏指针,缓冲区的引用和副本仍然在后台使用,因此有可能遇到您未曾期望的行为。 例如,当您开始遍历集合索引时,索引可能会在迭代过程中被您意外修改。
var numbers = [1, 2, 3]
for number in numbers
{
print(number)
numbers = [4, 5, 6] //<- accident ???
}
for number in numbers
{
print(number)
}
在这里,我们只是使numbers
数组指向循环内的新数组。 那number
指向什么呢? 通常将其称为悬挂引用,但在这种情况下,Swift会在循环持续时间内隐式创建对数组缓冲区副本的引用。 这意味着print语句实际上将打印出1、2和3而不是1、4、5...。这很好! Swift可以使您从不确定的行为或应用程序崩溃中解救出来,尽管您可能也不希望得到该输出。 您的同级开发人员不会期望您的集合在枚举过程中发生变异,因此通常,在枚举过程中要格外小心,不要更改集合。
因此,Swift 4在编译时具有出色的安全性强制措施,可以捕获这些安全漏洞。 在许多情况下,直到运行时与用户进行交互,该漏洞才存在。 Swift还包括动态检查,它也可以在运行时捕获许多问题,但是跨线程执行的代价太高了,因此不对多线程代码执行。 动态检查将捕获很多但不是全部违规,因此,首先编写安全代码仍然很重要!
到了这一点,让我们转到另一个非常常见的漏洞领域—代码注入攻击。
注入和格式化字符串攻击
当您在应用程序中将输入字符串作为您不希望使用的命令进行解析时,就会发生格式字符串攻击。 尽管纯Swift字符串不容易受到格式字符串的攻击,但是Objective-C NSString
和Core Foundation CFString
类却可以从Swift获得。 这两个类都具有诸如stringWithFormat
类的方法。
假设用户可以从UITextField
输入任意文本。
let inputString = "String from a textfield %@ %d %p %ld %@ %@" as NSString
如果直接处理格式字符串,这可能是一个安全漏洞。
let textFieldString = NSString.init(format: inputString) //bad
let textFieldString = NSString.init(format: "%@", inputString) //good
Swift 4尝试通过返回0或NULL来处理丢失的格式字符串参数,但是尤其需要关注的是,该字符串是否将被传递回Objective-C运行时。
NSLog(textFieldString); //bad
NSLog("%@", textFieldString); //good
虽然大多数情况下,错误的方式只会导致崩溃,但攻击者可以精心制作格式字符串,将数据写入堆栈中的特定内存位置,以更改应用程序的行为(例如更改isAuthenticated
变量)。
另一个重要原因是NSPredicate
,它可以接受用于指定从Core Data检索什么数据的格式字符串。 像LIKE
和CONTAINS
这样的子句允许使用通配符,应避免使用它们,或至少将其仅用于搜索。 这样做的目的是避免枚举帐户,例如,在攻击者输入“ a *”作为帐户名的情况下。 如果将LIKE
子句更改为==
,则意味着字符串必须字面上匹配“ a *”。
其他常见的攻击是通过用单引号字符尽早终止输入字符串来进行的,因此可以输入其他命令。 例如,可以通过输入') OR 1=1 OR (password LIKE '*
输入到UITextField
') OR 1=1 OR (password LIKE '*
来绕过登录。该行翻译为“密码与任何东西都一样”,这完全绕过了身份验证。解决方案是完全转义通过在代码中添加自己的双引号进行任何注入尝试,这样,来自用户的任何其他引号都将被视为输入字符串的一部分,而不是特殊的终止字符:
let query = NSPredicate.init(format: "password == \"%@\"", name)
防范这些攻击的另一种方法是简单地搜索并排除您知道在字符串中可能有害的特定字符。 示例包括引号,甚至点和斜杠。 例如,当输入直接传递到FileManager
类时,可能会进行目录遍历攻击 。 在此示例中,用户输入“ ../”以查看路径的父目录,而不是预期的子目录。
let userControllerString = "../" as NSString
let sourcePath = NSString.init(format: "%@/%@", Bundle.main.resourcePath! , userControllerString)
NSLog("%@", sourcePath)
//Instead of Build/Products/Debug/Swift4.app/Contents/Resources, it will be Build/Products/Debug/Swift4.app/Contents
let filemanager:FileManager = FileManager()
let files = filemanager.enumerator(atPath: sourcePath as String)
while let file = files?.nextObject()
{
print(file)
}
如果该字符串用作C字符串,则其他特殊字符可能包括NULL终止字节。 指向C字符串的指针需要一个NULL终止字节。 因此,可以简单地通过引入NULL字节来操作字符串。 如果存在诸如needs_auth=1
类的标志,或者默认情况下打开访问并显式关闭访问(例如is_subscriber=0
,则攻击者可能希望尽早终止字符串。
let userInputString = "username=Ralph\0" as NSString
let commandString = NSString.init(format: "subscribe_user:%@&needs_authorization=1", userInputString)
NSLog("%s", commandString.utf8String!)
// prints subscribe_user:username=Ralph instead of subscribe_user:username=Ralph&needs_authorization=1
解析HTML,XML和JSON字符串也需要特别注意。 使用它们的最安全方法是使用Foundation的本机库,该库为每个节点提供对象,例如NSXMLParser
类。 Swift 4向外部格式(例如JSON)引入了类型安全的序列化。 但是,如果您正在使用自定义系统读取XML或HTML,请确保不能使用用户输入中的特殊字符来指示解释程序。
-
<
必须成为<
。 -
>
应该替换为>
。 -
&
应该成为&
。 - 在属性值内部,任何
“
或'
需要分别变为"
和&apos
。
这是删除或替换特定字符的快速方法的示例:
var myString = "string to sanitize;"
myString = myString.replacingOccurrences(of: ";", with: "")
URL处理器内部是注入攻击的最后一个方面。 检查以确保没有在自定义URL处理程序openURL
和didReceiveRemoteNotification
直接使用用户输入。 验证URL是否正是您所期望的,并且不允许用户随意输入信息来操纵您的逻辑。 例如,与其让用户选择t=es84jg5urw
索引导航到堆栈中的哪个屏幕, t=es84jg5urw
如只允许使用不透明标识符(例如t=es84jg5urw
特定屏幕。
如果您在应用程序中使用WKWebView
,那么最好检查一下也会在其中加载的URL。 您可以覆盖decidePolicyFor navigationAction
,它使您可以选择是否要继续URL请求。
一些已知的webview技巧包括加载开发人员不希望的自定义URL方案,例如app-id:
启动完全不同的app或sms:
发送文本。 请注意,嵌入式Web视图不会显示带有URL地址或SSL状态的栏(锁定图标),因此用户无法确定连接是否受信任。
例如,如果Web视图为全屏,则URL可能会被劫持,其网页外观类似于您的登录屏幕,只是将凭据定向到恶意域。 过去的其他攻击包括跨站点脚本攻击,这些攻击已泄漏Cookie甚至整个文件系统。
防止所有上述攻击的最佳方法是花时间使用本机UI控件设计界面,而不是简单地在应用程序中显示基于Web的版本。
到目前为止,我们一直在研究相对简单的攻击。 但是,让我们以运行时可能发生的更高级的攻击结束。
运行时黑客
就像Swift在与C交互时变得更容易受到攻击一样,与Objective-C的接口也为表带来了单独的漏洞。
我们已经看到了NSString
和格式字符串攻击的问题。 另一点是,Objective-C作为一种语言更具动态性,可以传递松散的类型和方法。 如果您的Swift类是从NSObject
继承的,那么它将对Objective-C运行时攻击开放。
最常见的漏洞涉及将重要的安全方法动态交换为另一种方法。 例如,如果用户通过了验证,则返回的方法可以替换为几乎总是返回true的另一个方法,例如isRetinaDisplay
。 尽量减少使用Objective-C,可使您的应用更强大地抵抗此类攻击。
在Swift 4中,如果从Objective-C类继承的类上的方法或这些类本身被@attribute
标记,则这些方法仅在Objective-C运行时公开。 即使使用@objc
属性,也经常会调用Swift函数。 当该方法具有@objc
属性但从未从Objective-C实际调用时,可能会发生这种情况。
换句话说,Swift 4引入了更少的@objc
推断,因此与以前的版本相比,这限制了攻击面。 尽管如此,为了支持运行时功能,基于Objective-C的二进制文件需要保留许多无法剥离的类信息。 例如,对于逆向工程师来说,这足以重建类接口以找出要修补的安全性部分。
在Swift中,二进制文件中公开的信息较少,并且函数名称被修饰。 但是,可以通过Xcode工具swift-demangle取消撤消操作。 实际上,Swift函数具有一致的命名方案 ,指示每个函数是否为Swift函数,类的一部分,模块名称和长度,类名称和长度,方法名称和长度,属性,参数以及返回类型。
这些名称在Swift 4中更短。如果您担心逆向工程,请通过转到“ 构建设置”>“部署”>“剥离Swift符号”并将选项设置为“ 是” ,确保应用程序的发行版本剥离符号。
除了混淆关键安全代码之外,您还可以要求将其内联。 这意味着在代码中调用该函数的任何位置,该代码都将在该位置重复,而不是仅存在于二进制文件的一个位置。
这样,如果攻击者设法绕过特定的安全检查,则不会影响位于代码其他位置的该检查的任何其他出现。 每张支票必须打补丁或钩住,这使得成功执行破解变得更加困难。 您可以像这样内联代码:
@inline(__always) func myFunction()
{
//...
}
结论
考虑安全性应该是发展的重要组成部分。 仅仅期望语言安全是可以导致本来可以避免的漏洞。 Swift在iOS开发中很流行,但可用于macOS桌面应用程序,tvOS,watchOS和Linux(因此,您可以将它用于服务器端组件,这些组件可能更容易执行代码)。 应用程序沙箱可能被破坏,例如在越狱设备允许未签名的代码运行的情况下,因此在调试时仍然要考虑安全性并注意Xcode通知,这一点很重要。
最后一个技巧是将编译器警告视为错误。 您可以通过转到“ 构建设置”并将“ 将警告视为错误 ”设置为“ 是 ”来强制Xcode执行此操作。 迁移到Xcode 9以获得改进的警告时,请不要忘记现代化项目设置,最后但并非最不重要的一点是,立即采用Swift 4来利用可用的新功能!
无论您是刚刚开始使用基础知识还是想探索更高级的主题,我们都构建了完整的指南来帮助您学习Swift 。
翻译自: https://code.tutsplus.com/tutorials/secure-coding-in-swift-4--cms-29835