mac app 逆向之动态注入

2020年05月04日

跑通了一个最简单的动态注入的 demo,记录一下以防忘记。

原理相关

在 Mac 上,应用最终会 build 成可执行的二进制文件,而逆向就是为了改变应用原本的功能。这里实现的一种方法是:通过编写动态链接库将额外逻辑注入到应用中。 本文用到了 insert_dylib 注入工具:

Command line utility for inserting a dylib load command into a Mach-O binary.

这个工具应该是可以直接修改应用二进制文件,使应用加载时加载自己编写的动态库。

Objective-C的首选hook方案为Method Swizzle。

步骤

先用 objective-c 实现一个最简单的应用

Person.h

 1#import <Foundation/Foundation.h>
 2
 3NS_ASSUME_NONNULL_BEGIN
 4
 5@interface Person : NSObject
 6
 7@property (nonatomic) NSString *name;
 8@property (nonatomic) NSNumber *age;
 9
10@end
11
12NS_ASSUME_NONNULL_END

Person.m

 1#import "Person.h"
 2
 3@implementation Person
 4
 5- (NSString *)description
 6{
 7    return [NSString stringWithFormat:@"<%@: %@>", self.name, self.age];
 8}
 9
10@end

main.m

 1#import <Foundation/Foundation.h>
 2#import "Person.h"
 3
 4int main(int argc, const char * argv[]) {
 5    @autoreleasepool {
 6        NSLog(@"hello liqiang");
 7        
 8        Person *p = [[Person alloc] init];
 9        [p setName:@"liqiang"];
10        [p setAge:@26];
11        
12        NSLog(@"person: %@", p);
13    }
14    return 0;
15}

编译执行后会输出:

2020-05-04 17:09:19.146 hello[45102:759505] hello liqiang
2020-05-04 17:09:19.147 hello[45102:759505] person: <liqiang: 26>

hook 项目

新建 Library 类型的项目。下面的 hookCommon 相关代码实现的功能是替换应用中对应的方法为自己编写的方法,应该是 Method Swizzl

hookCommon.h

 1#import <Foundation/Foundation.h>
 2#import <objc/runtime.h>
 3
 4@interface hookCommon : NSObject
 5
 6/**
 7 替换对象方法
 8
 9 @param originalClass 原始类
10 @param originalSelector 原始类的方法
11 @param swizzledClass 替换类
12 @param swizzledSelector 替换类的方法
13 */
14void hookMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector);
15
16/**
17 替换类方法
18 
19 @param originalClass 原始类
20 @param originalSelector 原始类的类方法
21 @param swizzledClass 替换类
22 @param swizzledSelector 替换类的类方法
23 */
24void hookClassMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector);
25
26@end

hookCommon.m

 1#import "hookCommon.h"
 2
 3@implementation hookCommon
 4
 5/**
 6 替换对象方法
 7 
 8 @param originalClass 原始类
 9 @param originalSelector 原始类的方法
10 @param swizzledClass 替换类
11 @param swizzledSelector 替换类的方法
12 */
13void hookMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector) {
14    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
15    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
16    if (originalMethod && swizzledMethod) {
17        method_exchangeImplementations(originalMethod, swizzledMethod);
18    }
19}
20
21/**
22 替换类方法
23 
24 @param originalClass 原始类
25 @param originalSelector 原始类的类方法
26 @param swizzledClass 替换类
27 @param swizzledSelector 替换类的类方法
28 */
29void hookClassMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector) {
30    Method originalMethod = class_getClassMethod(originalClass, originalSelector);
31    Method swizzledMethod = class_getClassMethod(swizzledClass, swizzledSelector);
32    if (originalMethod && swizzledMethod) {
33        method_exchangeImplementations(originalMethod, swizzledMethod);
34    }
35}
36
37@end

然后替换掉 Person 类中的 description 方法,功能是调用原逻辑前先打印一行自己的逻辑代码。

Person_hook.h

 1#import <Foundation/Foundation.h>
 2
 3NS_ASSUME_NONNULL_BEGIN
 4
 5@interface NSObject (PersonHook)
 6
 7+ (void) hookPerson;
 8
 9@end
10
11NS_ASSUME_NONNULL_END
12

Person_hook.m

#import "Person_hook.h"
#import "hookCommon.h"

@implementation NSObject (PersonHook)

// hook person description
- (NSString *) personDescription
{
    NSLog(@"in personDescription");
    return [self personDescription];
}

+ (void) hookPerson
{
    NSLog(@"in hookPerson");
    hookMethod(objc_getClass("Person"), @selector(description), [self class], @selector(personDescription));
}

@end

main.m

1#import <Foundation/Foundation.h>
2#import "Person_hook.h"
3
4static void __attribute__((constructor)) initialize(void) {
5    NSLog(@"hook common inject success!");
6    
7    [NSObject hookPerson];
8}
9

__attribute__((constructor)) 修饰函数之后,便会在应用加载前执行,也就是相当于我们注入代码的逻辑入口。上面逻辑中会用方法 personDescription 替换 Person 类中的 description 方法,注意此方法中会调用自己,其实不会造成死循环,因为运行时两个方法已经交换了,所以是调用的原始逻辑。

实验

将上面 hook 项目 build 之后,得到 libhookCommon.dylib,然后将此动态库和第一步应用二进制文件 hello 放在同一目录,加上用到 insert_dylib 注入工具,然后执行注入命令:

1./insert_dylib libhookCommon.dylib hello

生成 hello_patched 程序,运行此程序:

1./hello_patched

得到的输出为:

2020-05-04 17:23:18.461 hello_patched[45156:766500] hook common inject success!
2020-05-04 17:23:18.461 hello_patched[45156:766500] in hookPerson
2020-05-04 17:23:18.461 hello_patched[45156:766500] hello liqiang
2020-05-04 17:23:18.461 hello_patched[45156:766500] in personDescription
2020-05-04 17:23:18.462 hello_patched[45156:766500] in personDescription
2020-05-04 17:23:18.462 hello_patched[45156:766500] person: <liqiang: 26>

通过输出可以看出已经加入了自己的逻辑进来了。

思考

一般要想修改某个应用的逻辑功能,肯定是拿不到源代码的,所以通过二进制代码理解程序逻辑这一步,才是难点中的难点。必须要了解汇编,一些逆向的工具,还需要经验等等。而本文也仅是作为新手的我所能了解到的一些皮毛而已。

参考