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