~/posts/binary/ccleaner-helper-socket-task-authorization.md

CCleaner 助手:连接权限与任务授权

沿 CCleaner 1.18.30 的 Unix 套接字、十一字节帧标记与动作分发追到 NSTask,区分安装同意、参数切分和高权限执行,并限定历史运行结果的证据范围。

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

AI 翻译,尚未人工审核

目录
  1. 0x00安装同意不等于请求授权
  2. 0x01连接入口与高权限端点
  3. 0x02两个标记各占十一字节
  4. 0x03动作八把路径交给 NSTask
  5. 0x04root 输出证明了哪一条链
  6. 0x05把通用启动接口收回业务边界
  7. 0x06参考资料

macOS 版 CCleaner 1.18.30 的特权助手暴露了一个值得单独审阅的接口:本地进程提交可执行文件路径,root 助手负责启动。风险不在 JSON 或 Unix 套接字本身,而在于连接能力被扩展成了通用的高权限任务执行能力。

以下把安装授权、连接可达性、消息分发和子进程身份分开核对。样本范围仅限 1.18.30;代码与运行结果来自该版本的分析记录,离线模型用于核对字节和参数语义,并未重新运行 macOS 提权实验。其他版本和具体修复版本没有据此确认。

安装同意不等于请求授权

Info.plist · 字段节选json
{
"CFBundleIdentifier": "com.piriform.ccleaner",
"CFBundleShortVersionString": "1.18.30",
"CFBundleVersion": "1.18.30"
}
对象 · 3 个键 · 126 B

SMPrivilegedExecutables 中的助手标识是 com.piriform.ccleaner.CCleanerAgent。应用请求管理员批准安装后,后续本地客户端是否有权调用每项功能,仍需由服务独立判断。安装时输入一次密码,不会为未来每条消息自动补上身份校验。

CCleaner 安装特权助手的界面示意图,两个身份输入框为空。

界面示意图:展示安装授权,不表示本次运行记录。

完整磁盘访问与 root 身份也分属不同层次:前者涉及 TCC 保护的数据类别,后者涉及 Unix 凭据;两者都不是其他系统保护的统一开关。界面显示的系统版本字符串同样不足以证明精确 OS 构建。

连接入口与高权限端点

/Library/LaunchDaemons/com.piriform.ccleaner.CCleanerAgent.plist 将 Program 指向 /Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent。相关的套接字配置如下;这里把实际 plist 键整理为表,而不是给出可直接安装的完整配置。

连接边界5 行
键或观察 值 含义
SockFamily Unix 文件系统命名的本地 IPC
SockType Stream 字节流,不自带应用帧边界
SockPathMode 438 八进制 0666
SockPathName /var/run/com.piriform.ccleaner.CCleanerAgent.socket 客户端连接位置
助手运行身份 root 历史运行记录中的身份
flowchart LR
  accTitle: 本地请求跨入特权进程
  accDescr: 本地进程经 Unix 套接字向 CCleanerAgent 发送封装 JSON,助手分发动作 8 并启动子进程。
  A["本地进程"] --> B["Unix Stream socket"]
  B --> C["CCleanerAgent · root"]
  C --> D["AgentAction = 8"]
  D --> E["NSTask 子进程"]

438 十进制等于 0666 八进制。这个模式没有按普通用户组收紧套接字权限,但实际连接还受路径、ACL 与运行状态影响;即使连接成功,服务仍可检查对端凭据和操作权限。因此,宽松模式是入口证据,不是独立的提权结论。字段定义见 Apple 的 launchd.plist 手册。

两个标记各占十一字节

CCleanerAgent::sendMessageToRunningAgent: 按顺序追加固定前缀、NSJSONSerialization 生成的 JSON 和固定后缀,再写入 remoteFH。两个标记分别是 --613493r-- 与 --r394316--,开头和结尾都保留两个连字符。

帧标记字节表 · 中间 JSON 未列入
000000002D2D363133343933722D2D2D2D723339
00000010343331362D2D
  1. PREFIX0x00–0x0A
  2. SUFFIX0x0B–0x15

上图为了比较标记,直接排列两组常量;它不是完整报文,第二组的偏移 11 也不是其在实际帧中的偏移。真实布局是 PREFIX || JSON || SUFFIX,后缀位置取决于 JSON 的 UTF-8 字节数。

frame_model.pypython
import json
PREFIX = b'--613493r--'
SUFFIX = b'--r394316--'
message = {
    'AgentAction': 8,
    'Command': '/usr/bin/id',
    'CommandArgumentsString': '',
}
wire = PREFIX + json.dumps(message, separators=(',', ':')).encode() + SUFFIX
assert len(PREFIX) == len(SUFFIX) == 11
assert json.loads(wire[len(PREFIX):-len(SUFFIX)]) == message

这条紧凑 JSON 的完整模型报文是 91 字节:前后标记各 11 字节,中间 JSON 为 69 字节。离线校验覆盖全部 91 个真前缀截断、4 组字符串往返和模式换算;这些结果验证模型,不代表服务端已经处理了同样的边界。

流式套接字可能短读,也可能一次读到多个帧。标记出现在 JSON 字符串中、长度上限及跨次读取状态,都需要按服务端实现另行验证,单帧往返测试不证明存在或不存在额外解析漏洞。

动作八把路径交给 NSTask

SocketListener::parseMessage: 解析 JSON 后读取 AgentAction,转为整数并分发。动作 8 取出 Command 和 CommandArgumentsString,将参数文本按字面空格拆成数组,随后进入 runTask:arguments:。

runTask:arguments: · API 摘录objective-c
[NSTask launchedTaskWithLaunchPath:command arguments:arguments];

助手随后等待子进程退出。这里的关键是客户端控制了启动路径;NSTask 接收路径和 argv,自身并不把参数作为 shell 命令解释。历史演示显式选择 /bin/sh,shell 语义来自被启动的程序,而不是分号在 NSTask 中自动生效。API 的路径与参数约定见 Apple 文档。

componentsSeparatedByString:@" " 不是 shell 词法分析器。下列 Python 字面分隔模型展示相同的分隔原则:连续或首尾空格留下空字段,引号仍是普通字符。Foundation 的规则见 NSString 分隔接口。

字面空格分隔模型text
'' => ['']
'a  b' => ['a', '', 'b']
' a ' => ['', 'a', '']
'"a b"' => ['"a', 'b"']

因此,分析时要记录最终 argv,而不是只抄下界面中的参数文本。带空格的路径、引号或转义字符,均可能让实际参数与人的直觉不同。

root 输出证明了哪一条链

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

以上是历史终端中 id 结果的节选,省略其余组列表和无关的主机信息。它对应普通用户发起请求、助手启动子进程、子进程报告 uid=0 的一次运行,不是离线模型的输出。

sequenceDiagram
  accTitle: 从请求到观察到的身份
  accDescr: 助手解析请求、启动子进程并等待结束;历史子进程输出显示 uid 为零。
  participant C as 本地客户端
  participant H as Root 助手
  participant P as 子进程
  C->>H: 封装 JSON,AgentAction 8
  H->>H: 读取 Command 并切分参数
  H->>P: NSTask 启动
  Note over P: 历史 id 输出:uid=0
  P-->>H: 退出
  H->>H: 等待结束

演示用两阶段脚本和本地连接带回输出;这个交互通道不是缺陷成立的必要条件。权限边界已经在助手接受不受业务约束的启动路径时跨过。对修复的验证应关注请求为何获准,而不是复现整套交互包装。

installCCleanerLibFromPath: 与 installCCleanerToolFromPath: 也值得检查,但现有证据没有闭合这两条路线。方法名与安装职责只能支持进一步检查路径、所有权及签名,尚未证明任意文件替换。

把通用启动接口收回业务边界

助手审阅顺序4 步
  1. 1

    确定实际身份

    核对启动配置、运行进程和套接字权限,分开记录安装授权、服务身份与客户端可达性。

  2. 2

    跟踪每次授权

    从接收请求追到敏感操作,找到对端身份与操作权限的实际判断,不以安装弹窗或固定帧头替代证据。

  3. 3

    缩小可委托动作

    优先暴露固定业务操作,约束路径、参数和对象所有权;避免由客户端选择任意可执行程序。

  4. 4

    验证拒绝与成功两侧

    用未授权客户端、获准客户端和无效参数分别检查结果,同时观察是否产生子进程及其有效身份。

这个样本的有效结论是:可达入口、动作 8、受控路径、NSTask 和 root 结果彼此对应。更换 IPC 形式不自动解决授权问题;审核新实现时,应重新核对每项能力在何处、依据什么身份被授予。

参考资料

NORMAL~/posts/binary/ccleaner-helper-socket-task-authorization.md§--
0%zh-CN