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

至此,待注入的动态库就准备好了,后续该库便可以重导出系统动态库的符号。

  • Title: macOS 修改 MachO 文件实现动态库注入(四)
  • Author: 孙康
  • Created at : 2025-03-16 17:30:00
  • Updated at : 2025-03-18 15:27:41
  • Link: https://conradsun.github.io/2025/03b81ddf97.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
macOS 修改 MachO 文件实现动态库注入(四)