一、工具
在逆向的过程中,我们经常需要往目标执行文件中注入自己的逻辑,从而实现 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 并没有被修改。
四、总结
动态库注入的流程总结如下:
- 判断 Load Command 类型(LC_REEXPORT_DYLIB/LC_LOAD_WEAK_DYLIB/LC_LOAD_UPWARD_DYLIB/LC_LOAD_DYLIB);
- 判断动态库是否已经注入(通过遍历 Load Command 段,取出几种 dylib 的 LC 的 name 字段跟加载动态库的路径做比对);
- 判断 Load Command 段末尾是否还有空白位置(1. segment 中进行页对齐时会产生空白区域;2. 避免改变可执行文件大小;3. MachO 里几乎所有操作都基于偏移量进行计算,如果直接插入新的数据,会导致后面的偏移量都需要调整。);
- 新建 dylib_command 并替换到对应位置的空白区域;
- 修改 header 内容;
- 把动态库复制到应用内的 Frameworks 文件夹中,重签名后打包成 ipa 包并安装到手机;
- iOS 系统使用 dyld 加载应用,加载的 Load Command 时会找到对应的动态库并进行动态链接。