MacOS hacking part 5: shellcode running. Simple NASM and C (Intel) examples
﷽
Hello, cybersecurity enthusiasts and white hackers!

Today, we’ll continue to go dive into macOS shellcoding for x86_64: how to write shellcode in Assembly and execute it from a C program - just like we do on Linux. But you’ll quickly see: macOS plays by different rules. I’ll show you what works, what doesn’t, and why.
practical example 1
Let’s make the smallest possible “shellcode” - just write Meow\n to stdout, then exit.
First of all, we need to push the string Meow\n onto the stack:
mov rax, 0x0a776f654d ; "\nwoeM" in little-endian: 0x4d 65 6f 77 0a
push rax ; put the string (plus null bytes) onto the stack
Here we encode Meow\n as a single 64-bit value and push it onto the stack. After this instruction, rsp points to our string in memory.
Then we prepare arguments for the write syscall:
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, rsp ; pointer to our string ("Meow\n")
mov rdx, 5 ; string length (5 bytes)
rdi is the first argument (fd = 1, meaning stdout), rsi is the second argument (pointer to buffer) and rdx is the third argument (number of bytes to write).
Then call the write syscall:
mov rax, 0x2000004 ; syscall number for write (macOS x86_64)
syscall
As I wrote in the previous posts, write is syscall 4, so the full value is 0x2000004.
Finally, exit the process cleanly:
mov rax, 0x2000001 ; syscall number for exit
xor rdi, rdi ; exit code 0
syscall
The logic is pretty simple: exit is syscall 1 (0x2000001). Then, we zero out rdi to specify exit code 0 (success).
So the full source code is looks like this meow.asm:
global start
section .text
start:
mov rax, 0x0a776f654d ; "\nwoeM" in little-endian
push rax ; string now on stack
mov rdi, 1 ; fd = 1 (stdout)
mov rsi, rsp ; pointer to "Meow\n"
mov rdx, 5 ; length = 5 bytes
mov rax, 0x2000004 ; syscall: write
syscall
mov rax, 0x2000001 ; syscall: exit
xor rdi, rdi ; exit code 0
syscall
demo
Let’s check this code first, assembly and link:
nasm -f macho64 meow.asm -o meow.o
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o meow meow.o

Then run it (in my case Mac OS X Sonoma VM):
./meow

As you can see, everything is worked as expected, perfectly! =^..^=
practical example 2
My favorite part: get the shellcode bytes!
To run the shellcode from C, you need the opcodes. Extract them with otool:

or via objdump:

Now convert these bytes into a shellcode string:
unsigned char code[] =
"\x48\xb8\x4d\x65\x6f\x77\x0a\x00\x00\x00"
"\x50\xbf\x01\x00\x00\x00"
"\x48\x89\xe6\xba\x05\x00\x00\x00"
"\xb8\x04\x00\x00\x02\x0f\x05"
"\xb8\x01\x00\x00\x02\x48\x31\xff\x0f\x05";
So, let’s go to create code for run it. On linux, you can do it like this:
/*
run.c - a small skeleton program to run shellcode
*/
// bytecode here
char code[] = "my shellcode here";
int main(int argc, char **argv) {
int (*func)(); // function pointer
func = (int (*)()) code; // func points to our shellcode
(int)(*func)(); // execute a function code[]
// if our program returned 0 instead of 1,
// so our shellcode worked
return 1;
}
On Linux, you’d use execstack -s or compile with -z execstack.
But on macOS, there’s NO execstack. The stack is always non-executable, and there’s no easy way to change that.
Solution? On macOS use mmap to allocate an executable memory page, copy your shellcode there, and jump to it!
As a safety and compatibility measure, we check the memory page size (usually 4096 bytes):
size_t pagesize = sysconf(_SC_PAGESIZE);
We’ll allocate memory aligned to a full page for shellcode execution. The stack is not executable on modern macOS. So we use mmap to create a new memory region that is:
void *exec = mmap(0, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON, -1, 0);
This memory region is readable (PROT_READ), writable (PROT_WRITE) and executable (PROT_EXEC). MAP_ANON and MAP_PRIVATE make it a fresh, anonymous private page.
Then, copy our shellcode bytes into the new RWX page:
memcpy(exec, code, sizeof(code));
And execute the shellcode:
((void(*)())exec)();
This jumps into your shellcode, which will write Meow\n to stdout and exit the process.
So, the full source code is looks like this:
/*
* hack.c
* running shellcode on macOS
* author @cocomelonc
* https://cocomelonc.github.io/macos/2025/07/08/malware-mac-5.html
*/
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
unsigned char code[] =
"\x48\xb8\x4d\x65\x6f\x77\x0a\x00\x00\x00"
"\x50\xbf\x01\x00\x00\x00"
"\x48\x89\xe6\xba\x05\x00\x00\x00"
"\xb8\x04\x00\x00\x02\x0f\x05"
"\xb8\x01\x00\x00\x02\x48\x31\xff\x0f\x05";
int main() {
// get the system memory page size
size_t pagesize = sysconf(_SC_PAGESIZE);
// allocate an executable memory page (page-aligned)
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;
}
// copy the shellcode into the allocated memory
memcpy(exec, code, sizeof(code));
// execute the shellcode
((void(*)())exec)();
// free the allocated memory (optional, since program will exit)
// munmap(exec, pagesize);
return 0;
}
demo
Let’s go to see everything in action. Compile:
clang -o hack hack.c

And run it:

Why this matters? Modern macOS disables stack execution, so you must use mmap with PROT_EXEC. If you forget this, you’ll get bus error or segmentation fault when trying to run code from the stack or heap.
Honestly, this pattern is universal for in-memory shellcode execution in C on macOS and other modern *nix OSes.
This example is for Intel (x86_64):

On ARM64/M1, syscalls, calling conventions, and opcodes are different. We need to write separate ARM64 ASM, assemble with nasm -f macho64, and test!
But I decided it would be better to make a separate article for ARM/M1 assembly: even experienced readers often don’t understand both at once. If you mix x86_64 and ARM64, you’ll get a mess.
conclusion
Running raw shellcode on macOS is absolutely possible - but you can’t just drop it on the stack like in the old days. You need to allocate RX memory yourself.
For malware devs, red teamers, or CTF nerds: this is essential for any post-exploitation payload.
I hope this post is useful for malware researchers, macOS/Apple security researchers, ASM/C/C++ 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
Apple Open Source: Releases
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