一、工具

在逆向的过程中,我们经常需要往目标执行文件中注入自己的逻辑,从而实现 hook 的目的。

我们常用 optool 工具实现动态库注入。

Command Line Tool for interacting with MachO binaries on OSX/iOS

了解这个工具的原理,有助于我们对 MachO 文件有更深入的了解。

二、源码解析

关键代码如下:

BOOL insertLoadEntryIntoBinary(NSString *dylibPath, NSMutableData *binary, struct thin_header macho, uint32_t type) {
    // 判断 Load Command 类型
    if (type != LC_REEXPORT_DYLIB &&
        type != LC_LOAD_WEAK_DYLIB &&
        type != LC_LOAD_UPWARD_DYLIB &&
        type != LC_LOAD_DYLIB) {
        LOG("Invalid load command type");
        return NO;
    }
    // 判断动态库是否已经注入
    // parse load commands to see if our load command is already there
    uint32_t lastOffset = 0;
    if (binaryHasLoadCommandForDylib(binary, dylibPath, &lastOffset, macho)) {
        // there already exists a load command for this payload so change the command type
        uint32_t originalType = *(uint32_t *)(binary.bytes + lastOffset);
        if (originalType != type) {
            LOG("A load command already exists for %s. Changing command type from %s to desired %s", dylibPath.UTF8String, LC(originalType), LC(type));
            [binary replaceBytesInRange:NSMakeRange(lastOffset, sizeof(type)) withBytes:&type];
        } else {
            LOG("Load command already exists");
        }
        
        return YES;
    }
    
    // create a new load command
    unsigned int length = (unsigned int)sizeof(struct dylib_command) + (unsigned int)dylibPath.length;
    unsigned int padding = (8 - (length % 8));
    
    // 判断 Load Command 段末尾是否还有空白位置
    // check if data we are replacing is null
    NSData *occupant = [binary subdataWithRange:NSMakeRange(macho.header.sizeofcmds + macho.offset + macho.size,
                                                            length + padding)];

    // All operations in optool try to maintain a constant byte size of the executable
    // so we don't want to append new bytes to the binary (that would break the executable
    // since everything is offset-based–we'd have to go in and adjust every offset)
    // So instead take advantage of the huge amount of padding after the load commands
    if (strcmp([occupant bytes], "\0")) {
        NSLog(@"cannot inject payload into %s because there is no room", dylibPath.fileSystemRepresentation);
        return NO;
    }
    
    LOG("Inserting a %s command for architecture: %s", LC(type), CPU(macho.header.cputype));
    
    // 新建 dylib_command 并替换到对应位置的空白区域
    struct dylib_command command;
    struct dylib dylib;
    dylib.name.offset = sizeof(struct dylib_command);
    dylib.timestamp = 2; // load commands I've seen use 2 for some reason
    dylib.current_version = 0;
    dylib.compatibility_version = 0;
    command.cmd = type;
    command.dylib = dylib;
    command.cmdsize = length + padding;
    
    unsigned int zeroByte = 0;
    NSMutableData *commandData = [NSMutableData data];
    [commandData appendBytes:&command length:sizeof(struct dylib_command)];
    [commandData appendData:[dylibPath dataUsingEncoding:NSASCIIStringEncoding]];
    [commandData appendBytes:&zeroByte length:padding];
    
    // remove enough null bytes to account of our inserted data
    [binary replaceBytesInRange:NSMakeRange(macho.offset + macho.header.sizeofcmds + macho.size, commandData.length)
                      withBytes:0
                         length:0];
    // insert the data
    [binary replaceBytesInRange:NSMakeRange(lastOffset, 0) withBytes:commandData.bytes length:commandData.length];
    
    // 修改 header 内容,
    // fix the existing header
    macho.header.ncmds += 1;
    macho.header.sizeofcmds += command.cmdsize;
    
    // this is safe to do in 32bit because the 4 bytes after the header are still being put back
    [binary replaceBytesInRange:NSMakeRange(macho.offset, sizeof(macho.header)) withBytes:&macho.header];
    
    return YES;
}

三、MachO 文件变化

利用 MachOView 工具查看注入动态库后的 MachO 文件,可以发现在 Load Command 段的后面多了一个 LC,此 LC 明确了动态库的加载路径;且可发现其他段的 offset 并没有被修改。

image-20220307113428421

四、总结

动态库注入的流程总结如下:

  1. 判断 Load Command 类型(LC_REEXPORT_DYLIB/LC_LOAD_WEAK_DYLIB/LC_LOAD_UPWARD_DYLIB/LC_LOAD_DYLIB);
  2. 判断动态库是否已经注入(通过遍历 Load Command 段,取出几种 dylib 的 LC 的 name 字段跟加载动态库的路径做比对);
  3. 判断 Load Command 段末尾是否还有空白位置(1. segment 中进行页对齐时会产生空白区域;2. 避免改变可执行文件大小;3. MachO 里几乎所有操作都基于偏移量进行计算,如果直接插入新的数据,会导致后面的偏移量都需要调整。);
  4. 新建 dylib_command 并替换到对应位置的空白区域;
  5. 修改 header 内容;
  6. 把动态库复制到应用内的 Frameworks 文件夹中,重签名后打包成 ipa 包并安装到手机;
  7. iOS 系统使用 dyld 加载应用,加载的 Load Command 时会找到对应的动态库并进行动态链接。

image-20220308202107300