反汇编 ELF - PC 设置为 0?

逆向工程 安卓 手臂 汇编
2021-06-13 09:40:16

我试图反汇编一个 ELF 文件,它是在 armv7a (Android) 上执行的共享对象文件。我看到了一个奇怪的方块。看来PC,程序计数器寄存器被设置为0我是否错过了什么或做错了什么?


该过程进入0x1708ARM 模式。下面是我从 ELF 文件中反汇编的奇怪的汇编代码块。

; section: .plt
; function: function_1708 at 0x1708 -- 0x1718
0x1708:   04 e0 2d e5       str lr, [sp, #-4]!
0x170c:   04 e0 9f e5       ldr lr, [pc, #4]
0x1710:   0e e0 8f e0       add lr, pc, lr
0x1714:   08 f0 be e5       ldr pc, [lr, #8]!
; data inside code section at 0x1718 -- 0x171c
0x1718:   b4 77 00 00                                        |.w..            |

执行 line 后0x170cLR应将寄存器设置为地址处的值0x1718值为0x77b4(此文件以小端存储)。继续。

0x1710: lr += 0x1710 + 8  // lr = 0x8ecc
0x1714: pc = *(lr + 8)    // pc = *(0x8ed4)
        lr += 8  

并且 0x8ed4 在.got节中。

; section: .got
0x8eac:   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   |................|
0x8ebc:   00 00 00 00 58 70 00 00  e0 6e 00 00 00 00 00 00   |....Xp...n......|
0x8ecc:   00 00 00 00 00 00 00 00  00 00 00 00 08 17 00 00   |................|
0x8edc:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8eec:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8efc:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f0c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f1c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f2c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f3c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|

似乎 的值0x8ed4为零。我从 追踪到这个奇怪的块JNI_OnLoad(),所以在执行这个块之前不应该修改任何数据。

我做错了什么,还是这是 ARM 架构的特定行为?

2个回答

事实上,您所描述的行为来自 GOT/PLT 部分的通常行为。它们用于在运行时将程序调用动态链接到共享库函数。

事实上,一个共享库可以在进程内存中的任何地方加载,没有办法静态预测它会在哪里弹出。因此,GOT/PLT 在运行时动态加载每个库函数的地址并缓存它以供进一步使用。

它的工作方式非常简单,PLT(过程链接表)只是一堆小代码小工具(程序中调用的每个库的函数一个,加上一个位于 PLT 部分开头的通用代码)。

而且,GOT(全局偏移表)只是一个用于存储(和缓存)库函数地址的表。

起初,GOT 全部设置为零,因为还没有解析函数地址。GOT 一直被填充,程序被执行并调用新函数(有些函数可能很少被调用,它们的地址可能在 GOT 中很少被填充)。

当库函数被调用时,程序计数器进入 PLT(见下图)并执行专门为该函数编写的代码(每个代码小工具都有自己的偏移量以从 GOT 获取正确的地址)。但是,起初,GOT 只包含零,因此您需要对其进行初始化。

要初始化它,您可以跳转到 PLT 的最开头并找到程序要调用的库函数的地址。您执行此代码,然后在 GOT 中写入函数的地址。

PLT/GOT Schema(之前)

一旦你缓存了函数的地址,如果你调用你的函数并直接跳转到它,你就不再需要重新计算地址(如下图所示)。

PLT/GOT 架构(之后)

对于 ARM,您有两种类型的 GOT/PLT 模式:

在你的情况下,这是一个直接的 PLT(但它似乎是相当旧的代码,我怀疑是一个旧的编译器左右,因为新的更好地ip代替lr它)。

; section: .plt
; function: function_1708 at 0x1708 -- 0x1718
0x1708:   04 e0 2d e5     str lr, [sp, #-4]! <-- Save lr on the stack
0x170c:   04 e0 9f e5     ldr lr, [pc, #4] <-- Get a point of reference in memory
0x1710:   0e e0 8f e0     add lr, pc, lr <-- Load offset to the next step
0x1714:   08 f0 be e5     ldr pc, [lr, #8]! <-- Jump to the next step

请注意,PLT 是一个静态部分,因此0x170c将始终保持不变(因此,pc将始终加载相同的值0x170c)。

参考

虽然另一个答案没有错,但它实际上并没有涵盖真正的问题:为什么调用零地址不会导致崩溃?

AFAIK 没有官方标准或完整的文档,但事实上前几个 GOT 条目是特殊/保留的,由动态加载器/链接器(有时也称为解释器)用于其自身目的:

  • GOT[0]指向_DYNAMIC当前模块符号,它是Elf32_Dyn带有正确解析动态符号所需的各种信息的标签(条目)列表的开始请参阅ELF 规范

  • GOT[1]在运行时使用指向link_map包含进程中存在的所有动态图像列表结构的指针填充此列表驻留在动态链接器中(ld.so在 Linux 系统上和linker在 Android上的二进制文件)。

  • GOT[2]也由动态链接器填充并指向解析器函数_dl_runtime_resolve在 glibc 中,在 Android 上不确定)。

您引用的代码段是解析器存根,它在第一次调用外部符号时调用(您可以看到所有 GOT 条目都初始化为其地址,1708)。它获取GOT[2]并跳转到它,它应该在动态链接器中结束,它会查找符号,修补 GOT 条目并作为最后一步跳转到函数。

正如我所说,没有(AFAIK)官方文档,只是随机的 ELF ABI 规范、glibc 源代码和各种博客文章。我建议您在调试器中逐步执行此代码以查看实际发生的情况,并将其与源代码进行匹配。除了来自 @ perror 的链接之外,我还发现这篇文章解释了一些保留的 GOT 条目(尽管适用于 x64 Linux 而不是 ARM Android)。