MacOS hacking part 3: shellcoding. x86_64 assembly intro. Simple NASM examples

cocomelonc.github.io · cocomelonc · 9 months ago · tutorial
quality 7/10 · good
0 net
MacOS hacking part 3: shellcoding. x86_64 assembly intro. Simple NASM 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 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 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 ﷽