Android Linker Analysis for Reverse
- Android
前言
安卓源码会持续更新,本文分析基于如下版本。
1git clone https://android.googlesource.com/platform/bionic
2git check out 42992d6413ea2b241d245fa456d36cd8b4eae2d8
0. System.loadLibrary
安卓 App 中使用 native 关键字标记的函数具体实现在相应的 so 文件中,也就是说 App 需要先加载相应的 so 文件。关于 so 文件的具体格式,在后文会详细解释重点的内容,在这只需要知道是 linux 的动态链接库即可,和 windows 下的 dll 文件类似。
在实现上,安卓代码会使用 static {} 块调用 System.loadLibrary 来进行初始化。使用 static{} 包裹的代码会在这个类第一次被加载的时候调用。这是一个相当早的时机,可以确保native方法被正常注册,避免后续逻辑中调用native方法出现错误。不过System.loadLibrary 本身并不是 native 函数,不过它在实际的调用的时候,会通过反射跳转到 nativeLoad 这个 native 方法,切入native层。
1public class MainActivity extends AppCompatActivity {
2 static {
3 System.loadLibrary("myapplication"); // 1. 加载此类时,优先执行。
4 }
5 public native String stringFromJNI();
6 public void test(){
7 String str = stringFromJNI();
8 }
9}
刚才说过,安卓 App 会使用 static {} 块先初始化,再调用native方法,但 nativeLoad 是用来实现加载so的 native 方法,为什么它可以不用加载相应的 so 呢?原因是 nativeLoad 的实现是由安卓虚拟机提供的,对于大于安卓 10 的版本,可以在 /apex/com.android.art/lib64/libopenjdkjvm.so 路径下找到这个文件,用反编译器或者类似的工具都可以看到 nativeLoad 的具体实现,在启动一个应用的时候,安卓虚拟机会加载这个so文件,由系统完成相应的方法注册,所以可以直接用。nativeLoad 的源码可以在这里找到。中间还会有几层跳转,这里我们只给出关键的调用关系,分析相对重要的函数。
大致的调用链如下,建议直接打开 LoadNativeLibrary 的实现。最关键的两个部分都在这里实现。
flowchart LR;
Runtime_nativeLoad --> JVM_NativeLoad;
JVM_NativeLoad --> JavaVMExt::LoadNativeLibrary;
JavaVMExt::LoadNativeLibrary-->OpenNativeLibrary;
1. JavaVMExt::LoadNativeLibrary
这个函数做了两件比较关键的事:先是调用 OpenNativeLibrary,在它的内部调用了android_ext_dlopen链接 so 文件。然后是调用了so 文件的 JNI_Onload 函数。这也是常说的,JNI_OnLoad 函数会在 so 被加载的时候调用,但就和 C 语言的 main 函数不是程序最早的入口点一样。在 android_ext_dlopen 也会执行两部分 so 内部的函数进行初始化,这里提前说一下是 DT_INIT和DT_INIT_ARRAY。并且是优先执行 DT_INIT,再执行 DT_INIT_ARRAY。
LoadNativeLibrary 的代码比较长,有比较多的处理错误的逻辑,主要是一个承上启下的作用。这里我只留下和 OpenNativeLibrary、JNI_Onload有调用关系的部分,并且修改了大部分函数的参数,太长了影响观看,在这里知道先后调用顺序即可。修改的参数用形如 $PARAMS["lib_path"] 表示,知道大概传递了什么即可,后文也会使用类似的表达。
这里需要注意调用 OpenNativeLibrary 传入的 needs_native_bridge 为 false
1bool JavaVMExt::LoadNativeLibrary(...const std::string& path,...)
2{
3 bool needs_native_bridge = false;
4 // 函数调用了 android_dlopen_ext, 传递了这个内部函数的返回值。
5 void* handle = android::OpenNativeLibrary($PARAMS["lib_path",&needs_native_bridge]);
6 Check(handle) // * 实际不存在!中间过程的抽象
7
8 void* sym = library->FindSymbol("JNI_OnLoad", nullptr, android::kJNICallTypeRegular); // * 实际上会调用 dlsym 来查找 JNI_Onload。
9 if (sym == nullptr)
10 { // 对于一个 so 来说可以没有 JNI_OnLoad
11 was_successful = true;
12 } else {
13 using JNI_OnLoadFn = int(*)(JavaVM*, void*);
14 JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
15 int version = (*jni_on_load)(this, nullptr); // 此处调用 JNI_Onload
16 if (IsSdkVersionSetAndAtMost(runtime_->GetTargetSdkVersion(), SdkVersion::kL)) {
17 EnsureFrontOfChain(SIGSEGV);
18 }
19 if (version == JNI_ERR) { // JNI Onload 的返回版本号,比较特殊。
20 log(Fault)
21 } else if (JavaVMExt::IsBadJniVersion(version)) {
22 log(Fault)
23 } else {
24 was_successful = true;
25 }
26 }
27
28 library->SetResult(was_successful);
29 return was_successful;
30}
2. OpenNativeLibrary
这个函数里面的分支也相当的多,前面的一些处理主要是为了系统库的so加载准备的,比较容易触发的函数优先处理。对于 App 只看最下面这个分支就好了。这里应该是实际 App 加载 so 会执行的流程。这里主要的逻辑是找到 App 的 namespace,把 so 加载到对应的 namespace。通过调用 ns->Load(path) 进行实际的加载。
1void* OpenNativeLibrary(JNIEnv* env,
2 const char* path,
3 jobject class_loader,
4 const char* caller_location,
5 bool* needs_native_bridge,
6 char** error_msg)
7{
8 ...
9 NativeLoaderNamespace* ns;
10 const char* ns_descr;
11 {
12 ...
13 std::lock_guard<std::mutex> guard(g_namespaces_mutex);
14 ns = g_namespaces->FindNamespaceByClassLoader(env, class_loader);
15 ns_descr = "class loader";
16 ...
17 }
18 *needs_native_bridge = ns->IsBridged();
19 Result<void*> handle = ns->Load(path); // -> 在此处调用 `android_dlopen_ext`
20 if (!handle.ok()) {
21 *error_msg = strdup(handle.error().message().c_str());
22 return nullptr;
23 }
24}
2.1 NativeLoaderNamespace::Load
needs_native_bridge 在前文说过,为 false 所以这里会调用上面的分支,也就是函数最终调用到了 android_dlopen_ext 实现功能。这个函数是 android 为 dlopen 这个函数的扩展,大体上他们的功能是类似的,都是动态加载一个 so 文件。
1Result<void*> NativeLoaderNamespace::Load(const char* lib_name) const {
2 if (!IsBridged()) {
3 android_dlextinfo extinfo;
4 extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
5 extinfo.library_namespace = this->ToRawAndroidNamespace();
6 void* handle = android_dlopen_ext(lib_name, RTLD_NOW, &extinfo);
7 if (handle != nullptr) {
8 return handle;
9 }
10 } else {
11 ...
12 }
13 return Error() << GetLinkerError(IsBridged());
14}
3. android_dlopen_ext -> do_dlopen
这个函数开始,就进入了真正对一个 so 文件进行处理的过程,同样在开始的时候会有较多的跳转,中间几乎没有什么逻辑处理,这里我只给出 android_dlopen_ext 和 do_dlopen 的参数,大部分参数都在重复的传递。
flowchart LR;
android_dlopen_ext-->__loader_android_dlopen_ext;
__loader_android_dlopen_ext -->dlopen_ext;
dlopen_ext --> do_dlopen;
1__attribute__((__weak__))
2void* android_dlopen_ext(const char* filename, int flag, const android_dlextinfo* extinfo) {
3 const void* caller_addr = __builtin_return_address(0);
4 return __loader_android_dlopen_ext(filename, flag, extinfo, caller_addr);
5}
6void* __loader_android_dlopen_ext($PARMS) {
7 return dlopen_ext(filename, flags, extinfo, caller_addr);
8}
9static void* dlopen_ext($PARMS)
10{
11 ScopedPthreadMutexLocker locker(&g_dl_mutex);
12 g_linker_logger.ResetState();
13 void* result = do_dlopen(filename, flags, extinfo, caller_addr);
14 if (result == nullptr) {
15 __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
16 return nullptr;
17 }
18 return result;
19}
3.1 dl_dlopen
dl_open 也是个胶水层的函数,第一部分的逻辑主要是在进行路径转换,安卓有一部分的库so并不直接在 system 路径下,可能在 apex 路径下。第二歩处理的逻辑查看是否启用了 HWASAN || ASAN 两个内存检查工具,如果启用则使用由工具处理过的系统代替。对于一般的 so 可以直接看最后一部分即可。
1void* do_dlopen(const char* name, int flags,
2 const android_dlextinfo* extinfo,
3 const void* caller_addr) {
4
5 soinfo* const caller = find_containing_library(caller_addr);
6 android_namespace_t* ns = get_caller_namespace(caller);
7 ... 这里省略两个路径转化的部分代码。
8 // * 加载一般的 So.
9 ProtectedDataGuard guard;
10 // * 链接过程
11 soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
12 loading_trace.End();
13 // * link 成功
14 if (si != nullptr) {
15 void* handle = si->to_handle();
16 // * call init function
17 si->call_constructors();
18 failure_guard.Disable();
19 LD_LOG(kLogDlopen,
20 "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
21 si->get_realpath(), si->get_soname(), handle);
22 return handle;
23 }
24
25 return nullptr;
26}
这个函数的结构是需要关注的,首先通过translated_name进行链接,接着再调用了si->call_constructors(),来执行 so 的自定义的初始化函数。这种初始化的函数一般是由__attribute__((constructor)) 标明的。所以当JNIOnload中没有对文件进行解密之类的操作的时候,有可能是在更早的时机,比如这里。同时,对于一个动态链接库来说,也不有再早的时机执行代码了。
1void soinfo::call_constructors() {
2 .../*调用 DT_INIT 指向的函数*/
3 call_function("DT_INIT", init_func_, get_realpath());
4 // 调用 `DT_INIT_ARRAY` 里的函数
5 call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());
6 ...
7}
3.2 find_library
find_library会调用到find_libraries,并且处理了大部分链接的工作,处理依赖、加载到内存、符号重定位等。
下面的内容则是find_libraries的片段,包含主要的逻辑。首先并不是只能处理一个so,实际上是可以同时处理很多的,它毕竟是通过so的名字去查找的,代码则如下所示。当我们动态链接一个so的时候,load_tasks的数据结构则类似于load_task = ["LoadTask("libA.so",caller,ns,<ptr,readers_map>)"]。ElfReader是安卓Bionic用来读取Elf文件格式的结构体,这里会为每一个so分配一个相应的ElfReade。
1 typedef std::vector<LoadTask*> LoadTaskList;
2 std::unordered_map<const soinfo*, ElfReader> readers_map;
3 LoadTaskList load_tasks;
4 //* 存储要被加载的 so,这里只有一个
5 for (size_t i = 0; i < library_names_count; ++i) {
6 const char* name = library_names[i];
7 load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
8 }
接着会为so_info这个结构体分配内存,没什么特别的,c 语言的风格的 new Obj。
1 if (soinfos == nullptr) {
2 // * 计算加载的 soinfos_size, 对于 dlopen 来说,只是一个的大小。
3 // * 同时分配了内存大小
4 size_t soinfos_size = sizeof(soinfo*)*library_names_count;
5 soinfos = reinterpret_cast<soinfo**>(alloca(soinfos_size));
6 memset(soinfos, 0, soinfos_size);
7 }
3.3 find_library_internal
之后,会开始优先将所有依赖的so添加至load_tasks的任务队列中,展平化的加载可以避免单个so单次处理的重复判断。
1 // Step 1: expand the list of load_tasks to include
2 // all DT_NEEDED libraries (do not load them just yet)
3 for (size_t i = 0; i<load_tasks.size(); ++i) {
4 LoadTask* task = load_tasks[i]; // * LoadTask: libA.so
5 soinfo* needed_by = task->get_needed_by(); //* need_by : caller
6 // * is_dt_needed = true && ((caller != start_with:caller) || false ) = false
7 bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
8 // * task.extinfo = extinfo
9 task->set_extinfo(is_dt_needed ? nullptr : extinfo);
10 // * task.dt_needed = false
11 task->set_dt_needed(is_dt_needed);
12
13 // Note: start from the namespace that is stored in the LoadTask. This namespace
14 // is different from the current namespace when the LoadTask is for a transitive
15 // dependency and the lib that created the LoadTask is not found in the
16 // current namespace but in one of the linked namespaces.
17 // * for dlopen -> start_ns = ns
18 android_namespace_t* start_ns = const_cast<android_namespace_t*>(task->get_start_from());
19
20 LD_LOG(kLogDlopen, "find_library_internal(ns=%s@%p): task=%s, is_dt_needed=%d",
21 start_ns->get_name(), start_ns, task->get_name(), is_dt_needed);
22 // * ns,task,stack_var,load_task[],unkown
23 // * 加载 ELF头, PHT头, SHT头, 把 DT_NEED , 依赖库添加进去。 不会把自己设置成 linked.
24 if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) {
25 return false;
26 }
27
28 soinfo* si = task->get_soinfo();
29
30 if (is_dt_needed) {
31 needed_by->add_child(si);
32 }
33
34 // When ld_preloads is not null, the first
35 // ld_preloads_count libs are in fact ld_preloads.
36 bool is_ld_preload = false;
37 if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) {
38 ld_preloads->push_back(si);
39 is_ld_preload = true;
40 }
41
42 if (soinfos_count < library_names_count) {
43 soinfos[soinfos_count++] = si;
44 }
45
46 }
Todo
2025年3月1日14:52:20