MacOS hacking part 3: shellcoding. x86_64 assembly intro. Simple NASM examples
﷽
Hello, cybersecurity enthusiasts and white hackers!
In this post, we’ll go over two examples of macOS x86_64
assembly code, explaining how syscalls are used for output and executing commands directly from assembly. These examples showcase the power of low-level system calls and will be particularly helpful for those learning assembly or wanting to understand macOS (Intel) system internals better.
practical example
Let’s create a simple assembly code for printing message: Meow-meow!
First of all, we need:
global start
This marks the start
label as the entry point of the program. When the code is executed, the system begins execution at this label.
At the next step we need to writing output to stdout
. For this we need a syscall number for write
on macOS. In macOS, the syscall table is not readily accessible in the same way it might be in Linux. macOS uses XNU (X is Not Unix), the kernel that powers macOS, iOS, and other Apple operating systems. The syscall table is not stored in a single, easily identifiable file like in Linux, but it is part of the kernel code.
How to find the syscall table on macOS x86_64
?
The syscall table for macOS can be found in the XNU kernel source code. Specifically, it is located in the XNU source files under the bsd
directory. For x86_64
systems, you can explore the source code that defines the syscalls.
The XNU kernel is open source, so I try to check the implementation of system calls by navigating through the XNU GitHub repo for the latest kernel source:
Mac OS X or BSD has divided system call numbers into “classes”:
If the system call is write and exit, the higher order bits are 2
because the class is SYSCALL_CLASS_UNIX
. All Unix system calls are 0×2000000
+ unix syscall number.
We load 0x2000004
into rax
, which is the system call number for write
in macOS. This syscall allows us to write data to a file descriptor (in this case, stdout
):
mov rax, 0x2000004 ; write syscall number for macOS
Then, we pass 1
in the rdi
register to indicate that we want to write to stdout
. File descriptor 1
refers to stdout
. The rsi
register holds the address of the message we want to print. Here, the message is "Meow-meow!"
, defined in the .data
section. The rdx
register holds the length of the message. This is dynamically calculated by subtracting the start address of msg
from the current position ($
), which gives the string length:
mov rax, 0x2000004 ; write syscall number for macOS (Intel)
mov rdi, 1 ; 1 for stdout
mov rsi, msg ; message to print
mov rdx, msg.len ; length of the message
Finally, executing the syscall:
syscall
The kernel will take over, write the message "Meow-meow!"
to stdout
, and return control back to our program.
At the next step we need simple normal exit from our program:
mov rax, 0x2000001 ; exit syscall number
mov rdi, 0 ; exit status 0
syscall
As you can see, the logic is pretty simple: we load 0x2000001
into rax
, which is the exit
syscall number in macOS. The rdi
register is used to pass the exit status. 0
typically means successful execution. Finally, the syscall
is made to exit the program.
Of course, we added the data section:
section .data
msg: db "Meow-meow!", 10
.len: equ $ - msg
So, full source code for this program is looks like this meow.asm
:
global start
section .text
start:
mov rax, 0x2000004 ; write syscall number for macOS
mov rdi, 1 ; 1 for stdout
mov rsi, msg ; message to print
mov rdx, msg.len ; length of the message
syscall ; make the syscall
mov rax, 0x2000001 ; exit syscall number
mov rdi, 0 ; exit status 0
syscall ; make the exit syscall
section .data
msg: db "Meow-meow!", 10
.len: equ $ - msg
demo
Let’s go to compile and run this code. Copy to the MacOS VM (macos-sonoma
in my case) and compile:
nasm -f macho64 meow.asm -o meow.o
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o meow meow.o
And run:
./meow
As you can see, everything is worked perfectly as expected! =^..^=
practical example 2
Let’s go to run simple shell. For this we need using execve
to execute a shell. The logic is pretty simple, first of all, setting up the execve
syscall:
xor rax, rax
mov rax, 0x200003b ; execve syscall on macOS x86_64
At the next step, we need to setting up arguments for execve
:
lea rdi, [rel path] ; rdi = filename (char *filename)
xor rsi, rsi ; rsi = argv (NULL)
xor rdx, rdx ; rdx = envp (NULL)
lea rdi, [rel path]
- the rdi
register holds the filename of the program to execute
. Here, we load the address of the string path (which contains /bin/bash
) into rdi
.
xor rsi, rsi
- the rsi
register is for the arguments to the program (argv
). We set it to NULL
here to indicate no arguments.
xor rdx, rdx
- the rdx
register is for the environment variables (envp
). We also set it to NULL
here, meaning no environment variables are passed.
Then, making the execve
syscall:
syscall ; execve("/bin/bash", NULL, NULL)
and fallback exit:
mov rax, 0x2000001 ; sys_exit
xor rdi, rdi
syscall
What’s going on here? If for some reason the execve
syscall fails, the program will call exit(0)
to exit gracefully. 0x2000001
is the syscall number for exit
, and rdi
is set to 0
, which is the exit status.
And of course, we need a data section like this:
section .data
path: db "/bin/bash", 0 ; OR: db "/bin/zsh", 0
As you can see, path
is the path to the shell that will be executed (/bin/bash
). It is null-terminated (i.e., ends with 0
).
So, full source code for the second example is looks like this hack.asm
:
global start
section .text
start:
; sys_execve = 0x200003b
xor rax, rax
mov rax, 0x200003b ; execve syscall on macOS x86_64
lea rdi, [rel path] ; rdi = filename (char *filename)
xor rsi, rsi ; rsi = argv (NULL)
xor rdx, rdx ; rdx = envp (NULL)
syscall ; execve("/bin/bash", NULL, NULL)
; fallback: exit(0)
mov rax, 0x2000001 ; sys_exit
xor rdi, rdi
syscall
section .data
path: db "/bin/bash", 0 ; OR: db "/bin/zsh", 0
demo 2
Let’s go to compile and run this code. Copy to the MacOS VM (macos-sonoma
in my case) and do the same steps as in the fisrt example:
nasm -f macho64 hack.asm -o hack.o
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o hack hack.o
For checking correctness we can run something like this:
file hack
And run the binary via:
./hack
As you can see, we got a simple shell! Everything is worked as expected! =^..^=
You can use objdump
to disassemble our executable (hack
or meow
) into machine code (hexadecimal byte values) and then format it into shellcode:
objdump -M intel -d meow
objdump -M intel -d hack
This will output the disassembly of our binaries in intel syntax:
In this post, we’ve explored how to use syscalls in macOS assembly to perform essential tasks like writing output to the screen and executing external programs. These syscalls form the backbone of low-level programming on macOS, and understanding how to utilize them gives you powerful control over the operating system.
If you found this guide helpful and want to learn more, keep an eye out for upcoming posts where we’ll dive into more complex topics, like creating persistent malware, evading detection, and hooking system calls.
I hope this post is useful for malware researchers, macOS/Apple security researchers, C/C++ programmers, spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.
XNU source code
Apple: XNU source code
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