16 minutes
Understanding the Auto-Update Mechanisms of 8 Common macOS Apps
Given that many large enterprises now have well funded teams building agentic harnesses for vulnerability research, I, with my makeshift harness and 20$ Claude Pro subscription, am not able to find issues that are niche enough to not have been already found by the enterprise harnesses. This has been evidenced by the fact the last ~10 reports I submitted to programs of various sizes have all been closed as duplicate. Therefore, I decided to look at other things I can write about here.
One of the things I have always been interested in is to understand how different companies solve the same security problems in their software. Does everyone eventually arrive at the same solution(s) or does each have a bespoke way to address the risks? Is one solution better than the other or does that depend on the nature of the software? I have only worked at 2 companies - but they develop very different types of software and also have different target customers. From looking at software at work, I found that, when feasible, different apps developed by the same company tend to solve the problems in similar ways. Therefore, I wanted to look at a apps from a few different companies.
The mechanism I decided to study is auto-update. The inspiration actually came from an notification that one of apps I looked at (I cannot remember which) was updated. Auto-update is a somewhat opaque process. It runs at some cadence and installs new software onto your device. The most interesting case of this workflow is when an unprivileged process ends up causing a root-privileged process to write files, run installers, or execute code on its behalf. This can lead to various interesting threats such as privilege escalation, code execution, malware and many others.
I spent a few evenings picking apart the update mechanisms of eight macOS apps I actually have installed - Zoom, Logi Options+, Adobe Creative Cloud, Google Chrome, Microsoft Teams, Docker Desktop, BetterDisplay, and VS Code - using Ghidra and Ghidra MCP for the closed-source binaries and direct source reading for the open ones. This is a distillation of what I learnt. Unfortunately I did not find any vulnerabilities, but I learnt a lot - both about the architecture of these software, and doing agentic reverse engineering exercises.
Methodology
The first task on any of these apps is figuring out where the update logic actually lives. Many apps have multiple binaries and the real update logic lives entirely in a separate service from the user application. For eg. Adobe uses Adobe Desktop Common / ADS for every Adobe app. Microsoft apps like Teams are updated by Microsoft AutoUpdate (MAU), a machine-wide shared service used by every Microsoft Mac app.
So step one, every time, became a checklist done before opening Ghidra:
launchdinventory - read every plist in/Library/LaunchAgents,/Library/LaunchDaemons, and~/Library/LaunchAgentsthat mentions the vendor. AMachServiceskey means an on-demand XPC service (privilege-separated design); its absence plus aStartIntervalmeans a periodically-spawned process instead.- Privileged helper tools in
/Library/PrivilegedHelperTools/- aSMJobBless-installed, root-owned binary here is almost always where the real security-relevant logic sits. - Shared/vendor support trees - folders literally named
Update*/Installer*/AutoUpdate*, or generic “common services” directories that multiple products from the same vendor share. - The app bundle itself - is there any updater code in
Contents/Frameworks? - On-disk state - manifest caches, staging directories, and any SQLite databases.
- Code-signing identity of every candidate binary, recorded up front, so I could later check whether the privileged side actually pins to it.
Next, I did a systemic trace for every app - architecture (who checks, who downloads, who installs, and which of those run as root), the update-check request and manifest format, version-comparison/gating policy, download and staging behavior, and integrity verification. Integrity verification got more attention than the rest of the items, especially if an unprivileged process downloads the update while a privileged process installs it. I was looking for a full code-signature or chain-of-trust check, performed by the privileged process itself, pinned to the vendor’s real identity, or if using a local IPC, the privileged process using a secure mechanism of detecting who is calling it (like using an audit token). For any suspicious findings (there weren’t many), I made it a point to verify the leads through live testing.
Eight apps, four architectures
The 8 apps represent both cases I noted above - ones where a privileged process performs the actual installation, and ones where download and install are both performed with the current user’s privileges. Five - Zoom, Logi Options+, Adobe Creative Cloud, Teams, and Chrome - use an unprivileged front-end that hands off to a separate root-privileged process. Four of these (Zoom, Teams via Microsoft AutoUpdate, Creative Cloud, Chrome) have an unprivileged process download the update, which then hands-off the actual installation to a root privileged daemon over XPC. LogiOptions+ performs almost all the steps as a privileged daemon; the unprivileged process only triggers an update check periodically. The remaining three - BetterDisplay, Docker Desktop, and VS Code - update entirely as the logged-in user, with no privileged process anywhere in the path.
Looking at how each one actually gets built, rather than just the privileged-vs-not split, they fall into three families:
Uses XPC to a separate privileged daemon - Zoom, Creative Cloud, Chrome, Teams (via Microsoft AutoUpdate). All four hand the actual install off from an unprivileged front end to a separate root-privileged daemon over Mach XPC, and all four daemons follow the same shape: authenticate the connecting caller’s code identity before accepting a request at all, then independently re-verify the artifact’s signature/chain of trust themselves rather than trusting whatever the unprivileged side already decided. Zoom’s and Creative Cloud’s daemons re-run the full verification from scratch; Chrome’s privileged helper requires a code-signing chain pinned to Google’s Team ID before it will run the installer as root; Teams’ daemon runs three separate checks on the connecting peer (a bundle-path check, an audit-token-derived identity pinned to Microsoft’s Team ID, and an exact version match) before it will even accept the connection, then relocates the package into its own cache directory and locks it down before verifying - taking ownership of the file before trusting anything about it.
No separate privileged daemon in the update path itself - Logi Options+ and Docker Desktop, for very different reasons. Logi Options+ is privileged the whole time - a single always-root daemon, kept alive by launchd, does the checking, downloading, and installing itself; there’s no handoff at all, so the trust boundary is just “is this one process’s own signature-verification code correct,” not “does a daemon correctly authenticate its caller.” Docker Desktop’s own app-update mechanism is genuinely unprivileged: it pre-checks that /Applications/Docker.app is user-writable rather than escalating if it isn’t. But Docker Desktop does have privileged components elsewhere - com.docker.vmnetd/com.docker.socket, its VM-networking helpers - and configuring them goes through an AppleScript do shell script ... with administrator privileges call. Depending on the update content, after the update is completed, Docker Desktop may require entering the admin password to do port management and other network operations.
Uses an existing framework - BetterDisplay (Sparkle) and VS Code (Squirrel.Mac, via Electron’s stock autoUpdater). Neither reimplements the update flow, and neither runs a privileged daemon either - both replace the app bundle in place as the logged-in user, with Sparkle’s own Autoupdate helper and Squirrel.Mac’s install pipeline doing the file swap unprivileged. Squirrel.Mac’s SQRLCodeSignature only ever does the self-referential thing - pin to whatever code-signing requirement the currently installed app satisfies. Sparkle’s SUUpdateValidator accepts the update if either an embedded (Ed)DSA public key signature is valid or the new bundle’s Apple code signature matches the currently-installed bundle’s, so Sparkle has its own version of Squirrel.Mac’s self-referential check built in as one of two independently sufficient paths.
A few interesting quirks worth mentioning:
- Docker Desktop is unprivileged by design (pre-flight checks confirm
/Applications/Docker.appis user-writable), but its DMG-mount step explicitly passeshdiutil attach -noverify, disablinghdiutil’s own built-in image checksum. The copy-into-place helper it calls next contains one real application-validation step - it launches the copied app as a dry run, falling back to a genuine Gatekeeper (spctl) check if that launch fails. Whether that step runs is gated by a single boolean field on the updater’s internal struct, set once at construction and read the same way by both the full-install and delta-update paths. - VS Code’s Squirrel.Mac reads the code-signing designated requirement off the currently installed app and requires the downloaded update to satisfy that same requirement.
- Chrome’s updater wraps the update payload in a CRX3 package (the same format Chrome uses for extensions) and verifies its signature against a pinned public-key hash before the embedded
.dmgis ever mounted or its install scripts run - plus a per-request nonce covering a signed response, which none of the other apps do. - Teams, via Microsoft AutoUpdate, has the strongest IPC peer-authentication design of the group: it derives the caller’s
SecCodefrom the audit token and checks it against aSecRequirementCreateWithStringpinned to Microsoft’s exact Team ID and three specific bundle identifiers, plus an exact-version match against the daemon’s own build.
Building a threat model across all five privileged-handoff apps
After the initial assessment, I decided to dig deeper into the five apps that have a privileged installer - since that presented a more interesting threat surface. I enumerate eight possible threats, each framed around the same question: what could go wrong specifically because an unprivileged process’s decisions end up driving a privileged one. (These may not be all the threats - it’s been in a while since I did threat modeling as a full time job…)
1. MITM on the update-check/download connection. If an on-path attacker can get between the client and the update server, they can serve a crafted manifest or payload that eventually reaches the root-privileged install step - unless the connection is over TLS with real certificate-chain and hostname validation. The ideal mechanism to force TLS would be to use (App Transport Security (ATS))[https://developer.apple.com/documentation/security/preventing-insecure-network-connections]. Zoom, Chrome, and Creative Cloud use HTTPS-only update channels with no app-specific ATS exception scoped to the update host itself. Logi Options+ does have an ATS exception for plain HTTP on *.logitech.io, but it’s reserved for an unrelated branding/region feature and isn’t used by the update workflow. Teams’ unprivileged Update Assistant carries NSAllowsArbitraryLoads = true in its Info.plist - disabling ATS’s enforcement app-wide, meaning nothing at the OS level stops it from falling back to a connection with no certificate verification at all - though every manifest/CDN endpoint actually observed was still https://.
2. Package integrity not verified correctly before the privileged side installs it. The question here is whether the more-privileged side independently verifies a strong cryptographic signature pinned to the vendor’s identity, rather than trusting a checksum the unprivileged side already computed, or the server’s word. All five meet this baseline, with genuinely different implementations:
- Logi Options+ does RSA-SHA256 verification against a bundled, on-disk public key, in the process that’s root throughout, with key-rotation support.
- Zoom runs two independent checks - a manifest checksum in the unprivileged updater, and a separate root daemon that independently re-verifies the installer’s full signature chain to Apple’s Root CA, pinned to Zoom’s exact Developer ID Installer identity.
- Chrome verifies the update as a CRX3 package against a pinned public-key hash before ever unpacking it, and its privileged helper additionally requires a full code-signing chain pinned to a specific Team ID before running the installer as root.
- Creative Cloud’s installer daemon checks both the connecting caller’s live code identity and does a real chain-of-trust validation on the target binary itself.
- Teams’ privileged daemon performs a certificate validation using (
SecTrust/SecTrustEvaluateWithError)[https://developer.apple.com/documentation/security/sectrust] chain evaluation pinned to Organization “Microsoft Corporation” / Team IDUBF8T346G9, chained to Apple’s root, before installing. It also carries a second, optional, post-install re-check (acodesign --verify --deepwith no-Rrequirement string, identity confirmed by text-matching the CLI output against the literal string"UBF8T346G9").
3. TOCTOU race between the privileged integrity check and the privileged use of the package. A correct signature check is defeated if there’s a window between “root verifies path X” and “root executes path X” during which a lower-privileged attacker can swap what’s at X. All five meet the baseline of verify-then-execute with no external re-fetch in between. Logi Options+’s pattern is only safe because every directory in the relevant path chain is root-owned and non-writable by unprivileged processes. The other apps use fresh, uniquely-named temp locations or keep the verify-and-install calls adjacent in the same function with no round-trip between them. In Teams every package is relocated into the helper’s own cache directory and has its file attributes locked down immediately after the move, before any verification runs - so by the time the chain-of-trust check happens, the file is already out of the unprivileged agent’s reach.
4. Local privilege escalation via unauthenticated IPC to the privileged helper. If a root-privileged helper accepts local connections without verifying the caller’s identity, any unprivileged process - trusted or not - can ask it to do privileged things, and nothing downstream matters. All five validate the connecting peer’s code identity before acting, with genuinely different implementations:
- Logi Options+’s root daemon checks the connecting process’s on-disk executable against a code-signing requirement pinned to Logitech’s own Developer ID, anchored to Apple’s root
- Zoom’s privileged daemon queries the caller’s code-signing status via the kernel before servicing any request.
- Chrome’s privileged helper validates the connecting client’s kernel-verified audit token against a process requirement - a bundle-identifier allow-list plus a matching signing identity - before accepting any connection.
- Creative Cloud’s installer daemon runs two separate identity checks against a pinned allow-list of specific bundle identifiers, each with its own minimum-version floor.
- Teams’ is arguably the strongest of the group: its daemon runs three checks before accepting a connection - first a sanity check that the caller is actually running from Microsoft’s own installed application path, then a cryptographic identity check tied to the specific connection itself, confirming that identity is pinned to Microsoft’s own signing certificate, and finally an exact build-version match against the daemon’s own - refusing to talk to a version-skewed client at all.
5. The privileged handler’s own logic being abused to act on unprivileged input. Given a connection from your genuinely legitimate, correctly-authenticated peer, can the peer itself be manipulated to issue a malicious request?
- Zoom and Chrome sidestep this by architecture rather than by validation: the privileged daemon’s request shape is kept so narrow (“verify and install this specific staged artifact”) that there’s essentially no attacker-shapeable data in it to abuse.
- Creative Cloud daemon’s peer authentication is strong (six pinned Adobe bundle identities, a real chain-of-trust check on the target binary) - but once one of those six legitimate, authenticated callers connects, the daemon executes whatever command-line arguments that request carries with no further validation. Whether any of the six binaries can be abused to reach the privileged update process was not evaluated.
- Teams’ daemon’s install handler re-validates every package path itself before trusting it (symlinks rejected, resolved path must fall under the helper’s own cache directory or
/var/folders, independent of what the caller claims), and the one field it doesn’t independently re-check - the target app’s install path - is resolved locally from Microsoft AutoUpdate’s own on-disk app registry rather than taken from the network manifest, so there’s no path for a compromised or malicious update-check response to influence it. - Logi Options+ ships a local WebSocket RPC server but it’s disabled in production builds.
6. Path traversal into privileged writes. If a root-privileged write/copy/exec destination is built from unvalidated manifest or caller supplied data, the file write can overwrite arbitrary files. This at the lower end can lead to some lost data but on the worse end can lead to rendering systems unusable by overwriting critical system files. Chrome, Logi Options+, Creative Cloud, and Zoom all mitigate this risk by one of these two methods - explicit traversal validation on every extracted path (Chrome, Logi Options+), or building destination paths only from fixed directories plus generated identifiers or stripped basenames, never raw caller-supplied paths (Creative Cloud, Zoom). Teams’ unprivileged side stages downloads under NSTemporaryDirectory() which is the per-user temp directory ($TMPDIR), created drwx------ and owned solely by the logged-in user. Therefore only the current user can tamper with this folder and even then, the daemon’s own mandatory chain-of-trust check runs independently on the file after relocation, so a same-user-substituted file would still be caught before installation.
7. Downgrade/rollback of the privileged install to an older, vulnerable version. A valid signature doesn’t stop an attacker from getting root to install an older, still-validly-signed release with a known-fixed vulnerability. Chrome disables rollback by default and requires explicit policy to re-enable it. Teams runs a genuine numeric, component-wise version comparison before offering an update, and rejects anything that isn’t newer unless the candidate’s BaselineVersion field exactly matches what’s currently installed - a narrow, targeted re-install/repair path. Logi Options+ seems to have a gap here: its update-readiness check is a plain string-equality check on build ID. Any build ID that differs from the current one, older or newer, passes and proceeds to installation once signature verification succeeds. Creative Cloud has no client-side version comparison at all; the server decides what to offer based on the client’s reported version, so the server is fully trusted on this axis. Zoom explicitly supports server-directed downgrades as a designed feature.
Conclusion
Through this analysis (albeit on a very small sample set), I mostly have the answer to the two questions I started with (scoped specifically to the update mechanisms):
Does everyone eventually arrive at the same solution(s) or does each have a bespoke way to address the risks? The insight here is that it largely depends on the problem. There are definitely shared concepts that are provided either by macOS itself or by 3rd party frameworks that provide a foundation to build on to mitigate certain threats while for other threats the solution is bespoke. Even when the same foundational layer is used by different apps, the implementation on top of the foundational layer differs to different degrees.
Is one solution better than the other or does that depend on the nature of the software? Not necessarily. Some are more “modern” in the sense that they use newer macOS features but they also tend to be much more defense in depth oriented. Other solutions are relatively simple but achieve the same or similar levels of protection. It comes down to a few different factors - what does the developer consider to be risks to the app, what resources are available to the development team, etc. A smaller team with a relatively large scope of responsibilities is unlikely to rewrite their app everytime a new macOS feature is available if the app is safe enough per their risk appetite
A Few Words About the Future of My Blog
Now that I have offloaded most of my vulnerability research to my agents, writing about what the agents found does not give me the same joy (unless the finding was genuinely interesting - but until now its been generic XSS, SQL Injection, etc) - as I am not the one doing the heavy lifting. At the same time, I am now reading a lot more about 2 topics in particular:
- Applied AI research - such as prompt optimization, LLM training, AI security, etc.
- Program Analysis - the mathematical / formal methods, algorithms, etc.
Realistically, I will probably be writing more about my learnings from applying learnings from AI research into PoC projects than program analysis. There may still be the occasional CVE post - if my agents find something genuinely worth sharing. I’ll stop rambling now. As always, thanks for reading!
b3f3041 @ 2026-09-17