Beyond the good ol' LaunchAgents - 35 - Persist through the NVRAM - The 'apple-trusted-trampoline'

This is part 35 in the series of “Beyond the good ol’ LaunchAgents”, where I try to collect various persistence techniques for macOS. For more background check the introduction.

TL;DR - This is a practically completely useless persistence, as this can be only set and enabled when SIP is actually disabled. On the other hand I still find it a pretty amazing way to persist, as we can do that by putting a binary into NVRAM and get that executed. Here follows the details of the discovery.

When I recently wrote about the launchd boot task persistence, I kept reading through the massive list of various boot tasks. Eventually one caught my attention.

<key>rc.trampoline</key>
<dict>
	<key>Program</key>
	<string>/System/Library/CoreServices/rc.trampoline</string>
	<key>PerformAfterUserspaceReboot</key>
	<true/>
	<key>AllowCrash</key>
	<true/>
</dict>

It’s called rc.trampoline. I’m somewhat familiar with the other rc options, so I thought that this would be something similar, but also more unique as the file sits in a SIP protected location. I couldn’t find any information about what this could be, so I loaded the binary to IDA and after decompilation, and some editing I finally arrived to the following (almost) fully reversed code. You can actually compile this if you wish.

#include <objc/runtime.h>
#include <Foundation/Foundation.h>
#include <IOKit/IOKitLib.h>
#include <spawn.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <sys/wait.h>
#include <sys/stat.h>

typedef uint32_t csr_config_t;

#define CSR_ALLOW_UNRESTRICTED_FS (1 << 1)

extern int csr_check(csr_config_t);
extern int csops(pid_t pid, unsigned int  ops, void * useraddr, size_t usersize);

void handle_error() {
	printf("Something went wrong, I'm lazy to implement an error for each case now...\n");
	exit(1);
}

int main() {
    io_registry_entry_t ioEntry;
    io_object_t ioObject;
    CFMutableDictionaryRef ioProperties = NULL;
    mach_port_t mainPort = 0;

    if (IOMasterPort(bootstrap_port, &mainPort) != KERN_SUCCESS) {
        handle_error();
    }

    ioEntry = IORegistryEntryFromPath(mainPort, "IODeviceTree:/options");
    if (!ioEntry) {
        handle_error();
    }

    ioObject = ioEntry;
    if (IORegistryEntryCreateCFProperties(ioEntry, &ioProperties, NULL, 0) != KERN_SUCCESS) {
        handle_error();
    }

    IOObjectRelease(ioObject);
    if (!ioProperties) {
        handle_error();
    }

    // Retrieve "apple-trusted-trampoline" property
    CFStringRef trampolineKey = CFSTR("apple-trusted-trampoline");
    NSString *trampolineValue = CFDictionaryGetValue(ioProperties, trampolineKey);

    if (!trampolineValue || ![trampolineValue length]) {
        handle_error();
    }

    // Generate file path in temporary directory
    NSString *tempDir = NSTemporaryDirectory();
    NSURL *fileURL = [NSURL fileURLWithPath:tempDir];
    NSURL *finalURL = [fileURL URLByAppendingPathComponent:@"apple-trusted-trampoline.bin"];
    NSString *filePath = [finalURL path];

    // Write trampoline to file
    BOOL writeSuccess = [trampolineValue writeToFile:filePath atomically:NO];
    if (!writeSuccess) {
        const char *filePathCStr = [filePath UTF8String];
        errx(1, "Failed to write binary file to %s, it must be writable.", filePathCStr);
    }


    chmod([filePath UTF8String], 0700);

    // Prepare to spawn a new process
    pid_t childPid = 0;
    posix_spawn_file_actions_t fileActions;
    posix_spawnattr_t spawnAttr;
    posix_spawn_file_actions_init(&fileActions);
    posix_spawnattr_init(&spawnAttr);
    posix_spawnattr_setflags(&spawnAttr, POSIX_SPAWN_START_SUSPENDED);

    const char *argv[] = {"apple-trusted-trampoline.bin", NULL};
    int spawnResult = posix_spawn(&childPid, [filePath UTF8String], &fileActions, &spawnAttr, (char *const *)argv, NULL);

    if (spawnResult != 0) {
        handle_error();
    }

    // Perform checks on the spawned process
    int csopsFlags = 0;
    int checkResult = csops(childPid, 0, &csopsFlags, sizeof(csopsFlags));
    int csrCheckResult = csr_check(CSR_ALLOW_UNRESTRICTED_FS);

    // Clean up and terminate child process if necessary
    NSString *unlinkPath = [finalURL path];
    unlink([unlinkPath UTF8String]);

    if (csrCheckResult != 0 && (checkResult != 0 || (csopsFlags & 0x4000000) == 0)) {
        fprintf(stderr, "Killing child, it did not pass the platform check.\n");
        kill(childPid, SIGKILL);
        exit(0);
    }

    // Resume child process and wait for it to finish
    kill(childPid, SIGCONT);
    int status = 0;
    waitpid(childPid, &status, 0);

    return 0;
}

Let me summarize what this does:

  1. The binary will first lookup the IO registry entry IODeviceTree:/options (which as I learned later is the NVRAM).
  2. Once found it will look for the key apple-trusted-trampoline
  3. If found it will read the data held by this key and write it out to the temporary directory, under the name apple-trusted-trampoline.bin
  4. Next it will execute the binary in a SUSPENDED state and then using the process’ code signature it checks if it’s a platform binary or not.
  5. If it is a platform binary it will let it run, otherwise it will terminate it.
  6. The file is also deleted in the process.

I honestly found this pretty dope. By setting an NVRAM variable you can get the system execute code if it’s an Apple signed binary. Interestingly, the key apple-trusted-trampoline doesn’t exists by default.

So I created a small tool, which will read a file and write it to the IO Registry. I started to play around how to trigger the actual execution, and also start reversing launchd as it didn’t want to work at first.

Turned out that the rc.trampoline boot arg has to be set in order to run the rc.trampoline boot task. launchd will check quite a few boot-args when it starts up (there is one for most of the items in the boot task list), including the one we need.

flag_trampoline = sub_100041188(bootargs, (__int64)"rc.trampoline=") != 0;

It will set a flag if the relevant boot arg is found and later it will check that flag and if set, it will run the rc.trampoline boot task.

if ( flag_trampoline )
      v50 = sub_100042D68("rc.trampoline", 0LL);

This is all nice, but there are two issues:

  1. We can’t set the boot-args in NVRAM is SIP is enabled (unless we find a bypass).
  2. The function which starts this boot task will only run if SIP is disabled (which is verified using csr_check).

Thus a SIP bypass is not enough, we really need to disable SIP in order for this to work. I told you it’s useless. Still I wanted to see how this all plays out, so I disabled SIP, set the boot-args and also set apple-trusted-trampoline. The trick is that the boot task can be blocking, launchd will not continue until it returns, thus if we execute a shell, or perl, or something similar, we will be greeted with a black screen for awful lot of time. So we need to run something that actually returns. Also it has to be an Apple binary. To further complicate things, I found that you can only put about 390kB data in this variable.

When we finally execute a binary, we can find that in the logs:

2024-10-10 22:46:37.628685+0200 0x4aa      Default     0x0                  0      0    kernel: Boot args: rc.trampoline=1
2024-10-10 22:52:32.252685+0200 0x2a0      Default     0x0                  0      0    kernel: [System Event] [78064438] [INFO] [Subsystem: launchd] [Event: rc.trampoline] Doing boot task

You might wonder about LaunchConstraints and how I could run perl or sh outside their locations. This is not in play here as SIP will be disabled, so we can run any binary.

I will leave it up to the reader to find out how to run your own stuff with these limitations.

That’s really it. Totally useless, but still very interesting. The boot task was introduced in macOS Mojave, and I booted up every OS and looked for the real apple-trusted-trampoline but couldn’t find it on any of the OSes. I truly wonder why it is there and what it is used for “normally”. If anyone finds a copy of this binary please let me know :)

All related code to this research can be found here:

GIST - rc.trampoline research