每种语言是否都将跳转表编译到自定义位置,或者ELF/PE头中是否有特定的位置



我正试图弄清楚跳转表(一种将子例程名称与其地址配对的数据表)在可执行文件中的位置,以及它是否基于语言、编译器,或者是否在PE/ELF二进制文件的头部中有标准的位置。是哪一个?我如何定位这些表格或找到关于它们放置位置的文档?


到目前为止我尝试了什么:

首先,我阅读了PE/ELF标题的每一部分,并没有确定哪一部分会是跳转表。

由于我发现学习编译器如何工作的前景非常可怕,我想到的最直接的方法是用子程序分解二进制文件,并找到引用跳转目的地和其他部分的二进制文件的一部分。在这个编译成ELF格式的C程序开始时,我发现了以下部分:

0000000000001020 <.plt>:
1020:   ff 35 1a 2f 00 00       pushq  0x2f1a(%rip)        # 3f40 <_GLOBAL_OFFSET_TABLE_+0x8>
1026:   f2 ff 25 1b 2f 00 00    bnd jmpq *0x2f1b(%rip)        # 3f48 <_GLOBAL_OFFSET_TABLE_+0x10>
102d:   0f 1f 00                nopl   (%rax)
1030:   f3 0f 1e fa             endbr64 
1034:   68 00 00 00 00          pushq  $0x0
1039:   f2 e9 e1 ff ff ff       bnd jmpq 1020 <.plt>
103f:   90                      nop
1040:   f3 0f 1e fa             endbr64 
1044:   68 01 00 00 00          pushq  $0x1
1049:   f2 e9 d1 ff ff ff       bnd jmpq 1020 <.plt>
...

我认为这可能就是跳转表的样子,这些地址是各种动态链接的库的偏移量。我之前看到过用.plt引用ELF标题部分,但最初不清楚它是否是一个跳转表。进一步研究表明:

PLT代表过程链接表,简单地说,它用于调用中地址未知的外部过程/函数链接的时间,并由位于的动态链接器解析运行时间。

GOT代表全局偏移表,类似地用于解决地址。PLT和GOT以及其他重新定位信息在这篇文章中详细解释。

我仍在努力寻找这一部分中的哪一个跳转(如果有的话)指向我程序中的子例程。也许GOT是我下一步需要关注的地方。


如果需要更多上下文,以下是我询问的原因:

我一直在研究二进制补丁,特别是用于跟踪恶意软件行为的挂钩技术,以及恶意软件如何阻止这种跟踪。挂钩(只是将控制流重定向到中间函数,然后重定向到最初的目的地的指令)可以去很多地方,比如修补到内存中的共享二进制文件(libs),甚至修补到内核子例程,但如果我理解正确,它们有时也会直接注入到可执行二进制文件中的子例程中。

我正在研究的是攻击者阻止这些放置在二进制文件中的钩子的可能性。假设攻击者从恶意软件执行之初就使用了一个不确定的(从受害者的角度来看)跳转目的地。现在,假设一个分析师或自动启发式分析工具试图分解一个程序,可能是在沙盒环境中,以确定程序的行为,但该程序为该跳转目的地地址所连接的web服务器在未来某个日期执行时,只会返回程序恶意控制流的入口点。在此之前,它会返回一个使程序以良性方式运行的地址。这是教科书上的逃避,x86/-64架构的可变长度特性使其成为可能。我最近发表了一张图表,尽我所能将问题集可视化。

但是,如果编译器在程序中构建了跳转表,那么分析员或威胁检测系统仍然可以知道要跳转到并分析这些子程序的入口点的位置。一旦这些子例程在目标条件下在运行时执行,还可以分析寄存器,以找到例程从何处执行的地址(x86调用约定包括此信息,以便子例程知道返回到何处),并且从该信息中,分析员还可以知道其他有效的指令边界,以便在处开始反汇编

我对编译器的工作原理几乎一无所知,也读过PE/ELF文件头的规范,但也许我遗漏了一些东西。如果能给我一个正确的指示,我将不胜感激。

跳转表不一定是一个将子程序名称与其地址配对的数据表,正如@MSchangers在评论中指出的那样。通常是单个子例程中的控制流,特别是switch/case语句。考虑Duff’s Device,一个可以预期跳转表的经典例子(如果不是没有表的计算跳转):

void send(int *to, int *from, size_t count)
{
size_t n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7:      *to = *from++;
case 6:      *to = *from++;
case 5:      *to = *from++;
case 4:      *to = *from++;
case 3:      *to = *from++;
case 2:      *to = *from++;
case 1:      *to = *from++;
} while (--n > 0);
}
}

MSVC编译如下,跳转目标为事例标签:

mov     eax, DWORD PTR $LN17@send[r10+r8*4]
add     rax, r10
jmp     rax
$LN10@send:
mov     eax, DWORD PTR [rdx]
add     rdx, 4
mov     DWORD PTR [rcx], eax
$LN11@send:
mov     eax, DWORD PTR [rdx]
add     rdx, 4
mov     DWORD PTR [rcx], eax

https://godbolt.org/z/e64vKzq4d


PE文件格式没有定义任何与跳转表相关的内容。有一些指针表,比如被称为导入函数/数据的导入地址表的指针表,或者TLS回调表,但没有跳转表。

MSVC恰好将跳转表放在代码部分中,靠近using函数。它使跳转表变为只读并且更难覆盖。

尽管跳转表没有必需的部分,但它们可能仍会以某种方式进行注释。32位x86将使用绝对地址(而不是RIP相对地址),因此如果存在重定位表,则这些跳转表将作为连续指针范围进入重定位表。不确定SEH或控制流保护数据,它可能也包括跳转表注释。

最新更新