~/posts/binary/controlplane-helper-authorization-shell-expansion.md

ControlPlane 助手:授权与签名前的命令展开

固定到 ControlPlane 1.6.7 源码,分开检查安装信任、BAS 权利判断与文件签名;通过离线模型核对 shell 展开时序和成功响应的误导,限定历史 root 结果的成立条件。

date[31:24]
read[23:16]
8 分钟
cat[15:8]
二进制

AI 翻译,尚未人工审核

目录
  1. 0x00版本、安装与运行身份
  2. 0x01用 plist 与节区定位入口
  3. 0x02客户端复用减少协议工作,不增加权利
  4. 0x03命令表决定授权分支
  5. 0x04签名验证前已经进入 shell
  6. 0x05传输成功也不等于安装成功
  7. 0x06修复落在解释器边界与结果传播
  8. 0x07参考资料

ControlPlane 1.6.7 的助手会验证待安装工具的代码签名,但文件路径先经过 system() 启动的 shell。若路径包含命令替换,求值发生在 codesign 开始检查文件之前;签名验证失败也不会撤销已经发生的展开。

分析对象是 macOS x86_64 旧版应用与 CPHelperTool。以下将固定版本源码、历史运行记录和本地模型分开使用:先确认请求进入处理函数的授权条件,再解释字符串如何变成高权限操作,避免把一次成功运行扩大成所有用户状态下的结论。

版本、安装与运行身份

版本依据5 行
项目 值
应用版本 1.6.7
发布日期(UTC) 2019-08-21
对应提交 9ae1973647d09d0078188b0cbd201bc00f3ddce5
助手 CFBundleVersion 8
分析记录日期 2025-07-15

产品发布时间与分析日期分属两条时间线。官方发布页 对应的是 2019 年版本,后续源码讨论固定到上述提交,不以当前默认分支代替历史实现。本文未确认具体修复版本。

安装状态3 步
  1. 1

    复制应用

    将应用放入 Applications 只是部署客户端,尚未建立 root 服务。

  2. 2

    初始化上下文

    创建默认上下文属于应用配置,与安装助手是不同动作。

  3. 3

    安装助手

    客户端检查现有助手及版本,必要时请求 kSMRightBlessPrivilegedHelper,再调用 SMJobBless。

ControlPlane 初始化提示与助手安装授权的界面示意图,身份输入框为空。

界面示意图:应用初始化与高权限组件安装是两个阶段。

安装实现 使用 SMJobCopyDictionary 查询已有服务,并检查版本及代码要求。安装信任关系决定哪个应用可以安装哪个助手,不自动替每个后续 Unix 套接字请求验证身份。

用 plist 与节区定位入口

安装后的端点
  • /Library/PrivilegedHelperTools
    • com.dustinrue.CPHelperTool
  • /var/run
    • com.dustinrue.CPHelperTool.socket
2 个目录,2 个文件

运行记录中出现上述端点;套接字存在只说明服务入口可见,实际可达性仍需权限和连接结果支持。以下将 launchd 套接字配置与助手的 SMAuthorizedClients 合并展示,键值来自两个不同 plist,并非可直接替换的完整文件。

launchd 与助手 plist 摘录json
{
"Sockets": {
"MasterSocket": {
"SockFamily": "Unix",
"SockPathMode": 438,
"SockPathName": "/var/run/com.dustinrue.CPHelperTool.socket",
"SockType": "Stream"
}
},
"SMAuthorizedClients": [
"identifier com.dustinrue.ControlPlane and certificate leaf[subject.CN] = \"Developer ID Application: Dustin Rue\""
]
}
对象 · 2 个键 · 352 B

SockPathMode = 438 对应 0666。它没有按普通用户组收紧模式,但路径权限、对端检查和请求授权仍是独立条件。SMAuthorizedClients 中的要求用于安装关系,并不意味着自定义消息处理自动拥有同等的调用者校验。

CPHelperTool · 历史 Mach-O 节区Mach-O 64
架构
x86_64
节区2
名称地址大小文件偏移
__TEXT.__info_plist0x1000073AB0x2700x000073AB
__TEXT.__launchd_plist0x10000761B0x29C0x0000761B

两个节区数据来自历史 otool 记录:0x270 = 624,29611 + 624 = 30235;后一节区大小 0x29c = 668。文件偏移与虚拟地址分别记录,偏移只属于这份二进制布局,提取时还应检查实际文件长度和对应架构切片。

客户端复用减少协议工作,不增加权利

历史签名输出节选2 行
对象 架构 CodeDirectory flags TeamIdentifier
ControlPlane x86_64 0x0 YV4RHGCYFA
CPHelperTool x86_64 0x0 YV4RHGCYFA

两者记录中的签名身份为 Developer ID Application: Dustin Rue。主应用的 flags=0x0 与未启用 Hardened Runtime 相符;历史运行通过 DYLD_INSERT_LIBRARIES 加载动态库,但该 flags 值不保证任意系统、进程或启动方式都接受注入。

flowchart LR
  accTitle: 客户端复用与助手边界
  accDescr: 动态库在客户端运行,复用助手初始化和 BAS 传输,助手仍独立执行权利检查和命令处理。
  A["客户端中的动态库"] --> B["helperToolInit:"]
  B --> C["BAS 请求"]
  C --> D["Unix socket"]
  D --> E["逐项权利检查"]
  E --> F["特权处理函数"]

动态库替换 ToggleRemoteLoginAction 的 execute: 实现,并主动触发该方法。真正复用的是 helperToolInit:、助手标识和 BAS 请求路径,而不是远程登录这个业务动作;方法替换仍须匹配 Objective-C 的 self、_cmd 与显式参数 ABI。

helperToolInit: 创建 AuthorizationRef 并调用 BASSetDefaultRules。一个有效授权引用是会话入口,不等同于已获得所有命令权利。客户端中已有协议代码,也不会消除服务端的检查。

命令表决定授权分支

flowchart TD
  accTitle: 助手分发与条件授权
  accDescr: main 进入 BASHelperToolMain 和 HandleConnection;匹配到具有权利名称的命令时,先通过 AuthorizationCopyRights,再执行回调。
  A["main"] --> B["BASHelperToolMain"]
  B --> C["HandleConnection"]
  C --> D["BASRead 外部授权表示"]
  D --> E["AuthorizationCreateFromExternalForm"]
  E --> F["BASReadDictionary"]
  F --> G["FindCommand"]
  G --> H{"匹配且存在 rightName?"}
  H -->|"是"| I["AuthorizationCopyRights"]
  H -->|"否"| J{"commandProcStatus == noErr?"}
  I --> J
  J -->|"是"| K["commandProcs[index]"]
  J -->|"否"| L["错误响应"]
  K --> M["响应与清理"]
  L --> M

以上是 BAS 连接处理 的成功读取路径概括,读取或对象创建失败还会提前转入错误处理。命令表与处理函数表按索引对应;若匹配命令带有 rightName,先执行 AuthorizationCopyRights,只有 commandProcStatus == noErr 才调用回调。

命令规则节选2 行
命令 rightName 源码默认规则
InstallTool com.dustinrue.ControlPlane.InstallTool default
SetDisplaySleepTime com.dustinrue.ControlPlane.SetDisplaySleepTime allow

default 与 allow 是不同规则。源码中的默认规则用于缺失权利项的初始化,实际运行还要查看授权数据库;它不保证覆盖已有配置。没有 rightName 时,框架跳过这一层检查,处理函数需自行承担适用的授权责任。

检查标志包含 kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed。客户端 BAS 路径还存在预授权步骤,因此应区分客户端预授权、服务端复查和用户是否看到认证提示。策略与缓存凭据影响交互;Apple 的授权说明 也明确区分身份认证与权利授予。

本地布尔模型覆盖了命令存在、权利名存在和权利获准的 8 种组合,仅验证上述分支关系。冷启动、缓存凭据过期及不同用户身份下的 macOS 结果尚未逐项运行,所以进入 DoInstallTool 必须保留“权利检查已经通过”这个前提。

签名验证前已经进入 shell

DoInstallTool 从请求取出 srcPath 与 toolName,将路径转换成 C 字符串。下列为命令构造摘录,换行经过整理;它展示数据边界,不是完整函数。

DoInstallTool · asprintfc
asprintf(&valCodeSignCmd,
    "codesign -v -R=\"certificate leaf[subject.CN] = \\\"%s\\\" "
    "and anchor apple generic\" \"%s\"",
    kSigningCertCommonName, pFilename);

asprintf 管理输出缓冲区分配,并不转义路径中的 shell 语法。代码随后把整串文本传给 system();双引号保留路径中的空格,却仍允许 $(...) 命令替换。

签名要求表达式由 codesign 解释,外层命令则先由 shell 解释;两种语法层次不要混在一起。certificate leaf[subject.CN] 与 anchor apple generic 是签名条件,后者的定义见 Apple 要求语言。它们没有保护进入 codesign 之前的 shell 展开。

下面用始终返回失败的普通函数代替验证器,只输出标记和参数,不调用系统助手或真正的签名工具:

shell_order.shbash
#!/usr/bin/env bash
set -u
exec 2>&1
mock_codesign() {
    printf 'verifier argument: <%s>\n' "$1"
    return 1
}
mock_codesign "$(printf 'substitution ran\n' >&2; printf 'fixture.bin')"
status=$?
printf 'verifier status: %s\n' "$status"
test "$status" -eq 1
Bash 5.1.16 · 本地模型输出
substitution ran
verifier argument: <fixture.bin>
verifier status: 1

该模型在 Windows 的 MSYS Bash 5.1.16 运行,结果显示替换先执行,验证器随后收到展开后的 fixture.bin 并返回 1。它验证 shell 命令替换语义,不代表重新执行了 macOS 的 system() 或 root 助手。

传输成功也不等于安装成功

BASExecuteRequestInHelperTool 的返回值描述传输过程;响应内的 kBASErrorKey 描述命令状态,安装操作另外写入 Success。三者要分开读取。

DoInstallTool · reduced control flowc
bool success = true;
OSStatus retval = noErr;

/* String conversion and allocation succeeded in this excerpt. */
if (system(valCodeSignCmd) == 0) {
    OSStatus fsret = FSPathCopyObjectSync(
        pFilename, "/usr/local/bin", toolName,
        NULL, kFSFileOperationOverwrite);
    if (fsret != noErr)
        success = false;
}
/* No else branch sets success to false here. */

固定提交的 DoInstallTool 还有一个结果表达问题:在转换和分配成功的前提下,system() 返回非零时,函数跳过复制,却没有把 success 改成 false,也没有更新初始的 retval = noErr。本地控制流模型得到以下三种结果:

结果模型,不是 macOS 实测3 行
验证状态 文件复制 retval Success
非零 未调用 0 true
0 返回错误 0 false
0 成功 0 true

这说明成功提示既可能掩盖没有复制,也可能与命令回调状态不同;它不是签名通过的独立证据。模型只覆盖列出的成功转换与分配路径,未把其他错误分支简化成同样行为。

历史身份结果节选
$ id
uid=0(root) gid=0(wheel) ...

历史终端另外记录了 uid=0(root),比客户端自报成功更接近实际执行身份;它仍只支持该次运行,不证明没有授权条件。交互通道和部署下载流程均不是解释根因所必需的环节。

修复落在解释器边界与结果传播

处理签名校验时,应直接使用代码签名 API,或以固定程序路径和参数数组启动验证器,让文件名保持数据。授权已经通过也不意味着参数可信:路径范围、文件所有权及验证后到复制前的对象一致性仍需审阅。

同时应把验证器失败、复制失败和成功分别传播到命令状态与业务响应;测试断言应包含实际文件结果,而非只有客户端的 success 文本。这个案例最重要的区别是:签名验证算法没有被证明失效,失守的是调用它之前的命令解释边界。

参考资料

NORMAL~/posts/binary/controlplane-helper-authorization-shell-expansion.md§--
0%zh-CN