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

CCleaner's helper: socket access versus task authorization

Trace CCleaner 1.18.30 from its Unix socket and eleven-byte framing markers to NSTask. Separate installation approval, argument splitting, and privileged execution from the limits of the historical evidence.

date[31:24]
read[23:16]
6 min
cat[15:8]
Binary

AI translation, not yet reviewed

Contents
  1. 0x00Installation approval is not request authorization
  2. 0x01The connection crosses into a privileged endpoint
  3. 0x02Each framing marker is eleven bytes
  4. 0x03Action eight passes a path to NSTask
  5. 0x04What the root output establishes
  6. 0x05Constrain the delegated operation
  7. 0x06References

CCleaner 1.18.30 for macOS exposes a useful boundary to audit: a local client supplies an executable path, and a root helper launches it. The problem is neither JSON nor Unix sockets. It is the expansion of connection access into a general-purpose privileged execution capability.

Installation approval, socket reachability, request dispatch, and child-process identity need separate evidence. The scope here is the examined 1.18.30 implementation and its recorded results. Offline models check bytes and argument handling; they are not a new macOS privilege-escalation run. Other affected versions and a specific fixed release have not been established.

Installation approval is not request authorization

Info.plist · selected fieldsjson
{
"CFBundleIdentifier": "com.piriform.ccleaner",
"CFBundleShortVersionString": "1.18.30",
"CFBundleVersion": "1.18.30"
}
object · 3 keys · 126 B

The helper identifier in SMPrivilegedExecutables is com.piriform.ccleaner.CCleanerAgent. After an administrator approves installation, the service must still decide which subsequent clients may invoke each operation. A password entered during setup does not authenticate every future message.

Interface illustration of the CCleaner helper installation dialog, with both identity fields empty.

Interface illustration: installation approval, not a capture from a new test.

Full Disk Access and root identity are separate mechanisms. The former concerns TCC-protected data categories; the latter concerns Unix credentials. Neither is a universal switch for other system protections. An OS version string displayed by an application is also insufficient to identify an exact system build.

The connection crosses into a privileged endpoint

/Library/LaunchDaemons/com.piriform.ccleaner.CCleanerAgent.plist points Program at /Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent. The table extracts the relevant plist keys; it is not a complete installable job definition.

Connection boundary5 rows
Key or observation Value Meaning
SockFamily Unix Filesystem-named local IPC
SockType Stream Byte stream without application framing
SockPathMode 438 Octal 0666
SockPathName /var/run/com.piriform.ccleaner.CCleanerAgent.socket Client connection endpoint
Helper identity root Identity in the historical runtime record
flowchart LR
  accTitle: Local request crosses into a privileged process
  accDescr: A local process sends framed JSON over a Unix socket to CCleanerAgent, which dispatches action 8 and launches a child.
  A["Local process"] --> B["Unix Stream socket"]
  B --> C["CCleanerAgent · root"]
  C --> D["AgentAction = 8"]
  D --> E["NSTask child"]

Decimal 438 equals octal 0666. That mode does not restrict socket permissions to a particular ordinary-user group, but actual access also depends on the path, ACLs, and runtime state. A connected client may still face peer-credential checks and operation-specific authorization. The mode is evidence about the entry point, not a complete escalation finding. See Apple's launchd.plist manual for the fields.

Each framing marker is eleven bytes

CCleanerAgent::sendMessageToRunningAgent: appends a fixed prefix, JSON produced by NSJSONSerialization, and a fixed suffix, then writes the buffer through remoteFH. The markers are --613493r-- and --r394316--; both retain two hyphens at each end.

Framing constants · intervening JSON omitted
000000002D2D363133343933722D2D2D2D723339
00000010343331362D2D
  1. PREFIX0x00–0x0A
  2. SUFFIX0x0B–0x15

This byte view places the two constants together for comparison. It is not a complete message, and offset 11 is not the suffix offset in a real frame. The wire layout is PREFIX || JSON || SUFFIX; the JSON's UTF-8 byte length determines where the suffix starts.

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

This compact model message contains 91 bytes: two 11-byte markers and 69 JSON bytes. Offline checks covered all 91 proper-prefix truncations, four string round trips, and the mode conversion. These checks validate the model, not the service's handling of the same cases.

A stream socket can return a partial frame or several frames in one read. Markers inside JSON strings, size limits, and state across reads require separate checks against the actual parser. A single-frame round trip neither establishes nor rules out another parsing defect.

Action eight passes a path to NSTask

SocketListener::parseMessage: decodes JSON, reads AgentAction as an integer, and dispatches it. Action 8 obtains Command and CommandArgumentsString, splits the latter on literal spaces, and enters runTask:arguments:.

runTask:arguments: · API excerptobjective-c
[NSTask launchedTaskWithLaunchPath:command arguments:arguments];

The helper then waits for the child to exit. The decisive capability is control over the launch path. NSTask takes a path and argv; it does not itself interpret those arguments as a shell command. The historical demonstration explicitly selected /bin/sh, so shell semantics came from the chosen executable, not from a semicolon gaining special meaning inside NSTask. See Apple's API documentation.

componentsSeparatedByString:@" " is not a shell tokenizer. The Python literal-separator model below illustrates the same splitting rule: adjacent and boundary spaces leave empty fields, while quote characters remain ordinary data. Foundation documents this behavior in NSString's separator API.

Literal-space modeltext
'' => ['']
'a  b' => ['a', '', 'b']
' a ' => ['', 'a', '']
'"a b"' => ['"a', 'b"']

Record the final argv rather than treating the displayed argument string as the invocation. Spaces in paths, quotes, and escape characters can all make the resulting array differ from what a reader expects.

What the root output establishes

id · historical excerpt
$ id
uid=0(root) gid=0(wheel) ...

This is an excerpt from the historical terminal's id output, with the remaining groups and unrelated host details omitted. It belongs to a run in which an ordinary client submitted the request, the helper launched a child, and that child reported uid=0. It is not output from the offline model.

sequenceDiagram
  accTitle: From a request to the observed identity
  accDescr: The helper parses a request, launches a child, and waits for it. The historical child output reports uid zero.
  participant C as Local client
  participant H as Root helper
  participant P as Child process
  C->>H: Framed JSON, AgentAction 8
  H->>H: Read Command and split arguments
  H->>P: NSTask launch
  Note over P: Historical id output: uid=0
  P-->>H: Exit
  H->>H: Wait completes

The demonstration used a two-stage script and a local connection to retrieve output. That interaction wrapper is not required for the defect: the boundary is crossed when the helper accepts a launch path outside a constrained business operation. Fix verification should focus on why the request was accepted, rather than recreating the entire interactive wrapper.

installCCleanerLibFromPath: and installCCleanerToolFromPath: merit review as well, but the available evidence does not close either path. Their names and installation role justify checking paths, ownership, and signatures; they do not establish arbitrary file replacement.

Constrain the delegated operation

Helper review order4 steps
  1. 1

    Establish actual identities

    Correlate the job configuration, running process, and socket permissions. Record installation approval, service identity, and client reachability separately.

  2. 2

    Trace authorization on each request

    Follow reception through the sensitive operation and locate the actual peer and permission checks. An installation dialog or framing constant is not a substitute.

  3. 3

    Reduce the delegated capability

    Prefer fixed business operations with constrained paths, arguments, and object ownership over a client-selected executable.

  4. 4

    Test both acceptance and rejection

    Compare an unapproved client, an approved client, and invalid arguments. Observe whether a child is created and which effective identity it holds.

For this sample, the reachable endpoint, action 8, controlled launch path, NSTask, and root result form a consistent chain. Changing the IPC mechanism alone does not settle authorization. A new implementation needs a fresh audit of where each capability is granted and which identity justifies it.

References

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