文章目录
Mac 开发(一) 苹果沙盒机制sandbox简介
mac沙盒实战demo点击这里下载:【MacFileAccessInSandbox】
1 Mac sandbox简介
1.1 关于应用程序沙盒
- 什么是沙盒?
维基百科的解释:
- 在计算机安全领域,沙盒(英语:sandbox,又译为沙箱)是一种安全机制,为运行中的程序提供的隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用
- 沙盒通常严格控制其中的程序所能访问的资源,比如,沙盒可以提供用后即回收的磁盘及内存空间。在沙盒中,网络访问、对真实系统的访问、对输入设备的读取通常被禁止或是严格限制。从这个角度来说,沙盒属于虚拟化的一种。
- 沙盒中的所有改动对操作系统不会造成任何损失。通常,这种技术被计算机技术人员广泛用于测试可能带毒的程序或是其他的恶意代码
在OS X以及IOS系统中限制了进程对一些资源的访问权限,例如网络、某些特殊路径、文件的读写等等,限定了进程的一些行为,从而保证进程不会做出超越权限的操作。
下面看看苹果对沙盒的解释:
应用沙箱是macOS提供的一种访问控制技术,在内核级别执行。如果应用程序受到威胁,它的目的是防止系统和用户数据受到损害。通过Mac应用商店发布的应用必须采用应用沙箱。通过开发者ID在Mac应用商店外签名和分发的应用程序也可以(在大多数情况下应该)使用应用沙箱。
Mac OSX自从10.6系统开始引入沙盒机制,规定发布到Mac AppStore的应用,必须遵守沙盒约定。沙盒对应用访问的系统资源,硬件外设,文件,网络,XPC,都做了严格的限制,这样能防止恶意的App通过系统漏洞,攻击系统,获取控制权限,保证了OSX系统的安全。
沙盒相当于给每个App一个独立的空间,你只能在自己的小天地里面玩。要获取自己空间之外的资源必须获得授权。
那么苹果为啥要限制提交Appstore的app必须使用苹果的沙盒机制呢?
1.2 为啥要用沙盒机制
复杂的系统总是会有漏洞,而软件的复杂性只会随着时间的推移而增加。无论您多么小心地采用安全编码实践并防范bug,攻击者只需要通过一次防御就可以成功。虽然应用沙箱不能阻止对您的应用程序的攻击,但它可以最小化一个成功的攻击所造成的伤害。
非沙箱应用程序拥有运行该应用程序的用户的全部权限,并可以访问用户可以访问的任何资源。如果该应用程序或与之链接的任何框架包含安全漏洞,攻击者可能会利用这些漏洞来控制该应用程序,这样,攻击者就可以做用户可以做的任何事情。
为了缓解这个问题,应用沙箱策略有两个方面:
- 应用沙箱允许你描述你的应用如何与系统交互。然后,系统授予应用程序完成工作所需的访问权限,仅此而已。
- 通过打开和保存对话框、拖放和其他熟悉的用户交互,应用沙箱允许用户透明地授予应用额外的访问权限。
2 沙盒原理
- 沙盒的大致工作流程入下图所示:
1
进程尝试进行一次系统调用(system call),调用内核功能。2、3
MAC层需要根据该进程的安全策略判断此次系统调用是否可以执行。4、5、6、7、8、9
如果存在策略的话,通过sandbox.kext(hook函数)和AppleMatch.kext(沙盒的profile解析)两个内核扩展实现权限的检查。10
返回调用结果
与沙盒系统相关的模块大致如下:
.libSystem.dylib
: 提供sandbox_init
、sandbox_free_error
等函数。libSandbox.dylib
: 提供解析,编译,生成*.sb
的沙盒profile
的函数。sandbox.kext
:提供了system call
的hook
函数AppleMatch.kext
:提供了解析profile
的函数
- 结构图大致如下:
- 沙盒的工作流程大致可以总结为:
- 通过
sandbox_init
初始化某沙盒策略脚本并编译为二进制文件- 在进程进行
system call
时,通过TrustedBSD
提供的hook
模块,利用Sandbox.kext
提供的system call hook
函数,结合沙盒策略进行判断,该进程是否有权限执行该system call
。
3 xcode中开启沙盒权限
3.1 XCode Capabilities 开启Sandbox权限
3.1.1 Capabilities 开启Sandbox权限
应用开发完成提交到App Store时,必须进行沙盒化。切换到工程target设置Tab的Capabilities中。
- 第一项就是App Sandbox开关,点击ON,表示应用使用沙盒。
上图的选项的一些解释如下:
- Network:网络访问控制
Incoming Connections (Server)
: 应用做为Server对外提供HTTP,FTP等服务时需要打开。如果你的App担任服务器角色,需要连接通信需要开启此权限。Outgoing Connections (Client)
: 做为客户端,访问服务器时需要打开。如果你的App需要作为客户端进行socket连接通信需要开启此权限。
- Hardware:硬件资源控
它包含下面这些子项:
Camera
: 如果你需要开启摄像头功能,勾选此项。Audio Input
: 如果你需要获取音频 输入权限(如麦克风),勾选此项。USB
: 如果你需要使用USB传输文件,需要开启此功能
4:Printing
: 如果你需要打印文件里面的内容,需要开启此功能
- App Data:获取系统的联系人,位置,日历服务时需要打开
Contacts
: 如果要访问联系人,需要勾选此项Location
: 如果需要定位,需要勾选此项。Calendar
: 如果需要访问日历,需要勾选此项。
- File Access:文件和用户目录的访问控制,分为禁止
none
,只读,读写3类
User Selected File
:文档类应用或者需要用户选择打开某个文件时,需要选择合适的访问权限.Downloads Folder
: 如果需要访问当前用户 Downloads文件夹,需要勾选此项,可以设置为只读,或者可读可写Pictures Folder
: 如果需要访问当前用户 Pictures文件夹,需要勾选此项,可以设置为只读,或者可读可写Music Folder
: 如果需要访问当前用户 Music文件夹,需要勾选此项,可以设置为只读,或者可读可写Movies Folder
: 如果需要访问当前用户 Movies文件夹,需要勾选此项,可以设置为只读,或者可读可写
特别注意:如果应用中不需要的权限项,一律不要打开。否则App Review团队会拒绝你的应用上架.
3.1.2 Entitlements 直接变xml,开启Sandbox权限
实际上,在沙盒中每个需要访问权限的项都对应一个key,对应的value,YES 或 NO表示是否允许访问。当你选择了项后,都会记录在一个扩展名为.entitlements的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.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.movies.read-write</key>
<true/>
<key>com.apple.security.assets.music.read-only</key>
<true/>
<key>com.apple.security.assets.pictures.read-only</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
<key>com.apple.security.print</key>
<true/>
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.itunes</string>
</array>
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<true/>
<key>com.apple.security.temporary-exception.shared-preference.read-only</key>
<array>
<string>com.apple.iphoto</string>
<string>com.apple.photobooth</string>
<string>com.apple.photos</string>
</array>
</dict>
</plist>
用plist属性显示如下:
应用打包时会对这个文件进行签名。
当应用运行期间要获取某个权限时,系统都会通过.entitlements
去检查应用是否有授权,如果没有就拒绝访问。
3.2 Mac 文件沙盒化实战
3.2.1 没有使用sandbox使可以任意访问文件
mac沙盒实战demo点击这里下载:【MacFileAccessInSandbox】
-
首先我们创建一个Mac app工程,我这里选择的是Swift版本的工程。
-
添加一些调用api的UI控件
-
连接绑定UI事件
-
实现一个简单的打开文件功能如下:
extension ViewController {
fileprivate func openFile() {
let openPanel = NSOpenPanel()
openPanel.prompt = "Open"
openPanel.allowsMultipleSelection = true
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = true
openPanel.resolvesAliases = true
openPanel.nameFieldLabel = "Open File"
openPanel.title = "Open"
openPanel.allowedFileTypes = ["txt"]
openPanel.begin { (response) in
if response == .OK {
let urls = openPanel.urls
for url in urls {
UserDefaults.standard.set(url, forKey: LastSaveFilePathKey)
UserDefaults.standard.synchronize()
self.filePathField.stringValue = url.path
let text = try? String(contentsOf: url, encoding: .utf8 )
self.textV.string = text ?? ""
}
}
}
}
}
- 接下来我们实现保存文件功能
/// 保存文件
fileprivate func saveFile() {
guard let lastSelectUrl = UserDefaults.standard.value(forKey: LastSaveFilePathKey) as? URL else {
return
}
let text = textV.string
try? text.write(to: lastSelectUrl, atomically: true, encoding: .utf8)
}
接下来我们验证一些保存文件功能:
没有修改内容前:
查看真实文件:
我们在不使用sandbox的功能的时候,是可以正常读写文件的,如下是我先把sandbox功能关闭:
注意:
有两种方式关闭sandbox:
- 直接在Capabilities中将Sandbox删除
- 不使用证书签名的方式编译,沙盒机制只会在证书签名的方式下才生效。
这里我采用的是第二种方式关闭sandbox,如下图所示,先在buildsetting中搜索Signing选项,找到 “Code Signing Identity” 双击把里面的内容删除。
或者选择Other
- 我们先来看看关闭sandbox的情况:
关闭沙盒后,我们可以随意访问任意的有权限的目录的文件,如下面是我在关闭沙盒的情况下可以访问电脑桌面的任意文件,这里我选择的是:/Users/kongyulu/Desktop/笔记/test.txt
- 这里我增加了再次启动app时读取上一次保存路径的文件功能,如下:
fileprivate func initData() {
guard let path = UserDefaults.standard.value(forKey: LastSaveFilePathKey) as? String else {
return
}
filePathField.stringValue = path
let url = URL(fileURLWithPath: path)
do {
let text = try String(contentsOf: url)
textV.string = text
} catch {
}
}
关闭沙盒的情况下,可以正常访问,并读取到给的路径的文件的内容,如下:
- 接下来我们来加上证书签名,让Sandbox生效,这个时候,我们会发现我刚刚保存的那个沙盒之外的路径
在APP启动时不能读取到txt文本的内容了,点击保存按钮也无法将文件修改,这是为啥呢?
苹果爸爸规定:我们程序刚刚启动的时候是不能去访问沙盒之外的路径的,苹果默认只允许访问APP它自己沙盒的内容是不受限制的,如果要访问沙盒之外的路径是需要用户授权的,我们可以调用
NSOpenPanel
类 弹出一个对话框给用户去选择他要打开的文件,当用户点击了OK按钮,则表示用户已经授权了这个文件,这个时候我们需要通过bookmark去保存这个已经授权的文件路径信息(我们可以保存到系统偏好Prefer里面,使用UserDefaults.standard.set(url, forKey: LastSaveFilePathKey)
),下次App启动的时候,直接从bookmark获取到URL ,然后调用allowedURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:NULL];
函数获取到授权的URL, 这个时候我们去访问url路径下的文件,可以直接访问,它原理用户给定的是什么权限,现在就可以得到什么权限。不需要再次弹出对话框让用户去选择路径授权了。
- 那么问题来了,怎么去保存这个bookmark, 获取它呢,这里我在我的demo里面都给出了方法,可以自己去查看这个demo: 【MacFileAccessInSandbox】
主要涉及到的代码是:
- (BOOL)requestAccessPermissionsForFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(SandboxFileSecurityScopeBlock)block {
NSParameterAssert(fileURL);
NSURL *allowedURL = nil;
// standardize the file url and remove any symlinks so that the url we lookup in bookmark data would match a url given by the askPermissionForURL method
fileURL = [[fileURL URLByStandardizingPath] URLByResolvingSymlinksInPath];
// lookup bookmark data for this url, this will automatically load bookmark data for a parent path if we have it
NSData *bookmarkData = [self.bookmarkPersistanceDelegate bookmarkDataForURL:fileURL];
if (bookmarkData) {
// resolve the bookmark data into an NSURL object that will allow us to use the file
BOOL bookmarkDataIsStale;
allowedURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:NULL];
// if the bookmark data is stale we'll attempt to recreate it with the existing url object if possible (not guaranteed)
if (bookmarkDataIsStale) {
bookmarkData = nil;
[self.bookmarkPersistanceDelegate clearBookmarkDataForURL:fileURL];
if (allowedURL) {
bookmarkData = [self persistPermissionURL:allowedURL];
if (!bookmarkData) {
allowedURL = nil;
}
}
}
}
// if allowed url is nil, we need to ask the user for permission
if (!allowedURL) {
allowedURL = [self askPermissionForURL:fileURL];
if (!allowedURL) {
// if the user did not give permission, exit out here
return NO;
}
}
// if we have no bookmark data and we want to persist, we need to create it
if (persist && !bookmarkData) {
bookmarkData = [self persistPermissionURL:allowedURL];
}
if (block) {
block(allowedURL, bookmarkData);
}
return YES;
}
-
这个函数是先去这个请求的路径是否已经保存了,如果保存了是否可以获得它的bookmark, 如果能获取这个bookmkark,则通过
[NSURL URLByResolvingBookmarkData:]
这个方法去获取授权的URL,获得后就可以直接访问路径的文件了,如果没有授权或者保存过bookmark,则从新去弹框,让用户授权。 -
获取授权路径后,需要调用
startAccessingSecurityScopedResource
开启访问权限,在使操作完文件后,需要调用stopAccessingSecurityScopedResource
关闭访问权限,防止恶意程序直接访问文件。
- (BOOL)accessFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(SandboxFileAccessBlock)block {
NSParameterAssert(fileURL);
NSParameterAssert(block);
BOOL success = [self requestAccessPermissionsForFileURL:fileURL persistPermission:persist withBlock:^(NSURL *securityScopedFileURL, NSData *bookmarkData) {
// execute the block with the file access permissions
@try {
[securityScopedFileURL startAccessingSecurityScopedResource];
block();
} @finally {
[securityScopedFileURL stopAccessingSecurityScopedResource];
}
}];
return success;
}
- Swift 代码实现bookmark 权限保存
// MARK: - 沙盒权限
extension AppDelegate {
func persistPermissionURL(_ url:URL) {
//根据url生产bookmarkData
let bookmarkData = try? url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
print("persistPermissionURL bookmarkData\(url) \(String(describing: bookmarkData))")
//存储bookmarkData
if let bookmarkData = bookmarkData {
UserDefaults.standard.set(bookmarkData, forKey: url.path)
UserDefaults.standard.synchronize()
}
}
func accessFileURL(ofURL url:URL, withBlock block:FilePermissionBlock) -> Bool {
let defaults = UserDefaults.standard
guard let bookmarkData = defaults.value(forKey: url.path) as? Data else {
print("bookmakrData = nil")
return false
}
var bookmarkDataIsStale = false
let allowedUrl = try? URL(resolvingBookmarkData: bookmarkData, options: [.withoutUI, .withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale)
//bookmarkDataIsStale == true 时,需要重新创建新的bookmarkData
if bookmarkDataIsStale {
persistPermissionURL(url)
}
if let allowedUrl = allowedUrl, allowedUrl.startAccessingSecurityScopedResource() {
block()
allowedUrl.stopAccessingSecurityScopedResource()
return true
}
return false
}
}