6 minute read

Hello, cybersecurity enthusiasts and white hackers!

malware

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:

malware

Mac OS X or BSD has divided system call numbers into “classes”:

malware

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

malware

And run:

./meow

malware

malware

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

malware

For checking correctness we can run something like this:

file hack

malware

And run the binary via:

./hack

malware

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:

malware

malware

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