如何找到PIE二进制文件的负载重定位?

时间:2019-03-08 15:55:48

标签: c linux elf aslr position-independent-code

我需要获取正在运行的进程内部堆栈的基地址。这将使我能够打印出可被addr2line理解的原始堆栈跟踪信息(已删除运行二进制文件,但addr2line可以访问符号)。 我通过检查argv[0]的elf标题来做到这一点:我读取入口点并将其从&_start中减去:

#include <stdio.h>
#include <execinfo.h>
#include <unistd.h>
#include <elf.h>
#include <stdio.h>
#include <string.h>
void* entry_point = NULL;
void* base_addr = NULL;
extern char _start;

/// given argv[0] will populate global entry_pont
void read_elf_header(const char* elfFile) {
  // switch to Elf32_Ehdr for x86 architecture.
  Elf64_Ehdr header;
  FILE* file = fopen(elfFile, "rb");
  if(file) {
    fread(&header, 1, sizeof(header), file);
    if (memcmp(header.e_ident, ELFMAG, SELFMAG) == 0) {
        printf("Entry point from file: %p\n", (void *) header.e_entry);
        entry_point = (void*)header.e_entry;
        base_addr = (void*) ((long)&_start - (long)entry_point);
    }
    fclose(file);
  }
}

/// print stacktrace
void bt() {
    static const int MAX_STACK = 30;
    void *array[MAX_STACK];
    auto size = backtrace(array, MAX_STACK);
    for (int i = 0; i < size; ++i) {
        printf("%p ", (long)array[i]-(long)base_addr );
    }
    printf("\n");
}

int main(int argc, char* argv[])
{
    read_elf_header(argv[0]);
    printf("&_start = %p\n",&_start);
    printf("base address is: %p\n", base_addr);
    bt();

    // elf header is also in memory, but to find it I have to already have base address
    Elf64_Ehdr * ehdr_addr = (Elf64_Ehdr *) base_addr;
    printf("Entry from memory: %p\n", (void *) ehdr_addr->e_entry);

    return 0;
}

示例输出:

Entry point from file: 0x10c0
&_start = 0x5648eeb150c0
base address is: 0x5648eeb14000
0x1321 0x13ee 0x29540f8ed09b 0x10ea 
Entry from memory:  0x10c0

然后我可以

$ addr2line -e a.out 0x1321 0x13ee 0x29540f8ed09b 0x10ea
/tmp/elf2.c:30
/tmp/elf2.c:45
??:0
??:?

如何在不访问argv的情况下获得基址?我可能需要在main()(全局变量的初始化)之前打印跟踪。不能选择启用ASLR或PIE。

2 个答案:

答案 0 :(得分:4)

这段代码产生的值与Linux上的base_addr相同:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>

Dl_info info;
void *extra = NULL;
dladdr1(&_start, &info, &extra, RTLD_DL_LINKMAP);
struct link_map *map = extra;
printf("%#llx", (unsigned long long)map->l_addr);

dladdr1 manual page说以下RTLD_DL_LINKMAP

  

RTLD_DL_LINKMAP

     

获取指向匹配文件的链接映射的指针。的     extra_info参数指向指向link_map结构(即struct link_map **)的指针,该结构定义为:

  struct link_map {
      ElfW(Addr) l_addr;  /* Difference between the
                             address in the ELF file and
                             the address in memory */
      char      *l_name;  /* Absolute pathname where
                             object was found */
      ElfW(Dyn) *l_ld;    /* Dynamic section of the
                             shared object */
      struct link_map *l_next, *l_prev;
                          /* Chain of loaded objects */
      /* Plus additional fields private to the
         implementation */
  };

请注意,-ldl必须链接到动态加载例程。

答案 1 :(得分:4)

  

如何在不访问argv的情况下获取基址?我可能需要在main()之前打印痕迹

有几种方法:

  1. 如果已安装/proc(几乎总是如此),则可以从/proc/self/exe中读取ELF标头。
  2. 您可以使用dladdr1(),如Antti Haapala的回答所示。
  3. 您可以使用_r_debug.r_map,它指向已加载的ELF图像的链接列表。该列表中的第一个条目对应于a.out,其l_addr包含您要查找的重定位。此解决方案等效于dladdr1,但不需要链接到libdl
  

您能否提供3的示例代码?

确定:

#include <link.h>
#include <stdio.h>

extern char _start;
int main()
{
  uintptr_t relocation = _r_debug.r_map->l_addr;
  printf("relocation: %p, &_start: %p, &_start - relocation: %p\n",
         (void*)relocation, &_start, &_start - relocation);
  return 0;
}

gcc -Wall -fPIE -pie t.c && ./a.out
relocation: 0x555d4995e000, &_start: 0x555d4995e5b0, &_start - relocation: 0x5b0
  

2和3是否都可移植?

我认为它们具有同等的可移植性:dladdr1是GLIBC扩展,也存在于Solaris中。 _r_debug早于Linux,并且也可以在Solaris上运行(我没有实际检查过,但我相信可以)。它也可以在其他ELF平台上使用。