自实现linker加固so遇到的问题记录

自定义linker加固so是主流的加壳方式,通过实现linker程序来加载,链接加固后的so文件。最后为了让加固后的so中的代码能与其他模块交互,需要修正壳(自定义linker)的soinfo为加固so。我在编写自定义linker代码时,在将加固的后的so加载,链接和修复soinfo后尝试通过dlopen测试能不能获取加固so的句柄,进一步通过dlsym获取其符号信息时遇到了一个问题:dlopen失败,无法找到加固后的so。我的加固so文件为libpluginso.so,通过adb shell setprop debug.ld.all dlerror,dlopen查看调试日志。

按道理不应该,因为我前面已经在解析加固so文件的时候将soinfo结构体的soname_修改为了libpluginso.so,为什么dlopen找不到呢?于是我又尝试dlopen获取一下壳so(libnative-lib.so)的句柄,发现其并没有直接返回壳so的句柄,而是重新将壳so又加载了一份并返回了新的句柄。

这是为什么呢?因为我本身就是在壳so中调用的代码,在壳so代码中dlopen(壳so)不应该是直接返回壳so的句柄吗,为什么又加载了一份壳so。回想之前分析的android链接器命名空间,我猜想有可能是链接器命名空间的问题,在此查看linker调试日志发现新加载的壳so的caller是null,然后find_libraries函数使用命名空间ns=anonymous去搜索加载壳so,按道理我在壳so中调用dlopen,调用者应该是壳so,并且壳so是由java代码加载的,所以命名空间ns(调用者命名空间)应该是classloader-namespace才对。

dlopen(name="libnative-lib.so", flags=0x2, extinfo=(null), caller="(null)", caller_ns=(anonymous)@0x729ec8a5b0, targetSdkVersion=32) ...
find_libraries(ns=(anonymous)): task=libnative-lib.so, is_dt_needed=0

查看linker源码,dl_dlopen函数首先会调用find_containing_library获取调用者的soinfo结构体。

查看find_containing_library函数发现其是通过遍历soinfo链表并通过调用者地址判断匹配在哪个soinfo的地址空间中,这就说明白了为什么之前的日志显示caller为null。因为在dlopen时壳so的soinfo结构体已经被我更改为了加固so的地址空间,所以通过调用者地址(壳so的地址)去匹配soinfo的地址空间是找不到任何匹配项的。

soinfo* find_containing_library(const void* p) {
  // Addresses within a library may be tagged if they point to globals. Untag
  // them so that the bounds check succeeds.
  ElfW(Addr) address = reinterpret_cast<ElfW(Addr)>(untag_address(p));
  for (soinfo* si = solist_get_head(); si != nullptr; si = si->next) {
    if (address < si->base || address - si->base >= si->size) {
      continue;
    }
    ElfW(Addr) vaddr = address - si->load_bias;
    for (size_t i = 0; i != si->phnum; ++i) {
      const ElfW(Phdr)* phdr = &si->phdr[i];
      if (phdr->p_type != PT_LOAD) {
        continue;
      }
      //通过调用者的地址与soinfo的地址空间进行匹配
      if (vaddr >= phdr->p_vaddr && vaddr < phdr->p_vaddr + phdr->p_memsz) {
        return si;
      }
    }
  }
  return nullptr;
}

同样在修复了soinfo后调用dlopen加载加固so也是同样的问题,当匹配不到调用者的soinfo时就会返回nullptr。而get_caller_namespace在获取调用者命名空间时,如果调用者soinfo为null就会认为调用者命名空间为anonymous。

static android_namespace_t* get_caller_namespace(soinfo* caller) {
  return caller != nullptr ? caller->get_primary_namespace() : g_anonymous_namespace;
}

调用者命名空间是anonymous,而壳so是被加载到了classloader-loader命名空间中了,所以在anonymous命名空间中是找不到壳so和加固so的。

但是这并不影响其他模块so通过dlopen打开加固so,因为其他模块只要是与壳so属于同一个classloader那么其就在同一个命名空间中,此模块so中的代码也就能在此classloader-namespace中找到加固so的soinfo结构体,并成功返回对应的句柄使用。

posted @ 2023-02-07 01:05  怎么可以吃突突  阅读(590)  评论(0编辑  收藏  举报