在 Mach-O 可执行文件中,如何找到存根针对的函数?

逆向工程 操作系统 符号 男子气概
2021-06-18 03:57:59

在暴露我的问题之前,这是我对整个事情的理解,以便如果我说错了,您可以纠正我。

在 Mach-O 文件中(至少在 x86 上),该__TEXT.__stubs部分通常包含存根,所有存根都由单个间接跳转组成,如下所示:

; __TEXT.__stubs
; symbol stub for unlink:
0x100000f46:  jmpq   *0xc4(%rip)
; symbol stub for puts:
0x100000f4c:  jmpq   *0xc6(%rip)

这些指向该__DATA.__nl_symbol_ptr部分内的某个位置指针最初指向部分中的存根助手__TEXT.__stub_helper

; __TEXT.__stub_helper
; stub helper for unlink
0x100000f64:  pushq  $0x0
0x100000f69:  jmp    0x100000f54
; stub helper for puts
0x100000f6e:  pushq  $0xe
0x100000f73:  jmp    0x100000f54

存根助手调用dyld_stub_binder,它使用推送的参数来确定它是哪个存根以及它需要查找哪个函数,然后用__DATA.__nl_symbol_ptr解析的地址替换中的值,然后将控制权移交给找到的函数(然后正常返回调用代码)。

为了帮助调试,调试器找到存根并假装它们有符号。在这个示例程序中,每当 lldb 看到call 0x100000f58,它就会确定存根应该指向unlink,并call 0x100000f58 ; symbol stub for: unlink在反汇编中

但是, lldb 不使用推送值:它似乎依赖于静态链接器以相同的顺序放置未定义的符号和存根,或者它的一些变体。就像那样,它看起来更像是一种启发式方法,而不是一种确定哪个存根去哪里的精确方法,除非有其他东西阻止您篡改。

那么我如何可靠地找到存根调用了哪个函数呢?在存根助手中,常量pushq $constant是什么意思?

2个回答

数字参数是“压缩的 dyld 信息”字节码流的偏移量。参见https://stackoverflow.com/a/8836580(iOS/arm但仍然适用)

编写了一个 Python 脚本,用于解析入口点并从我的一个项目的 Mach-O 可执行文件中导入。诀窍是解析LC_DYLDLC_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_offbind_sizeweak_bind_offweak_bind_sizelazy_bind_offlazy_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_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_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))