Android脱壳3-so文件链接过程剖析

youncyb 发布于 20 天前 136 次阅读 reverse


该文档深入分析了Android系统中.so文件的链接过程,揭示了加载过程中链接符号的流程、符号解析机制、以及相关的动态库依赖关系。

1. 简介

上文说到,通过 so 的加载,安卓将 so 的相关信息放在了 task 对象,包含了以下重要信息:phdr_table 指针、shdr_table 指针、动态节区指针、动态字符串节区指针。

本文将剖析 so 的链接过程,so 的动态链接信息位于动态节区(.dynamic),动态节区的包含了多个格式相同的结构体,这些结构体类型如下:

d_tag 控制着结构体的类型;

d_val 不同类型的结构体有着不同的意思;

d_ptr 结构体的偏移地址。

typedef struct dynamic {
  Elf32_Sword d_tag;
  union {
    Elf32_Sword d_val;
    Elf32_Addr d_ptr;
  } d_un;
} Elf32_Dyn;

2. 填充 soinfo 结构体

task.read()task.load() 阶段,就已经对 soinfo 结构体进行了部分赋值,此时已有的信息包括:

phdr
phnum
load_bias

有了 load_bias,只需要知道.dynamic 节区所在,便可填充 soinfo 结构体的剩余部分。

for (auto&& task : load_tasks) {
  soinfo* si = task->get_soinfo();
  if (!si->is_linked() && !si->prelink_image()) {
    return false;
  }
  register_soinfo_tls(si);
}

2.1 prelink_image

首先调用 phdr_table_get_dynamic_section() 找到.dynamic 节区地址,具体方式:

  1. 通过 phdr 找到 $p_type=PT_DYNAMIC$ 的条目;
  2. 计算 $.dynamic_addr=load_bias+p_vaddr$
bool soinfo::prelink_image() {
  /* Extract dynamic section */
  ElfW(Word) dynamic_flags = 0;
  phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);
......
}
  
/////////////////////////////////////
void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                    ElfW(Addr) load_bias, ElfW(Dyn)** dynamic,
                                    ElfW(Word)* dynamic_flags) {
  *dynamic = nullptr;
  for (size_t i = 0; i<phdr_count; ++i) {
    const ElfW(Phdr)& phdr = phdr_table[i];
    if (phdr.p_type == PT_DYNAMIC) {
      *dynamic = reinterpret_cast<ElfW(Dyn)*>(load_bias + phdr.p_vaddr);
      if (dynamic_flags) {
        *dynamic_flags = phdr.p_flags;
      }
      return;
    }
  }
}

遍历动态节区的每个条目,复制给 soinfo 结构体。可以注意到,在解析关于 SONAME 时,直接跳过了,原因是 SONAME 只是偏移量(动态字符串表),所以,需要先解析动态字符串表,才能解析 SONAME。

uint32_t needed_count = 0;
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
  DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
        d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
  switch (d->d_tag) {
    case DT_SONAME:
      // this is parsed after we have strtab initialized (see below).
      break;

    case DT_HASH:
      nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
      nchain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
      bucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8);
      chain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8 + nbucket_ * 4);
      break;

	...	...

    case DT_STRTAB:
      strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
      break;

    case DT_STRSZ:
      strtab_size_ = d->d_un.d_val;
      break;

    case DT_SYMTAB:
      symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
      break;
      ... ...
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
  switch (d->d_tag) {
    case DT_SONAME:
      set_soname(get_string(d->d_un.d_val));
      break;
    case DT_RUNPATH:
      set_dt_runpath(get_string(d->d_un.d_val));
      break;
  }
}

prelink_image() 函数执行完毕,soinfo 结构体也完成了填充,此时动态链接的所有信息也都知道了。

3. 重定位

3.1 重定位简介

3.1.1 含义

重定义是指将 “导入符号” 的引用和符号的真实地址绑定的过程。简单来说,当调用函数或者全局变量、静态变量时,操作系统通过重定位生成的约定,去某张表获取符号的真实地址,如果是函数,则跳转到该地址执行,如果是变量则从该地址取值。

3.1.2 原因

一个程序由多个子程序构成,当程序被编译后,总是被设置从 0 地址开始加载,但是,每个子程序都从 0 地址开始加载,那么便乱套了。所以需要设置这些子程序从不同的地址加载,如:A 从 0x100 加载,B 从 0x200 加载。

3.1.3 原理

  1. GOT 表 GOT 表是指.got 段和.got.plt 段,它们的区别是.got.plt 是给外部函数使用。GOT 表在链接时,会被修改为 “导入符号” 的真实地址。

  2. PLT 表 PLT 表是指.plt 段,是一个中间跳转表。PLT 表在链接时,会被修改为如何跳转到 GOT 表的函数所在位置。

  3. 如何寻找符号真实地址

    如图 1,当调用外部函数 strcmp 时,首先会跳转到.plt 段。通过 LDR PC, xxx 会修改 PC 寄存器地址,而这个地址指向 GOT 表中 strcmp 函数,如图 2 所示。而 GOT 表中则指定了 strcmp 的真实地址,如图 3 所示。

    image-20220429174803099

    图 1

    image-20220429174931386

    图 2

    image-20220429175541863

    图 3

  4. 依据什么修改 GOT 表?

    在第 3 点中说到,在运行时,会根据某种规则修改 got 表。这种规则就是重定位表,重定位表常见的是:

    .rel.dyn 和.rel.plt,常用于 Android 32 位;

    还有.rela.dyn 和.rela.plt,和.rel 区别在于其数据结构多了一个 append 加数,Android 64 位一般是这种格式;

    还有一种在 Android 6.0 之后支持的格式:.rel.dyn.aps2,.rela.plt.aps2,aps2 是 sleb128 编码格式的数据,读取时需要特别的解码逻辑。

    上述 3 种格式都是同一种数据,32 位重定位表的表项结构如下:

    typedef struct elf32_rela {
      Elf32_Addr r_offset;
      Elf32_Word r_info;
      Elf32_Sword r_addend;
    } Elf32_Rela;
    
    typedef struct elf32_rel {
      Elf32_Addr r_offset;
      Elf32_Word r_info;
    } Elf32_Rel;
    

    r_offset:表示该符号在 got 表中的位置;

    r_info:R_SYM 表示符号在符号表的索引。

    #define ELF32_R_SYM(info)             ((info)>>8)
    #define ELF32_R_TYPE(info)            ((unsigned char)(info))
    #define ELF32_R_INFO(sym, type)       (((sym)<<8)+(unsigned char)(type))
    
    #define ELF64_R_SYM(info)             ((info)>>32)
    #define ELF64_R_TYPE(info)            ((Elf64_Word)(info))
    #define ELF64_R_INFO(sym, type)       (((Elf64_Xword)(sym)<<32)+ \ 
                                            (Elf64_Xword)(type))
    

    r_addend:地址加数,当存在时,符号真实地址还需要+r_addend。

    image-20220506214636439

    image-20220429175541863

3.2 代码实例

介绍完重定位原理,接着看 Android 源码,前面系统通过 prelink_image 函数,为 soinfo 结构体赋值了符号表地址、动态字符串表地址、哈希表地址、重定位表地址等等。

但通过了解重定位的原因,赋值 soinfo 结构体是不够的,还需要将导入的外部符号进行重新定位。重定位部分位于 link_image 函数。

bool linked = local_group.visit([&](soinfo* si) {
  // Even though local group may contain accessible soinfos from other namespaces
  // we should avoid linking them (because if they are not linked -> they
  // are in the local_group_roots and will be linked later).
  if (!si->is_linked() && si->get_primary_namespace() == local_group_ns) {
    const android_dlextinfo* link_extinfo = nullptr;
    if (si == soinfos[0] || reserved_address_recursive) {
      // Only forward extinfo for the first library unless the recursive
      // flag is set.
      link_extinfo = extinfo;
    }
    if (!si->link_image(global_group, local_group, link_extinfo, &relro_fd_offset) ||
        !get_cfi_shadow()->AfterLoad(si, solist_get_head())) {
      return false;
    }
  }

  return true;
});

如下代码所示,APS2 格式的重定位表,会使用 sleb128_decoder 函数进行解码,然后再调用 relocate 函数进行重定位。紧接着判断是否存在 rela,如果存在,则使用 rela 进行重定位,否则使用 rel 进行重定位。

bool soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
                        const android_dlextinfo* extinfo, size_t* relro_fd_offset) {
  // APS2格式编码的重定位表会进行特殊解码
  if (android_relocs_ != nullptr) {
    // check signature
    if (android_relocs_size_ > 3 &&
        android_relocs_[0] == 'A' &&
        android_relocs_[1] == 'P' &&
        android_relocs_[2] == 'S' &&
        android_relocs_[3] == '2') {
      DEBUG("[ android relocating %s ]", get_realpath());

      bool relocated = false;
      const uint8_t* packed_relocs = android_relocs_ + 4;
      const size_t packed_relocs_size = android_relocs_size_ - 4;

      relocated = relocate(
          version_tracker,
          packed_reloc_iterator<sleb128_decoder>(
            sleb128_decoder(packed_relocs, packed_relocs_size)),
          global_group, local_group);

      if (!relocated) {
        return false;
      }
    } else {
      DL_ERR("bad android relocation header.");
      return false;
    }
  }      
  ......
  #if defined(USE_RELA)
    if (rela_ != nullptr) {
      DEBUG("[ relocating %s rela ]", get_realpath());
      if (!relocate(version_tracker,
              plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
        return false;
      }
    }
    if (plt_rela_ != nullptr) {
      DEBUG("[ relocating %s plt rela ]", get_realpath());
      if (!relocate(version_tracker,
              plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
        return false;
      }
    }
  #else
    if (rel_ != nullptr) {
      DEBUG("[ relocating %s rel ]", get_realpath());
      if (!relocate(version_tracker,
              plain_reloc_iterator(rel_, rel_count_), global_group, local_group)) {
        return false;
      }
    }
    if (plt_rel_ != nullptr) {
      DEBUG("[ relocating %s plt rel ]", get_realpath());
      if (!relocate(version_tracker,
              plain_reloc_iterator(plt_rel_, plt_rel_count_), global_group, local_group)) {
        return false;
      }
    }
  #endif

relocate 函数通过 for 循环遍历重定位表,获取重定位表包含的 type(类型)、sym(符号表索引)、reloc(GOT 表的符号地址)、addend(rela 具有的额外加数)。

bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
                      const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
  const size_t tls_tp_base = __libc_shared_globals()->static_tls_layout.offset_thread_pointer();
  std::vector<std::pair<TlsDescriptor*, size_t>> deferred_tlsdesc_relocs;

  for (size_t idx = 0; rel_iterator.has_next(); ++idx) {
    const auto rel = rel_iterator.next();
    if (rel == nullptr) {
      return false;
    }

    ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
    ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);

    ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + load_bias);
    ElfW(Addr) sym_addr = 0;
    const char* sym_name = nullptr;
    ElfW(Addr) addend = get_addend(rel, reloc);

    DEBUG("Processing \"%s\" relocation at index %zd", get_realpath(), idx);
    if (type == R_GENERIC_NONE) {
      continue;
    }
    ....

第 7-15 行,通过 sym 索引获取符号名,然后通过 soinfo_do_lookup 函数获取符号信息。soinfo_do_lookup 函数通过哈希表快速定位符号,节省顺序寻找符号的时间。

需要解释一下,为何要用 soinfo_do_lookup 函数寻找符号。首先已经知道 sym,那么通过 symtab_[sym] 即可获取符号,但是这种方式,只是针对本模块的符号,对于 “导入符号”,本模块的符号表只会存储符号的 sym_name 和 sym_info 信息,如 strcmp,其他的值全为 0,图 4 所示。

所以,需要在外部模块进行寻找,而 soinfo_do_lookup 函数就是干这事的。

第 40 行,对于外部符号,通过 resolve_symbol_address 函数获取符号具体地址;

第 42-50 行,如果修改的段处于被保护状态,还需要进行保护解除操作;

第 55 行-x,根据 type,选择不同的赋值方式,如:R_GENERIC_JUMP_SLOT,则.got.plt 的指向的符号地址被修改 sym_addr + addend。

    if (sym == 0) {
		......
    } else if (ELF_ST_BIND(symtab_[sym].st_info) == STB_LOCAL && is_tls_reloc(type)) {
		......
      
    } else {
      sym_name = get_string(symtab_[sym].st_name);
      const version_info* vi = nullptr;

      if (!lookup_version_info(version_tracker, sym, sym_name, &vi)) {
        return false;
      }

      if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
        return false;
      }

      if (s == nullptr) {
		......
      } else { // We got a definition.
			......
        if (is_tls_reloc(type)) {
          if (ELF_ST_TYPE(s->st_info) != STT_TLS) {
            DL_ERR("reference to non-TLS symbol \"%s\" from TLS relocation in \"%s\"",
                   sym_name, get_realpath());
            return false;
          }
          if (lsi->get_tls() == nullptr) {
            DL_ERR("TLS relocation refers to symbol \"%s\" in solib \"%s\" with no TLS segment",
                   sym_name, lsi->get_realpath());
            return false;
          }
          sym_addr = s->st_value;
        } else {
          if (ELF_ST_TYPE(s->st_info) == STT_TLS) {
            DL_ERR("reference to TLS symbol \"%s\" from non-TLS relocation in \"%s\"",
                   sym_name, get_realpath());
            return false;
          }
          sym_addr = lsi->resolve_symbol_address(s);
        }
#if !defined(__LP64__)
        if (protect_segments) {
          if (phdr_table_unprotect_segments(phdr, phnum, load_bias) < 0) {
            DL_ERR("can't unprotect loadable segments for \"%s\": %s",
                   get_realpath(), strerror(errno));
            return false;
          }
        }
#endif
      }
      count_relocation(kRelocSymbol);
    }

    switch (type) {
      case R_GENERIC_JUMP_SLOT:
        count_relocation(kRelocAbsolute);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
                   reinterpret_cast<void*>(reloc),
                   reinterpret_cast<void*>(sym_addr + addend), sym_name);

        *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
        break;

image-20220506234728674

当重定位执行完成后,soinfo 结构体被设置为已链接状态。如果有被其他的 soinfo 结构体引用,则将 “引用计数” +1。至此,整个链接过程完成。返回到 do_dlopen 函数。

4. 执行构造函数

do_dlopen 函数中执行完 find_library 函数后,会执行当前共享对象的构造函数。

soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
loading_trace.End();

if (si != nullptr) {
  void* handle = si->to_handle();
  LD_LOG(kLogDlopen,
         "... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
         si->get_realpath(), si->get_soname(), handle);
  si->call_constructors(); // 调用构造函数,实际上也是.init段
  failure_guard.Disable();
  LD_LOG(kLogDlopen,
         "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
         si->get_realpath(), si->get_soname(), handle);
  return handle;
}

具体是去执行 DT_INIT 和 DT_INIT_ARRAY 包含的函数。

void soinfo::call_constructors() {
  if (constructors_called) {
    return;
  }
  constructors_called = true;

  if (!is_main_executable() && preinit_array_ != nullptr) {
    // The GNU dynamic linker silently ignores these, but we warn the developer.
    PRINT("\"%s\": ignoring DT_PREINIT_ARRAY in shared library!", get_realpath());
  }

  get_children().for_each([] (soinfo* si) {
    si->call_constructors();
  });

  if (!is_linker()) {
    bionic_trace_begin((std::string("calling constructors: ") + get_realpath()).c_str());
  }

  // DT_INIT should be called before DT_INIT_ARRAY if both are present.
  call_function("DT_INIT", init_func_, get_realpath());
  call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());

  if (!is_linker()) {
    bionic_trace_end();
  }
}