一、工具 在逆向的过程中,我们经常需要往目标执行文件中注入自己的逻辑,从而实现 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 并没有被修改。
...