Skip to main content

Understanding the Objective-C Runtime

The Objective-C runtime is a dynamic library that provides the foundation for Objective-C’s object-oriented features. Unlike statically compiled languages, Objective-C resolves method calls, manages objects, and handles message passing at runtime.

Key Runtime Capabilities

Message Dispatch

All method calls go through objc_msgSend for dynamic dispatch

Introspection

Query classes, methods, properties, and instance variables at runtime

Dynamic Modification

Add methods, exchange implementations, and modify classes on-the-fly

Type Encoding

Runtime type information stored as encoded strings
This dynamic nature makes Objective-C extremely powerful for reverse engineering - you can inspect and modify almost everything at runtime.

Runtime Architecture

Message Sending

Every method call in Objective-C is actually a message send:
// Source code
[myObject doSomethingWithValue:42];

// Compiled to
objc_msgSend(myObject, @selector(doSomethingWithValue:), 42);
The runtime performs these steps:
1

Selector Lookup

Find the method implementation (IMP) for the selector in the class’s method list
2

Cache Check

Check the method cache for recent lookups (optimization)
3

Inheritance Walk

If not found, walk up the inheritance chain to superclasses
4

Message Forwarding

If still not found, trigger message forwarding mechanism
5

Execute IMP

Call the function pointer (IMP) with the receiver and arguments

Class Structure

At runtime, each Objective-C class is represented by a Class structure containing:
struct objc_class {
    Class isa;                      // Metaclass pointer
    Class super_class;              // Superclass pointer
    const char *name;               // Class name
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;   // Instance variables
    struct objc_method_list **methodLists;  // Methods
    struct objc_cache *cache;       // Method cache
    struct objc_protocol_list *protocols;   // Protocols
};
Every class also has a metaclass - a class that describes the class itself. This is how class methods work in Objective-C.

Class Dumping

Class dumping extracts the interface declarations of all classes in a binary, revealing the app’s structure without access to source code.

Why Class Dump?

Class dumping provides:
  • Complete class hierarchies and inheritance relationships
  • All public, private, and internal method signatures
  • Property declarations and instance variable names
  • Protocol conformances
  • Type information for method parameters and return values

Tools for Class Dumping

The classic command-line tool for extracting class information:
# Install via Homebrew
brew install class-dump

# Dump classes from an app binary
class-dump /path/to/app/binary > headers.h

# Dump specific architecture on universal binaries
class-dump --arch arm64 /path/to/app/binary > headers.h
class-dump only works on unencrypted binaries. App Store apps must be decrypted first using tools like Clutch or frida-ios-dump.

Class Dump Output Example

UserManager.h
@interface UserManager : NSObject
{
    NSString *_currentUserId;
    BOOL _isPremiumUser;
    NSMutableDictionary *_userCache;
}

@property (nonatomic, copy) NSString *currentUserId;
@property (nonatomic, assign) BOOL isPremiumUser;

- (id)initWithUserId:(NSString *)userId;
- (void)loginWithEmail:(NSString *)email password:(NSString *)password completion:(void (^)(BOOL success, NSError *error))completion;
- (void)logout;
- (BOOL)isUserAuthenticated;
- (void)updateUserProfile:(NSDictionary *)profileData;
- (NSDictionary *)getUserData;

@end
This reveals:
  • Three instance variables (including their types)
  • Two properties
  • Six methods with complete signatures
  • The superclass (NSObject)

Method Listing

Beyond class dumping, you can enumerate and inspect methods at runtime.

Runtime Introspection APIs

Get all registered classes:
int numClasses = objc_getClassList(NULL, 0);
Class *classes = (Class *)malloc(sizeof(Class) * numClasses);
objc_getClassList(classes, numClasses);
Get all methods of a class:
unsigned int methodCount;
Method *methods = class_copyMethodList([MyClass class], &methodCount);
for (int i = 0; i < methodCount; i++) {
    SEL selector = method_getName(methods[i]);
    NSLog(@"%@", NSStringFromSelector(selector));
}
free(methods);
Get all properties:
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList([MyClass class], &propertyCount);
for (int i = 0; i < propertyCount; i++) {
    const char *name = property_getName(properties[i]);
    NSLog(@"%s", name);
}
free(properties);
Get instance variables:
unsigned int ivarCount;
Ivar *ivars = class_copyIvarList([MyClass class], &ivarCount);
for (int i = 0; i < ivarCount; i++) {
    const char *name = ivar_getName(ivars[i]);
    const char *type = ivar_getTypeEncoding(ivars[i]);
    NSLog(@"%s (%s)", name, type);
}
free(ivars);

Method Signature Analysis

Method signatures include type encodings:
- (void)setAge:(int)age;          // v@:i  (void, object, selector, int)
- (NSString *)name;                // @@:   (object, object, selector)
- (BOOL)isValid:(id)obj;          // B@:@  (BOOL, object, selector, object)

Type Codes

  • @ = object
  • : = SEL
  • v = void
  • i = int
  • B = BOOL
  • f = float
  • d = double

Modifiers

  • ^ = pointer
  • * = char*
  • # = Class
  • r = const
  • n = in
  • o = out

Structures

  • {name=...} = struct
  • [type] = array
  • (name=...) = union
  • bN = bitfield

Runtime Manipulation

The Objective-C runtime allows dynamic modification of classes and methods.

Adding Methods at Runtime

// Define a new method implementation
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic method called!");
}

// Add method to a class
Class myClass = [MyClass class];
class_addMethod(myClass, 
                @selector(dynamicMethod), 
                (IMP)dynamicMethodIMP, 
                "v@:");

// Now you can call it
MyClass *obj = [[MyClass alloc] init];
[obj dynamicMethod];

Method Replacement

// Get the original method
Method originalMethod = class_getInstanceMethod([MyClass class], @selector(originalMethod));

// Define replacement implementation
IMP newIMP = imp_implementationWithBlock(^(id self) {
    NSLog(@"Replaced implementation");
});

// Replace it
method_setImplementation(originalMethod, newIMP);
Modifying methods at runtime can cause crashes or unexpected behavior. Always test thoroughly and have fallback mechanisms.

Associated Objects

Add properties to existing classes without subclassing:
// Define a key (use static to ensure uniqueness)
static char kAssociatedObjectKey;

// Set associated object
objc_setAssociatedObject(myObject, 
                         &kAssociatedObjectKey, 
                         @"Custom data", 
                         OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// Get associated object
NSString *data = objc_getAssociatedObject(myObject, &kAssociatedObjectKey);
Association policies:
  • OBJC_ASSOCIATION_ASSIGN - Weak reference
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC - Strong reference
  • OBJC_ASSOCIATION_COPY_NONATOMIC - Copy the value
  • OBJC_ASSOCIATION_RETAIN - Strong, atomic
  • OBJC_ASSOCIATION_COPY - Copy, atomic

Frida for Runtime Inspection

Frida is the most powerful tool for runtime manipulation of iOS apps.

Basic Frida Script

frida-script.js
// Attach to running process
setTimeout(function() {
    Java.perform(function() {
        // List all loaded classes
        console.log("[*] Enumerating classes...");
        
        ObjC.enumerateLoadedClasses({
            onMatch: function(className) {
                if (className.includes("User")) {
                    console.log("[+] Found: " + className);
                    
                    // Get the class
                    var cls = ObjC.classes[className];
                    
                    // List methods
                    console.log("  Methods:");
                    cls.$ownMethods.forEach(function(method) {
                        console.log("    " + method);
                    });
                }
            },
            onComplete: function() {
                console.log("[*] Enumeration complete");
            }
        });
    });
}, 0);

Method Hooking with Frida

hook-login.js
// Hook a specific method
var targetClass = ObjC.classes.LoginViewController;
var targetMethod = targetClass["- loginWithEmail:password:completion:"];

Interceptor.attach(targetMethod.implementation, {
    onEnter: function(args) {
        // args[0] = self
        // args[1] = selector
        // args[2] = first argument (email)
        // args[3] = second argument (password)
        
        var email = ObjC.Object(args[2]);
        var password = ObjC.Object(args[3]);
        
        console.log("[*] Login attempt:");
        console.log("    Email: " + email.toString());
        console.log("    Password: " + password.toString());
    },
    onLeave: function(retval) {
        console.log("[*] Login method returned");
    }
});

Running Frida Scripts

# List running processes
frida-ps -Ua

# Attach to app and run script
frida -U -f com.example.app -l frida-script.js

# Attach to running app
frida -U "App Name" -l frida-script.js

# Spawn and attach
frida -U -f com.example.app --no-pause -l frida-script.js
Use frida-trace for quick method tracing without writing scripts:
frida-trace -U -f com.example.app -m "-[LoginViewController *]"

Swift Runtime Considerations

While Swift uses a different runtime model, it still interfaces with Objective-C:

Swift-ObjC Interoperability

// This Swift class is exposed to ObjC runtime
@objc class MySwiftClass: NSObject {
    @objc func myMethod() {
        print("Called from ObjC or Frida")
    }
}

// This one is NOT (pure Swift)
class PureSwiftClass {
    func pureSwiftMethod() {
        print("Not accessible from ObjC runtime")
    }
}
Swift classes marked with @objc or inheriting from NSObject are exposed to the Objective-C runtime and can be inspected/manipulated.
Pure Swift classes use static dispatch and vtables. They’re not accessible through the ObjC runtime but can still be analyzed with Swift-specific tools.
Swift symbols are name-mangled. Use swift-demangle or the SwiftNameDemangler.py script to decode them.

Practical Analysis Workflow

1

Decrypt the Binary

Use Clutch, frida-ios-dump, or CrackerXI to decrypt App Store apps
2

Class Dump

Extract all class headers to understand the app structure
class-dump MyApp > headers.h
3

Static Analysis

Load the binary in Ghidra or Hopper for disassembly analysis
4

Identify Target Methods

Find interesting methods from class dumps (authentication, encryption, etc.)
5

Runtime Hooking

Use Frida to hook and inspect method calls at runtime
6

Test and Validate

Verify your understanding by modifying behavior and observing results

Common Runtime Patterns

Singleton Detection

frida-singleton.js
// Find singleton accessor methods
ObjC.enumerateLoadedClasses({
    onMatch: function(className) {
        var cls = ObjC.classes[className];
        if (cls.$methods.some(m => m.includes("sharedInstance") || m.includes("shared"))) {
            console.log("[+] Potential singleton: " + className);
        }
    },
    onComplete: function() {}
});

Encryption Key Extraction

frida-keys.js
// Hook common crypto methods
var CCCrypt = Module.findExportByName("libcommonCrypto.dylib", "CCCrypt");
Interceptor.attach(CCCrypt, {
    onEnter: function(args) {
        console.log("[*] CCCrypt called");
        console.log("    Key: " + hexdump(args[3], { length: 32 }));
    }
});

Best Practices

Combine Approaches

Use both static and dynamic analysis for complete understanding

Start Broad

Begin with class dumps and general exploration before diving deep

Document Findings

Keep detailed notes on class relationships and method purposes

Test Safely

Use a jailbroken test device, not your primary device

Advanced Tools

Interactive runtime exploration:
# Attach to running app
cycript -p "App Name"

# Explore runtime
cy# [UIApp keyWindow]
cy# ObjectiveC.classes
cy# choose(LoginViewController)

Further Reading

Swift Name Mangling

Decode Swift mangled names in binaries

Method Swizzling

Detect and analyze method swizzling

Build docs developers (and LLMs) love