Memory Injection on macOS

XMAN

Active member
Memory Injection on macOS #1
bueno.

Been a long time since I've written any sort of tutorial, but lately I've been toying with memory injection, particularly on macOS.
Now there may be a number of reasons as to why you would want to do some sort of memory injection, and for my purposes, its so I can write a program that is started as a direct child to Dock.app, so I can take control of the macOS window server.

Your reasons may vary, but without further ado...

On systems running macOS, you can boil down memory injection into about 4 steps. First and foremost, allocating memory in a target task's memory region. From there, writing payload data to said memory, then setting access control for the region, and lastly creating and running a thread which will call and execute your payload.

From macOS 10.4 (Mac OS X as it was called then) and onward, Apple introduced a new virtual memory API. The Mach VM API, for our purposes, is going to have nearly identical function signatures, save for the mach_ prefix in method names.

To keep things relatively organized, I prefer to use a structure that contains all of my virtual memory related data. In addition to this, we will have a payload that is written in assembly. The assembly will be stored in a string we call payload.
Because of the architecture specific assembly, and the architecture agnostic nature of this thread, I won't be giving an actual payload, so keep in mind that our payload here is notional.

Code:
typdef struct {
    mach_vm_address_t addr;
    size_t size;
    vm_prot_t prot;
} vm_region_t;

char *payload = "...";

Code:
vm_region_t shellcode = {
    .addr = 0,
    .size = sizeof(payload),
    .prot = VM_PROT_READ | VM_PROT_EXECUTE
};

The access control flags are bitwise OR'd together to create a value which will later be passed to our vm_protect calls. If you need R/W on your memory region, VM_PROT_DEFAULT is equivalent to VM_PROT_READ and VM_PROT_WRITE.

As a pre-memory injection step, we'll need to get our task so that we can actually inject a payload into an address space.

Code:
task_t task;
task_for_pid(mach_task_self(), atoi(argv[1]), &task);

mach_vm_allocate(task, &shellcode.addr, shellcode.size, VM_FLAGS_ANYWHERE);

With our memory allocated in a given tasks' memory space, we'll go ahead and write our payload. Our payload, again, is notional for this thread. In actuality, it will be assembly that we've written and compiled, and in the string stored as the actual machine code.

Code:
mach_vm_write(task, shellcode.addr, payload, sizeof(payload));

And our last step is a call that will set our access control. If you don't set access control for our virtual memory region, you won't be able to execute it.

Code:
mach_vm_protect(task, shellcode.addr, shellcode.size, 0, shellcode.prot);


Now you've got a basic idea of the calls to inject your code into a task's memory region, you'll need to execute it. This is where your mileage may vary. You may want to spawn a thread which will execute some code you've written, or maybe you'll just want to execute the code as is.
That's about it for this thread. Very short, just a few function calls. As my last bit of information, to spawn a thread, you'll probably want to use the thread_create_running method, as it creates optimized threads and spawns them immediately.

For M1 you'll need to use the arm_thread_state64_t structure to store information regarding your thread. The __pc and __sp members are your program counter and stack pointers respectively. Specifically for the M1, you'll need to call ptrauth_sign_unauthenticated, and assign that to your program counter. The reason being is because the M1 is actually arm64e, which signs pointers to prevent unauthenticated pointers to be used. Also make sure that you target arm64e when compiling, otherwise you will more than likely get a protection failure error from thread_create_running.

For x86 CPUs you'll use the x86_thread_state64_t structure. __rip and __rsp are your instruction pointers and stack pointers respectively.

Now you're ready to do some memory injection. The rest is pretty much up to you, as this was only the ground work for doing it. Have fun.
 
Top