Weaponizing macOS auditd

September 11, 2025

auditd persistence

In the macOS persistence series theevilbit wrote up a technique for backdooring auditd configuration to execute arbitrary commands on the compromised system. Refer to the original article for details. The main points are:

  1. root privileges are required to set up the persistence;
  2. The adversary can backdoor the audit_warn shell script to run arbitrary commands;
  3. When the auditd warning is triggered - auditd process runs your shell commands.

The problem

In the offensive security area often there is a big difference between having a poc and weaponizing the technique. Unfortunately it often takes several hours to identify the potential issues and figure out the solution. Weaponization of auditd persistence was no exception for me.

My objective was to use the auditd persistence technique as the starting point of the execution chain, that would result in the execution of my reflective loader. The reflective loader would do the standard stuff and in the end I would get a nice interactive implant access to the compromised workstation. It did not look problematic at all; the different options for the execution vector were available. The loader was previously used with other execution chains. However, my beaconing process was crashing again and again.

The process spawn chain looked like the following: /usr/sbin/auditd -> /bin/sh audit_warn -> [implant host of your choice] -> (dylib load)[reflective loader]

I was getting the following crashes during the execution of my reflective loader:

"parentPid" : 65468,
"coalitionName" : "com.apple.auditd", 
...
"exception" : {"type":"EXC_BAD_ACCESS","signal":"SIGSEGV","subtype":"KERN_INVALID_ADDRESS at 0x000000012d348000"}
"vmRegionInfo" : "0x12d348000 is not in any region. Bytes after previous region: 1"
MALLOC_LARGE    12c800000-12d348000 [11.3M] rw-/rwx SM=PRV
---> GAP OF 0x2cb8000 BYTES
MALLOC_MEDIUM   130000000-1302d8000 [2912K] rw-/rwx SM=ZER
...

The loader attempted to access 0x12d348000 which was at the boundary of MALLOC_LARGE region but not mapped, triggering segmentation fault. The assumption was that for some reason enough contiguous memory was not allocated resulting in the crash.

I added the additional code to debug the potential memory constraints in my loader. In particular, I was interested in malloc zones. You can get the information about the zones using the following functions: malloc_get_all_zones and malloc_zone_statistics for each zone retrieved.

libmalloc manages memory via malloc zones - independent allocators with their own bookkeeping and locks. Some APIs can be used to create extra zones for the process.

To compare, I launched my loader with a simple host process and in the auditd persistence chain. The difference in available malloc zones is demonstrated below.

Simple execution of the loader:

Number of malloc zones: 3
Zone 0: DefaultMallocZone
  Blocks in use: 4599
  Size in use: 300384 bytes (0 MB)
  Max size in use: 0 bytes (0 MB)
  Size allocated: 1703936 bytes (1 MB)
Zone 1: MallocHelperZone
  Blocks in use: 549
  Size in use: 1677056 bytes (1 MB)
  Max size in use: 2609568 bytes (2 MB)
  Size allocated: 67108864 bytes (64 MB)
Zone 2: NWMallocZone
  Blocks in use: 22
  Size in use: 2672 bytes (0 MB)
  Max size in use: 19312 bytes (0 MB)
  Size allocated: 1048576 bytes (1 MB)

auditd initiated execution of the loader:

Number of malloc zones: 2
Zone 0: DefaultMallocZone
  Blocks in use: 4907
  Size in use: 1623472 bytes (1 MB)
  Max size in use: 2154576 bytes (2 MB)
  Size allocated: 37748736 bytes (36 MB)
Zone 1: NWMallocZone
  Blocks in use: 21
  Size in use: 2672 bytes (0 MB)
  Max size in use: 35824 bytes (0 MB)
  Size allocated: 2097152 bytes (2 MB)

MallocHelperZone was not present in the auditd initiated execution. MallocHelperZone is the scalable helper zone that serves as a fallback when nano zone allocations fail or exceed nano’s size limits. It handles larger allocations that require dynamic memory expansion. My loader used malloc to allocate large blocks of memory during the execution. The lack of MallocHelperZone resulted in the loader not being able to allocate large contiguous space in this scenario.

But why the zone was not present in the same host process when its execution was initiated by auditd? Apparently the reason was that a host process belonged to the process coalition com.apple.auditd. As described in *OS Internals by Jonathan Levin, process coaltion is a process grouping mechanism introduced in Darwin 14. Processes that joined coalition besides other things can share some memory configuration, such as inherit malloc zone capabilities.

In this particular case, com.apple.auditd coalition introduced some memory constraints, disabling MallocHelperZone for the member processes, resulting in the crash of my loader.

The solution

Once the issue was identified - the solution was quite simple: use mmap instead of malloc for large allocations to avoid the limitations caused by com.apple.auditd coalition.

Simply change your malloc allocation line to something like:

*data = mmap(NULL, dataLength, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

malloc operates using the zones, which were the subject to coalition-imposed restrictions. mmap allows to directly send a request to the kernel virtual memory subsystem and therefore malloc zones restrictions do not affect it.

Hopefully this will save time for somebody else.