Skip to main content

Helper 开发

TODO: 添加对应步骤截图

Helper 的特征

Privileged helper 和普通的 daemon 类似, 有如下特性:

  • 在系统范围内起作用: 安装到指定目录
  • 以 root 身份运行.
  • 整个系统范围内只有单个实例
  • 只能被有限的 APP 安装和管理
  • 只支持命令行工具

命名规范

  • Domain 逆序命名
  • 允许首字母大写和点
  • 推荐在最后带 "helper" 字样
  • 例如: AirPlayXPCHelper, com.apple.AccountHelper, com.apple.ProxyHelper

如何使用

如果有下面的需求, 则可以使用 Privileged helper 进行:

  • 当需要一次性的 root 权限时
  • 当需要简单的 root 操作时

例如:

  • 需要 root 权限的 installer 或 uninstaller
  • 作为 root 拷贝/删除文件
  • 作为 root 注册/解除 Daemon
  • 加载/卸载 KEXT

安装和注册的整体流程

  1. 创建一个 APP
  2. 创建 PrivilegedHelper 命令行工具
  3. 创建和 helper 关联的 launchd.plist 文件
  4. 满足签名需求
  5. 使用 SMJobless 安装/注册
  6. 通过 XPC 和客户端通信

如下是各个步骤的详细说明.

1 创建客户端 APP

创建一个简单的应用即可, 语言选择可以是 OC 或 Swift. 之后 Helper 是和这个 APP 在同一个工程中(不同的 Target), 主 App 去掉 sandbox 的标志.

2 创建 Helper 并进行配置

创建的命令行工具, 命名为 com.demo.DemoHelper(苹果推荐 Helper 和 BundleID 相同).

然后创建 Info.plist(命令行工具正常情况是没有 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>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1</string>
<key>CFBundleVersion</key>
<string>0</string>
</dict>
</plist>

由于需要把 plist 文件附加到命令行工具的最终二进制文件中, 因此还需要进行设置, 即在 Packaging 设置中:

  1. Create Info.plist section in Binary 设置为 true
  2. 确保 Info.plist File 指向的是对应的文件
  3. 确保 Product Bundle Identifier 是对应的 ID

设置后, 下一步就是将 Helper APP 复制到主 App 的 bundle 中, 即主 App 中按如下步骤配置:

在 Build Phase 中拷贝到对应目录: Contents/Library/LaunchServices:

  1. 确保主 App 不是 Sandbox 的.
  2. 在 Build Phase 中新建 Copy Files script
  3. 将 Helper 使用 Wrapper 目的地的方式复制到 Contents/Library/LaunchServices 目录.

3 创建 Helper 对应的 launchd.plist 配置

由于 Helper 也是一种 Daemon, 任何 daemon 都需要有对应的 launchd 配置. 因此需要创建 launchd plist 配置并通过 launchd 来管理.

launchd.plist 内容为: (由于 Ventura 上添加了后台运行程序的监控, 需要添加一个字段 AssociatedBundleIdentifiers 为主 APP 的 BundleID)

<plist version="1.0">
<dict>
<key>Label</key>
<string>com.demo.DemoHelper</string>
<key>MachServices</key>
<dict>
<key>com.demo.DemoHelper</key>
<true/>
</dict>
</dict>
</plist>

可以看到上面的 plist 内容和普通 daemon 对比少了很多东西, 这样是因为系统会自行觉得其他的参数(比如 Program/Program Arguments 这些 Key), 其中字段含义详见man page.

这个 plist 也需要嵌入到二进制文件中:

  1. 在 Helper 的 Build Setting 中添加如下(Other Linker Flag 中):

    OTHER_LDFLAGS = -sectcreate __TEXT __launchd_plist com.demo.DemoHelper/launchd.plist

    在 Xcode 中配置时, 实际是按上述每段配置一行.

4 配置主 App 和 Helper 对应关系

由于 Helper 只能由对应的 App 管理, 因此需要配置信息以便二者对应. 这些信息是在他们两个的 plist 中配置:

  1. 在 Helper 的 Info.plist 中添加 SMAuthorizedClients 数组, 数组中表示可以连接它的客户端签名需求, 比如主 App 的 bundle ID 是 com.demo.DemoApp, 则元素值可以是: identifier "com.demo.DemoApp"(开发用时).
  2. 在主 App 中需要指定 SMPrivilegedExecutables 数组, 这个是对应 Helper 的签名需求(SMJobBless codesign requirements), 第一个元素就是对应 Helper, 比如键值对的 Key 是 Helper 的 bundle ID com.demo.DemoHelper, 值就是 identifier "com.demo.DemoHelper"(开发用时).

如果仅开发, 则上述配置已能够满足 Helper 和主 App 的对应关系并且可以正常安装开发版本的 Helper 了.

如果是发布版本, 则需要将上述两个在主 App 和在 Helper 的 Info.plist 中配置的字段值设置为额外的签名保证.

有三种类型的额外签名保证:

  1. Apple Development: 即 Certificate author (subject.CN)

    anchor apple generic and certificate leaf[subject. CN] = "Apple Development: [email protected] (Z22M55HV7V)" and certificate 1[fie ld.1.2.840.113635.100.6.2.1] /* exists */
  2. 个人 Development: 即 Personal development team (subject.OU), 用于开发

    anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ and certificate leaf[subject.OU] = "$(DEVELOPMENT_TEAM)"
  3. 开发者 ID: 即 , 用于 Developer ID 签名发布

    anchor apple generic and (certificate leaf [field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[fie ld.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "$(DEVELOPMENT_ TEAM)")

额外签名需求也是需要在 Helper 和主 App 两端都配置:

  1. 在 helper 的 Info.plist 中 SMAuthorizedClients 数组第一个元素的值就要这样配置:

    anchor apple generic and identifier "需要连接 helper 的客户 bundleid" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "给客户端签名的 TEAMID")
  2. 在主 App 的 Info.plist 中 SMPrivilegedExecutables 数组第一个元素的值就要这样配置:

    anchor apple generic and identifier "Helper 的 BundleID" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "给 Helper 签名的 TEAMID")

这样配置后就可以满足开发/发布的全部签名需求了.

可以使用官方提供的这个 python 文件检查看签名需求是否设置对了匹配.

  1. 检查主 App 的整个配置: SMJobBlessUtil.py check /path/to/build_products/app
  2. 如果需要动态生成两个的签名对应, 则可以执行 SMJobBlessUtil.py setreq /path/to/build_products/app /path/to/app/Info.plist /path/to/helper/Info.plist

需要特别注意的是, plist 中签名需求内的任何双引号都要是 &quot;, 否则会验证不过!! 上述内容为了示意, 因此才直接打的双引号, 实际的看起来是下面的造型:

anchor apple generic and identifier &quot;一个BundleID&quot; and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = &quot;一个 TEAM ID&quot;)

使用 SMJobBless 安装注册 helper

具体代码可以参考: https://github.com/aronskaya/smjobbless/blob/master/Client/Util.swift

使用 SMJobBless 的前置条件是需要获取到名为 kSMRightBlessPrivilegedHelper 的权限. 因此在主 App 安装 helper 的代码类似如下:

func askAuthorization() -> AuthorizationRef? {
var auth: AuthorizationRef?
let status: OSStatus = AuthorizationCreate(nil, nil, [], &auth)
if status != errAuthorizationSuccess {
NSLog("[SMJBS]: Authorization failed with status code \(status)")
return nil
}
return auth
}

func blessHelper(label: String, auth: AuthorizationRef) -> Bool {
var error: Unmanaged<CFError>?
// 这个调用会弹出安装 Helper 提示
let blessStatus = SMJobBless(kSMDomainSystemLaunchd, label as CFString, auth, &error)
if !blessStatus {
NSLog("[SMJBS]: Helper bless failed with error \(error!.takeUnretainedValue())")
}
return blessStatus
}

// ...

// 使用时:
guard let authRef = askAuthorization() else { return }
blessHelper(label: "helper 的 label, 和 plist 中设置的一致, 一般就是 helper 的 bundle id", auth: authRef)

上面代码中 SMJobBless 调用会做这些事情:

  1. 将 helper 二进制文件先复制到一个安全的地方并设置 root 权限
  2. 验证复制出来的 helper 的签名需求是否和主 App 能够对应
  3. 如果验证成功, 将 helper 二进制文件再移动到 /Library/ PrivilegedHelperTools 目录中
  4. 读取 helper 二进制文件中内嵌的 launchd.plist 文件, 如果配置是对的, 则在 /Library/LaunchDaemons 中创建一个有效的 daemon plist 文件.
  5. 启动 helper 作为 system daemon.

XPC 连接

关于在 Helper 侧创建 XPC 监听, 以及在主 App 侧创建 XPC 连接的详细代码, 可以参考这个库: https://github.com/erikberglund/SwiftPrivilegedHelper

这个库的安装过程和连接过程保证都可以适当参考.

最后的注意事项

  • helper 中不要使用动态链接库, 除非是确定系统中肯定存在的动态链接库! 因为 helper 是只有二进制文件复制过去.
  • helper 对 swift 的支持在低版本系统中如果没有 runtime, 则需要带上 swift 的 runtime, 因此包可能非常大.