最近参与了一款帮助黑苹果
用户实现各种功能的工具性软件 HackintoshBuild 的开发。
该工具的基本原理是在各种复杂的shell
命令基础上套一层GUI
,增强可视化和交互性,实现各种功能,如:编译引导/驱动、挂载 EFI 分区、查看 SIP 状态、更换锁屏壁纸、查看 IOReg、解锁系统分区读写、修复权限重建缓存、开启安装软件未知来源、查看系统信息等等。
本篇文章将重点讨论下 macOS App 运行时获取 root 权限的几种方案。
应用架构
项目整体采用Swift 5.0
编写,部分Objective-C
混编。Development Target
设置为10.13
。
正如开头所说,本软件大量使用shell
命令作为功能基础,命名为 xxx.command。使用Process
(NSTask in Objc)指定launchPath
和arguments
,来跑各个具体的任务,监听并获取 output 达到与用户进行交互的目的。
let taskQueue = DispatchQueue.global(qos: .background)
taskQueue.async {
let task = Process()
task.launchPath = Bundle.main.path(forResource: "xxx", ofType:"command")
task.arguments = arguments // eg: selectedItem, buildLocation, httpProxy, etc.
task.terminationHandler = { task in
DispatchQueue.main.async(execute: {
// finish code here
}
}
let outputPipe = Pipe()
task.standardOutput = outputPipe
outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in
let output = outputPipe.fileHandleForReading.availableData
if output.count > 0 {
outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
let outputString = String(data: output, encoding: String.Encoding.utf8) ?? ""
// analyse outputString here
}
}
self.buildTask.launch()
self.buildTask.waitUntilExit()
}
代码提权
有些shell
命令是需要sudo
,也就是需要root
权限来完成的,比如:
$ sudo spctl --master-disable # 开启软件安装的未知来源
碰到这类情况,直接使用是肯定不行的,那么我们该如何处理?
方案一
AuthorizationExecuteWithPrivileges()
这个函数是 Security.framework 中的一员,使用非常方便。而且还有一个封装得非常好的库 STPrivilegedTask,用法和NSTask
几乎一样。
但是,虽然这个方法简单易用,但根据官方文档所示,AuthorizationExecuteWithPrivileges 函数的适用限制在 macOS 10.1–10.7,也就是说早在 OS X Lion 的时候就开始被Deprecated
了,现在居然还能用也挺令人迷惑的。经测试,在新版的系统使用该函数在某些情况下会提权失败,为了兼容以后的系统,最后不得不放弃了这个方案。这里不推荐使用这种方法,在本文也不做过多介绍。
- 优点:
- 上手简单,使用方便
- 缺点:
Deprecated
,不稳定、兼容性差
方案二
注册 LaunchdDaemon
注册LaunchdDaemon
的常用方法是通过launchd
工具加载一个与Daemon
程序相关的标准的plist
文件,由于launchd
需要高权限运行,所以启动的子程序自然也是高权限运行。这个过程一般放在 PKG 的安装脚本中完成,但当前越来越多的软件摒弃了 PKG 的打包方式,而是直接选择了打包成 App 来提升用户体验,此时安装辅助工具的工作也就要放到 App 运行过程中了。ServiceManagement
的 API 可以完成这样的操作。
通过ServiceManagement
注册LaunchdDaemon
是苹果推荐的一种提权方式,官方也提供了一个 SMJobBless 的Demo,需要用苹果开发者账号编译。具体思路是使用Security.framework
和ServiceManagement.framework
两个库,把需要root
权限的操作封装成一个 Target,作为项目的子程序,把该子程序注册LaunchdDaemon
。
成为LaunchdDaemon
后:
- 子进程会被放在 /Library/PrivilegedHelperTools
- 相应的
plist
配置文件会被放在 /Library/LaunchDaemons ,Launchd
加载该子进程会需要读取该配置文件
过程较为复杂,可以先学习一下Demo,然后尝试给自己的应用(这里以项目 MyApp 举例)添加:
1、关闭 App Sandbox
2、拷贝 Demo 中的 SMJobBlessUtil.py 到项目根目录
3、创建一个新 Target,选择 Command Line Tool,命名为 MyAppHelper
4、创建 MyAppHelper-Info.plist 文件并配置必要参数
5、创建 MyAppHelper-Launchd.plist 文件并配置必要参数
6、在 MyAppHelper Target 中配置 Other Linker Flags:
-sectcreate __TEXT __info_plist MyAppHelper/Info.plist
-sectcreate __TEXT __launchd_plist MyAppHelper/Launchd.plist
7、在 MyAppHelper Target 中配置 Product Module Name 和 Product Name 为 com.yourcompany.MyApp.Helper
8、选择 MyApp Target 配置 Copy Files,路径 Contents/Library/LaunchServices
9、生成签名参数(参考 Demo 中的 ReadMe.txt)
10、写入授权代码(参考 Demo 中的 SMJobBlessAppController.m)
11、运行测试,如果授权成功即可开始修改具体需要授权的部分代码行为
- 优点:
- 目前主流的提权方案
- 将高权限任务封装到独立的子程序按需调用,不会让整个程序处于高权限的状态,相对安全
- 子程序可实现开机启动、长驻后台、高权限的需求
- 缺点:
- 使用起来较为繁琐
- 弹出认证对话框的提示内容是”需要安装帮助程序”,这样的提示不够友好
- LaunchdDaemon 及其配置文件是需要安装到 /Library 下的,可能存在卸载残留的问题
方案三
AppleScript
do shell script "..." with administrator privileges
省略号部分填入shell
脚本即可。
AppleScript
脚本在代码中有两种执行方式:
- 直接用传统的
Process
执行 /usr/bin/osascript -e "do shell ..."let task = Process() task.launchPath = "/usr/bin/osascript" task.arguments = ["-e", "do shell script \"...\" with administrator privileges"] task.launch()
- 通过
NSAppleScript
执行let script = "do shell script \"...\" with administrator privileges" var error: NSDictionary? if let scriptObject = NSAppleScript(source: script) { let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) print(output.stringValue) }
使用传统的Process
方法要注意:
- 在所有
shell
执行完成后才会把stdout
返回,因此当启动的是Daemon
进程,就算使用Process
的NSFileHandleDataAvailable Notification
,也无法把stdout
分次读取出来,不能做到与用户进行良好的交互。 - 这种方法的认证窗口提示信息是 osascript wants to make changes.,对于小白用户来说会不会有一种这样的感觉,我明明安装的是 XXX.app,怎么来了一个
osascript
让我输入密码?- 解决方法:添加 with prompt "xxx" 参数自定义提示信息
NSAppleScript
方法使用时认证窗口的提示信息是“APP_NAME wants to make changes.”,这样的提示较为友好,但也要注意:
NSAppleScript
执行Daemon
进程的话会直到Daemon
退出才退出,即会一直占用线程- 解决方法:在子线程使用
NSAppleScript
,以免阻塞UI
线程
- 解决方法:在子线程使用
优点:
- 比上述“注册 LaunchdDaemon”的方法实现起来简单很多
- 不用担心卸载残留的问题,因为全部内容都存放于 xxx.app
总结
关于 macOS App 获取 root 权限的几种方案就讨论这么多,感兴趣的小伙伴们可以尝试一下。
软件最终采用了较为简单的AppleScript
方案。这个方案针对本工程还有个明显的优点:支持和shell
进行交互。比如在shell
中调用AppleScript
,然后用Process
直接调用:
#!/bin/bash
:' 调用方法:
osascript <<EOF
语句
EOF
'
path=$5
if [ "$(nasm -v)" = "" ] || [ "$(nasm -v | grep Apple)" != "" ]; then
echo "您尚未安装 nasm,现在为您安装"
osascript <<EOF
do shell script "mkdir -p /usr/local/bin || exit 1; cp ${path%/*}/nasm /usr/local/bin/ || exit 1; cp ${path%/*}/ndisasm /usr/local/bin/ || exit 1" with prompt "安装 nsam 需要授权" with administrator privileges
EOF
fi