Skip to content

Instantly share code, notes, and snippets.

@DerekSelander
Last active April 22, 2024 19:48
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DerekSelander/5bd86d983dd69c71a311ee559ec74d05 to your computer and use it in GitHub Desktop.
Save DerekSelander/5bd86d983dd69c71a311ee559ec74d05 to your computer and use it in GitHub Desktop.
Dumps Objective-C class/instance info at runtime
//
// MIT License
//
// Copyright (c) 2024 Derek Selander
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software. Attribution is requested but not
// required if Software is public facing
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// dynadump:
// Quick'n'Dirty imp of an ObjC class-dump primarily for inspecting dyld shared
// cache libraries on the host machine done via dlopen and public ObjC APIs. So hopefully,
// this code will better withstand breaking changes to objc/dyld internal changes.
// On ARM64, this will use hardware breakpoints to prevent the constructors from firing
// which will sometimes crash the process. On x86_64, this is disabled.
//
// to compile for jb'd iOS:
// xcrun -sdk iphoneos clang -fmodules -arch arm64 -Wl,-U,_dyld_shared_cache_for_each_image,-U,_dyld_image_path_containing_address -o /tmp/dynadump /path/to/this/file/main.m
// codesign -f -s - /tmp/dynadump
//
// to compile for macOS
// xcrun -sdk macos clang -arch arm64 -arch x86_64 -fmodules -o /usr/local/bin/dynadump /path/to/this/file/main.m -Wl,-U,_dyld_shared_cache_for_each_image,-U,_dyld_image_path_containing_address
// codesign -f -s - /usr/local/bin/dynadump
//
// to compile as a shared framework
// xcrun -sdk iphoneos clang -fmodules -o /tmp/dynadump.dylib /path/to/this/file/main.m -shared -DSHARED_DYNAMIC_DUMP
@import ObjectiveC;
@import Foundation;
@import Darwin;
@import OSLog;
@import MachO;
#import <mach-o/dyld_images.h>
/*********************************************************************/
# pragma mark - defines -
/*********************************************************************/
/// Use SHARED_DYNAMIC_DUMP to make a shared library
#ifndef SHARED_DYNAMIC_DUMP
#define DYNAMIC_DUMP_VISIBILITY static
#else
#define DYNAMIC_DUMP_VISIBILITY __attribute__((visibility("default")))
#endif
#define DYLD_LOADER_CLASS_MAGIC 'l4yd'
#define GOOD_E_NUFF_BUFSIZE 1000
#define do_copy_n_return(STR) { strcpy(buffer, (STR)); return 0; }
#define append_content(FORMAT, ...) { buff_offset += snprintf(buffer + buff_offset, GOOD_E_NUFF_BUFSIZE - buff_offset, FORMAT, ##__VA_ARGS__); }
#define ARM64_OPCODE_SIZE sizeof(uint32_t)
// error handling
#define HANDLE_ERR(E) {if ((E)) { log_error("Error: %d, %s \n", (E), mach_error_string((E)));}}
// stolen from objc4
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# else
# error unknown architecture for packed isa
# endif
/// -DUSE_CONSOLE for NSLog, else fprintf
#ifdef USE_CONSOLE
#define log_out(S, ...) NSLog(@ S, ##__VA_ARGS__);
#define log_error(S, ...) if (g_debug) { NSLog(@ "ERR: %5d", __LINE__);} NSLog(@ S, ##__VA_ARGS__);
#define log_debug(S, ...) if (g_debug) {NSLog(@ "dbg %4d: " S, __LINE__, ##__VA_ARGS__); }
#else
#define log_out(S, ...) fprintf(stdout, S, ##__VA_ARGS__);
#define log_error(S, ...) if (g_debug) { fprintf(stderr, "ERR: %5d", __LINE__);} fprintf(stderr, S, ##__VA_ARGS__);
#define log_debug(S, ...) if (g_debug) { fprintf(stdout, "dbg %4d: " S, __LINE__, ##__VA_ARGS__); }
#endif
/// arm64 debug stuff
#define S_USER ((uint32_t)(2u << 1))
#define BCR_ENABLE ((uint32_t)(1u))
#define SS_ENABLE ((uint32_t)(1u))
#define BCR_BAS ((uint32_t)(15u << 5))
#define DCYAN (g_color ? "\e[36m" : "")
#define DYELLOW (g_color ? "\e[33m" : "")
#define DMAGENTA (g_color ? "\e[95m" : "")
#define DRED (g_color ? "\e[91m" : "")
#define DPURPLE (g_color ? "\e[35m" : "")
#define DBLUE (g_color ? "\e[34m" : "")
#define DGRAY (g_color ? "\e[90m" : "")
#define DGREEN (g_color ? "\e[92m" : "")
#define DDARK_GREEN (g_color ? "\e[32m" : "")
#define DBOLD (g_color ? "\e[1m" : "")
#define DCYAN_UNDERLINE (g_color ? "\033[36;1;4m" : "")
#define DPURPLE_BOLD (g_color ? "\e[35;1m" : "")
#define DCYAN_LIGHT (g_color ? "\e[96m" : "")
#define DYELLOW_LIGHT (g_color ? "\e[93m" : "")
#define DSTRONG_RED (g_color ? "\e[31;4m" : "")
#define DLIGHT_BLUE (g_color ? "\e[94m" : "")
#define DCOLOR_END (g_color ? "\e[0m" : "")
#define DMETHOD_COLOR DCYAN
#define DPUNC_COLOR DGRAY
#define DPARAM_COLOR DYELLOW
#if __has_feature(ptrauth_calls)
#define DO_SIGN(X) (void*)__builtin_ptrauth_sign_unauthenticated((X), ptrauth_key_asia, 0)
#define DO_PROC_SIGN(X) (void*)__builtin_ptrauth_sign_unauthenticated((X), ptrauth_key_asib, 0)
#define DO_STRIP(X) X = (__typeof__((X)))ptrauth_strip((void*)(X), 0);
#define DO_STRIP_OBJC(X) X = (__bridge id)ptrauth_strip((__bridge void*)(X), 0);
#else
#define DO_SIGN(X) (X)
#define DO_PROC_SIGN(X) (X)
#define DO_STRIP(X)
#define DO_STRIP_OBJC(X)
#endif
/*********************************************************************/
# pragma mark - internal testing
/*********************************************************************/
#if 0
@interface TestInterface : NSObject
@property (nonatomic, strong) NSURL *someURL;
@end
@implementation TestInterface (MyCategory)
- (void)categoryMethod {}
@end
@implementation TestInterface
- (void)booopbeepbop{}
@end
#endif
/*********************************************************************/
# pragma mark - dyld declarations -
/*********************************************************************/
typedef struct dyld_shared_cache_s* dyld_shared_cache_t;
typedef struct dyld_image_s* dyld_image_t;
extern __attribute__((weak)) const char* dyld_image_path_containing_address(const void* addr);
extern __attribute__((weak)) void dyld_shared_cache_for_each_image(dyld_shared_cache_t cache, void (^block)(dyld_image_t image));
extern __attribute__((weak)) const char* dyld_shared_cache_file_path(void);
extern __attribute__((weak)) bool dyld_shared_cache_for_file(const char* filePath, void (^block)(dyld_shared_cache_t cache));
const char* my_dyld_image_get_installname(dyld_image_t image) {
extern __attribute__((weak)) const char* dyld_image_get_installname(dyld_image_t image);
if (dyld_image_get_installname) {
return dyld_image_get_installname(image);
}
return "???";
}
extern __attribute__((weak)) const struct mach_header* dyld_image_header_containing_address(const void* addr);
/*********************************************************************/
# pragma mark - struct declarations / globals -
/*********************************************************************/
/// For unwinding stack frames in an exception handler
struct fp_ptr {
struct fp_ptr *next;
void* address;
};
/// mig generated structs
#pragma pack(push, 4)
typedef struct {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
/* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
int64_t code[2];
} exc_req;
typedef struct {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} exc_resp;
#pragma pack(pop)
static void* safe_dlopen(const char *image) ;
extern Class objc_opt_class(id _Nullable obj);
void* server_thread(void *arg);
static void dump_objc_class_info(Class cls);
static void dump_objc_protocol_info(Protocol *prot);
static bool g_debug = false;
static bool g_color = true;
static int g_verbose = 0;
static bool can_use_dyld_apis = true;
static uintptr_t constructor_addresses[10] = { };
static uint8_t constructor_addresses_count = 0;
static mach_port_t exc_port = MACH_PORT_NULL;
static pthread_mutex_t exception_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t exception_cond = PTHREAD_COND_INITIALIZER;
static uintptr_t dyld_header = 0;
/*********************************************************************/
# pragma mark - internal -
/*********************************************************************/
static void* strip_pac(void* addr) {
#if defined(__arm64__)
static uint32_t g_addressing_bits = 0;
if (g_addressing_bits == 0) {
size_t len = sizeof(uint32_t);
if (sysctlbyname("machdep.virtual_address_size", &g_addressing_bits, &len,
NULL, 0) != 0) {
g_addressing_bits = -1; // if err, f it, just assume anything goes
}
}
uintptr_t mask = ((1UL << g_addressing_bits) - 1) ;
return (void*)((uintptr_t)addr & mask);
#else
return addr;
#endif
}
__attribute__((constructor)) static void grab_caller_address(void) {
uintptr_t return_address = (uintptr_t)strip_pac((void*)__builtin_return_address(0));
constructor_addresses[constructor_addresses_count++] = return_address - ARM64_OPCODE_SIZE;
if (getenv("DEBUG")) {
Dl_info info;
dladdr((void*)constructor_addresses[constructor_addresses_count-1], &info);
log_out("patching load address 0x%012lx %s\n", constructor_addresses[constructor_addresses_count-1], info.dli_sname);
}
dyld_header = (uintptr_t)dyld_image_header_containing_address((void*)return_address);
if (!dyld_shared_cache_for_each_image || !dyld_image_path_containing_address) {
can_use_dyld_apis = false;
}
}
@interface MERPLERPBURPDERP : NSObject
@end
@implementation MERPLERPBURPDERP
+ (void)load {
constructor_addresses[constructor_addresses_count++] = (uintptr_t)strip_pac((void*)__builtin_return_address(0)) - ARM64_OPCODE_SIZE;
if (getenv("DEBUG")) {
Dl_info info;
dladdr((void*)constructor_addresses[constructor_addresses_count-1], &info);
log_out("patching load address 0x%012lx %s\n", constructor_addresses[constructor_addresses_count-1], info.dli_sname);
}
}
@end
@implementation NSObject (BOWMEOWYAYHEY)
+ (void)load {
constructor_addresses[constructor_addresses_count++] = (uintptr_t)strip_pac((void*)__builtin_return_address(0)) - ARM64_OPCODE_SIZE;
if (getenv("DEBUG")) {
Dl_info info;
dladdr((void*)constructor_addresses[constructor_addresses_count-1], &info);
log_out("patching load address 0x%012lx %s\n", constructor_addresses[constructor_addresses_count-1], info.dli_sname);
}
}
@end
__attribute__((always_inline))
static uintptr_t get_cls_isa(Class cls) {
if (!cls) {
return 0;
}
uintptr_t *isa_packed = (__bridge void*)cls;
uintptr_t isa = (*isa_packed) & ISA_MASK;
return (uintptr_t)strip_pac((void*)isa);
}
__attribute__((always_inline))
static int get_object_type_description(const char *typeEncoding, char *buffer) {
int buff_offset = 0;
if (!typeEncoding) {
do_copy_n_return("void*");
}
if (!strcmp(typeEncoding, "@")) {
do_copy_n_return("id");
} else if (!strcmp(typeEncoding, "v")) {
do_copy_n_return("void");
} else if (!strcmp(typeEncoding, "^v")) {
do_copy_n_return("void*");
} else if (!strcmp(typeEncoding, ":")) {
do_copy_n_return("SEL");
} else if (!strcmp(typeEncoding, "B")) { // TODO there are 2 of these? B/b
do_copy_n_return("BOOL");
} else if (!strcmp(typeEncoding, "b")) {
do_copy_n_return("BOOL");
} else if (!strcmp(typeEncoding, "c")) {
do_copy_n_return("char");
} else if (!strcmp(typeEncoding, "i")) {
do_copy_n_return("int");
} else if (!strcmp(typeEncoding, "s")) {
do_copy_n_return("short");
} else if (!strcmp(typeEncoding, "q")) {
do_copy_n_return("long");
} else if (!strcmp(typeEncoding, "C")){
do_copy_n_return("unsigned char");
} else if (!strcmp(typeEncoding, "I")) {
do_copy_n_return("unsigned int");
} else if (!strcmp(typeEncoding, "S")) {
do_copy_n_return("unsigned short");
} else if (!strcmp(typeEncoding, "Q")) {
do_copy_n_return("unsigned long");
} else if (!strcmp(typeEncoding, "f")) {
do_copy_n_return("float");
} else if (!strcmp(typeEncoding, "d")) {
do_copy_n_return("double");
} else if (!strcmp(typeEncoding, "*")) {
do_copy_n_return("char*");
} else if (!strcmp(typeEncoding, "#")) {
do_copy_n_return("Class");
} else if (!strcmp(typeEncoding, "@?")) {
do_copy_n_return("^block");
}
size_t len = strlen(typeEncoding);
// Normal C struct type {_NSZone=} >>> NSZone*
if (typeEncoding[0] == '{' && len >= 4 && typeEncoding[len -1] == '}') {
if (g_verbose) { // print the complete struct if verbose else just the first name
snprintf(buffer, GOOD_E_NUFF_BUFSIZE, "struct %.*s", (int)(len - 3), &typeEncoding[1]);
} else {
char* found = strchr(&typeEncoding[1], '=');
snprintf(buffer, GOOD_E_NUFF_BUFSIZE, "struct %.*s", (int)(len - ((char*)&typeEncoding[1] - found)), &typeEncoding[1]);
}
return 0;
}
// handle objc instance @"some_objc_here"
if (typeEncoding[0] == '@' && len >= 4) {
if (typeEncoding[2] == '<') {
snprintf(buffer, GOOD_E_NUFF_BUFSIZE, "id%.*s", (int)(len - 3), &typeEncoding[2]);
return 0;
}
snprintf(buffer, GOOD_E_NUFF_BUFSIZE, "%.*s*", (int)(len - 3), &typeEncoding[2]);
return 0;
}
// handle pointers
if (typeEncoding[0] == '^') {
if (len > 1) {
char tmp[GOOD_E_NUFF_BUFSIZE];
get_object_type_description(&typeEncoding[1], tmp);
append_content("%s*", tmp);
return 0;
}
return 1;
}
if ( len >= 2 )
{
if (typeEncoding[0] <= 'm')
{
switch (typeEncoding[0])
{
case 'N':
append_content("inout ");
break;
case 'O':
append_content("bycopy ");
break;
case 'R':
append_content("byref ");
break;
case 'V':
append_content("oneway ");
break;
default:
do_copy_n_return(typeEncoding);
}
get_object_type_description(&typeEncoding[1], buffer + buff_offset);
return 0;
}
switch (typeEncoding[0])
{
case 'r':
append_content("const ");
break;
case 'o':
append_content("out ");
break;
case 'n':
append_content("in ");
break;
}
get_object_type_description(&typeEncoding[1], buffer + buff_offset);
return 0;
}
return 1;
}
static
int get_property_description(objc_property_t *property, char *buffer)
{
uint buff_offset = 0;
unsigned int attributeCount = 0;
objc_property_attribute_t *attributes = property_copyAttributeList(*property, &attributeCount);
append_content("%s@property ", DGREEN);
if (attributeCount >= 2) {
append_content("(")
}
for (int i = attributeCount - 1; i >= 0; i--) {
const char *name = attributes[i].name;
if (i == 0 && attributeCount >= 2) {
append_content(")%s ", DCOLOR_END);
}
if (!strcmp(name, "R")) {
append_content("readonly");
if (i != 1) {
append_content(", ")
}
} else if (!strcmp(name, "C")) {
append_content("copy");
if (i != 1) {
append_content(", ")
}
} else if (!strcmp(name, "&")) {
append_content("retain");
if (i != 1) {
append_content(", ")
}
} else if (!strcmp(name, "N")) {
append_content("nonatomic");
if (i != 1) {
append_content(", ")
}
} else {
if (!strcmp(name, "G")) {
append_content("getter=%s ", &name[2]);
}
else {
if (strcmp(name, "S")) {
log_debug("/* TODO DEREK S */")
// TODO: look up this one
if (!strcmp(name, "D")) {
// TODO: look up this one
log_debug("/* TODO DEREK D */")
} else if (!strcmp(name, "W")) {
append_content("weak ");
} else if (!strcmp(name, "T")) {
char tmp[GOOD_E_NUFF_BUFSIZE];
get_object_type_description(attributes->value, tmp);
append_content("%s%s%s ", DRED, tmp, DCOLOR_END);
}
else if (!strcmp(name, "V")) {
// Ignore this one, it's called a 'oneway'
} else {
}
}
// TODO: lookup setters
}
}
}
append_content("%s%s%s\n", DBOLD, property_getName(*property), DCOLOR_END);
free(attributes);
return 0;
}
static
void extract_and_print_method(Method method, const char *name, uintptr_t image_start, BOOL isClassMethod, BOOL pretendMethod) {
char buffer[GOOD_E_NUFF_BUFSIZE];
buffer[0] = '\0';
const char* returnType = method_copyReturnType(method);
if (get_object_type_description(returnType, buffer)) {
log_error("\nerror!\n failed to parse \"%s\"", returnType);
}
free((void*)returnType);
// ugly hack eeeeeeeeeeek
// pretendMethod means the method param is actually a objc_method_description* that
// is masquerading as a Method_t. objc_method_description are missing the IMP, but are
// identical to a "large" Method_t. So... if we ignore any IMP APIs, we get the same
// helpful APIs for pulling out type encodings
if (g_verbose && !pretendMethod) {
uintptr_t implementation = (uintptr_t)strip_pac((void*)method_getImplementation(method));
log_out( " %s/* +0x%08lx 0x%016lx %s */%s", DGRAY, implementation - image_start, implementation, name, DCOLOR_END);
}
log_out(" %s%c(%s%s%s%s%s)%s", DGRAY, isClassMethod ? '+' : '-', DCOLOR_END, DPARAM_COLOR, buffer, DCOLOR_END, DPUNC_COLOR, DCOLOR_END);
const char* method_name = sel_getName(method_getName(method));
char *cur_param = strchr(method_name, ':');
if (cur_param) {
char *prev_param = (char*)method_name;
int index = 0;
char tmp[GOOD_E_NUFF_BUFSIZE];
do {
log_out( "%s%.*s:%s", DMETHOD_COLOR, (int)(cur_param - prev_param), prev_param, DCOLOR_END);
// method_copyArgumentType 0 == self, 1 == SEL, 2... actual methods
char *argType = method_copyArgumentType(method, index + 2);
if (get_object_type_description(argType, tmp)) {
log_error("\nerror!\n failed to parse \"%s\"", argType);
}
free(argType);
// printts a type and param ie. (id)a2
log_out( "%s(%s%s%s%s%s)%s%sa%d%s", DGRAY, DCOLOR_END, DPARAM_COLOR, tmp, DCOLOR_END, DGRAY, DCOLOR_END, DGRAY, index + 1, DCOLOR_END);
prev_param = cur_param + 1;
cur_param = strchr(cur_param + 1, ':');
if (cur_param) {
log_out( " ");
}
index++;
} while (cur_param);
log_out( "%s;%s\n", DPUNC_COLOR, DCOLOR_END);
} else {
log_out( "%s%s%s%s;%s\n", DMETHOD_COLOR, method_name, DCOLOR_END, DPUNC_COLOR, DCOLOR_END);
}
}
static
const char* dsc_image_as_num(uint32_t num) {
if (!can_use_dyld_apis) {
return NULL;
}
const char *dsc_path = dyld_shared_cache_file_path();
__block const char *str = NULL;
dyld_shared_cache_for_file(dsc_path, ^(dyld_shared_cache_t cache) {
__block uint32_t counter = 0;
dyld_shared_cache_for_each_image(cache, ^(dyld_image_t image) {
if (str) {
return;
}
if (++counter == num) {
str = strdup(my_dyld_image_get_installname(image));
}
});
});
return str;
}
static
const char* dsc_image_as_path(const char *path) {
if (!can_use_dyld_apis) {
return NULL;
}
const char *dsc_path = dyld_shared_cache_file_path();
__block const char *outparam = NULL;
dyld_shared_cache_for_file(dsc_path, ^(dyld_shared_cache_t cache) {
dyld_shared_cache_for_each_image(cache, ^(dyld_image_t image) {
if (outparam) {
return;
}
const char* cur = my_dyld_image_get_installname(image);
if (!strcmp(path, cur)) {
outparam = cur;
}
});
});
return outparam;
}
static
const char* dsc_image_as_name(const char *name) {
if (!can_use_dyld_apis) {
return NULL;
}
const char *dsc_path = dyld_shared_cache_file_path();
__block const char *outparam = NULL;
dyld_shared_cache_for_file(dsc_path, ^(dyld_shared_cache_t cache) {
dyld_shared_cache_for_each_image(cache, ^(dyld_image_t image) {
const char* cur = my_dyld_image_get_installname(image);
if (outparam) {
return;
}
if (!strcmp(name, basename((char*)cur))) {
outparam = strdup(cur);
}
});
});
return outparam;
}
void dump_dsc_images(void) {
if (!can_use_dyld_apis) {
return;
}
const char *dsc_path = dyld_shared_cache_file_path();
dyld_shared_cache_for_file(dsc_path, ^(dyld_shared_cache_t cache) {
__block uint32_t counter = 0;
dyld_shared_cache_for_each_image(cache, ^(dyld_image_t image) {
log_out("%s%5d%s %s%s%s\n", DYELLOW, ++counter, DCOLOR_END, DCYAN, my_dyld_image_get_installname(image), DCOLOR_END);
});
});
}
static void thread_walkback_frames_to_safe_code(thread_t thread) {
#if defined(__arm64__)
arm_thread_state64_t state = {};
mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT;
HANDLE_ERR(thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, &count));
Dl_info pinfo, linfo;
dladdr((void*)state.__pc, &pinfo);
dladdr((void*)state.__lr, &linfo);
log_debug("caught message\n pc: 0x%012llx %s\n lr: 0x%012llx %s\n", state.__pc, pinfo.dli_sname, state.__lr, linfo.dli_sname);
#ifdef __arm64__
#elif __x86_64__
TODO: implement geriatric CPU arch
#endif
arm_debug_state64_t dbg = {};
mach_msg_type_number_t dbg_cnt = ARM_DEBUG_STATE64_COUNT;
HANDLE_ERR(thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&dbg, &dbg_cnt));
// lldb puts a breakpoint on _dyld_debugger_notification, so catch & release
Dl_info info = {};
uintptr_t pc = (uintptr_t)strip_pac((void*)state.__pc);
if (dladdr((void*)pc, &info) != 0) {
if (!strcmp(info.dli_sname, "_dyld_debugger_notification")) {
log_debug("it's _dyld_debugger_notification\n");
state.__pc = state.__lr;
HANDLE_ERR(thread_set_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, count));
return;
}
}
for (uint8_t i = 0; i < constructor_addresses_count; i++) {
if (constructor_addresses[i] == ((uintptr_t)strip_pac((void*)state.__pc))) {
if (g_debug) {
log_out("found caller 0x%012lx\n", constructor_addresses[i])
}
state.__pc += ARM64_OPCODE_SIZE;
HANDLE_ERR(thread_set_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, count))
return;
}
}
// This is for all other code that I've missed
uintptr_t sp = (uintptr_t)strip_pac((void*)state.__sp);
struct fp_ptr *frame = strip_pac((void*)state.__fp);
while (frame && frame->next != NULL) {
off_t offset = ((uintptr_t)frame - sp) + sizeof(struct fp_ptr);
sp += offset;
// walk back the stack frames looking for libobjc / [lib]dyld
const void* addr = strip_pac(frame->address);
const char* path = dyld_image_path_containing_address(addr);
if (!strcmp("/usr/lib/libobjc.A.dylib", path) ||
!strcmp("/usr/lib/system/libdyld.dylib", path) ||
!strcmp("/usr/lib/dyld", path)) {
break;
}
frame = strip_pac(frame->next);
}
state.__lr = (uintptr_t)strip_pac(frame->address);
state.__pc = (uintptr_t)strip_pac(frame->address);
state.__fp = (uintptr_t)strip_pac(frame->next);
state.__sp = sp;
HANDLE_ERR(thread_set_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, count))
#endif
}
void* server_thread(void *arg) {
pthread_setname_np("Exception Handler");
thread_t thread = (thread_t)(uintptr_t)arg;
mach_port_options_t options = {.flags = MPO_INSERT_SEND_RIGHT};
HANDLE_ERR(mach_port_construct(mach_task_self(), &options, 0, &exc_port));
HANDLE_ERR(thread_set_exception_ports(thread, EXC_MASK_ALL, exc_port, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, THREAD_STATE_NONE));
#if defined(__arm64__)
arm_debug_state64_t dbg = {};
mach_msg_type_number_t cnt = ARM_DEBUG_STATE64_COUNT;
HANDLE_ERR(thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&dbg, &cnt));
for (int i = 0; i < constructor_addresses_count; i++) {
dbg.__bvr[i] = (__int64_t)constructor_addresses[i];
dbg.__bcr[i] = S_USER|BCR_ENABLE|BCR_BAS;
}
HANDLE_ERR(thread_set_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&dbg, cnt));
#elif defined(__x86_64__)
#else
#error "da fuck you compiling?"
#endif
pthread_cond_signal(&exception_cond); // Signal the main thread
pthread_mutex_unlock(&exception_mutex);
kern_return_t kr = KERN_SUCCESS;
while(1) {
char buffer[GOOD_E_NUFF_BUFSIZE];
mach_msg_header_t *msg = (void*)buffer;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_id = 2405;
msg->msgh_local_port = exc_port;
msg->msgh_size = GOOD_E_NUFF_BUFSIZE;
if ((kr = mach_msg_receive(msg))) {
// other thread will mod -1 the port so ignore MACH_RCV_PORT_CHANGED
if (kr != MACH_RCV_PORT_CHANGED) {
fprintf(stderr, "recv err %s %x\n", mach_error_string(kr), kr);
HANDLE_ERR(kr);
}
break;
}
exc_req* req = ((exc_req*)msg);
thread_t thread = req->thread.name;
thread_walkback_frames_to_safe_code(thread);
msg->msgh_local_port = MACH_PORT_NULL;
msg->msgh_bits = MACH_RCV_MSG | MACH_SEND_TIMEOUT;
msg->msgh_id = 2505;
msg->msgh_size = sizeof(exc_resp);
exc_resp *resp = (exc_resp*)msg;
resp->NDR = NDR_record;
resp->RetCode = KERN_SUCCESS;
if ((kr = mach_msg_send(msg))) {
HANDLE_ERR(kr);
break;
}
}
return NULL;
}
/*********************************************************************/
# pragma mark - public -
/*********************************************************************/
DYNAMIC_DUMP_VISIBILITY
void dump_all_objc_classes(bool do_classlist, const char *path, const struct mach_header_64* header_pac) {
struct mach_header_64 *header = strip_pac((void*)header_pac);
// if we have the mach header we don't have to iterate all classes
if (header) {
unsigned long size = 0;
const char *segments[] = { "__DATA", "__DATA_CONST"};
for (int z = 0; z < 2; z++) {
// dirty knowledge of the layout but we need the protocol names
struct objc_protocol_t {
uintptr_t isa;
const char* name;
//.. more, but whatever
};
size = 0;
if (!do_classlist) {
struct objc_protocol_t** protocols = (void*)getsectiondata(header, segments[z], "__objc_protolist", &size);
for (int i = 0; i < (size / sizeof(uintptr_t)); i++) {
struct objc_protocol_t *prot = protocols[i];
if (prot->name) {
Protocol *p = objc_getProtocol(prot->name);
if (!p) {
continue;
}
dump_objc_protocol_info(p);
log_out("\n");
}
}
}
}
// at runtime all implementations are realized so we'll capture all classes
// and if there's a category that references the class, we'll note it.
// if the class hasn't been dumped at the category stage, we'll dump it there
NSMutableSet <Class>*classSet = [NSMutableSet set];
for (int z = 0; z < 2; z++) {
size = 0;
Class *classes = (__unsafe_unretained Class*)(void*)getsectiondata(header, segments[z], "__objc_classlist", &size);
for (int i = 0; i < size / sizeof(uintptr_t); i++) {
Class cls = classes[i];
if (class_respondsToSelector(cls, @selector(doesNotRecognizeSelector:))) {
[classSet addObject:cls];
} else {
log_error("non-NSObject root class, \"%s\", skipping\n", class_getName(cls));
continue;
}
if (do_classlist) {
Class supercls = class_getSuperclass(cls);
log_out("%s0x%016lx%s %s%s%s : %s%s%s\n", DGRAY, (uintptr_t)cls, DCOLOR_END, DYELLOW, class_getName(cls), DCOLOR_END, DGREEN, class_getName(supercls), DCOLOR_END);
} else {
dump_objc_class_info(cls);
}
}
}
if (!do_classlist) {
for (int z = 0; z < 2; z++) {
size = 0;
// Internal header eeeeeeeeek, but no APIs for categories : [
struct category_t {
const char *name;
Class cls;
void* instanceMethods;
void* classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
};
struct category_t **categories = (struct category_t**)getsectiondata(header, segments[z], "__objc_catlist", &size);
for (int i = 0; i < size / sizeof(uintptr_t); i++) {
struct category_t *cat = categories[i];
// TODO: not quite accurate, think of a better way of describing this to user, objc categories don't have the APIs to show which category they
// are coming from, so check if we've printed the cls already or see if
// they are in the same image as we're inspecting
if (![classSet containsObject:cat->cls]) {
if (dyld_image_header_containing_address((__bridge const void *)(cat->cls)) != (void*)header) {
log_out("%s// category for %s, which is declared in \"%s\"%s\n", DRED, class_getName(cat->cls), dyld_image_path_containing_address((__bridge const void *)(cat->cls)), DCOLOR_END);
}
dump_objc_class_info(cat->cls);
} else {
log_out("%s@interface%s %s%s (%s) %s// category%s\n", DMAGENTA, DCOLOR_END, DYELLOW, class_getName(cat->cls), cat->name, DGRAY, DCOLOR_END);
log_out("%s@end%s\n", DYELLOW, DCOLOR_END);
}
}
}
}
} else { // plan B is to just load everything and dump it
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
log_debug("found %d classes...\n", count);
for (int i = 0; i < count; i++) {
Class cls = classes[i];
void* isa = (void*)get_cls_isa(cls);
const char * curpath = dyld_image_path_containing_address(isa);
log_debug("%s %s\n", class_getName(cls), curpath);
if (!curpath) {
continue;
}
if (strcmp(curpath, path)) {
continue;
}
if (do_classlist) {
Class supercls = class_getSuperclass(cls);
log_out("%s0x%016lx%s %s%s%s : %s%s%s\n", DGRAY, (uintptr_t)cls, DCOLOR_END, DYELLOW, class_getName(cls), DCOLOR_END, DGREEN, class_getName(supercls), DCOLOR_END);
} else {
dump_objc_class_info(cls);
}
}
}
}
DYNAMIC_DUMP_VISIBILITY
void dump_objc_protocol_info(Protocol *p) {
unsigned int count = 0;
struct objc_method_description *descriptions = NULL;
if (!p) {
return;
}
Protocol *prot = (__bridge Protocol*)strip_pac((__bridge void *)(p));
const char* name = protocol_getName(prot);
log_out(" %s@protocol %s%s", DYELLOW_LIGHT, name, DCOLOR_END);
if (g_verbose) {
log_out(" %s// 0x%012lx%s", DGRAY, (uintptr_t)prot, DCOLOR_END);
}
log_out("\n");
// required class
descriptions = protocol_copyMethodDescriptionList(prot, YES, NO, &count);
for (uint i = 0; i < count; i++) {
struct objc_method_description *desc = &descriptions[i];
extract_and_print_method((Method)desc, name, 0, YES, YES);
}
free(descriptions);
count = 0;
// required instance
descriptions = protocol_copyMethodDescriptionList(prot, YES, YES, &count);
for (uint i = 0; i < count; i++) {
struct objc_method_description *desc = &descriptions[i];
extract_and_print_method((Method)desc, name, 0, NO, YES);
}
free(descriptions);
count = 0;
// optional class
descriptions = protocol_copyMethodDescriptionList(prot, NO, NO, &count);
bool did_print_optional = false;
if (count) {
did_print_optional = true;
log_out(" %s@optional%s\n", DYELLOW_LIGHT, DCOLOR_END);
}
for (uint i = 0; i < count; i++) {
struct objc_method_description *desc = &descriptions[i];
extract_and_print_method((Method)desc, name, 0, YES, YES);
}
free(descriptions);
count = 0;
// optional instance
descriptions = protocol_copyMethodDescriptionList(prot, NO, YES, &count);
if (count && did_print_optional == false) {
log_out(" %s@optional%s\n", DYELLOW_LIGHT, DCOLOR_END);
}
for (uint i = 0; i < count; i++) {
struct objc_method_description *desc = &descriptions[i];
extract_and_print_method((Method)desc, name, 0, NO, YES);
}
log_out(" %s@end%s\n", DYELLOW_LIGHT, DCOLOR_END);
}
DYNAMIC_DUMP_VISIBILITY
void dlopen_n_dump_objc_classes(const char *arg, const char*clsName, bool do_classlist) {
void* handle = NULL;
uint32_t dsc_num = strtod(arg, NULL);
const char *path = NULL;
// can we even open this thing? i.e. for cases like dynadump dump Foundation
handle = dlopen(arg, RTLD_NOLOAD);
if (dsc_num) {
path = dsc_image_as_num(dsc_num);
} else if (handle) {
path = strdup(arg);
} else {
path = dsc_image_as_name(arg);
log_debug("changing arg %s -> %s\n", arg, path);
// if we can't find a path we'll just start with the OG and fail later if needed
if (!path) {
path = strdup(arg);
}
}
// if (!path) {
// log_error("couldn't find a dsc image \"%s\"\n", arg);
// return;
// }
// use stderr, so everything else is still grep-able
fprintf(stderr, "%s%s%s\n", DCYAN, path, DCOLOR_END);
#if 0
#warning you've got normal dlopen going, derek
handle = dlopen(path, RTLD_NOW);
#else
if (getenv("NOEXC")) {
handle = dlopen(path, RTLD_NOW);
} else {
handle = safe_dlopen(path);
}
#endif
if (!handle) {
log_error("couldn't open \"%s\"\n", arg);
return;
}
task_dyld_info_data_t info;
mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
HANDLE_ERR(task_info(mach_task_self(), TASK_DYLD_INFO, (task_info_t)&info, &count));
struct dyld_all_image_infos *all_image_infos = (void*)info.all_image_info_addr;
struct dyld_image_info *imageArray = (void*)all_image_infos->infoArray;
const struct mach_header_64* header = NULL;
for (uint i = 0; i < all_image_infos->infoArrayCount; i++) {
struct dyld_image_info *info = &imageArray[i];
if (!strcmp(info->imageFilePath, path)) {
header = (void*)info->imageLoadAddress;
break;
}
}
// Try again with a different path in the case dlopen views the lib as a different nmae
if (!header) {
for (uint i = 0; i < all_image_infos->infoArrayCount; i++) {
struct dyld_image_info *info = &imageArray[i];
if (!strcmp(basename((char *)info->imageFilePath), basename((char *)path))) {
header = (void*)info->imageLoadAddress;
break;
}
}
}
if (clsName) {
Class cls = objc_getClass(clsName);
dump_objc_class_info(cls);
} else {
dump_all_objc_classes(do_classlist, path, header);
}
if (path) {
free((void*)path);
}
}
DYNAMIC_DUMP_VISIBILITY
void* safe_dlopen(const char *image) {
#if defined(__arm64__)
// setup a handler in case a constructor tries to take us down
pthread_mutex_lock(&exception_mutex);
static pthread_t exception_thread;
if (pthread_create(&exception_thread, NULL, server_thread, (void*)(uintptr_t)mach_thread_self())) {
return NULL;
}
pthread_detach(exception_thread);
pthread_cond_wait(&exception_cond, &exception_mutex);
void* handle = dlopen(image, RTLD_NOW);
// remove breakpoints
arm_debug_state64_t dbg = {};
mach_msg_type_number_t cnt = ARM_DEBUG_STATE64_COUNT;
HANDLE_ERR(thread_get_state(mach_thread_self(), ARM_DEBUG_STATE64, (thread_state_t)&dbg, &cnt));
for (int i = 0; i < constructor_addresses_count; i++) {
dbg.__bcr[i] &= ~BCR_ENABLE;
}
HANDLE_ERR(thread_set_state(mach_thread_self(), ARM_DEBUG_STATE64, (thread_state_t)&dbg, cnt));
// remove the handler so debuggers can catch f ups better
HANDLE_ERR(thread_set_exception_ports(mach_thread_self(), EXC_MASK_ALL, MACH_PORT_NULL, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, THREAD_STATE_NONE));
// yanking the port will break the while loop on in the server_thread
mach_port_mod_refs(mach_task_self(), exc_port, MACH_PORT_RIGHT_RECEIVE, -1);
return handle;
#else
return NULL;
#endif
}
static void _dump_ivar_description(id instanceOrCls, bool standaloneDescription) {
const char *imagePath = dyld_image_path_containing_address((__bridge const void * _Nonnull)(instanceOrCls));
Class cls = objc_opt_class(instanceOrCls);
bool isClass = (instanceOrCls == cls) ? true: false;
unsigned int ivarCount = 0;
if (standaloneDescription) {
if (isClass) {
log_out("%s", class_getName(cls));
} else {
log_out("%s <%p>", class_getName(cls), instanceOrCls);
}
Class superCls = class_getSuperclass(cls);
if (superCls) {
log_out(": %s ", class_getName(superCls));
}
if (imagePath) {
log_out(" %s(%s)%s\n", DYELLOW_LIGHT, imagePath, DCOLOR_END);
} else {
log_out(" %s(?)%s\n", DYELLOW_LIGHT, DCOLOR_END);
}
}
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
if (ivarCount) {
log_out("\n {\n");
}
for (uint i = 0; i < ivarCount; i++) {
const char *typeEncoding = ivar_getTypeEncoding(ivars[i]);
typeEncoding = typeEncoding ? typeEncoding : "";
const char *name = ivar_getName(ivars[i]);
long int offset = ivar_getOffset(ivars[i]);
char buffer[GOOD_E_NUFF_BUFSIZE];
get_object_type_description(typeEncoding, buffer);
if (isClass) {
log_out(" %s/* +0x%04lx */%s %s%s%s %s%s%s\n", DGRAY, offset, DCOLOR_END, DCYAN_LIGHT, buffer, DCOLOR_END, DCYAN, name, DCOLOR_END);
} else {
log_out(" %s/* +0x%04lx 0x%016lx */%s %s%s%s %s%s%s \n", DGRAY, offset, *(uintptr_t*)((uintptr_t)instanceOrCls + offset), DCOLOR_END, DCYAN_LIGHT, buffer, DCOLOR_END, DCYAN, name, DCOLOR_END);
}
}
free(ivars);
if (ivarCount) {
log_out(" }\n");
}
}
DYNAMIC_DUMP_VISIBILITY
void dump_ivar_description(id instanceOrCls) {
_dump_ivar_description(instanceOrCls, true);
}
DYNAMIC_DUMP_VISIBILITY
void dump_method_description(id instanceOrCls) {
Class cls = objc_opt_class(instanceOrCls);
const char* clsName = class_getName(cls);
uintptr_t image_start = (uintptr_t)dyld_image_header_containing_address((__bridge const void * _Nonnull)(cls));
unsigned int metaMethodCount = 0;
Class metaCls = objc_getMetaClass(clsName);
Class superCls = class_getSuperclass(cls);
if (!superCls) {
log_out( "%sNS_ROOT_CLASS%s ", DYELLOW, DCOLOR_END);
}
log_out( "%s@interface%s %s%s%s ", DMAGENTA, DCOLOR_END, DYELLOW, clsName, DCOLOR_END);
// superclass
if (superCls) {
const char* superClsName = class_getName(superCls);
if (superClsName) {
log_out( "%s: %s%s ", DYELLOW, superClsName, DCOLOR_END);
}
}
// protocols
unsigned int cnt = 0;
Protocol * __unsafe_unretained _Nonnull * _Nullable protocols = class_copyProtocolList(cls, &cnt);
for (int i = 0; i< cnt; i++) {
if (i == 0) {
log_out("%s<", DGREEN)
}
log_out("%s", protocol_getName(protocols[i]));
if (cnt > 1 && i < cnt - 1) {
log_out(", ")
}
if (i == cnt - 1) {
log_out(">%s", DCOLOR_END);
}
}
if (protocols) {
free((void*)protocols);
}
// ivars
_dump_ivar_description(cls, false);
// Properties
bool has_done_newline = false;
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
if (propertyCount) {
log_out( "\n\n %s// \"%s\" properties:%s\n", DGRAY, clsName, DCOLOR_END);
has_done_newline = true;
}
for (uint i = 0; i < propertyCount; i++) {
char buffer[GOOD_E_NUFF_BUFSIZE];
if (get_property_description(&properties[i], buffer)) {
log_error( "\nfailed to parse \"%s\"", property_getName(properties[i]));
}
log_out( " %s", buffer);
}
free(properties);
if (propertyCount) {
log_out( "\n");
}
// Class methods
if (metaCls) {
if (has_done_newline == false) {
has_done_newline = true;
log_out("\n\n");
}
Method *clsMethods = class_copyMethodList(metaCls, &metaMethodCount);
if (metaMethodCount) {
log_out( " %s// \"%s\" class methods:%s\n", DGRAY, clsName, DCOLOR_END);
}
for (uint i = 0; i < metaMethodCount; i++) {
extract_and_print_method(clsMethods[i], clsName, image_start, YES, NO);
}
if (metaMethodCount) {
log_out( "\n");
}
free(clsMethods);
}
// Instance methods
if (cls) {
unsigned int methodCount = 0;
Method *instanceMethods = class_copyMethodList(cls, &methodCount);
if (has_done_newline == false) {
log_out("\n\n");
has_done_newline = true;
}
if (methodCount) {
log_out( " %s// \"%s\" instance methods:%s\n", DGRAY, clsName, DCOLOR_END);
}
for (uint i = 0; i < methodCount; i++) {
extract_and_print_method(instanceMethods[i], clsName, image_start, NO, NO);
}
free(instanceMethods);
}
if (has_done_newline) {
log_out("\n");
}
log_out( "%s@end%s\n\n", DYELLOW, DCOLOR_END);
log_debug("leaving %s:%d\n", __FUNCTION__, __LINE__);
}
void dump_objc_class_info(Class cls) {
uintptr_t isa = get_cls_isa(cls);
const char* path = dyld_image_path_containing_address((const void*)isa);
log_out("%s//> %s %s%s\n", DGRAY, class_getName(cls), path, DCOLOR_END);
dump_method_description(cls);
}
__attribute__((constructor)) static void setup(void) {
g_color = getenv("NOCOLOR") ? false : true;
g_debug = getenv("DEBUG") ? true : false;
g_verbose = getenv("VERBOSE") ? 5 : 0;
if (isatty(STDOUT_FILENO) == 0) {
g_color = false;
}
if (getenv("COLOR")) {
g_color = true;
}
}
#ifndef SHARED_DYNAMIC_DUMP
void print_help(void) {
log_out("\n dynadump (built: %s, %s) - yet another class-dump done via dlopen & exception catching\n\n", __DATE__, __TIME__);
log_out("\tParameters:\n")
log_out("\tlist list all the dylibs in the dyld shared cache (dsc)\n");
log_out("\tlist $DYLIB list all the objc classes in a dylib $DYLIB\n");
log_out("\tdump $DYLIB dump all the ObjC classes found in a dylib on disk\n");
log_out("\tdump $DYLIB $CLASS dump a specific ObjC class found in dylib $DYLIB\n");
log_out("\tlist $DYLIB $CLASS Same cmd as above (convenience for listing then dumping)\n");
log_out("\n\tEnvironment Variables:\n");
log_out("\tNOCOLOR - Forces no color, color will be on by default unless piped\n");
log_out("\tCOLOR - Forces color, regardless of stdout destination\n");
log_out("\tVERBOSE - Verbose output\n");
log_out("\tNOEXC - Don't use an exception handler (on in x86_64)\n");
log_out("\tDEBUG - Used internally to hunt down f ups\n");
exit(1);
}
int main(int argc, const char * argv[]) {
if (argc == 1) {
print_help();
}
#if defined(__x86_64__)
setenv("NOEXC", "meow", 1);
#endif
// list the dsc images
if (!strcmp("list", argv[1])) {
if (argc == 2) {
dump_dsc_images();
} else if (argc == 3) {
dlopen_n_dump_objc_classes(argv[2], NULL, true);
} else if (argc == 4) {
dlopen_n_dump_objc_classes(argv[2], argv[3], true);
}
exit(0);
}
if (!strcmp("dump", argv[1])) {
if (argc < 3) {
log_error("dump <NUM|PATH_2_DYLIB\n");
exit(1);
}
dlopen_n_dump_objc_classes(argv[2], argc > 2 ? argv[3] : NULL, false);
exit(0);
}
print_help();
return 0;
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment