在暴露问题之前,这是我对整个问题的理解,以便您在我说错什么时可以纠正我。

在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  q4312078qx0
0x100000f69:  jmp    0x100000f54
; stub helper for puts
0x100000f6e:  pushq  q4312078qxe
0x100000f73:  jmp    0x100000f54


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

调试时,调试器会找到存根,并假装它们具有符号。在此示例程序中,只要lldb看到call 0x100000f58,它就会确定存根应指向unlink,并在反汇编中显示call 0x100000f58 ; symbol stub for: unlink。静态链接器将未定义的符号和存根以相同的顺序放置或以某种顺序放置。就像那样,它看起来更像是一种启发式方法,而不是一种精确的方法来确定哪个存根到达哪里,除非有其他阻止您篡改的东西。

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

#1 楼

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

评论


由于这个问题已经被查看了500多次,因此这是我不久前编写的字节码解码器的实现。

–zneak
16-11-28在2:57

@zneak:很好,也许您可​​以写出更详细的解释作为答案,以便其他人知道详细信息?我不介意您是否接受我的回答。

–伊戈尔·斯科钦斯基♦
16年11月28日在8:48

#2 楼

我编写了一个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,其地址分别来自libSystem.dylib,位于__TEXT + 0x40和__TEXT + 0x48。这意味着,如果您看到类似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加载程序命令中库的索引。例如,如果一个可执行文件对libSystem有一个LC_LOAD_DYLIB命令,而对libFoobar有一个q4312079q命令,则libSystem具有序数1,而libFoobar具有序数2。

有三个特殊值:ordinal -2表示该符号为在平面名称空间中查找(第一个带有该名称的符号的库获胜); ordinal -1在主可执行文件中查找符号,无论它是什么;序号0在此文件中查找符号。如上所述,序数1和序数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("q4312078q", 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))


评论


一项补充:库列表不仅包括LC_LOAD_DYLIB命令,而且还包括LC_LOAD_WEAK_DYLIB,LC_REEXPORT_DYLIB,LC_LAZY_LOAD_DYLIB和LC_LOAD_UPWARD_DYLIB(请参阅dyld源)

–伊戈尔·斯科钦斯基♦
16年11月29日在18:27

另外,刚刚找到此页面:newosxbook.com/articles/DYLD.html

–伊戈尔·斯科钦斯基♦
16年11月29日在18:28

稍后我将对其进行调查并更新脚本/答案。谢谢!

–zneak
16年11月29日在18:30