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

ControlPlane: authorization and expansion before signature checks

Audit ControlPlane 1.6.7's installation trust, BAS rights checks, and signing command. Local models distinguish shell-expansion timing and misleading success responses from the historical root result.

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

AI translation, not yet reviewed

Contents
  1. 0x00Version, installation, and runtime identity
  2. 0x01Locate the endpoint through plists and sections
  3. 0x02Reusing the client does not grant more rights
  4. 0x03The command table selects the authorization branch
  5. 0x04The shell runs before signature verification
  6. 0x05Transport success is not installation success
  7. 0x06Fix the interpreter boundary and result propagation
  8. 0x07References

ControlPlane 1.6.7 verifies the signature of a tool before installing it, but the path reaches a shell through system() first. If the path contains command substitution, evaluation happens before codesign checks the file. A later verification failure does not undo that evaluation.

The target is the historical macOS x86_64 application and CPHelperTool. Fixed-revision source, recorded runtime evidence, and local models serve different purposes here: establish the authorization conditions for reaching the handler, then trace how a string becomes a privileged operation. One successful run does not establish the outcome for every user state.

Version, installation, and runtime identity

Version evidence5 rows
Item Value
Application version 1.6.7
Release date (UTC) 2019-08-21
Commit 9ae1973647d09d0078188b0cbd201bc00f3ddce5
Helper CFBundleVersion 8
Analysis record date 2025-07-15

The product release and the analysis belong to different timelines. The official release page identifies the 2019 version. Source references below use that commit rather than today's default branch. A specific fixed release has not been established.

Installation states3 steps
  1. 1

    Copy the application

    Placing the app in Applications deploys the client; it does not by itself establish a root service.

  2. 2

    Initialize the context

    Creating a default context configures the application. It is distinct from installing the helper.

  3. 3

    Install the helper

    The client checks the existing helper and version. When needed, it requests kSMRightBlessPrivilegedHelper and invokes SMJobBless.

Interface illustration of ControlPlane initialization and helper installation authorization, with empty identity fields.

Interface illustration: application setup and privileged-component installation are separate stages.

The installation implementation queries the job with SMJobCopyDictionary and checks versions and code requirements. The installation trust relationship determines which application can install which helper. It does not automatically authenticate every subsequent custom Unix-socket request.

Locate the endpoint through plists and sections

Installed endpoints
  • /Library/PrivilegedHelperTools
    • com.dustinrue.CPHelperTool
  • /var/run
    • com.dustinrue.CPHelperTool.socket
2 directories, 2 files

The runtime record shows these endpoints. Socket presence establishes visibility, not successful access; permissions and connection results still matter. The following view combines the launchd socket configuration and the helper's SMAuthorizedClients from two separate plists. It is not a complete replacement file.

launchd and helper plist excerptsjson
{
"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\""
]
}
object · 2 keys · 352 B

SockPathMode = 438 is octal 0666. This mode does not restrict access to a particular ordinary-user group, but path permissions, peer checks, and request authorization remain separate conditions. The SMAuthorizedClients requirement governs installation; a custom message handler does not inherit equivalent caller validation automatically.

CPHelperTool · recorded Mach-O sectionsMach-O 64
Arch
x86_64
Sections2
NameAddressSizeOffset
__TEXT.__info_plist0x1000073AB0x2700x000073AB
__TEXT.__launchd_plist0x10000761B0x29C0x0000761B

The section values come from the historical otool record: 0x270 = 624, 29611 + 624 = 30235, and the next section is 0x29c = 668 bytes. File offsets and virtual addresses are separate quantities. These offsets describe one binary layout; extraction must also check the file length and the matching architecture slice.

Reusing the client does not grant more rights

Historical signature excerpts2 rows
Object Architecture CodeDirectory flags TeamIdentifier
ControlPlane x86_64 0x0 YV4RHGCYFA
CPHelperTool x86_64 0x0 YV4RHGCYFA

Both records name Developer ID Application: Dustin Rue. The application's flags=0x0 is consistent with Hardened Runtime being absent. The historical run loaded a library through DYLD_INSERT_LIBRARIES, but that flag value alone does not guarantee injection across every system, process type, or launch method.

flowchart LR
  accTitle: Client reuse and helper boundaries
  accDescr: A library runs in the client, reuses helper initialization and BAS transport, and reaches the helper's separate authorization and command handler.
  A["Library in client"] --> B["helperToolInit:"]
  B --> C["BAS request"]
  C --> D["Unix socket"]
  D --> E["Per-command authorization"]
  E --> F["Privileged handler"]

The library replaced ToggleRemoteLoginAction's execute: implementation and invoked the method directly. The useful machinery was helperToolInit:, helper identification, and BAS transport, not the remote-login operation itself. A replacement must still match the Objective-C ABI for self, _cmd, and explicit arguments.

helperToolInit: creates an AuthorizationRef and invokes BASSetDefaultRules. A valid authorization reference provides access to an authorization session; it is not a grant of every command right. Reusing the client's protocol code also leaves the server's checks in place.

The command table selects the authorization branch

flowchart TD
  accTitle: Helper dispatch and conditional authorization
  accDescr: Main enters BASHelperToolMain and HandleConnection. A valid command with a named right must pass AuthorizationCopyRights before its callback runs.
  A["main"] --> B["BASHelperToolMain"]
  B --> C["HandleConnection"]
  C --> D["BASRead external form"]
  D --> E["AuthorizationCreateFromExternalForm"]
  E --> F["BASReadDictionary"]
  F --> G["FindCommand"]
  G --> H{"Found and rightName set?"}
  H -->|"yes"| I["AuthorizationCopyRights"]
  H -->|"no"| J{"commandProcStatus == noErr?"}
  I --> J
  J -->|"yes"| K["commandProcs[index]"]
  J -->|"no"| L["Error response"]
  K --> M["Response and cleanup"]
  L --> M

This summarizes successful reads in BAS connection handling. Read and allocation failures can enter error handling earlier. The command and callback tables correspond by index. When a matched command has a rightName, the helper calls AuthorizationCopyRights first and invokes the callback only if commandProcStatus == noErr.

Selected command rules2 rows
Command rightName Source default rule
InstallTool com.dustinrue.ControlPlane.InstallTool default
SetDisplaySleepTime com.dustinrue.ControlPlane.SetDisplaySleepTime allow

default and allow are different rules. The source defaults initialize missing right definitions; the live authorization database still needs inspection, because existing definitions need not be replaced. If rightName is absent, the framework skips this check and the handler is responsible for any applicable authorization.

The flags include kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed. The client BAS path also contains preauthorization. Client preauthorization, the server's recheck, and whether the user sees a prompt are distinct events. Policy and cached credentials affect interaction; Apple's authorization guide distinguishes authentication from granting an operation's rights.

A local Boolean model checked all eight combinations of command presence, a named right, and a granted right. It validates this branch structure only. Cold starts, expired cached credentials, and different macOS user identities have not been exercised as a complete matrix. Reaching DoInstallTool therefore retains the prerequisite that its rights check has passed.

The shell runs before signature verification

DoInstallTool reads srcPath and toolName from the request and converts the path to a C string. This command-construction excerpt has been line-wrapped; it illustrates the data boundary rather than reproducing the entire function.

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

asprintf allocates the formatted buffer; it does not escape shell syntax in the path. The code then passes the entire string to system(). Double quotes preserve spaces in a filename but still allow $(...) command substitution.

The signature requirement is interpreted by codesign, while the surrounding command is interpreted by a shell first. These are separate language layers. certificate leaf[subject.CN] and anchor apple generic are signing conditions; Apple's requirement-language guide defines the latter. Neither protects the preceding shell expansion.

This minimal model substitutes an ordinary function that always fails for the verifier. It prints a marker and an argument without invoking the system helper or a real signing tool:

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 · local model output
substitution ran
verifier argument: <fixture.bin>
verifier status: 1

The model ran under MSYS Bash 5.1.16 on Windows. Substitution runs first; the verifier then receives fixture.bin and returns 1. This checks shell command-substitution semantics, not a new execution of macOS system() or the root helper.

Transport success is not installation success

BASExecuteRequestInHelperTool returns a transport status. The response's kBASErrorKey carries the command status, and the installation handler adds a separate Success value. Read all three independently.

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. */

The pinned DoInstallTool implementation has another result-reporting problem. If conversion and allocation succeed but system() returns nonzero, it skips copying without clearing success or changing the initial retval = noErr. A local control-flow model produces these three outcomes:

Result model, not macOS execution3 rows
Verification status File copy retval Success
Nonzero Not called 0 true
0 Returns an error 0 false
0 Succeeds 0 true

A success indication can therefore coexist with no copy, or differ from the callback status. It is not independent evidence that a signature passed. The model assumes the stated conversion and allocation success; it does not collapse the function's other error paths into the same behavior.

Historical identity excerpt
$ id
uid=0(root) gid=0(wheel) ...

The historical terminal separately records uid=0(root), which is stronger evidence of execution identity than a client-generated success message. It still supports that run, not the absence of authorization prerequisites. The interactive channel and download-and-deploy wrapper are unnecessary to explain the root cause.

Fix the interpreter boundary and result propagation

Use code-signing APIs directly, or launch a fixed verifier executable with an argument array so that the filename remains data. Passing authorization does not make every argument trustworthy. Path scope, ownership, and object consistency between verification and copying still require review.

Propagate verifier failure, copy failure, and success distinctly into command and business-level results. Tests should assert the actual filesystem outcome, not just a client's success text. The key distinction is that the signature algorithm was not shown to fail: the broken boundary precedes it, when the path enters the command interpreter.

References

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