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:
- The binary will first lookup the IO registry entry
IODeviceTree:/options
(which as I learned later is the NVRAM). - Once found it will look for the key
apple-trusted-trampoline
- 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
- 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.
- If it is a platform binary it will let it run, otherwise it will terminate it.
- 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:
- We can’t set the boot-args in NVRAM is SIP is enabled (unless we find a bypass).
- 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: