macOS 修改 MachO 文件实现动态库注入(四)
前面讲到将Mach-o文件依赖的系统动态库libSystem.B.dylib
替换为自定义的动态库会出现依赖符号缺失的问题,为了解决该问题,需要搜集系统库的导出符号。但该库在系统上找不到,所以符号搜集有些繁琐,也极有可能遗漏。本文介绍如何使用reexport_library
机制优雅地解决符号导出问题。本文所展示的代码片段来自开源项目 FishHook ,更多细节可参考该项目。
替换系统依赖库 上文讲到,因为字节对齐的原因,/usr/lib/libSystem.B.dylib
路径会占用 32 个字节。所以自定义的依赖库路径长度不要大于 31 字节,否则需要对后续二进制内容进行偏移。这里以/usr/local/lib/libinject.dylib
注入路径为例,替换系统依赖库。
示例代码如下,首先获取文件头的大小,以便定位到指令加载区。然后遍历该区域的所有指令,找到动态库依赖指令,即LC_LOAD_DYLIB
指令,然后获取指令中定义的动态库路径信息,获取路径时需要根据指令中的offset
字段定位路径字符串的起点,然后根据指令中的size
字段定位其终点。需要注意的是,指令的大小由于内存对齐的原因,一般会在末尾做补零操作,在获取路径后,需要将末尾补零去除。在定位到要替换的系统动态库依赖指令后,将其路径信息修改为要注入的路径,这里可以改为全路径,也可以改为相对路径,即@rpath/libinject.dylib
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 private func getSegmentCommand (data : Data ) -> segment_command_64? { return data.withUnsafeBytes { pointer in guard let segCmd = pointer.bindMemory(to: segment_command_64.self ).baseAddress else { print ("[ERROR] Failed to get segment command pointer." ) return nil } return segCmd.pointee } } private func getDylibCommand (data : Data ) -> dylib_command? { return data.withUnsafeBytes { pointer in guard let dylibCmd = pointer.bindMemory(to: dylib_command.self ).baseAddress else { print ("[ERROR] Failed to get dylib command pointer." ) return nil } return dylibCmd.pointee } } private func replaceDylib (offset : UInt64 , size : Int ) -> Bool { try? fileHandle.seek(toOffset: offset) fileHandle.write(Data (repeating: 0 , count: size)) try? fileHandle.seek(toOffset: offset) fileHandle.write(dylibPath.data(using: .utf8)! ) return true } private func injectDylib (header : mach_header, offset : UInt64 , is64bit : Bool ) -> Bool { var cmdOffset: UInt64 = 0 if is64bit { cmdOffset = offset + UInt64 (MemoryLayout <mach_header_64>.size) } else { cmdOffset = offset + UInt64 (MemoryLayout <mach_header>.size) } for _ in 0 ..< header.ncmds { let segData = machOData.subdata(in: Data .Index (cmdOffset)..< Int (cmdOffset)+ MemoryLayout <segment_command_64>.size) guard let segCmd = getSegmentCommand(data: segData) else { print ("[ERROR] Failed to get segment command pointer." ) return false } if (segCmd.cmd == LC_LOAD_DYLIB ) { guard let dylibCmd = getDylibCommand(data: segData) else { print ("[ERROR] Failed to get dylib command pointer." ) return false } let nameOffset = cmdOffset+ UInt64 (dylibCmd.dylib.name.offset) let nameSize = UInt64 (dylibCmd.cmdsize) - UInt64 (MemoryLayout <dylib_command>.size) let nameData = machOData.subdata(in: Data .Index (nameOffset)..< Int (nameOffset+ nameSize)) guard let libName = String (data: nameData, encoding: .utf8) else { return false } let trimmedName = libName.trimmingCharacters(in: nullCharacterSet) if trimmedName == replacePath { print ("[INFO] Find dylib for replacing, now repacking..." ) return replaceDylib(offset: nameOffset, size: Int (nameSize)) } } cmdOffset = cmdOffset + UInt64 (segCmd.cmdsize) } return false }
注入动态库生成 上一步将目标二进制文件依赖的系统库替换为了待注入的库,注入库除了需要实现hook逻辑,还需要解决系统库导出符号的问题。使用reexport_library
编译指令,可以重导出指定依赖库的导出符号,具体命令如下:
1 clang -dynamiclib ./syscall_hook.c -Xlinker -reexport_library /usr/lib/ssh-keychain.dylib -current_version 1.0 -compatibility_version 1.0 -o ./libinject.dylib
指令参数Xlinker
是一个特殊的选项,用于将后面的参数传递给链接器,如果不加会报错无法识别的reexport_library
参数。
符号重导出有没有成功,可以使用otool
命令检查。如下,ssh-keychain.dylib
括号中的reexport
表示该动态库是用于符号重导出的。
1 2 3 4 5 ➜ otool -L ./libinject.dylib ./libinject.dylib: ./libinject.dylib (compatibility version 1.0.0, current version 1.0.0) /usr/lib/ssh-keychain.dylib (compatibility version 1.0.0, current version 1.0.0, reexport) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
读者可能会疑惑,上文一直讲得是替换系统动态库libSystem.B.dylib
,而上面重导出的是ssh-keychain.dylib
。系统动态库在macOS系统上是用户不可见的,所以为了便于操作,可以先用ssh-keychain.dylib
站位,然后再把该动态库换成libSystem.B.dylib
,命令如下:
1 install_name_tool -change /usr/lib/ssh-keychain.dylib /usr/lib/libSystem.B.dylib ./libinject.dylib
至此,待注入的动态库就准备好了,后续该库便可以重导出系统动态库的符号。