linkHook_init_array自吐

xiaoeryu Lv5

本章的主要内容为

  • 通过源码分析init_array原理so加载执行流程
  • hook_linker init_array 自吐

在对linker进行hook之前先分析一下so文件的加载流程

在so被加载之后,如果so文件中存在.init.init_array段的话它们会先被动态链接器调用进行一些初始化的操作。所以这两个函数无论有没有被调用它都会优先执行

  • 例如这里我们并没有主动调用这个_init(),但是它还是会在so加载的时候主动执行

去源码中搜索一下加载so的dlopen函数调用过程

源码分析

测试用的设备是Android 10.0所以这里以Android 10.0的源码 为例,到目前最新的Android14为止对so的加载都是通过dlopen()进行的

void* do_dlopen(const char* name, int flags,
                const android_dlextinfo* extinfo,
                const void* caller_addr) {
    // 创建一个带有"dlopen: "前缀的跟踪对象,用于记录加载库的跟踪信息
  std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
  ScopedTrace trace(trace_prefix.c_str());
    // 创建一个带有"dlopen: - loading and linking"前缀的跟踪对象,用于记录加载和链接库的跟踪信息
  ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
    // 查找调用者的soinfo结构体
  soinfo* const caller = find_containing_library(caller_addr);
    // 获取调用者的命名空间
  android_namespace_t* ns = get_caller_namespace(caller);
    // 记录dlopen调用的详细信息
  LD_LOG(kLogDlopen,
         "dlopen(name=\"%s\", flags=0x%x, extinfo=%s, caller=\"%s\", caller_ns=%s@%p, targetSdkVersion=%i) ...",
         name,
         flags,
         android_dlextinfo_to_string(extinfo).c_str(),
         caller == nullptr ? "(null)" : caller->get_realpath(),
         ns == nullptr ? "(null)" : ns->get_name(),
         ns,
         get_application_target_sdk_version());
    // 创建自动清理对象,用于在作用域结束时调用purge_unused_memory函数
  auto purge_guard = android::base::make_scope_guard([&]() { purge_unused_memory(); });
    // 创建自动清理对象,用于在作用域结束时记录dlopen失败信息
  auto failure_guard = android::base::make_scope_guard(
      [&]() { LD_LOG(kLogDlopen, "... dlopen failed: %s", linker_get_error_buffer()); });
    // 检查flags是否包含无效标志,如果包含则返回nullptr
  if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL|RTLD_NODELETE|RTLD_NOLOAD)) != 0) {
    DL_ERR("invalid flags to dlopen: %x", flags);
    return nullptr;
  }
    // 检查extinfo是否包含无效标志,如果包含则返回nullptr
  if (extinfo != nullptr) {
    if ((extinfo->flags & ~(ANDROID_DLEXT_VALID_FLAG_BITS)) != 0) {
      DL_ERR("invalid extended flags to android_dlopen_ext: 0x%" PRIx64, extinfo->flags);
      return nullptr;
    }

    if ((extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD) == 0 &&
        (extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET) != 0) {
      DL_ERR("invalid extended flag combination (ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET without "
          "ANDROID_DLEXT_USE_LIBRARY_FD): 0x%" PRIx64, extinfo->flags);
      return nullptr;
    }

    if ((extinfo->flags & ANDROID_DLEXT_USE_NAMESPACE) != 0) {
      if (extinfo->library_namespace == nullptr) {
        DL_ERR("ANDROID_DLEXT_USE_NAMESPACE is set but extinfo->library_namespace is null");
        return nullptr;
      }
      ns = extinfo->library_namespace;
    }
  }

  // Workaround for dlopen(/system/lib/<soname>) when .so is in /apex. http://b/121248172
  // The workaround works only when targetSdkVersion < Q.
  std::string name_to_apex;
  if (translateSystemPathToApexPath(name, &name_to_apex)) {
    const char* new_name = name_to_apex.c_str();
    LD_LOG(kLogDlopen, "dlopen considering translation from %s to APEX path %s",
           name,
           new_name);
    // Some APEXs could be optionally disabled. Only translate the path
    // when the old file is absent and the new file exists.
    // TODO(b/124218500): Re-enable it once app compat issue is resolved
    /*
    if (file_exists(name)) {
      LD_LOG(kLogDlopen, "dlopen %s exists, not translating", name);
    } else
    */
    if (!file_exists(new_name)) {
      LD_LOG(kLogDlopen, "dlopen %s does not exist, not translating",
             new_name);
    } else {
      LD_LOG(kLogDlopen, "dlopen translation accepted: using %s", new_name);
      name = new_name;
    }
  }
  // End Workaround for dlopen(/system/lib/<soname>) when .so is in /apex.

  std::string asan_name_holder;

  const char* translated_name = name;
  if (g_is_asan && translated_name != nullptr && translated_name[0] == '/') {
    char original_path[PATH_MAX];
    if (realpath(name, original_path) != nullptr) {
      asan_name_holder = std::string(kAsanLibDirPrefix) + original_path;
      if (file_exists(asan_name_holder.c_str())) {
        soinfo* si = nullptr;
        if (find_loaded_library_by_realpath(ns, original_path, true, &si)) {
          PRINT("linker_asan dlopen NOT translating \"%s\" -> \"%s\": library already loaded", name,
                asan_name_holder.c_str());
        } else {
          PRINT("linker_asan dlopen translating \"%s\" -> \"%s\"", name, translated_name);
          translated_name = asan_name_holder.c_str();
        }
      }
    }
  }
    // 创建受保护的数据对象,用于在加载库期间保护数据
  ProtectedDataGuard guard;
    // 在命名空间中查找库并加载
  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();	// <<<<< 跟进看看之后的调用流程
      // 禁用dlopen失败的清理回调
    failure_guard.Disable();
      // 记录加载成功的信息,并返回句柄
    LD_LOG(kLogDlopen,
           "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
           si->get_realpath(), si->get_soname(), handle);
    return handle;
  }
    // 如果加载失败,则返回nullptr
  return nullptr;
}
  • 这段代码前面做了一堆非空值的判断
  • 然后调用si->call_constructors()来完成so函数的加载

进入si->call_constructors()DT_INIT以及DT_INIT_ARRAY会依次执行

  • 不论DT_INIT还是DT_INIT_ARRAY都是通过call_function()调用执行

  • 其中的call_array()是模板函数

    这里了解一下共享库初始化函数的执行流程

    1. call_function("DT_INIT", init_func_, get_realpath());

      这行代码调用了 call_function 函数,传递了三个参数:"DT_INIT"init_func_get_realpath()。其中:

      • "DT_INIT" 是一个字符串,用于标识所调用的初始化函数的类型,这里表示调用的是 DT_INIT 函数。
      • init_func_ 是一个函数指针,指向了共享库中的 DT_INIT 函数。
      • get_realpath() 是一个函数,用于获取共享库的路径。

      在函数内部,它会首先检查 init_func_ 是否为空,如果不为空,则调用 function() 函数执行初始化操作,即执行共享库中的 DT_INIT 函数。在执行前后,会通过 TRACE 打印调试信息。

    2. call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());

      这行代码调用了 call_array 函数,传递了五个参数:"DT_INIT_ARRAY"init_array_init_array_count_falseget_realpath()。其中:

      • "DT_INIT_ARRAY" 是一个字符串,用于标识所调用的初始化数组的类型,这里表示调用的是 DT_INIT_ARRAY 数组。
      • init_array_ 是一个指向初始化数组的指针,该数组包含了一系列的初始化函数。
      • init_array_count_ 是初始化数组中元素的数量。
      • false 表示不需要对初始化数组进行反向遍历。
      • get_realpath() 是一个函数,用于获取共享库的路径。

      call_array 函数会遍历 init_array_ 数组,依次调用数组中的初始化函数。在每次调用初始化函数前后,同样会通过 TRACE 打印调试信息。

      这两行代码的作用是在共享库加载过程中,执行共享库中的初始化函数,以完成一些必要的初始化工作,比如初始化全局变量、注册回调函数等。

  • 跟进去可以看到*call_function()*函数它的文件路径是在/bionic/linker/linker_soinfo.cpp

    /**
     * @brief 调用链接器构造函数
     * 
     * @param function_name 构造函数的名称
     * @param function 构造函数的函数指针
     * @param realpath 文件的实际路径
     */
    static void call_function(const char* function_name __unused,
                              linker_ctor_function_t function,
                              const char* realpath __unused) {
        // 如果构造函数为空指针或者指针的值为 -1,则直接返回,不进行任何操作
        if (function == nullptr || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {
            return;
        }
    
        // 打印调试信息,包括构造函数的名称、函数指针和文件路径
        TRACE("[ Calling c-tor %s @ %p for '%s' ]", function_name, function, realpath);
        
        // 调用构造函数,传递全局变量 g_argc、g_argv 和 g_envp
        function(g_argc, g_argv, g_envp);
        
        // 再次打印调试信息,表示构造函数调用结束
        TRACE("[ Done calling c-tor %s @ %p for '%s' ]", function_name, function, realpath);
    }
    
    /**
     * @brief 调用链接器析构函数
     * 
     * @param function_name 析构函数的名称
     * @param function 析构函数的函数指针
     * @param realpath 文件的实际路径
     */
    static void call_function(const char* function_name __unused,
                              linker_dtor_function_t function,
                              const char* realpath __unused) {
        // 如果析构函数为空指针或者指针的值为 -1,则直接返回,不进行任何操作
        if (function == nullptr || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {
            return;
        }
    
        // 打印调试信息,包括析构函数的名称、函数指针和文件路径
        TRACE("[ Calling d-tor %s @ %p for '%s' ]", function_name, function, realpath);
        
        // 调用析构函数
        function();
        
        // 再次打印调试信息,表示析构函数调用结束
        TRACE("[ Done calling d-tor %s @ %p for '%s' ]", function_name, function, realpath);
    }
    
    • 这两个函数的作用分别是调用链接器构造函数(constructor)和析构函数(destructor)。

用Frida获取模块的符号

function get_addr(){
    var linker_sym = Module.enumerateSymbols("linker")
    console.log("do_dlopen_addr => ", JSON.stringify(linker_sym))
}
  • linker分为32位和64位两个,32位环境名字是linker,64位环境是linker64

    在脚本打印出的符号中没有看到call_function(),可能是被优化掉了。用IDA确认一下

    1. 用adb命令从手机系统中导出linker库文件

    2. 在IDA中检查后没有发现call_function()

      • 可以看到分别调用了构造函数call_constructors()、析构函数call_destructors()call_array()
    3. 跟进去分析之后发现call_function()函数是被优化成代码片段,插入在call_constructors()call_array()中间了

      • 既然这样的话,根据反编译的伪代码我们可以尝试hook这个_dl_async_safe_format_log()通过这个函数来获取构造函数的名称function_name、函数指针function和文件路径realpath
      • 不过需要注意的是,这个函数执行有一个条件是_dl_g_ld_debug_verbosity >= 2,所以在hook这个函数之前先使用frida去这个变量的地址修改它的变量值让_dl_async_safe_format_log()符合执行条件。

通过上面的脚本获取到linker的地址之后,根据IDA反编译的结果知道它有5个参数,参照源码参数的类型将结果打印出来,因为函数被优化了所以参数类型跟源码中略有差别,多尝试几次就ok了。

function hook_constructors(linker_sym){
    for(var i = 0; i < linker_sym.length; i++){
        var name = linker_sym[i].name
        if(name.indexOf("__dl_g_ld_debug_verbosity") >= 0){
            var addr__dl_g_ld_debug_verbosity = linker_sym[i].address
            // console.log("addr__dl_g_ld_debug_verbosity => ", addr__dl_g_ld_debug_verbosity)
            ptr(addr__dl_g_ld_debug_verbosity).writeInt(2)
        }
        if(name.indexOf("__dl_async_safe_format_log") >= 0 && name.indexOf("va_list") < 0){
            // console.log("__dl_async_safe_format_log", JSON.stringify(linker_sym[i]))
            var addr__dl_async_safe_format_log = linker_sym[i].address
        }
    }
    if(addr__dl_async_safe_format_log){
        Interceptor.attach(addr__dl_async_safe_format_log, {
            onEnter: function(args){
                this.log_level = args[0]
                this.tag = ptr(args[1]).readCString()
                this.fmt = ptr(args[2]).readCString()
                if(this.fmt.indexOf("c-tor") >= 0 && this.fmt.indexOf("Done") < 0){
                    this.function_type = ptr(args[3]).readCString()
                    this.so_path = ptr(args[5]).readCString()
                    var strs = new Array()	// 创建一个车数组
                    strs = this.so_path.split("/")	// 字符分割
                    this.so_name = strs.pop()
                    this.func_offset = ptr(args[4]).sub(Module.findBaseAddress(this.so_name))
                    console.log("func_type: ", this.function_type,
                    "\nso_name: ", this.so_name,
                    "\nso_path: ", this.so_path,
                    "\nfunc_offset: ", this.func_offset.toString(16))
                }
            }, onLeave: function(retval){}
        })
    }
}
附件

hook脚本

  • 标题: linkHook_init_array自吐
  • 作者: xiaoeryu
  • 创建于 : 2024-04-10 09:16:22
  • 更新于 : 2024-08-06 22:19:29
  • 链接: https://github.com/xiaoeryu/2024/04/10/linkHook-init-array自吐/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
linkHook_init_array自吐