[iOS]原生swift的hotpatch可行性初探 续1

0x0 引子

之前在<原生swift的hotpatch可行性初探>对swift hotpatch的原理做一个简单的介绍和简单的示例, 但基础的原理分析并不能确定真实的可行性. 为此想通过这篇文章来做一个更复杂的例子.

0x1 先来一个简单的例子

来一个例子, 实现用js patch swift的方法, 功能包括:

  • 在js中通过类名/方法名/替换的方法, 来替换swift的方法
  • 在js中通过方法名来调用原有的swift方法

swift代码:

public class ViewController: UIViewController {
    override public func viewDidLoad() {
        super.viewDidLoad()
        patch_init()              // patch 初始化及完成patch操作
        aclass().hehe()        // 调用aclass的hehe方法, hehe方法里面会调用hehe1
    }
}

public class aclass {
    public func hehe() {
        hehe1()
    }
    
    public func hehe1() {
        print("hehe1")
    }
}

!!!请注意, 前方高能, 请务必阅读开头提到的上一篇文章!!!
patch方法的代码:

static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
void patched() {
    [jsFunction callWithArguments:nil];
}

void patch_init() {
    jsContext = [[JSContext alloc] init];
    jsContext[@"log"] = ^(NSString *message) {
        NSLog(@"%@", message);
    };
    
    jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
        void *class = (__bridge void *)objc_getClass(className.UTF8String);
        void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (!class || !raw_method_address) {
            NSLog(@"class or method note found!");
            return;
        }
        long offset = 0;
        for (long i=0; i<1024; i++) {
            if (*(long *)(class+i) == (long)raw_method_address) {
                offset = i;
                break;
            }
        }
        if (!offset) return;
        jsFunction = func;
        *(void **)(class+offset) = &patched;
    };
    
    jsContext[@"call"] = ^(NSString *methodName){
        void (*raw_method_address)() = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (raw_method_address) {
            raw_method_address();
        }
    };
    
    [jsContext evaluateScript:@"\
         function callback(){\
            log('patched hehe1');\
            log('calling raw method:');\
            call('_TFC9testswift6aclass5hehe1fT_T_');\
         }\
         patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_T_', callback);\
     "];
}

代码的最后一部分的js代码里面, 将testswift.aclass_TFC9testswift6aclass5hehe1fT_T_方法替换为了js写的callback方法, 而callback方法里面又调用了原始的_TFC9testswift6aclass5hehe1fT_T_方法.

运行结果:

2016-09-09 16:44:49.639 testswift[1725:677144] patched hehe1
2016-09-09 16:44:49.640 testswift[1725:677144] calling raw method:
hehe1

符合预期!

0x2 复杂一点

刚刚的两个方法里面没有参数, 也没有返回值, 实际上本身的复杂度就有了一定程度的降低, 在来点复杂的吧, 带上参数吧.
上代码, swift改成这样, 把hehe1加上参数String:

public class aclass {
    public func hehe() {
        hehe1("hehe")
    }
    
    public func hehe1(str: String) {
        print(str)
    }
}

那么问题来了, 这下就涉及到patch本身使用的语言及swift之间类型转换了.看了一下swift的短String的数据结构, 发现直接就是char*. 不过在传String值的时候, 实际上传了三个参数, char/int/void , 其中int是string长度, void*是留给objc的内存管理用的.
修改patch实现如下:

static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
void patched() {
    [jsFunction callWithArguments:@[@"patched"]];
}

void patch_init() {
    jsContext = [[JSContext alloc] init];
    jsContext[@"log"] = ^(NSString *message) {
        NSLog(@"%@", message);
    };
    
    jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
        void *class = (__bridge void *)objc_getClass(className.UTF8String);
        void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (!class || !raw_method_address) {
            NSLog(@"class or method note found!");
            return;
        }
        long offset = 0;
        for (long i=0; i<1024; i++) {
            if (*(long *)(class+i) == (long)raw_method_address) {
                offset = i;
                break;
            }
        }
        if (!offset) return;
        jsFunction = func;
        *(void **)(class+offset) = &patched;
    };
    
    jsContext[@"call"] = ^(NSString *methodName, NSString *parameter){
        void (*raw_method_address)(char *, long, long) = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (raw_method_address) {
            raw_method_address(parameter.UTF8String, strlen(parameter.UTF8String), 0);
        }
    };
    
    [jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) {
        NSLog(@"error: %@, %@", ctx.exception);
    }];
    
    [jsContext evaluateScript:@"\
         function callback(str){\
            log('calling: patched hehe1');\
            log('parameter: ' + str);\
            log('calling raw method:');\
            call('_TFC9testswift6aclass5hehe1fSST_', str);\ 
         }\
         patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fSST_', callback);\
     "];
}

跑一下结果:

calling: patched hehe1
parameter: patched
calling raw method:
patched

patch成功.
改动主要在于:

  • c方法patched里面调用js的callback增加了一个参数.
  • js方法call里面改变了调用原始方法的参数列表

0x3 再复杂一点

再搞复杂一点, 我们搞一个返回值, 并且返回值是一个原始的swift类, 我们在hook里面对swift类做一个修改!
swift代码:

public class aclass {
    public var p: UInt64 = 0xaaaaaaaaaaaaaaaa
    public func hehe() {
        let a = hehe1()
        print(a.p)
    }
    
    public func hehe1() -> aclass {
        return self
    }
}

正常情况下我们将看到输出12297829382473034410, 也就是0xaaaaaaaaaaaaaaaa的十进制. 我要把它改成1!

修改patch的实现:

static JSContext *jsContext = nil;
static JSValue *jsFunction = nil;
static void* selfHolder = NULL;

void *patched(void *self) {
    selfHolder = self;
    JSValue *ret = [jsFunction callWithArguments:nil];
    return (void *)[[ret toNumber] longLongValue];
}

void patch_init() {
    jsContext = [[JSContext alloc] init];
    jsContext[@"log"] = ^(NSString *message) {
        NSLog(@"%@", message);
    };
    
    jsContext[@"patch"] = ^(NSString *className, NSString *methodName, JSValue *func) {
        void *class = (__bridge void *)objc_getClass(className.UTF8String);
        void *raw_method_address = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (!class || !raw_method_address) {
            NSLog(@"class or method note found!");
            return;
        }
        long offset = 0;
        for (long i=0; i<1024; i++) {
            if (*(long *)(class+i) == (long)raw_method_address) {
                offset = i;
                break;
            }
        }
        if (!offset) return;
        jsFunction = func;
        *(void **)(class+offset) = &patched;
    };
    
    jsContext[@"call"] = (NSNumber*)^(NSString *methodName, NSString *parameter){
        __block void *ret = 0;
        void*(*raw_method_address)(void *) = dlsym(RTLD_DEFAULT, methodName.UTF8String);
        if (raw_method_address) {
            ret = raw_method_address(selfHolder);
        }
        return [[NSNumber alloc] initWithLong:(long)ret];
    };
    
    jsContext[@"memory_write"] = ^(NSNumber *ptr, NSNumber *off, NSNumber *val) {
        long long pointer = [ptr longLongValue];
        long offset = [off longValue];
        long long value = [val longLongValue];
        *(long *)(pointer+offset) = value;
    };
    
    [jsContext setExceptionHandler:^(JSContext *ctx, JSValue *v) {
        NSLog(@"error: %@, %@", ctx, ctx.exception);
    }];
    
    [jsContext evaluateScript:@"\
         function callback(){\
            log('calling: patched hehe1');\
            log('calling raw method:');\
            var ret = call('_TFC9testswift6aclass5hehe1fT_S0_');\
            memory_write(ret, 16, 1);\
            return ret;\
         }\
         patch('testswift.aclass', '_TFC9testswift6aclass5hehe1fT_S0_', callback);\
     "];
}

跑一下结果:

2016-09-09 19:10:31.138 testswift[2040:722528] calling: patched hehe1
2016-09-09 19:10:31.139 testswift[2040:722528] calling raw method:
1

请注意左下角的小1, 修改成功!
这里主要增加了memory_write方法来写内存数据, 用于修改对象的属性. 这里的offset 16可以通过工具来计算. 由于在hehe1中用到了self, swift方法也隐含了对self的传递, 所以在调用原有方法的之前保存了一下self指针, 在调原有方法的时候用.

0x4 小结

上面的patch的方法, 都具有一定的特定性, 无法满足所有需求, 但已经为可行性做了一个更强力的证明.
在改进通用性和易用性后, swift hotpatch的雏形就出来了.
另外在调用原始方法时, 因为不确定参数表, 需要使用libffi, 或者一个自定义的汇编实现来解决.
试验性的代码比较挫, 各位看官见谅!

上一篇:[C in ASM(ARM64)]第一章 一些实例


下一篇:Python3之Requests模块详解