正在获取主可执行文件的ELF标头

Getting the ELF header of the main executable

本文关键字:ELF 标头 可执行文件 获取      更新时间:2023-10-16

出于各种目的,我试图在不解析/proc/self/maps的情况下获得主可执行文件的ELF头的地址。我已经尝试解析dlopen/dlinfo函数给出的link_list链,但它们不包含l_addr指向主可执行文件的基地址的条目。有没有任何方法可以在不解析/proc/self/maps的情况下做到这一点(标准或非标准)?

我尝试做的一个例子:

#include <stdio.h>
#include <elf.h>
int main()
{
    Elf32_Ehdr* header = /* Somehow obtain the address of the ELF header of this program */;
    printf("%pn", header);
    /* Read the header and do stuff, etc */
    return 0;
}

dlopen(0, RTLD_LAZY)返回的void *指针会给您一个struct link_map *,它对应于主可执行文件。

调用dl_iterate_phdr还返回回调的第一次执行时的主可执行文件的条目。

链接映射中的.l_addr == 0和使用dl_iterate_phdr时的dlpi_addr == 0可能会使您感到困惑。

之所以会发生这种情况,是因为l_addr(和dlpi_addr)实际上并没有记录ELF映像的加载地址。相反,它们会记录已应用于该图像的重定位

通常,主可执行文件被构建为在0x400000(对于x86_64Linux)或0x08048000(对于ix86Linux)加载,并在同一地址加载(即,它们不重新定位)。

但是,如果您将可执行文件与-pie标志链接,那么它将在0x0处链接,并且它将被重新定位到其他地址。

那么,如何到达ELF标题?

2023更新:

一个更简单的方法(如果依赖于未记录的详细信息),只是在struct link_map中的l_ld地址上调用dladdr,然后使用其中的dli_fbase吗Simon Kissane

的确如此。这里有一个简单得多的解决方案:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
int main()
{
  void *dyn = _DYNAMIC;
  Dl_info info;
  if (dladdr(dyn, &info) != 0) {
    printf("a.out loaded at %pn", info.dli_fbase);
  }
  return 0;
}
gcc -g -Wall -Wextra x.c -ldl && ./a.out
a.out loaded at 0x556433ea0000  # high address here because my GCC defaults to PIE.
gcc -g -Wall -Wextra x.c -ldl -no-pie && ./a.out
a.out loaded at 0x400000
gcc -g -Wall -Wextra x.c -ldl -no-pie -m32 && ./a.out
a.out loaded at 0x8048000

2012年的原始解决方案:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <link.h>
#include <stdio.h>
#include <stdlib.h>
static int
callback(struct dl_phdr_info *info, size_t size, void *data)
{
  int j;
  static int once = 0;
  if (once) return 0;
  once = 1;
  printf("relocation: 0x%lxn", (long)info->dlpi_addr);
  for (j = 0; j < info->dlpi_phnum; j++) {
    if (info->dlpi_phdr[j].p_type == PT_LOAD) {
      printf("a.out loaded at %pn",
             (void *) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));
      break;
    }
  }
  return 0;
}
int
main(int argc, char *argv[])
{
  dl_iterate_phdr(callback, NULL);
  exit(EXIT_SUCCESS);
}
    
$ gcc -m32 t.c && ./a.out
relocation: 0x0
a.out loaded at 0x8048000
$ gcc -m64 t.c && ./a.out
relocation: 0x0
a.out loaded at 0x400000
$ gcc -m32 -pie -fPIC t.c && ./a.out
relocation: 0xf7789000
a.out loaded at 0xf7789000
$ gcc -m64 -pie -fPIC t.c && ./a.out
relocation: 0x7f3824964000
a.out loaded at 0x7f3824964000

更新:

为什么手册页上写着";基地址";而不是搬迁?

这是一个bug;-)

我猜测手册页早在prelinkpieASLR存在之前就已经编写好了。在没有预链接的情况下,共享库总是链接到地址0x0处的加载,然后relocationbase address成为一体。

当info指向主可执行文件时,dlpi_name为什么指向空字符串?

这是一个实施上的意外。

其工作方式是内核open(2)生成可执行文件,并将打开的文件描述符传递给加载程序(在auxv[]向量中,作为AT_EXECFDEverything加载程序通过读取该文件描述符来了解它所获得的可执行文件。

在UNIX上,没有简单的方法可以将文件描述符映射回打开时的名称。首先,UNIX支持硬链接,并且可能有多个文件名引用同一个文件。

较新的Linux内核还传递用于execve(2)可执行文件的名称(也在auxv[]中,作为AT_EXECFN)。但这是可选的,即使在传入时,glibc也不会将其放入.l_name/dlpi_name中,以免破坏依赖于名称为空的现有程序。

相反,glibc将该名称保存在__progname__progname_full中。

在未使用AT_EXECFN的系统上,加载程序coudreadlink(2)的名称来自/proc/self/exe,但也不能保证安装/proc文件系统,因此有时仍会留下一个空名称。

有一个glibc dl_iterate_phdr()函数。我不确定它是否能满足你的需求,但据我所知:

"dl_iterate_phdr()函数允许应用程序在运行时查询,以找出它加载了哪些共享对象。"http://linux.die.net/man/3/dl_iterate_phdr