我编写了一个 Python 脚本,用于解析入口点并从我的一个项目的 Mach-O 可执行文件中导入。诀窍是解析LC_DYLD
或LC_DYLD_ONLY
加载器命令。这两个命令对三个导入表进行编码:绑定符号、弱符号和惰性符号。
struct dyld_info_command {
uint32_t cmd;
uint32_t cmdsize;
uint32_t rebase_off;
uint32_t rebase_size;
uint32_t bind_off;
uint32_t bind_size;
uint32_t weak_bind_off;
uint32_t weak_bind_size;
uint32_t lazy_bind_off;
uint32_t lazy_bind_size;
uint32_t export_off;
uint32_t export_size;
};
有趣的领域是bind_off
,bind_size
,weak_bind_off
,weak_bind_size
,lazy_bind_off
和lazy_bind_size
。每对都对可执行文件中包含导入表操作码的数据块的偏移量和大小进行编码。
这些表中的每一个都可以看作有四个(有用的)列:段、段偏移、库名称和符号名称。段和段偏移量一起指示符号的实际地址将被写入的地址(例如,如果您有__TEXT
和 0x40,这在概念上意味着*(__TEXT+0x40) == resolvedSymbolAddress
)。
该表被编码为用于压缩目的的操作码流。操作码控制一个状态机,该状态机包含一个潜在符号的状态,具有操作该状态的操作,以及“绑定”一个符号的操作(获取所有该状态并使其成为符号表的一部分)。例如,您可以看到:
- 将段设置为 __TEXT
- 将偏移量设置为 0x40
- 将库设置为 libSystem.dylib
- 将符号名称设置为“printf”
- 绑定符号
- 将偏移量设置为 0x48
- 将符号名称设置为“scanf”
- 绑定符号
在这个序列的末尾,你会得到两个符号:printf 和 scanf,它们的地址分别位于 __TEXT+0x40 和 __TEXT+0x48,来自 libSystem.dylib。这意味着,如果您看到类似jmp [__TEXT+0x48]
(间接跳转到包含在 处的地址__TEXT+0x48
)的指令,您就知道您将要scanf
。这就是您可以告诉存根的目的地的方法。
每个操作码至少为 1 个字节,以 0xCI 分隔(其中 C 是命令名称,I 是立即数,均为 4 位)。当命令需要更大的操作数(或更多操作数)时,它们以ULEB-128格式编码(除了BIND_OPCODE_SET_ADDEND_SLEB
,它使用带符号的 LEB,但我们并不真正关心它以查找导入)。
def readUleb(data, offset):
byte = ord(data[offset])
offset += 1
result = byte & 0x7f
shift = 7
while byte & 0x80:
byte = ord(data[offset])
result |= (byte & 0x7f) << shift
shift += 7
offset += 1
return (result, offset)
库实际上并不是通过它们在命令流中的名称来标识的。相反,库由他们确定基于一个“库序”,这是所有的内库的只是指数LC_LOAD_DYLIB
,LC_LOAD_WEAK_DYLIB
,LC_REEXPORT_DYLIB
和LC_LOAD_UPWARD_DYLIB
加载命令。例如,如果一个可执行文件有一个LC_LOAD_DYLIB
用于 libSystem的命令,然后有一个用于 libFoobar的命令,则 libSystem 的序号为 1,libFoobar 的序号为 2。
有三个特殊值: ordinal -2 表示在平面命名空间中查找符号(具有该名称的符号的第一个库获胜);ordinal -1 在主可执行文件中查找符号,无论它是什么;序数 0 在此文件中查找符号。正如我们上面所说的,序数 1 及以上指的是图书馆。
符号名称在命令 blob 中编码为以空字符结尾的字符串。
每个操作码都可以在代码中轻松描述,因此我将不再介绍每个操作码。
BIND_OPCODE_DONE = 0
BIND_OPCODE_SET_DYLIB_ORDINAL_IMM = 1
BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB = 2
BIND_OPCODE_SET_DYLIB_SPECIAL_IMM = 3
BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM = 4
BIND_OPCODE_SET_TYPE_IMM = 5
BIND_OPCODE_SET_ADDEND_SLEB = 6
BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB = 7
BIND_OPCODE_ADD_ADDR_ULEB = 8
BIND_OPCODE_DO_BIND = 9
BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB = 10
BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED = 11
BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB = 12
def parseImports(self, offset, size):
pointerWidth = self.bitness / 8
slice = self.data[offset:offset+size]
index = 0
name = ""
segment = 0
segmentOffset = 0
libOrdinal = 0
stubs = []
def addStub():
stubs.append((segment, segmentOffset, libOrdinal, name))
while index != len(slice):
byte = ord(slice[index])
opcode = byte >> 4
immediate = byte & 0xf
index += 1
if opcode == BIND_OPCODE_DONE:
pass
elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_IMM:
libOrdinal = immediate
elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:
libOrdinal, index = self.__readUleb(slice, index)
elif opcode == BIND_OPCODE_SET_DYLIB_SPECIAL_IMM:
libOrdinal = -immediate
elif opcode == BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM:
nameEnd = slice.find("\0", index)
name = slice[index:nameEnd]
index = nameEnd
elif opcode == BIND_OPCODE_SET_TYPE_IMM:
pass
elif opcode == BIND_OPCODE_SET_ADDEND_SLEB:
_, index = self.__readUleb(slice, index)
elif opcode == BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segment = immediate
segmentOffset, index = self.__readUleb(slice, index)
elif opcode == BIND_OPCODE_ADD_ADDR_ULEB:
addend, index = self.__readUleb(slice, index)
segmentOffset += addend
elif opcode == BIND_OPCODE_DO_BIND:
addStub()
elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB:
addStub()
addend, index = self.__readUleb(slice, index)
segmentOffset += addend
elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED:
addStub()
segmentOffset += immediate * pointerWidth
elif opcode == BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB:
times, index = self.__readUleb(slice, index)
skip, index = self.__readUleb(slice, index)
for i in range(times):
addStub()
segmentOffset += pointerWidth + skip
else:
sys.stderr.write("warning: unknown bind opcode %u, immediate %u\n" % (opcode, immediate))