Linker加载器保护总结

转自:https://blog.csdn.net/liumengdeqq/article/details/79247091

1.前言


Android代码层加固保护目前主要为dex层加固及native层加固,当前Native层加固手段主要为加壳、混淆、OLLVM,VM、加载器保护、代码段加密运行时解密等方案. 本文主要提供native层的基于加载器的保护思路及实现中遇到的一些问题,文后会给出碰到的问题,欢迎各位不吝赐教,源码基于android 7.0进行分析

2.SO装载与链接


2.1 do_dlopen

调用 dl_open 后,中间经过 dlopen_ext, 到达第一个主要函数 do_dlopen:

void* do_dlopen(const char* name, int flags, const android_dlextinfo* extinfo,void* caller_addr) 
{
  ......省略部分
  ProtectedDataGuard guard;
  soinfo* si = find_library(ns, name, flags, extinfo, caller);
  if (si != nullptr) {
    si->call_constructors();
    return si->to_handle();
  }
  return nullptr;
}

加载部分核心实现 fin_library内部主要实现so的装载、链接、合法性校验,及加载其他依赖库, si->call_constructors执行init init_array初始化so库,si->to_handle低于7.0版本 直接返回soinfo句柄,7.0版本返回soinfo在全局hash表的对应的hash值,这部分也是后面遇到的主要问题所在

2.2 find_libraries

加载SO库及其依赖的库,并调用相关函数对依赖的库进行预加载及链接设置链接标志

static bool find_libraries(android_namespace_t* ns,
                           soinfo* start_with,
                           const char* const library_names[],
                           size_t library_names_count, soinfo* soinfos[],
                           std::vector<soinfo*>* ld_preloads,
                           size_t ld_preloads_count, int rtld_flags,
                           const android_dlextinfo* extinfo,
                           bool add_as_children) {
 	......
  // all DT_NEEDED libraries (do not load them just yet)
  for (size_t i = 0; i<load_tasks.size(); ++i) {
	......
	//加载并查找所有依赖库将节映射到内存空间,返回所有依赖的SO库
    if(!find_library_internal(ns, task,&zip_archive_cache, &load_tasks, rtld_flags)) {
      return false;
    }
	......
  LoadTaskList load_list;
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    auto pred = [&](const LoadTask* t) {
      return t->get_soinfo() == si;
    };
	//未链接的SO库加入需链接的链表中
    if (!si->is_linked() &&
        std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) {
      load_list.push_back(task);
    }
  }
  shuffle(&load_list);
  //映射节区到内存
  for (auto&& task : load_list) {
    if (!task->load()) {
      return false;
    }
  }
  ......
  //链接所有未链接SO库
  bool linked = local_group.visit([&](soinfo* si) {
    if (!si->is_linked()) {
      if (!si->link_image(global_group, local_group, extinfo)) {
        return false;
      }
    }
    return true;
  });

  if (linked) {
    local_group.for_each([](soinfo* si) {
      if (!si->is_linked()) {
        si->set_linked();
      }
    });
    failure_guard.disable();
  }
  ......
  return linked;
}

该函数体现了整个SO加载的相关流程主要有以下几个方面
1.装载:创建ElfReader对象,通过 ElfReader 对象的 Load 方法将 SO 文件装载到内存
2.查找目标库所有依赖的SO加入任务列表
3.对任务列表所有未进行链接的库进行预装载
4.对任务列表所有未链接的库进行链接

2.3 find_library_internal
static bool find_library_internal(android_namespace_t* ns,
                                  LoadTask* task,
                                  ZipArchiveCache* 	zip_archive_cache,
                                  LoadTaskList* load_tasks,
                                  int rtld_flags) {
  if (find_loaded_library_by_soname(ns, task->get_name(), &candidate)) {
    task->set_soinfo(candidate);
    return true;
  }
  ......省略
  if (load_library(ns, task, zip_archive_cache,load_tasks, rtld_flags)) {
    return true;
  } 
  ......省略
  return false;
 }

find_loaded_library_by_soname通过名称查找模块是否已加载已加载的话直接返回已加载模块句柄否则调用load_library进行加载

2.4 load_library

申请soinfo 将so文件映射到内存空间并填充soinfo信息

static bool load_library(android_namespace_t* ns,
                         LoadTask* task,
                         LoadTaskList* load_tasks,
                         uint32_t rtld_flags,
                         const std::string& realpath) {
 
 ......省略部分
 
  soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
  if (si == nullptr) {
    return false;
  }

  task->set_soinfo(si);

  // 读取elf文件节表信息
  if (!task->read(realpath.c_str(), file_stat.st_size)) {
    soinfo_free(si);
    task->set_soinfo(nullptr);
    return false;
  }
  ......
  //  设置SO查找路径及名称信息
  const ElfReader& elf_reader = task->get_elf_reader();
  for (const ElfW(Dyn)* d = elf_reader.dynamic(); d->d_tag != DT_NULL; ++d) {
    if (d->d_tag == DT_RUNPATH) {
      si->set_dt_runpath(elf_reader.get_string(d->d_un.d_val));
    }
    if (d->d_tag == DT_SONAME) {
      si->set_soname(elf_reader.get_string(d->d_un.d_val));
    }
  }
 //	查找所有未加载的依赖库
  for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {
    load_tasks->push_back(LoadTask::create(name, si, task->get_readers_map()));
      //int handle = (int)dlopen(name,RTLD_NOW);
      gLog->MLOGD("HANDLE is%d name is %s",si, name);
  });

  return true;
}

通过调用task->read将elf文件头部信息映射到内存中供后续装载及链接时使用,并对ELF文件合法性进行检查,

2.5 ElfReader::Read
bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
 CHECK(!did_read_);
 CHECK(!did_load_);
  name_ = name;
  fd_ = fd;
  file_offset_ = file_offset;
  file_size_ = file_size;

  if (ReadElfHeader() && //读取elf头
      VerifyElfHeader() &&// 校验elf头
      ReadProgramHeaders() &&// 读取程序头
      ReadSectionHeaders() &&//	读取节表
      ReadDynamicSection()) {//	读取动态节区
    did_read_ = true;
  }

  return did_read_;
}
2.6 ElfReader::load
bool ElfReader::Load(const android_dlextinfo* extinfo) {
  CHECK(did_read_);
  CHECK(!did_load_);
  if (ReserveAddressSpace(extinfo) && 
      LoadSegments() &&
      FindPhdr()) {
    did_load_ = true;
  }
  return did_load_;
}
2.6.1.ReserveAddressSpaceSO 
	计算在内存中需要的空间load_size,然后使用 mmap 匿名映射,预留出相应的空间。
2.6.2.LoadSegments
	1.遍历 program header table,找到类型为 PT_LOAD 的 segment:计算 segment 在内存空间中的起始地址 segstart 和结束地址 seg_end,seg_start 等于虚拟偏移加上基址load_bias,同时由于 mmap 的要求,都要对齐到页边界得到 seg_page_start 和 seg_page_end。
	2.计算 segment 在文件中的页对齐后的起始地址 file_page_start 和长度 file_length。
	3.使用 mmap 将 segment 映射到内存,指定映射地址为 seg_page_start,长度为 file_length,文件偏移为 file_page_start。
2.7 soinfo_alloc
static soinfo* soinfo_alloc(android_namespace_t* ns, const char* name,
                           struct stat* file_stat, off64_t file_offset,
                            uint32_t rtld_flags) {
	//new 一个soinfo结构体对象
  soinfo* si = new (g_soinfo_allocator.alloc()) soinfo(ns, name,file_stat,
   	                                                  file_offset, rtld_flags);
  sonext->next = si;// 加入到存有所有 soinfo 的链表中
  sonext = si;
  si->generate_handle();若为7.0版本则生成soinfo对应的hash表句柄
  ns->add_soinfo(si);
  TRACE("name %s: allocated soinfo @ %p", name, si);
  return si;
}

Linker 为 每个 SO 维护了一个soinfo结构,调用 dlopen时,返回的句柄其实就是一个指向该 SO 的 soinfo 指针。soinfo 保存了 SO 加载链接以及运行期间所需的各类信息。
load_library 在为 SO 分配 soinfo 后,会将装载结果更新到 soinfo 中,后面的链接过程就可以直接使用soinfo的相关字段去访问 SO 中的信息。

bool load() {
    ElfReader& elf_reader = get_elf_reader();
    if (!elf_reader.Load(extinfo_)) {
      return false;
    }
    si_->base = elf_reader.load_start();
    si_->size = elf_reader.load_size();
    si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
    si_->load_bias = elf_reader.load_bias();
    si_->phnum = elf_reader.phdr_count();
    si_->phdr = elf_reader.loaded_phdr();
    return true;
  }
2.8 soinfo->prelink_image与soinfo->link_image
1.prelink_image()函数 的主要工作是解析(遍历)PT_DYNAMIC 段(.dynamic节)里面的内容,并将解析到的内容保存到soinfo结构中
2.link_image()函数的主要工作是对根据.rela.dyn 、.rela.plt的信息修改got表中的内容也就是做重定位,relocate() 就是具体的实现函数

3.加壳技术

在 Android 环境中,Native 层的加壳主要是针对动态链接库 SO

SO: 即被保护的目标 SO。
loader: 自身也是一个 SO,系统加载时首先加载 loader,loader 首先还原出经过加密、压缩、变换的 SO,再将 SO 加载到内存,并完成链接过程,使 SO 可以正常被其他模块使用。
加壳工具: 将被保护的 SO 加密、压缩、变换,并将结果作为数据与 loader 整合为 packed SO。
下面对 SO 加壳的关键技术进行简单介绍。

3.1 loader 执行时机

Linker 加载完 loader 后,loader 需要将被保护的 SO 加载起来,这就要求 loader 的代码需要被执行,而且要在 被保护 SO 被使用之前,SO 的初始化函数便可以满足这个要求,同时在 Android 系统下还可以使用 JNI_ONLOAD 函数,因此 loader 的执行时机有两个选择:
SO 的 init 或 initarray
jni_onload

3.2 loader 完成 SO 的加载链接

loader 开始执行后,首先需要在内存中还原出 SO,SO 可以是经过加密、压缩、变换等手段,也可已单纯的以完全明文的数据存储,这与 SO 加壳的技术没有必要的关系,在此不进行讨论。
在内存中还原出 SO 后,loader 还需要执行装载和链接,这两个过程可以完全模仿 Linker 来实现

4. 难点

SO 加壳的最关键技术点在于 soinfo 的修复,由于 Linker 加载的是 loader,而实际对外使用的是被保护的 SO,所以 Linker 维护的 soinfo 可以说是错误,loader 需要将自己维护的 soinfo 中的部分信息导出给 Linker 的soinfo。对于非7.0版本可以直接通过dlopen(self)来获取soinfo句柄,而7.0版本由于返回的是soinfo对应的hash值所以无法直接对返回的信息进行操作,也就无法将自己的soinfo信息加入到linker维护的soinfo链表中去,目前有方案是通过获取linker 的mps信息自己解析soinfo再进行修复,但是未发现有具体的方案。

另一种思路

后续通过对乐固SO加载器的逆向工程提供了另一种思路,可以用移花接木四个字来形容,还原的部分代码一并提供。
legu idb文件
链接:https://pan.baidu.com/s/1eiqrYfJYumNGJe8iZHB8ew 密码:48x5
还原部分代码
链接:https://pan.baidu.com/s/1OQAYRJFbWwoVjuQEoFdBpw 密码:0dz6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值