diff --git a/research/custom_loader/a.mm b/research/custom_loader/a.mm index 914528c..cfbe6c0 100644 --- a/research/custom_loader/a.mm +++ b/research/custom_loader/a.mm @@ -1,4 +1,5 @@ #import +#include @interface Foo : NSObject @end @@ -15,11 +16,20 @@ @implementation Bar + (void)load { NSLog(@"%@", self); + printf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"); } @end -@implementation Baz : Bar -@end +// @implementation Baz : Bar +// @end + + + +__attribute__((constructor)) static void +hmmge() { +// create a dummy blank function to be replaced to call OBJC load +printf("hmmge\n"); +} int main(int argc, const char * argv[]) { @autoreleasepool { diff --git a/research/custom_loader/b.cc b/research/custom_loader/b.cc index 515e469..585f4a2 100644 --- a/research/custom_loader/b.cc +++ b/research/custom_loader/b.cc @@ -780,6 +780,7 @@ void build_cache(struct libcache& cache, void* main) { } void fix_objc(struct libcache_item* libfixing, struct libcache& cache); +void fix_initializer(struct libcache_item* libfixing, struct libcache& cache); void fix(struct libcache& cache) { // now we have function to find exported symbols // it supports full name search or hash search @@ -938,6 +939,82 @@ void fix(struct libcache& cache) { // } fix_objc(libfixing, cache); + fix_initializer(libfixing, cache); +} + + +typedef void *(*readClass_t)(void *, bool, bool); +typedef void *(*realizeClassWithoutSwift_t)(void *, void*); +typedef void *(*remapClass_t)(void *); +typedef void *(*load_method_t)(void*, void*); +typedef void *(*sel_lookUpByName_t)(const char*); +typedef void (*addClassTableEntry_t)(void *); +typedef void (*schedule_class_load_t)(void *); + +typedef void *(*objc_autoreleasePoolPush_t)(); +typedef void (*objc_autoreleasePoolPop_t)(void *); + +struct custom_initializer_t { + uint64_t* loadable_classes; + uint32_t* loadable_classes_used; + sel_lookUpByName_t sel_lookUpByName; + objc_autoreleasePoolPush_t objc_autoreleasePoolPush; + objc_autoreleasePoolPop_t objc_autoreleasePoolPop; + remapClass_t remapClass; + schedule_class_load_t schedule_class_load; + uint64_t* cls; + size_t ncls; +}; + +// global variable for PoC +struct custom_initializer_t* custom_initializer_i; + +void volatile +custom_initializer(int argc, const char *const argv[], const char *const envp[], + const char *const apple[], const struct ProgramVars *vars) { + printf("run custom initializers %p\n", custom_initializer_i); + // return; + uint64_t* loadable_classes = custom_initializer_i->loadable_classes; + uint32_t* loadable_classes_used = custom_initializer_i->loadable_classes_used; + sel_lookUpByName_t sel_lookUpByName = custom_initializer_i->sel_lookUpByName; + objc_autoreleasePoolPop_t objc_autoreleasePoolPop = custom_initializer_i->objc_autoreleasePoolPop; + objc_autoreleasePoolPush_t objc_autoreleasePoolPush = custom_initializer_i->objc_autoreleasePoolPush; + remapClass_t remapClass = custom_initializer_i->remapClass; + schedule_class_load_t schedule_class_load = custom_initializer_i->schedule_class_load; + + for (int i = 0; i < custom_initializer_i->ncls; i++) { + void* cls0 = (void*)custom_initializer_i->cls[i]; + void* cls = remapClass(cls0); + if (!cls) continue; + schedule_class_load(cls); + } + + printf("loadable_classes %llx %x\n", *loadable_classes, *loadable_classes_used); + + struct loadable_class_t { + void* cls; + void* method; + }; + struct loadable_class_t *classes = (struct loadable_class_t*)*loadable_classes; + int used = *loadable_classes_used; + *loadable_classes = 0; + // *loadable_classes_allocated = 0; + *loadable_classes_used = 0; + void* sel = sel_lookUpByName("load"); + // Call all +loads for the detached list. + void* pool = objc_autoreleasePoolPush(); + for (int i = 0; i < used; i++) { + void* cls = classes[i].cls; + load_method_t load_method = (load_method_t)classes[i].method; + printf("call load of class %p %p\n", cls, load_method); + if (!cls) continue; + (load_method)(cls, sel); + } + // Destroy the detached list. + if (classes) free(classes); + objc_autoreleasePoolPop(pool); + + free(custom_initializer_i); } void fix_objc(struct libcache_item* libfixing, struct libcache& cache) { @@ -954,17 +1031,6 @@ void fix_objc(struct libcache_item* libfixing, struct libcache& cache) { // "mov rcx, 123;" // "call r12;"); - typedef void *(*readClass_t)(void *, bool, bool); - typedef void *(*realizeClassWithoutSwift_t)(void *, void*); - typedef void *(*remapClass_t)(void *); - typedef void *(*load_method_t)(void*, void*); - typedef void *(*sel_lookUpByName_t)(const char*); - typedef void (*addClassTableEntry_t)(void *); - typedef void (*schedule_class_load_t)(void *); - - typedef void *(*objc_autoreleasePoolPush_t)(); - typedef void (*objc_autoreleasePoolPop_t)(void *); - void* header = libfixing->header; const uint32_t magic = *(uint32_t *)header; char *ptr = (char *)header; @@ -1010,8 +1076,11 @@ void fix_objc(struct libcache_item* libfixing, struct libcache& cache) { for (int ptr_i = 0; ptr_i < size / 8; ptr_i++) { // this pointer is rebased by dyld and points to the correct class interface // for some reason, we can skip this and it should still work - readClass((void*)data_ptr[ptr_i], false, false); - realizeClassWithoutSwift((void*)data_ptr[ptr_i], 0); + void* newCls = readClass((void*)data_ptr[ptr_i], false, false); + if (newCls != (void*)data_ptr[ptr_i]) { + realizeClassWithoutSwift(newCls, 0); + } + printf("add class init (%llx)%p\n", data_ptr[ptr_i], newCls); } } else if (custom_strncmp(secname, "__objc_nlclsbruh", 16) == 0) { @@ -1050,40 +1119,17 @@ void fix_objc(struct libcache_item* libfixing, struct libcache& cache) { printf("build nonlazy class at (%llx)%p\n", data_ptr[ptr_i], cls); } - printf("loadable_classes %llx %llx %llx\n", *loadable_classes, *loadable_classes_used, *loadable_classes_allocated); - for (int ptr_i = 0; ptr_i < size / 8; ptr_i++) { - void* cls = remapClass((void*)data_ptr[ptr_i]); - schedule_class_load(cls); - printf("add class load (%llx)%p\n", data_ptr[ptr_i], cls); - } - printf("loadable_classes %llx\n", *loadable_classes); - - void* pool = objc_autoreleasePoolPush(); - { - struct loadable_class_t { - void* cls; - void* method; - }; - struct loadable_class_t *classes = (struct loadable_class_t*)*loadable_classes; - int used = *loadable_classes_used; - *loadable_classes = 0; - *loadable_classes_allocated = 0; - *loadable_classes_used = 0; - void* sel = sel_lookUpByName("load"); - printf("selector %p\n", sel); - // Call all +loads for the detached list. - for (i = 0; i < used; i++) { - void* cls = classes[i].cls; - load_method_t load_method = (load_method_t)classes[i].method; - printf("call load of class %p %p\n", cls, load_method); - if (!cls) continue; - (load_method)(cls, sel); - } - // Destroy the detached list. - if (classes) free(classes); - } - objc_autoreleasePoolPop(pool); - printf("loadable_classes %llx\n", *loadable_classes); + custom_initializer_i = (custom_initializer_t*)malloc(sizeof(custom_initializer_t)); + custom_initializer_i->sel_lookUpByName = sel_lookUpByName; + custom_initializer_i->loadable_classes = loadable_classes; + custom_initializer_i->loadable_classes_used = loadable_classes_used; + custom_initializer_i->objc_autoreleasePoolPush = objc_autoreleasePoolPush; + custom_initializer_i->objc_autoreleasePoolPop = objc_autoreleasePoolPop; + custom_initializer_i->schedule_class_load = schedule_class_load; + custom_initializer_i->remapClass = remapClass; + custom_initializer_i->cls = data_ptr; + custom_initializer_i->ncls = size / 8; + // printf("loadable_classes %llx\n", *loadable_classes); } sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; } @@ -1096,6 +1142,135 @@ void fix_objc(struct libcache_item* libfixing, struct libcache& cache) { } } +void fix_initializer(struct libcache_item* libfixing, struct libcache& cache) { + // fix the initializers + // The Objective-C runtime loads the NSObject after this lib booted + // So all calls to NSObject (and its children classes) will segfault/throw error + // + // So we will fix the main initializers, which runs after all Objective-C setup + // The initializers will run these Objective-C classes' load methods + // + // (THIS IDEA IS TESTED AND WILL NOT WORK) + // As of now, we assume the main executable has a __mod_init_func section + // In practice, we should always inject an empty __mod_init_func + // But if the binary already has a __mod_init_func section, this section must + // reallocate into a different section to allow for a bigger size (oldsize+1) + // + // This idea can't work because dyld check the pointer to be inside the image, + // which is not if we point it here + // + // (THIS IS THE CORRECT IDEA) + // So for Objective-C binaries, the load chain happens like this + // [(no objc?)lib init] -> [objc runtime] -> [foundations] -> + // [main obj-c load] -> [main constructor] -> [main] + // We need to inject between [objc runtime] and [main] + // + // There could be many ways to do this. I discovered 1 method of doing this. + // + // The idea is to hijack the main function to do the rest of the initalizations. + // By fixing the LC_MAIN command, we can make dyld jump to anywhere we want as main. + // But the command can't be edited at runtime. + // And pointing to the function we want needs a workaround. + // + // So we will write a shellcode in the binary and point main to that shellcode. + // The shellcode basically loads the address of the initalization function, + // call it, then call main, and return. + // + // The shellcode must be able ot get the current pc address to correctly calculate + // address from any offset. In arm64, we can use `adr x8, 0`. + // If we know where the shellcode is, we can effectively calculate the header of main. + // Now, everything is easy, just need offsets to anywhere we want and we can get it. + // + // Now the address of the initalization function can be fetched using many methods, + // but it resides inside this library. To reduce redundance work, we can write the + // address of this function somewhere inside main, which will then be easily found. + // + // As a result, we choose the space before __text to write the shellcode, + // the space after __DATA to write the address for initalization function. + // Because all segment is allocated/pre-allocated with page alignement, + // we can be pretty sure that there are free space. + // (note: __TEXT segment is aligned to the end of the page, free space in the middle) + // + // Below is the shellcode. + // + // sub sp, sp, #0x10 + // str x30, [sp] + // adr x8, 0 + // movz x9, #0x3d68 ; offset at this point + // sub x8, x8, x9 + // str x8, [sp, #8] + // movz x9, #0x81d8 + // add x9, x8, x9 + // ldr x9, [x9] + // blr x9 + // ldr x8, [sp, #8] + // movz x9, #0x3e3c ; offset to original main + // add x9, x8, x9 + // blr x9 + // ldr x30, [sp] + // add sp, sp, #0x10 + // ret + + void* header = libfixing->header; + const uint32_t magic = *(uint32_t *)header; + char *ptr = (char *)header; + if (magic == magic64) { + ptr += 0x20; + } else { + ptr += 0x20 - 0x4; + } + + const uint32_t ncmds = *((uint32_t *)header + 4); + char* command_ptr = ptr; + + uint64_t linkedit_vmaddr; + uint64_t linkedit_fileoffset; + uint64_t slide; + for (int i = 0; i < ncmds; i++) { + const uint32_t cmd = *((uint32_t *)ptr + 0); + const uint32_t cmdsize = *((uint32_t *)ptr + 1); + if (cmd == LC_SEGMENT_64) { + char *name = (char *)((uint64_t *)ptr + 1); + uint64_t vmaddr = *((uint64_t *)ptr + 3); + uint64_t fileoffset = *((uint64_t *)ptr + 5); + // this assumes that __TEXT comes before __DATA_CONST + printf("segment %s\n", name); + if (custom_strcmp(name, "__TEXT") == 0) { + slide = (uint64_t)header - vmaddr; + uint64_t nsect = *((uint32_t *)ptr + 8*2); + char* sections_ptr = (char*)((uint32_t*)ptr + 18); + for (int sec = 0; sec < nsect; sec++) { + char* secname = sections_ptr; + printf("section %s\n", secname); + if (custom_strcmp(secname, "__init_offsets") == 0) { + uint64_t addr = *((uint64_t*)sections_ptr + 4); + uint64_t size = *((uint64_t*)sections_ptr + 5); + uint32_t *data_ptr = (uint32_t*)(addr + slide); + + printf("found initializer at %p\n", data_ptr); + } + sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; + } + } else if (custom_strcmp(name, "__DATA") == 0) { + uint64_t nsect = *((uint32_t *)ptr + 8*2); + char* sections_ptr = (char*)((uint32_t*)ptr + 18); + sections_ptr += (16 * 2 + 8 * 2 + 4 * 8) * (nsect - 1); + + uint64_t addr = *((uint64_t*)sections_ptr + 4); + uint64_t size = *((uint64_t*)sections_ptr + 5); + + uint64_t* dummy = (uint64_t*)(addr + slide + size); + *dummy = (uint64_t)custom_initializer; + printf("add custom main-peg at %p\n", dummy); + } else if (custom_strcmp(name, "__LINKEDIT") == 0) { + linkedit_vmaddr = vmaddr; + linkedit_fileoffset = fileoffset; + } + } + ptr += cmdsize; + } +} + void test(struct libcache& cache) { uint32_t libsystem_hash = calculate_libname_hash(&cache, "/usr/lib/libSystem.B.dylib");