某莔の异次元空间
macOS App - 给代码提升执行权限
更新于:1 个月前 标签:macOSCocoa

最近参与了一款帮助黑苹果用户实现各种功能的工具性软件 HackintoshBuild 的开发。

该工具的基本原理是在各种复杂的shell命令基础上套一层GUI,增强可视化和交互性,实现各种功能,如:编译引导/驱动、挂载 EFI 分区、查看 SIP 状态、更换锁屏壁纸、查看 IOReg、解锁系统分区读写、修复权限重建缓存、开启安装软件未知来源、查看系统信息等等。

本篇文章将重点讨论下 macOS App 运行时获取 root 权限的几种方案。

应用架构

项目整体采用Swift5.0编写,部分Objective-C混编。Development Target设置为10.13

正如开头所说,本软件大量使用shell命令作为功能基础,命名为 xxx.command。使用Process(NSTask in Objc)指定launchPatharguments,来跑各个具体的任务,监听并获取 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,不稳定、兼容性差

方案二:使用 ServiceManagement 注册 LaunchdDaemon

这种方法是目前苹果官方推荐的一种提权方式,官方也提供了一个 SMJobBlessDemo,需要用苹果开发者账号编译,而且使用起来比较复杂。带来的好处是,将高权限任务封装到独立的子程序,将按需调用,不会让整个程序处于高权限的状态,会相对安全一些,子程序便能轻松实现开机自启、常驻后台、高权限的需求。

通过研究官方的Demo,我发现具体思路是使用Security.frameworkServiceManagement.framework两个库,把需要root权限的操作封装成一个 Target,作为项目的子程序,把该子程序注册LaunchdDaemon。通过launchd工具加载一个与Daemon程序相关的标准的plist文件,由于launchd需要高权限运行,所以启动的子程序自然而然也是高权限运行。

成为LaunchdDaemon后:

  • 子进程会被放在 /Library/PrivilegedHelperTools
  • 相应的plist配置文件会被放在 /Library/LaunchDaemonsLaunchd加载该子进程会需要读取该配置文件

使用过程较为复杂,可以先学习一下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进程,就算使用ProcessNSFileHandleDataAvailable 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
点评一下
Powered By AraComment
快来抢沙发吧~
文章索引