Threat Intelligence & APTs • macOS

macOS Malware Analysis: The __c5cfg Enigma

Unraveling the C5 Binary: Beacon Object File Loading, Reflective Code Execution, and Command-and-Control Indicators.

Table of Contents
18 Sections

Unraveling the C5 Binary: Beacon Object File Loading, Reflective Code Execution, and Command-and-Control Indicators.

Mac malware Investigation
Figure: Mac malware Investigation Click to zoom ↗

Triage

Whenever you get handed a suspicious file on macOS, fight the urge to double-click it. Attackers routinely strip extensions or disguise an executable

with a folder icon. Treat every unknown file as live ordnance until you know what it is.

Open Terminal and inspect what you are actually holding:

TELEMETRY / DISASSEMBLY
file e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9

The output gives us our first breadcrumbs:

TELEMETRY / DISASSEMBLY
e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e: Mach-O 64-bit x86_64 executable,
flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|NO_REEXPORTED_DYLIBS|PIE|HAS_TLV_DESCRIPTORS>

Basic properties

History

Four things jump out immediately:

1.Mach-O Format: This is a native Mach-O binary. Mach-O is Apple's binary format, equivalent to PE on Windows or ELF on Linux.
2.Architecture: It was compiled for 64-bit Intel (x86_64). Modern Apple Silicon Macs still execute this seamlessly through Rosetta 2 translation.
3.Thin Binary: Legitimate Mac software usually ships as a "Universal" binary bundling both ARM64 and x86_64 slices. This author compiled only for x86_64, keeping the artifact small and targeted.
4.Hashes: Renaming samples to their SHA-256 hash keeps your workspace clean and gives you an exact fingerprint to query threat intelligence feeds.

Code Signing & Gatekeeper Checks

macOS relies on strict gatekeeping. Real apps carry digital signatures backed by an official Apple Developer ID and validated through Apple

notarization servers.

Run codesign to see who built this:

JSX
codesign -dvvv --entitlements :- e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e 
Terminal output of the codesign.
Figure: Terminal output of the codesign. Click to zoom ↗
JSX
     Identifier=component-macos-x86_64-555549449e504bd2e37d61009e5863936efeae5f                                                                           
     Format=Mach-O thin (x86_64)                                                                                                                          
     CodeDirectory v=20400 size=824 flags=0x2(adhoc) hashes=19+2 location=embedded                                                                        
     CDHash=a0cf22a892a632b1ecd8d51839c3d4c6c3e8ac1f                                                                                                      
     Signature=adhoc                                                                                                                                      
     TeamIdentifier=not set                                                                                                                               
     Sealed Resources=none   

Why Ad-Hoc Signing Screams Suspicious

Apple Silicon kernels mandate code signing. If a Mach-O file has no code signature at all, the kernel kills the process on launch.

Malware authors solve this with ad-hoc signing. It satisfies the kernel requirement for execution, but provides zero identity validation.

Notice the two giveaways:

▪Signature=adhoc: No Apple-issued developer certificate was used.
▪TeamIdentifier=not set: Legitimate enterprise or App Store binaries will always show an official Apple Developer Team ID here.
▪CDHash: Keep note of a0cf22a892a632b1ecd8d51839c3d4c6c3e8ac1f. Endpoint detection systems use this hash directly for detections.

Gatekeeper Bypasses and Ad-Hoc Signing

How Gatekeeper Actually Works

Many people assume Gatekeeper scans every binary that runs on a Mac. That is not how macOS works.

Gatekeeper relies on a specific file tag called the Quarantine Attribute (com.apple.quarantine).

When you download a file through a web browser, Slack, or email, macOS slaps this extended attribute onto the file. When you double-click that file, macOS checks for the tag. Finding it, Gatekeeper halts execution to check:

1.Does this file have an Apple Developer signature?
2.Is this file notarized by Apple?

If the answer is no, macOS pops up a warning blocking the app from running.

macOS Gatekeeper popup saying "App cannot be opened because it is from an unidentified developer"
Figure: macOS Gatekeeper popup saying "App cannot be opened because it is from an unidentified developer" Click to zoom ↗

How Malware Authors Sidestep Gatekeeper

Attackers use several tricks to prevent Gatekeeper from ever checking their payloads:

1.Terminal Droppers (No Quarantine Flag):  

Command-line tools like curl, wget, or staging scripts do not attach the quarantine attribute. If an attacker delivers an implant through a bash one-liner, Gatekeeper stays asleep.

2.Stripping Extended Attributes:

If a file does get quarantined, a simple command removes it:

JSX
xattr -d com.apple.quarantine ./implant

Without the quarantine tag, macOS treats the file as if it was created locally, skipping Gatekeeper checks entirely.

3.Container Tricks:

Attackers package binaries inside disk images (.dmg) or custom archives to mess with how quarantine flags propagate down to inner payloads.

Why Ad-Hoc Signing Exists

If an attacker skips Gatekeeper, why bother signing the file at all?

Because Apple Silicon Macs enforce kernel-level execution rules. On ARM64 hardware, the kernel refuses to run any native binary that lacks a Code Directory. Unsigned code crashes immediately with a SIGKILL error.

Attackers bypass this restriction using ad-hoc signing:

JSX
codesign -s - --force ./implant 

The -s - flag tells macOS to generate a local cryptographic hash for each code page without requiring a paid Apple Developer certificate.

This gives the attacker two advantages:

▪The binary satisfies the kernel requirement to execute.
▪The attacker avoids paying Apple $99/year for a Developer ID that would get identified and revoked within days.

Mach-O Anatomy - Finding the Hidden Config Section

Executable files are not just a blob of code. The operating system organizes them into defined segments and sections.

In Mach-O files:

▪Segments define broad memory protections. For example, TEXT holds read-only executable code, while DATA holds writable variables.
▪Sections live inside segments and store specific categories of content.

You can list every section inside a binary using Apple's built-in otool utility:

JSX
otool -l e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e | grep "sectname" 
Terminal output of otool -l highlighting the abnormal c5cfg and global sections among normal compiler sections
Figure: Terminal output of otool -l highlighting the abnormal c5cfg and global sections among normal compiler sections Click to zoom ↗

Normal Sections vs. Attacker Injections

Most of this list represents standard compiler boilerplate:

▪__text: The raw machine instructions executed by the CPU.
▪__cstring: Plaintext string literals defined in the code.
▪__stubs: Small trampoline routines that jump to external functions in shared libraries.
▪__data: Initialized global variables.

Then two weird entries show up: __c5cfg and __global.

Standard toolchains like Clang, GCC, and Rust never produce a section named __c5cfg. Malware authors inject custom sections using specific compiler directives, such as:

JSX
  __attribute__((section("__DATA,__c5cfg")))   

They do this to tuck encrypted configurations, embedded shellcode, or C2 profiles into an out-of-the-way spot in the binary. The string c5 often refers to a Command and Control framework profile.

When you spot an unfamiliar section name during static analysis, note down its virtual address and offset. We will extract and carve this exact section later.

The Library Anomaly

When analyzing an unknown binary, you want to know what building blocks it leans on. On macOS, running otool -L shows all shared dynamic libraries (.dylib and frameworks) linked to the executable.

JSX
otool -L e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e 
Terminal output of otool -L showing only a single dependency on libSystem.B.dylib
Figure: Terminal output of otool -L showing only a single dependency on libSystem.B.dylib Click to zoom ↗

Why This Is Odd

Most Mac software links against multiple frameworks to get work done:

▪Foundation.framework for file manipulation and process tasks.
▪CFNetwork.framework or libcurl.dylib for internet communication.
▪Security.framework for certificates and encryption.

Our sample links to only one file: /usr/lib/libSystem.B.dylib.

libSystem is the standard C runtime umbrella on macOS, roughly equivalent to libc on Linux. Almost every compiled binary links against it by default.

This creates an interesting gap. We have an ad-hoc signed binary with a custom configuration section (__c5cfg), yet it does not import any standard high-level frameworks.

How does a program interact with the operating system without those libraries? To answer that, we must inspect the exact functions it requests from libSystem.

Inspecting Imported Symbols - What Is Missing?

When a Mach-O binary runs, macOS uses its dynamic linker (dyld) to hook up external functions provided by shared libraries.

By running nm with the -u flag, we can view every external symbol the binary expects the operating system to supply:

JSX
nm -u e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e
Screenshot 2026-10-27 at 1.37.35 AM.png
Figure: Screenshot 2026-10-27 at 1.37.35 AM.png Click to zoom ↗

Breaking Down the Import List

Take a close look at what is present:

▪Basic memory operations: _memcpy, _memset, and ___bzero copy or clear raw memory buffers.
▪Virtual memory allocation: _mmap, _munmap, and _mprotect map raw pages into memory, free them, or alter page permissions (like making memory executable).
▪Thread and signal helpers: __tlv_bootstrap handles thread-local variables, while _sigaltstack configures an alternate signal stack.

Now ask yourself what is missing:

▪No standard I/O: No printf, puts, malloc, or free.
▪No file handling: No fopen, read, write, or stat.
▪No network sockets: No socket, connect, send, or recv.
▪No process execution: No fork, execve, or posix_spawn.

This is a massive anomaly. A program cannot open files or communicate over a network using only memcpy and mmap.

At this point, we have two possibilities:

1.The binary does no I/O at all (which seems unlikely given our findings).
2.The author is deliberately hiding their system interactions to evade security tools that monitor standard library calls.

Before we start disassembling code, let us pull out all printable strings to find concrete evidence of what this binary actually talks about.

Extracting Evidence from Strings - The Mask Comes Off

String analysis is one of the quickest ways to verify what a program is designed to do. Developers and malware authors alike leave behind error logs, paths, protocol names, and command syntaxes.

We run strings with a minimum length filter of 5 characters to weed out binary noise:

JSX
strings -n 5 e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e | head -n 45
Terminal output showing ChaCha20 constant, TCC paths, and InstanceDataParse BOF APIs
Figure: Terminal output showing ChaCha20 constant, TCC paths, and InstanceDataParse BOF APIs Click to zoom ↗

Embedded Cryptography (expand 32-byte k)

The literal string expand 32-byte k is not random. It is the exact 16-byte initialization constant mandated by RFC 7539 for the ChaCha20 stream cipher. The author built custom encryption directly into the binary.

Headless Shell Execution (/bin/zsh)

Notice the environment variables right below it: TERM=dumb, HOME=/tmp, and /dev/null. This is the classic setup used when spawning detached, non-interactive reverse shells.

Internal Network Reconnaissance (portscan)

The binary contains a built-in TCP port scanner: usage: portscan <host> <ports> with default targets 80,443,22,8080. Attackers use this to map internal networks after landing on a victim machine.

macOS Privacy Probing (TCC and Full Disk Access)

On modern macOS, Apple protects user privacy through TCC (Transparency, Consent, and Control). Protected directories like ~/Desktop, ~/Documents, ~/Downloads, and /Library/Application Support/com.apple.TCC/TCC.db require user approval.

The strings fda.system, fda.user, granted, and denied prove this binary deliberately probes sensitive directories to test whether it has Full Disk Access privileges.

In-Memory BOF Loader (InstanceDataParse, EncryptedBofTooShort)

In the Windows world, Cobalt Strike uses Beacon Object Files (BOFs), small compiled C objects executed in memory using functions like BeaconDataParse and BeaconPrintf.

Here, the author ported that exact architecture to macOS, renaming the APIs to InstanceDataParse, InstanceOutput, and InstancePrintf. The binary accepts encrypted object files over the wire (EncryptedBofTooShort), decrypts them, and executes them in memory.

Remember our puzzle from Inspecting Imported Symbols Part The binary imports _mmap and _mprotect. Now we know why:

it allocates executable memory at runtime to run in-memory BOF modules.

How In-Memory BOF Execution Works

When a normal application runs on a Mac, the operating system reads the executable file from the hard drive through execve(). Apple security tools like XProtect and commercial endpoint software monitor every file written to disk.

If an attacker drops a new hacking tool onto the SSD, security software scans the file immediately and flags it.

To avoid this, sophisticated implants use in-memory execution. The attacker never writes their secondary modules to disk. Instead, the implant downloads raw, compiled code over an encrypted network connection and runs it entirely inside RAM.

In post-exploitation frameworks, these lightweight modules are known as Beacon Object Files (BOFs)

Mechanics of mmap and mprotect

Modern operating systems enforce a strict security policy known as W^X (Write XOR Execute). A block of memory can be writable, or it can be executable, but the operating system tries hard to prevent memory from being both at once.

This creates a technical hurdle for the attacker. How do you write new code into memory, and then execute it?

This is where the two symbols we uncovered come into play:

illustrating the four-stage memory transition from mmap (RW) to mprotect (RX) to execution
Figure: illustrating the four-stage memory transition from mmap (RW) to mprotect (RX) to execution Click to zoom ↗

Here is what the implant code actually does:

Step 1: Carving Out Memory (mmap)

The implant requests a fresh virtual memory page from the macOS kernel:

JSX
   void *buffer = mmap(NULL, bof_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); 

At this point, the memory is writable. The implant can copy data into it, but if it tries to execute that memory, the CPU throws a segmentation fault.

Step 2: Decrypting and Linking Symbols in RAM

The implant decrypts the incoming payload using ChaCha20.

Because a BOF is an unlinked object file, it cannot run on its own yet. It contains empty placeholders where function calls should be. The implant acts as a mini-linker, resolving function pointers like InstanceDataParse and InstanceOutput so the payload knows where to send output.

Step 3: Flipping the Switch (mprotect)

Once the payload is copied and ready, the attacker no longer needs write permissions. They need execute permissions:

JSX
   mprotect(buffer, bof_size, PROT_READ | PROT_EXEC);

This system call tells the kernel: lock this memory down against writes, and allow the CPU to run code here. 

Step 4: Execution

The implant casts the memory address into a function pointer and calls it:

JSX
   ((void(*)())buffer)();

The payload executes, extracts its target data, hands the results back to the implant, and exits. The implant then calls _munmap to wipe the memory clean.

The entire attack happens in RAM without touching the file system.

Hunting for Network Indicators - Finding the C2 Server

Every Command and Control implant needs a way to call home. If it cannot receive tasks, it cannot do damage. By looking at the final lines of our strings output, we find the exact network settings hardcoded into this binary:

JSX
strings -n 4 e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e | tail -n 25 
Terminal output showing 172.20.10.3, /instance, and the spoofed Windows Chrome User-Agent
Figure: Terminal output showing 172.20.10.3, /instance, and the spoofed Windows Chrome User-Agent Click to zoom ↗

Three Critical Threat Intel Takeaways

1. Hardcoded IP and Endpoint

▪C2 Server: 172.20.10.3
▪URI Path: /instance

The implant reaches out to this IP over HTTP to fetch commands. Network defenders can immediately blacklist this IP and monitor firewall logs for outbound connections hitting /instance.

2. The Spoofed User-Agent Anomaly

Notice the User-Agent string:

JSX
   Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36

This is a Mach-O binary compiled specifically for macOS. Yet, its outbound web traffic claims to be a Windows 10 PC running Google Chrome.

Attackers often spoof common Windows browser User-Agents so their beacon traffic blends in with corporate network traffic. But from a detection standpoint, this creates a massive discrepancy. When network monitoring tools see an Apple MacBook sending HTTP requests claiming to be Windows 10, that alerts security teams immediately.

3. Registration Token (X+SC)

The token _X+SC serves as an implant identifier. When the payload first connects to the server, it hands this token over so the C2 operator knows which campaign or payload version just checked in.

Stepping into Binary Ninja: The Entry Point

We open our sample in Binary Ninja:

Once analysis finishes, press g on your keyboard, type _main, and hit Enter.

Disassembly of main (0x1000009f8)

The function breaks down into three distinct basic blocks:

main graph view
Figure: _main graph view Click to zoom ↗
JSX
; --- Block 1: Function Prologue ---
1000009f8  push    rbp
1000009f9  mov     rbp, rsp
1000009fc  sub     rsp, 0x20

; --- Block 2: Environment Walk Loop ---
100000a00  cmp     qword [rdx], 0x0
100000a04  lea     rdx, [rdx+0x8]
100000a08  jne     0x100000a00

; --- Block 3: Signal Stack & Main Execution ---
100000a0a  lea     rdi, [rel data_100011f68]
100000a11  call    qword [rdi]
100000a13  lea     rdi, [rbp-0x18]
100000a17  mov     qword [rdi], rax       ; ss_sp: buffer pointer
100000a1a  mov     qword [rdi+0x8], 0x40000 ; ss_size: 256 KB (0x40000)
100000a22  and     dword [rdi+0x10], 0x0  ; ss_flags: 0
100000a26  xor     esi, esi                ; NULL (old_stack)
100000a28  call    _sigaltstack            ; sigaltstack(&alt_stack, NULL)
100000a2d  mov     edi, eax
100000a2f  call    sub_1000027d5
100000a34  xor     edi, edi                ; arg1 = 0
100000a36  call    _instance_main          ; Launch primary engine
100000a3b  xor     eax, eax                ; return 0
100000a3d  add     rsp, 0x20
100000a41  pop     rbp
100000a42  retn

Reading the Assembly

Even if you are new to x86_64 assembly, the flow is easy to follow once you know the register conventions.

1. Walking the Environment (Block 2)

In the standard System V AMD64 calling convention, function arguments arrive in specific registers:

▪rdi: first argument (argc)
▪rsi: second argument (argv)
▪rdx: third argument (envp pointer array)

Look at the three instructions in Block 2:

JSX
100000a00  cmp     qword [rdx], 0x0
100000a04  lea     rdx, [rdx+0x8]
100000a08  jne     0x100000a00

cmp qword [rdx], 0x0 tests whether the current environment pointer is NULL. If it is not, lea rdx, [rdx+0x8] steps 8 bytes forward to the next pointer, and jne loops back. It simply walks the environment array to find where it ends.

2. A Safety Net sigaltstack

Block 3 populates a 24-byte structure on the stack (rbp-0x18) corresponding to macOS stack_t:

▪[rdi] receives the memory pointer from rax.
▪[rdi+0x8] is set to 0x40000 (262,144 bytes, or 256 KB).
▪[rdi+0x10] is set to 0 (ss_flags).

Then it calls _sigaltstack.

Why does an implant set up an alternate signal stack before doing anything else?

Because if the implant crashes while running in-memory shellcode or unmapped memory pages, the operating system normally terminates the process immediately. With a dedicated signal stack in place, the implant can catch segmentation faults (SIGSEGV), recover gracefully, and stay alive to report the failure back to the operator.

3. The Handoff to instancemain

Finally, look at lines 100000a34 and 100000a36:

JSX
     xor     edi, edi          ; zero out rdi (argument 0)                                                                                                
     call    _instance_main    ; jump to core engine 

The binary wipes rdi and calls _instance_main. That is where all the malicious functionality starts.

Disassembling instancemain - Syscalls and Config Validation

When _main finishes setting up the alternate signal stack, it branches directly to _instance_main (0x100000a43).

n Binary Ninja, press g and jump to 0x100000a43. The first basic block performs two critical operations:

JSX
100000a5a  mov     edi, 0x9f8
100000a5f  call    sub_10000cc5f       ; Custom allocator
100000a64  test    rax, rax
100000a67  je      0x100002792         ; Exit on allocation failure
...
100000aa7  mov     rdi, rbx
100000aaa  call    sub_100003ea0       ; Load & validate __c5cfg

Let us dive into those two subroutines.

The Direct mmap Syscall Allocator (sub10000cc5f)

Earlier , we noticed the binary imported _mmap, but never imported malloc. Double-clicking sub_10000cc5f reveals why: the author wrote their own heap allocator using a raw kernel system call.

Binary Ninja Disassembly of sub10000cc5f
Figure: Binary Ninja Disassembly of sub_10000cc5f Click to zoom ↗
JSX
10000cc64  mov     rsi, rdi          ; size = 0x9f8 (2552 bytes)
10000cc69  mov     rdx, 0x3          ; prot = PROT_READ | PROT_WRITE (0x3)
10000cc73  mov     r8, 0xffffffffffffffff ; fd = -1 (anonymous mapping)
10000cc7d  mov     eax, 0x20000c5    ; Darwin BSD Syscall 197: mmap
10000cc82  mov     r10d, 0x1002      ; flags = MAP_ANON | MAP_PRIVATE
10000cc88  xor     edi, edi          ; addr = NULL
10000cc8a  xor     r9d, r9d          ; offset = 0
10000cc8d  0f 05                      syscall ; Execute directly in kernel
10000cc8f  sbb     rcx, rcx          ; Check Carry Flag for error
10000cc92  xor     rax, rcx
10000cc95  sub     rax, rcx

How Direct Darwin Syscalls Work

On macOS x86_64, system calls are categorized into classes. The BSD layer uses the class number 0x2000000

Look at line 10000cc7d:

JSX
mov eax, 0x20000c5

Hex 0xC5 in decimal is 197. In the macOS XNU kernel source code, syscall 197 is SYS_mmap.

By loading 0x20000C5 directly into eax and calling syscall (0f 05), the malware bypasses any userland security hooks or monitoring software placed on libSystem.dylib.

Validating the c5cfg Section (sub100003ea0)

Once memory is allocated, _instance_main calls sub_100003ea0. Jump to 0x100003ea0 in Binary Ninja.

This subroutine is the gatekeeper for the configuration block.

Binary Ninja Disassembly of sub100003ea0 showing the references to 0x100010040 (CONFIGBLOCK)
Figure: Binary Ninja Disassembly of sub_100003ea0 showing the references to 0x100010040 (_CONFIG_BLOCK) Click to zoom ↗
JSX
; --- Step 1: Check Header Magic ---
100003eb1  lea     r14, [rel 0x100010040]      ; Pointer to __c5cfg section
100003eb8  lea     rdx, [rel data_10000dc60]   ; Pointer to f2 f8 f2 f8 f2 f8 f2 f8
100003ebf  push    0x8                         ; 8 bytes
100003ec1  pop     rcx
100003ec2  mov     rdi, r14
100003ec5  mov     rsi, rcx
100003ec8  call    sub_10000397d               ; memcmp(cfg, magic_header, 8)
100003ecd  test    al, 0x1
100003ecf  je      0x100003f1f                 ; If invalid, jump to fallback config

; --- Step 2: Check Footer Magic ---
100003ed1  lea     rdi, [rel 0x10001043c]      ; Pointer to cfg + 0x3fc (footer)
100003ed8  lea     rdx, [rel data_10000dc68]   ; Pointer to f8 f2 f8 f2
100003edf  push    0x4                         ; 4 bytes
100003ee1  pop     rcx
100003ee2  mov     rsi, rcx
100003ee5  call    sub_10000397d               ; memcmp(cfg_footer, magic_footer, 4)
100003eea  test    al, 0x1
100003eec  je      0x100003f1f                 ; If invalid, jump to fallback config

; --- Step 3: Copy Active Configuration ---
100003f02  lea     rsi, [rel 0x100010048]      ; cfg + 8 (C2 host string)
100003f09  lea     rdx, [rbp-0xa30]            ; local destination buffer
100003f10  mov     ecx, 0x360                  ; 864 bytes to copy
100003f15  mov     rdi, rdx
100003f18  rep movsb byte [rdi], [rsi]         ; Fast copy into runtime state

Look at how cleanly the assembly tells the story:

1.It points r14 to 0x100010040, the exact virtual address of the __c5cfg section we found.
2.It compares the first 8 bytes against the magic header bytes (f2 f8 f2 f8 f2 f8 f2 f8).
3.It jumps to offset 0x3FC at the end of the section and checks the 4-byte footer (f8 f2 f8 f2).
4.If both magic markers match, it fires rep movsb to copy 864 bytes of configuration data directly into the active implant structure.

If either check fails, it jumps to 0x100003f1f, which loads a hardcoded fallback configuration.

Mapping the Command Dispatcher

Once an implant establishes communications, it sits in a listening loop waiting for operator instructions. When a task arrives from the C2 server, a central dispatcher inspects the command opcode and routes execution to the correct handler.

jump to address 0x100004082.

How Relative Jump Tables Work in Assembly

Instead of writing fifty nested if/else statements, compilers optimize large switch statements into jump tables.

assembly instructions at 0x100004133:

JSX
100004133  lea     ecx, [rax-0x1]            ; Normalize command code (1-based to 0-based)\
100004136  cmp     ecx, 0x3b                ; Check if command > 59 (0x3b)\
100004139  ja      0x10000417b              ; Default case: out of bounds\
10000413e  lea     rax, [rel data_100006bc0] ; rax = Base of jump table\
100004145  movsxd  rcx, dword [rax+rcx\*4]   ; Load 32-bit signed relative offset\
100004149  add     rcx, rax                 ; Absolute address = base + offset\
10000414c  jmp     rcx                      ; Jump directly to command handler!

Here is what happens step by step:

1.rax holds the incoming command ID. The code subtracts 1 so the array index starts at 0.
2.cmp ecx, 0x3b ensures the command number is within the valid range (0 through 59).
3.lea rax, [rel data_100006bc0] loads the address of an array containing 60 relative offsets.
4.movsxd rcx, dword [rax+rcx*4] reads the 4-byte offset corresponding to that command ID.
5.add rcx, rax adds the offset to the table base address to calculate the final destination.
6.jmp rcx jumps straight to the handler.

Cataloging the Implant Capabilities

By following each branch target from the jump table, we can map out every action the operator can take on a compromised Mac:

CommandHandler AddressUnderlying MechanismWhat It Does
shell0x100006d78Direct fork, execve, /bin/zshSpawns a headless zsh shell session
ps0x1000082b0Kernel sysctl(KERN_PROC)Enumerates all running processes
kill0x100007ffdDirect kill syscallTerminates any process by PID
ls0x10000894aDirect getdirentries64Lists directory contents
cat0x1000087e0Direct open, read, closeReads target files off disk
upload0x100008dc4Direct open(O_CREAT), writeWrites attacker payloads to local disk
cd / pwd0x100009348chdir, fcntl(F_GETPATH)Gets or changes the working directory
portscan0x100005c22Direct socket, connectMulti-port internal scanner (80, 443, 22, 8080)
netstat0x10000565csysctl(NET_RT_IFLIST)Dumps active network interfaces
tcc0x100007334Directory probingTests for Full Disk Access (fda.system, fda.user)
inject0x1000053cfDYLD_INSERT_LIBRARIESTargets non-hardened apps for process injection
bof0x100005f50mmap, mprotect, ChaCha20In-memory Beacon Object File execution engine

It provides full remote administration capabilities without relying on external utilities or standard system frameworks.

Reversing Stack Strings: The R Key Trick in Binary Ninja

While tracing the jump table targets, you will notice instructions like this:

JSX
  1000056f8  mov  rax, 0x68746170203A6D72

That huge integer looks like an address or encryption constant, but it is actually a Stack String. 

Attackers build strings directly on the CPU stack instead of storing them in the __cstring section. Because each 64-bit register holds 8 bytes, an 8-character string can be loaded in a single instruction.

In Binary Ninja, you do not have to decode little-endian hex in your head. Simply click on 0x68746170203A6D72 and press R.

Binary Ninja instantly formats the value into ASCII:

JSX
1000056f8  mov  rax, 'rm: path'

Looking at the surrounding instructions confirms the handler purpose:

▪1000056f8: mov rax, 'rm: path'
▪1000056ea: mov rax, ' require’

Together, they assemble the error message "rm: path required". When you see these string patterns, you can immediately rename the handler functions in your disassembly to match their true command names.

Addresses and Constants to Convert in Binary Ninja

Address in Binary NinjaRaw Hexadecimal ConstantConverts to ASCIICommand
0x1000056f80x68746170203A6D72rm: pathFile deletion (rm)
0x1000057450x70203A7269646B6Dmkdir: pCreate directory (mkdir)
0x10000579c0x5C637273203A766Dmv: src\Rename/Move (mv)
0x1000058000x5C637273203A7063cp: src\File copy (cp)
0x100006df80x65203A6C6C656873shell: eInteractive shell (shell)
0x10000809d0x502064656C6C694BKilled PProcess termination (kill)
0x1000088c50x746170203A746163cat: patFile read (cat)
0x100008ccb0x6E6E6163203A736Cls: cannDirectory listing (ls)
0x100008e930x646564616F6C7055UploadedFile upload (upload)
0x1000093e40x6E6366203A647770pwd: fcnPrint working directory (pwd)
0x1000095ab0x68746170203A6463cd: pathChange directory (cd)

The Direct Syscall Evasion Technique

we uncovered a paradox. The binary imported zero networking or process-spawning functions from libSystem. Yet, earlier, we saw handlers for /bin/zsh, port scanning, file transfers, and process termination.

How does an executable talk to the network or spawn shells without importing the functions to do so?

It bypasses the standard C library entirely and executes raw kernel system calls.

go to address 0x10000729b (inside the shell execution handler).

Disassembly at 0x10000729b showing mov eax, 0x200003b
Figure: Disassembly at 0x10000729b showing mov eax, 0x200003b Click to zoom ↗

assembly block:

JSX
10000729b  mov  eax, 0x200003b    ; 0x3b = 59 (SYS_execve)
1000072a0  syscall                ; Direct transition to XNU kernel
1000072a2  sbb  rcx, rcx           ; Check Carry Flag for error
1000072a5  xor  rax, rcx
1000072a8  sub  rax, rcx

at 0x10000802e (the kill command):

the kill command
Figure: the kill command Click to zoom ↗
JSX
10000802e  mov  eax, 0x2000025  ; 0x25 = 37 (SYS_kill)
100008033  syscall

0x100008a04 (the ls directory enumeration handler):

ls directory enumeration handler
Figure: ls directory enumeration handler Click to zoom ↗
JSX
100008a04  mov  eax, 0x2000158  ; 0x158 = 344 (SYS_getdirentries64)
100008a09  syscall

The Complete Direct Syscall Reference Table

To bypass security monitoring on libSystem.dylib, the malware implements its own mini-kernel interface. Here is every single direct system call identified in the binary, mapped to its Darwin syscall number and purpose:

1. Network Communication & Sockets (10 sites)

Syscall NameBSD IDRegister Value (eax)CountPurpose in Implant
SYS_socket970x20000613Creates raw network socket descriptors
SYS_connect980x20000622Connects to C2 server and port scan targets
SYS_setsockopt1050x20000693Sets socket timeouts and non-blocking flags
SYS_sendto1330x20000851Sends outbound encrypted C2 payloads
SYS_recvfrom290x200001d1Receives inbound task packets from C2

2. Process Execution & Shell Control (8 sites)

Syscall NameBSD IDRegister Value (eax)CountPurpose in Implant
SYS_fork20x20000021Clones process to spawn background shells
SYS_execve590x200003b1Executes /bin/zsh with attacker arguments
SYS_pipe420x200002a1Creates unidirectional pipes for shell I/O
SYS_dup2900x200005a3Redirects stdin, stdout, stderr to pipes
SYS_wait470x20000073Waits for child process exit status
SYS_exit10x20000011Terminates failed child execution paths

3. File System Operations & Exfiltration (46 sites)

Syscall NameBSD IDRegister Value (eax)CountPurpose in Implant
SYS_open50x20000059Opens files for reading or creation
SYS_read30x20000036Reads file content or pipe streams
SYS_write40x20000043Writes uploaded payloads to target files
SYS_close60x200000629Closes open file and socket descriptors
SYS_getdirentries643440x20001581Enumerates files and folders for ls
SYS_fstatat644700x20001d61Gathers file metadata (permissions, size)
SYS_unlink100x200000a1Deletes target files from disk (rm)
SYS_rmdir1370x20000891Removes directories
SYS_mkdir1360x20000881Creates new directories
SYS_rename1280x20000801Renames or moves files (mv)
SYS_chdir120x200000c1Changes current working directory (cd)

4. Host Reconnaissance & Process Management (13 sites)

Syscall NameBSD IDRegister Value (eax)CountPurpose in Implant
SYS_sysctl2020x20000ca7Queries process list (kinfo_proc) and interface list (iflist)
SYS_kill370x20000252Terminates processes by PID (kill)
SYS_getpid200x20000141Obtains own process ID
SYS_getuid240x20000181Checks real user ID
SYS_geteuid250x20000191Checks effective user ID (checks for root)
SYS_gettimeofday1160x20000741High-precision timing for beacon sleep intervals
SYS_getentropy5000x20001f41Fetches cryptographic seed bytes from kernel

5. Memory Management & I/O Multiplexing (13 sites)

Syscall NameBSD IDRegister Value (eax)CountPurpose in Implant
SYS_mmap1970x20000c51Custom heap and executable BOF allocation
SYS_munmap730x20000498Frees memory pages after module execution
SYS_fcntl920x200005c1Queries current working directory path (F_GETPATH)
SYS_poll2300x20000e61Monitors multiple file descriptors for events
SYS_select930x200005d1Synchronous I/O multiplexing for network timeouts

Total: 34 distinct syscalls across 100 call sites. 

Anatomy of a Darwin Kernel Syscall

To understand why this works, you have to look at how macOS handles system calls on x86_64 hardware:

1.The Class Mask (0x2000000):

macOS kernels (XNU) divide system calls into classes. Mach calls use 0x1000000, while BSD calls use 0x2000000.

When you see an immediate value like 0x200003B, strip away 0x2000000 to find the BSD system call number:

▪0x3B in hex = 59 in decimal.
▪Checking Apple's open-source sys/syscall.h header reveals that syscall 59 is SYS_execve.
2.Register Calling Conventions:

Userland function calls pass arguments in rdi, rsi, rdx, rcx, r8, and r9. But raw x86-64 system calls swap rcx for r10 because the CPU hardware uses rcx internally to save the return address.

3.The Carry Flag Trick:

Notice the instruction immediately following every syscall:

TELEMETRY / DISASSEMBLY
    sbb rcx, rcx

In the Darwin kernel, system calls do not return negative error numbers like Linux. Instead, macOS sets the CPU Carry Flag (CF) if a call fails. The sbb rcx, rcx instruction (subtract with borrow) turns rcx into -1 if the Carry Flag is set, or 0 if it succeeded. The subsequent xor and sub instructions convert the result into a clean error code.

Why Malware Uses Direct Syscalls

Most endpoint detection and response (EDR) software monitors macOS by placing inline hooks on popular C library functions in libSystem.B.dylib. When an app calls execve() or socket(), the security agent intercepts the call, checks the arguments, and decides whether to block it.

By placing the syscall number directly into eax and issuing syscall (0f 05), the malware bypasses userland library hooks. The CPU jumps directly from user space into the XNU kernel.

For security analysts, finding direct syscalls in an application is an immediate red flag. Legitimate developers almost never write raw assembly syscalls because Apple changes internal syscall numbers between major OS updates.

Detection Engineering

JSX
rule macOS_Backdoor_C5_Implant {
    meta:
        description = "Detects custom macOS x86_64 C2 implant featuring in-memory BOF execution and direct BSD syscalls"
        author = "Chandra Kant Bauri"
        date = "2026-10-28"
        hash = "e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e"

    strings:
        // Dedicated custom configuration section name and magic markers
        $section_c5cfg = "__c5cfg" fullword ascii
        $cfg_magic_header = { f2 f8 f2 f8 f2 f8 f2 f8 }
        $cfg_magic_footer = { f8 f2 f8 f2 }

        // Session and protocol indicators
        $proto_session = "c5-session-v1" fullword ascii
        $proto_magic   = "c5-magic" fullword ascii

        // In-memory BOF) loader artifacts
        $bof_error = "EncryptedBofTooShort" fullword ascii
        $bof_api1  = "InstanceDataParse" fullword ascii
        $bof_api2  = "InstanceOutput" fullword ascii
        $bof_api3  = "InstancePrintf" fullword ascii
        $bof_api4  = "InstanceFormatAlloc" fullword ascii

        // TCC / Full Disk Access permission 
        $tcc_path = "/Library/Application Support/com.apple.TCC" ascii
        $tcc_fda1 = "fda.system" fullword ascii
        $tcc_fda2 = "fda.user" fullword ascii

    condition:
        
        uint32(0) == 0xfeedfacf and
        
        filesize < 5MB and

        (
            // Signature A: Contains the custom config section and magic markers
            ($section_c5cfg and ($cfg_magic_header or $cfg_magic_footer)) or

            // Signature B: Custom C5 protocol identifiers
            ($proto_session and $proto_magic) or

            // Signature C: In-memory BOF TCC probing
            ($bof_error and 2 of ($bof_api*) and 1 of ($tcc_*))
        )
}

How the YARA Rule Works: Key Points

▪Anchor 1 (uint32(0) == 0xfeedfacf): Checks the first 4 bytes for 64-bit Mach-O magic. If a file is a PDF, image, or text document, YARA skips it immediately without scanning further.
▪Anchor 2 (filesize < 5MB): Prevents the scanner from wasting CPU cycles on massive disk images or databases.
▪Targeted Strings:
▪__c5cfg: Matches the unique custom configuration section.
▪{ f2 f8 f2 f8 f2 f8 f2 f8 }: Matches the 8-byte configuration header magic.
▪c5-session-v1 and c5-magic: Match the custom C2 protocol handshake.
▪EncryptedBofTooShort and InstanceDataParse: Match the in-memory Beacon Object File (BOF) loader.
▪fda.system and /Library/Application Support/com.apple.TCC: Match the privacy permission probing routines.

Detection Logic (Multi-Tier Condition):

▪Branch A: Detects the custom Mach-O section and its magic markers.
▪Branch B: Detects the custom network protocol tokens.
▪Branch C: Detects the in-memory BOF execution engine paired with TCC probing.

False-Positive Considerations: The combination of ad-hoc Mach-O signing, in-memory BOF loader indicators, and __c5cfg configuration markers may provide strong detection confidence. However, no YARA rule can guarantee zero false positives without testing against a representative benign corpus.

Speed: The rule contains no open-ended regular expressions, which can reduce scanning overhead. Actual performance depends on the rule, file size, scanner, and environment; microsecond-level scanning is not guaranteed.

MITRE ATT&CK Mapping and Indicators of Compromise

TacticTechnique IDTechnique NameEvidence in Sample
ExecutionT1059.004Unix ShellSpawns /bin/zsh with direct pipes
Defense EvasionT1106Native API (Direct Syscalls)Uses 100 direct Darwin kernel syscalls to bypass library hooks
Defense EvasionT1027Obfuscated / Stack StringsBuilds string error messages on the fly in CPU registers
Defense EvasionT1036MasqueradingSpoofs Windows 10 Chrome User-Agent from a macOS host
DiscoveryT1082System Information DiscoveryQueries sysctl, getpid, getuid, and geteuid
DiscoveryT1057Process DiscoveryLists active processes via sysctl(KERN_PROC)
DiscoveryT1046Network Service DiscoveryIntegrated TCP port scanner targeting ports 80, 443, 22, and 8080
DiscoveryT1548Privacy / TCC ProbingProbes /Library/Application Support/com.apple.TCC for Full Disk Access
Command & ControlT1071.001Web ProtocolsHTTP beaconing to hxxp[:]//172.20.10.3/instance
ExecutionT1620Reflective Code LoadingExecutes in-memory Beacon Object Files using mmap and mprotect

Indicators of Compromise (IOCs)

File Fingerprints

▪SHA-256: e2b8c46e9673699357002778d81335d78a42b008af5c2f9296cc95ae9934fa9e
▪SHA-1: e925d1bd31b0e33cbe9d2ab652710f5fad192f6b
▪MD5: 461d5a69b7eb0df487f9cb47214cc56e
▪Code Directory Hash (CDHash): a0cf22a892a632b1ecd8d51839c3d4c6c3e8ac1f
▪Signing Identifier: component-macos-x86_64-555549449e504bd2e37d61009e5863936efeae5f

Network Indicators

▪C2 Server IP: 172.20.10.3
▪Port: 80 (HTTP)
▪C2 URI Path: /instance
▪Suspicious Outbound User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
▪Protocol Tokens: c5-session-v1, c5-magic, _X+SC

Host Artifacts

▪Anomalous Mach-O Section: __DATA,__c5cfg
▪Exported Configuration Symbol: _CONFIG_BLOCK
▪Configuration Header Magic Bytes: f2 f8 f2 f8 f2 f8 f2 f8
▪Configuration Footer Magic Bytes: f8 f2 f8 f2

What We Learned

Analyzing macOS malware requires a disciplined workflow. By refusing to run the binary blindly and taking it step by step, we turned a bare hash into a complete forensic picture:

1.Terminal Triage is Fast: Using simple command-line tools like file, codesign, and otool, we spotted an ad-hoc signature, identified a suspicious __c5cfg section, and noticed a lack of standard network libraries in under five minutes.
2.Strings Tell Stories: Printable strings revealed the presence of ChaCha20 encryption, interactive zsh shells, internal port scanning, and Cobalt Strike-style Beacon Object File loaders.
3.Disassembly Confirms Reality: Stepping into Binary Ninja showed us the exact mechanics. We observed the binary allocate memory through direct BSD kernel syscalls, validate its configuration block, and dispatch commands through a 60-branch jump table.

macOS is not immune to sophisticated malware. Attackers can write low-level code that bypasses userland security hooks by making direct syscalls to the Darwin kernel. As analysts, learning to identify direct syscalls, extract custom sections, and craft resilient YARA rules is an important part of malware analysis.

Copied