吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9298|回复: 20
收起左侧

[Android 原创] Android 动态修改Linker实现LD_PRELOAD全局库PLT Hook

  [复制链接]
beichen 发表于 2021-1-21 11:04
本帖最后由 beichen 于 2021-1-21 11:04 编辑

前言

我们知道linux系统中存在 LD_PRELOAD 环境变量更改库的链接顺序,影响库的导入函数重定位,而Android使用linux是内核也包含 LD_PRELOAD环境变量,具体使用路径在 linker_main.cpp 中(本文分析源码如未特别提及则都基于最新主分支)。在执行linker初始化时访问了该环境变量,后续优先加载将其作为全局库从而影响之后该进程加载的所有库的符号链接,因此后续我们将围绕该环境变量展开

环境变量的初始化、获取、修改

环境变量的获取:获取环境变量使用libc中的getenv方法,而其中使用的全局变量声明为extern "C" char** environ;,该导出符号在libc.so库中,因此我们也可以通过dlsym(libchandler, "environ")来遍历当前进程的所有环境变量。

环境变量的修改:调用libc中的putenv,如果不存在则添加,存在则会替换为新值

环境变量的初始化:在linker_main.cpp文件中的linker_main方法中有调用__libc_init_AT_SECURE(args.envp),该函数具体代码如下:

void __libc_init_AT_SECURE(char** env) {
  // Check that the kernel provided a value for AT_SECURE.
  errno = 0;
  unsigned long is_AT_SECURE = getauxval(AT_SECURE);
  if (errno != 0) __early_abort(__LINE__);

  // Always ensure that STDIN/STDOUT/STDERR exist. This prevents file
  // descriptor confusion bugs where a parent process closes
  // STD*, the exec()d process calls open() for an unrelated reason,
  // the newly created file descriptor is assigned
  // 0<=FD<=2, and unrelated code attempts to read / write to the STD*
  // FDs.
  // In particular, this can be a security bug for setuid/setgid programs.
  // For example:
  // https://www.freebsd.org/security/advisories/FreeBSD-SA-02:23.stdio.asc
  // However, for robustness reasons, we don't limit these protections to
  // just security critical executables.
  //
  // Init is excluded from these protections unless AT_SECURE is set, as
  // /dev/null and/or /sys/fs/selinux/null will not be available at
  // early boot.
  if ((getpid() != 1) || is_AT_SECURE) {
    __nullify_closed_stdio();
  }

  if (is_AT_SECURE) {
    __sanitize_environment_variables(env);
  }

  // Now the environment has been sanitized, make it available.
  environ = __libc_shared_globals()->init_environ = env;

  __initialize_personality();
}

该函数初始化了environ全局变量,但是该初始化很明显是在linker可执行文件内初始化的跟我们上面所说的在libc.so中不同,且这个时候还根本没有装载libc.so这个库,这就要看__libc_shared_globals()是如何返回的,查找申明结果查找到两个,一个是在libc_init_dynamic.cpp

extern "C" libc_shared_globals* __loader_shared_globals();
__LIBC_HIDDEN__ libc_shared_globals* __libc_shared_globals() {
  return __loader_shared_globals();
}

一个是在libc_init_static.cpp

__LIBC_HIDDEN__ libc_shared_globals* __libc_shared_globals() {
  static libc_shared_globals globals;
  return &globals;
}

可以看出第一个是调用一个外部函数,而第二个是直接返回静态变量,这是因为libc编译成静态库libc.a使用libc_init_static,编译成动态库libc.so使用libc_init_dynamic,因此libc.so中并不存在__libc_shared_globals方法的实现,使用IDA分析libc.so也可发现__libc_shared_globals是导入函数。那么谁实现了该函数呢,正是 linker 实现了该函数并导出该符号,使用IDA分析 linker 可发现该导出函数。因为 linker 模块本身要使用一些libc函数,而它本身又作为动态链接器无法依赖其它库,因此自身静态链接了一份libc,所以上面使用getenv实际上是访问了linker中局部静态变量globals数据

环境变量的值设置:上述初始化环境变量使用__libc_init_AT_SECURE(args.envp)args.envp正是该环境变量,函数调用向上回溯__linker_init_post_relocation(),继续向上回溯__linker_init(),该函数是 linker 的入口函数,其有关环境变量设置代码如下:

extern "C" ElfW(Addr) __linker_init(void* raw_args) {
  // Initialize TLS early so system calls and errno work.
  // args中包含后续使用的环境变量
  KernelArgumentBlock args(raw_args);
  bionic_tcb temp_tcb __attribute__((uninitialized));
  linker_memclr(&temp_tcb, sizeof(temp_tcb));
  __libc_init_main_thread_early(args, &temp_tcb);

  ...
}

KernelArgumentBlock源码

#pragma once

#include <elf.h>
#include <link.h>
#include <stdint.h>
#include <sys/auxv.h>

#include "platform/bionic/macros.h"

// When the kernel starts the dynamic linker, it passes a pointer to a block
// of memory containing argc, the argv array, the environment variable array,
// and the array of ELF aux vectors. This class breaks that block up into its
// constituents for easy access.
class KernelArgumentBlock {
 public:
  explicit KernelArgumentBlock(void* raw_args) {
    uintptr_t* args = reinterpret_cast<uintptr_t*>(raw_args);
    argc = static_cast<int>(*args);
    argv = reinterpret_cast<char**>(args + 1);
    // 此处初始化环境变量
    envp = argv + argc + 1;

    // Skip over all environment variable definitions to find the aux vector.
    // The end of the environment block is marked by a NULL pointer.
    char** p = envp;
    while (*p != nullptr) {
      ++p;
    }
    ++p; // Skip the NULL itself.

    auxv = reinterpret_cast<ElfW(auxv_t)*>(p);
  }

  // Similar to ::getauxval but doesn't require the libc global variables to be set up,
  // so it's safe to call this really early on.
  unsigned long getauxval(unsigned long type) {
    for (ElfW(auxv_t)* v = auxv; v->a_type != AT_NULL; ++v) {
      if (v->a_type == type) {
        return v->a_un.a_val;
      }
    }
    return 0;
  }

  int argc;
  char** argv;
  char** envp;
  ElfW(auxv_t)* auxv;

 private:
  BIONIC_DISALLOW_COPY_AND_ASSIGN(KernelArgumentBlock);
};

查看源码可知原来环境变量紧跟着可执行程序参数argv后面,而可执行文件都是通过exec簇函数调用,最终都进行系统调用 int execve(const char* __file, char* const* __argv, char* const* __envp); 该环境变量默认是继承的父进程

如何设置LD_PRELOAD环境变量

上面分析通过 putenv 方法确实可以添加LD_PRELOAD环境变量,但是在本进程中执行时机太晚,因为 linker 在启动的时候才获取LD_PRELOAD环境变量,后面就算是设置成功但是也不会触发了,那如何才能生效呢,有以下几种方法

  1. 在该进程的父进程设置该环境变量,然后再启动该进程,根据继承关系得到该环境变量,由于Android特性所有应用进程都是zygote的子进程,似乎设置zygote进程环境变量比较完美,但实际情况并非如此,见下面分析
  2. 使用exec簇函数手动设置程序启动的环境变量,Android Java层Runtime.exec设置环境变量就是使用该方法,只是是fork子进程后在子进程调用exec簇函数
  3. 使用Wrap shell script以全新进程运行程序,参考官网解释

接下来分析每种方式,我们的目的是在Hook每个App中的函数。

  1. 方式1针对非Apk执行(如直接执行可执行文件)等可行,而这相当于在shell中设置环境变量或使用Runtime.exec执行。而正常Apk执行是要经过zygote进程fork,看似在zygote进程设置然后所有APP进程都继承,虽然如此但执行时机已经太晚,因为APP进程从zygote进程fork下来其linker已经初始化执行过了,且zygote连Java环境都已经准备好还预加载了一些代码和资源,因此后续不会在获取LD_PRELOAD环境变量,有关源码如下

    private Runnable handleChildProc(ZygoteArguments parsedArgs,
                FileDescriptor pipeFd, boolean isZygote) {
            /*
            * By the time we get here, the native code has closed the two actual Zygote
            * socket connections, and substituted /dev/null in their place.  The LocalSocket
            * objects still need to be closed properly.
            */
    
            closeSocket();
    
            Zygote.setAppProcessName(parsedArgs, TAG);
    
            // End of the postFork event.
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            if (parsedArgs.mInvokeWith != null) {
                // 不为null则是第三种Wrap shell script情况
                WrapperInit.execApplication(parsedArgs.mInvokeWith,
                        parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
                        VMRuntime.getCurrentInstructionSet(),
                        pipeFd, parsedArgs.mRemainingArgs);
    
                // Should not get here.
                throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
            } else {
                if (!isZygote) {
                    // 这里直接执行初始化跳转至ActivityThread执行
                    return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
                            parsedArgs.mDisabledCompatChanges,
                            parsedArgs.mRemainingArgs, null /* classLoader */);
                } else {
                    return ZygoteInit.childZygoteInit(parsedArgs.mTargetSdkVersion,
                            parsedArgs.mRemainingArgs, null /* classLoader */);
                }
            }
        }
    
    Runnable processOneCommand(ZygoteServer zygoteServer) {
            String[] args;
    
            try {
                args = Zygote.readArgumentList(mSocketReader);
            } catch (IOException ex) {
                throw new IllegalStateException("IOException on command socket", ex);
            }
    
            ...略
    
            pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids,
                    parsedArgs.mRuntimeFlags, rlimits, parsedArgs.mMountExternal, parsedArgs.mSeInfo,
                    parsedArgs.mNiceName, fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
                    parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, parsedArgs.mIsTopApp,
                    parsedArgs.mPkgDataInfoList, parsedArgs.mWhitelistedDataInfoList,
                    parsedArgs.mBindMountAppDataDirs, parsedArgs.mBindMountAppStorageDirs);
    
            try {
                if (pid == 0) {
                    // in child
                    zygoteServer.setForkChild();
    
                    zygoteServer.closeServerSocket();
                    IoUtils.closeQuietly(serverPipeFd);
                    serverPipeFd = null;
    
                    return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote);
                } else {
                    // In the parent. A pid < 0 indicates a failure and will be handled in
                    // handleParentProc.
                    IoUtils.closeQuietly(childPipeFd);
                    childPipeFd = null;
                    handleParentProc(pid, serverPipeFd);
                    return null;
                }
            } finally {
                IoUtils.closeQuietly(childPipeFd);
                IoUtils.closeQuietly(serverPipeFd);
            }
        }

    当然还可在init进程设置环境变量,zygoteinit进程fork下来则继承该环境变量

  2. 使用Runtime.exec执行,该情况的弊端在于本进程无法生效,后续子进程生效,而正常情况APP内执行Runtime.exec也只是启动shell执行一些短暂脚本,因此APK进程并不会被Hook。使用exec簇函数在当前进程执行,那完全是替换当前进程,该方式与方法3是相同道理,只是方法3执行时机是在zygote进程fork后立即执行,而本方式是在App执行自身代码时才能执行,相当于二次重启APP,但在实际APK执行却无法成功
  3. 使用Wrap shell script执行,通过文档其最基本脚本如下  

    #!/system/bin/sh
    exec "$@"

    通过执行shell脚本内部也是调用exec簇函数,官方文档打包wrap.sh也要求APK必须是可调试的android:debuggable="true",还要设置android:extractNativeLibs="true"以及将wrap.sh打包到原生库中,这些参数作为启动参数传递给zygote,在Zygote.java中有如下代码

    static void applyInvokeWithSystemProperty(ZygoteArguments args) {
        if (args.mInvokeWith == null) {
            args.mInvokeWith = getWrapProperty(args.mNiceName);
        }
    }
    public static String getWrapProperty(String appName) {
        if (appName == null || appName.isEmpty()) {
            return null;
        }
    
        String propertyValue = SystemProperties.get("wrap." + appName);
        if (propertyValue != null && !propertyValue.isEmpty()) {
            return propertyValue;
        }
        return null;
    }

    也就是可以设置全局属性wrap.apppackage来指定Wrap shell script脚本位置方便我们测试。接着上面handleChildProc函数分析,当parsedArgs.mInvokeWith != null执行WrapperInit.execApplication源码如下

     public static void execApplication(String invokeWith, String niceName,
            int targetSdkVersion, String instructionSet, FileDescriptor pipeFd,
            String[] args) {
        StringBuilder command = new StringBuilder(invokeWith);
    
        final String appProcess;
        if (VMRuntime.is64BitInstructionSet(instructionSet)) {
            appProcess = "/system/bin/app_process64";
        } else {
            appProcess = "/system/bin/app_process32";
        }
        command.append(' ');
        command.append(appProcess);
    
        // Generate bare minimum of debug information to be able to backtrace through JITed code.
        // We assume that if the invoke wrapper is used, backtraces are desirable:
        //  * The wrap.sh script can only be used by debuggable apps, which would enable this flag
        //    without the script anyway (the fork-zygote path).  So this makes the two consistent.
        //  * The wrap.* property can only be used on userdebug builds and is likely to be used by
        //    developers (e.g. enable debug-malloc), in which case backtraces are also useful.
        command.append(" -Xcompiler-option --generate-mini-debug-info");
    
        command.append(" /system/bin --application");
        if (niceName != null) {
            command.append(" '--nice-name=").append(niceName).append("'");
        }
        command.append(" com.android.internal.os.WrapperInit ");
        command.append(pipeFd != null ? pipeFd.getInt$() : 0);
        command.append(' ');
        command.append(targetSdkVersion);
        Zygote.appendQuotedShellArgs(command, args);
        preserveCapabilities();
        Zygote.execShell(command.toString());
    }

    也就是将我们的Wrap shell script当作shell执行参数,其展开命令如下

    sh -c mywrap.sh /system/bin/app_process64 -Xcompiler-option --generate-mini-debug-info /system/bin --application --nice-name=App包名 com.android.internal.os.WrapperInit pipeFd 手机SDK版本 android.app.ActivityThread seq=数字

    其中 android.app.ActivityThread seq=数字 是启动Apk传递过来的参数args,回过头来方式2使用exec构造该执行参数即可。这种方式从app_process启动Apk与启动Java程序类似,只是相应参数,uid,Java环境不同,Apk包含Android上下文而Java程序则不包含。使用app_process执行必然又会需要动态链接,因此重新运行linker使我们设置的环境变量生效。因此我们可以修改Wrap shell script脚本如下

     #!/system/bin/sh
     export LD_PRELOAD=/data/local/tmp/ld-test.so
     exec "$@"

    设置全局属性setprop wrap.app.package path/to/mywrap.sh,或者直接设置属性setprop wrap.app.package LD_PRELOAD=/data/local/tmp/ld-test.so根据shell命令展开都能生效,注意预加载so库的位数要与APP位数匹配,脚本和so的执行权限和selinux属性

三种方式都有其优劣势,方式1如果在init进程设置环境变量,那么zygote就会包含,进而传染给所有APP,但是同样所有APP都被Hook无法指定APP生效,且修改难度大。方式2使用exec子进程生效而本进程由于执行时机较晚不会成功。方式3则有更多的权限限制及打包限制。

测试 LD_PRELOAD

方式1 init进程注入其结果显而易见且操作困难(解锁boot.img,修改init/启动脚本.rc等总之要在很早的时机拿到执行权限,可以通过Magisk实现,这里不做讲解,当然也可以重新编译源码模拟测试)因此不做测试。

方式2 Runtime.exec子进程生效不做测试

方式3 设置Wrap shell script测试如下,为了方便直接设置全局属性wrap.app.package=mywrap.sh预加载库ld-test.so源码如下

#include <dlfcn.h>
#include <android/log.h>
#include <cstring>
#include "hook_module.h"

#define C_API extern "C"
#define PUBLIC_METHOD __attribute__ ((visibility ("default")))

#define LOGD(format, ...) __android_log_print(ANDROID_LOG_DEBUG, "PreloadTest", format, ##__VA_ARGS__);

C_API PUBLIC_METHOD int access(const char *pathname, int mode) {
    static int (*orig_access)(const char *pathname, int mode) = nullptr;
    if (orig_access == nullptr) {
        void *libc = dlopen("libc.so", 0);
        if (libc == nullptr) {
            return 0;
        }
        orig_access = reinterpret_cast<int (*)(const char *, int)>(dlsym(libc, "access"));
        if (orig_access == nullptr) {
            return 0;
        }
    }
    // 避免陷入死循环,高版本调用__android_log_print会访问access函数请求/dev/socket/logdw权限
    // 因此此时再调用__android_log_print则会陷入死循环
    if (strcmp("/dev/socket/logdw", pathname) == 0){
        return orig_access(pathname, mode);
    }
    LOGD("Hook模块监听access函数,路径: %s, mode: %d", pathname, mode);
    if (strcmp("/data/local/tmp/ld-test.txt", pathname) == 0){
        // 如果是我们测试文件访问则返回-1没有该文件
        return -1;
    }
    return orig_access(pathname, mode);
}

static __attribute__((constructor)) void Init() {
    LOGD("LD_PRELOAD模块被加载");
}

Java相关源码如下

package org.sfandroid.nativehook.test;

import android.os.Bundle;
import android.util.Log;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

import org.sfandroid.nativehook.test.databinding.ActivityMainBinding;

import java.io.File;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private ActivityMainBinding binding;

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        findViewById(R.id.btn_test).setOnClickListener(this);
        findViewById(R.id.btn_test1).setOnClickListener(this);
        // Example of a call to a native method
    }

    public native String stringFromJNI();
    public native void execShell();
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_test:
//                stringFromJNI();
                Log.d("PreloadTest", "文件存在: " + new File("/data/local/tmp/ld-test.txt").exists());
                break;
            case R.id.btn_test1:
                try {
                    execShell();
                } catch (Throwable e) {
                    Log.e("PreloadTest", "执行错误", e);
                }
                break;
        }
    }
}

使用Android11官方x86_64模拟器,测试64位,将预加载库库放至/data/local/tmp目录下,并创建data/local/tmp/ld-test.txt测试文件,关闭selinux方便后续测试,命令如下

adb root
adb push path/to/ld-test.so /data/local/tmp
adb shell touch /data/local/tmp/ld-test.txt
adb shell chmod 777 /data/local/tmp/libld-test.so
adb shell chmod 666 /data/local/tmp/ld-test.txt
adb shell setenforce 0

当我们未设置wrap.org.sfandroid.nativehook.test属性时执行测试结果如下

7667-7667/? D/PreloadTest: 文件存在: true

上面我们创建了测试文件因此它是真实存在的而当我们执行adb shell setprop wrap.org.sfandroid.nativehook.test "LD_PRELOAD=/data/local/tmp/libld-test.so"后重新运行APP测试结果如下

7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /dev/urandom, mode: 4
7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /dev/__properties__/property_info, mode: 4
7806-7806/? D/PreloadTest: LD_PRELOAD模块被加载
7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /dev/boringssl/selftest/1cca4bc9f53317a49929af0b9283b0f20d38a542739d7db04ff0aa7ca9b9dcd1, mode: 0
7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /dev/boringssl/selftest/1cca4bc9f53317a49929af0b9283b0f20d38a542739d7db04ff0aa7ca9b9dcd1, mode: 0
7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /vendor/overlay, mode: 0
7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /vendor/overlay/config/config.xml, mode: 0

...省略部分

7806-7806/? D/PreloadTest: Hook模块监听access函数,路径: /data/local/tmp/ld-test.txt, mode: 0
7806-7806/? D/PreloadTest: 文件存在: false

可以看到libld-test.so确实被首先加载,并影响了Java层调用File.exist(native层调用access函数)结果,从而达到屏蔽的效果。

我们尝试了wrap.sh脚本方式需要设置环境变量或指定打包那么再测试一下直接构造exec执行参数试试,跟踪Zygote.exeShell最终调用execv函数,而它return execve(name, argv, environ);,因此只需要将LD_PRELOAD添加到当前进程环境变量然后调用execv函数即可,有关native代码如下

extern "C"
JNIEXPORT void JNICALL
Java_org_sfandroid_nativehook_test_MainActivity_execShell(JNIEnv *env, jobject thiz) {
    char *ld_preload = getenv("LD_PRELOAD");
    if (ld_preload != nullptr) {
        // 已经包含了LD_PRELOAD环境变量则不用再执行了
        LOGD("已经存在LD_PRELOAD变量: %s", ld_preload);
        return;
    }
    putenv((char *) "LD_PRELOAD=/data/local/tmp/libld-test.so");
    // pipeFd是开机后的一个socket文件写入,只要不重启该值不会变,为了方便本次设备为40固定即可
    const char *args[] = {"/system/bin/sh", "-c",
                          "/system/bin/app_process64 -Xcompiler-option --generate-mini-debug-info /system/bin --application --nice-name=org.sfandroid.nativehook.test com.android.internal.os.WrapperInit 40 30 android.app.ActivityThread seq=130",
                          nullptr};
    execv(args[0], (char *const *) args);
}

这就相当于手动执行wrap.sh脚本,只是时间在App应用执行时,原理虽然通过但是执行却得到以下结果

596-789/? W/InputDispatcher: channel '7c0090b org.sfandroid.nativehook.test/org.sfandroid.nativehook.test.MainActivity (server)' ~ Consumer closed input channel or an error occurred.  events=0x9
596-789/? E/InputDispatcher: channel '7c0090b org.sfandroid.nativehook.test/org.sfandroid.nativehook.test.MainActivity (server)' ~ Channel is unrecoverably broken and will be disposed!
596-2056/? I/system_server: oneway function results will be dropped but finished with status OK and parcel size 4
0-0/? I/binder: undelivered TRANSACTION_COMPLETE
0-0/? I/binder: undelivered transaction 858386, process died.
9687-9687/? D/PreloadTest: Hook模块监听access函数,路径: /dev/urandom, mode: 4
9687-9687/? D/PreloadTest: Hook模块监听access函数,路径: /dev/__properties__/property_info, mode: 4
9687-9687/? D/PreloadTest: LD_PRELOAD模块被加载
9687-9687/? D/PreloadTest: Hook模块监听access函数,路径: /system/bin/app_process64, mode: 1
596-2930/? I/WindowManager: WIN DEATH: Window{7c0090b u0 org.sfandroid.nativehook.test/org.sfandroid.nativehook.test.MainActivity}
596-2930/? W/InputDispatcher: Attempted to unregister already unregistered input channel '7c0090b org.sfandroid.nativehook.test/org.sfandroid.nativehook.test.MainActivity (server)'
596-2921/? I/ActivityManager: Process org.sfandroid.nativehook.test (pid 9687) has died: fg  TOP 
9687-9687/? I/sh: type=1400 audit(0.0:180): avc: denied { execute } for path="/data/local/tmp/libld-test.so" dev="dm-5" ino=65540 scontext=u:r:untrusted_app:s0:c153,c256,c512,c768 tcontext=u:object_r:shell_data_file:s0 tclass=file permissive=1 app=org.sfandroid.nativehook.test
300-300/? I/Zygote: Process 9687 exited due to signal 9 (Killed)
596-2056/? I/system_server: oneway function results will be dropped but finished with status OK and parcel size 4
0-0/? I/init: Untracked pid 9723 received signal 9
596-2921/? W/ActivityTaskManager: Force removing ActivityRecord{c639d07 u0 org.sfandroid.nativehook.test/.MainActivity t34}: app died, no saved state

ActivityManager: Process org.sfandroid.nativehook.test (pid 9687) has died: fg  TOP最终进程会死亡被kill掉,当然你有可能看不到PreloadTest: LD_PRELOAD模块被加载该日志,因为进程过早就被杀死掉了,多次尝试可能会查看到(我也试了很多次才出现),这证明exec执行确实我们的环境变量已生效,只是因为其它原因被杀死而已。这与wrap.sh方式相同只是执行时机一个在zygote fork后就执行,一个在APP启动后才执行,而之所以被杀也不难猜测,因为APP环境全部都准备好的system_server里已经包含该APP的进程信息,而我们执行execv又把这些数据给清除掉了,这理所当然会被system_server认为APP可能已经死亡了因此被杀死。而在zygote fork后执行该APP环境根本还未执行,system_server没有它的记录,因此可以成功。具体原因可以分析源码,这里就不分析了,因为这离我们主题更远,且我们不走这条路径。

综上分析可实现的路径要么在父进程设置子进程生效要么就设置Wrap shell script,而我们针对APP指定进程生效最简单方法是设置wrap.app.package全局属性,但同时也需要root权限,注入init进程太麻烦也无法指定APP生效。即使LD_PRELOAD生效我们也无法在Hook模块内拦截dlopen,dlsym等函数,因为我们Hook模块内部要使用它获取原函数地址,就算自己实现dlopen,dlsym函数但是在Android7.0以上的命名空间限制caller的地址都变成我们的Hook模块内的地址,那么跨命名空间调用就会存在问题。

经过以上分析引入了我们本文章的主题 动态修改Linker实现LD_PRELOAD效果 该方式避免了上面的缺点,优点如下

  1. APP进程内生效,可以指定进程
  2. 可以重打包到APP内部无需权限,也可进程注入
  3. 可以HOOK指定模块(如libandroid-runtime.so等)
  4. 可以Hook dlopen,dlsym等符号,不受命名空间限制
  5. 进程fork子进程默认继承

LD_PRELOAD原理分析

上面已经分析过在linker_main函数中获取该环境变量,紧接着往下分析

static ElfW(Addr) linker_main(KernelArgumentBlock& args, const char* exe_to_load) {
  ...省略

  if (!getauxval(AT_SECURE)) {
    ldpath_env = getenv("LD_LIBRARY_PATH");
    if (ldpath_env != nullptr) {
      INFO("[ LD_LIBRARY_PATH set to \"%s\" ]", ldpath_env);
    }
    // 获取该环境变量
    ldpreload_env = getenv("LD_PRELOAD");
    if (ldpreload_env != nullptr) {
      INFO("[ LD_PRELOAD set to \"%s\" ]", ldpreload_env);
    }
  }

  ...省略

  // Use LD_LIBRARY_PATH and LD_PRELOAD (but only if we aren't setuid/setgid).
  parse_LD_LIBRARY_PATH(ldpath_env);
  // 解析环境变量
  parse_LD_PRELOAD(ldpreload_env);

  std::vector<android_namespace_t*> namespaces = init_default_namespaces(exe_info.path.c_str());

  if (!si->prelink_image()) __linker_cannot_link(g_argv[0]);

  // add somain to global group
  si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
  // ... and add it to all other linked namespaces
  for (auto linked_ns : namespaces) {
    if (linked_ns != &g_default_namespace) {
      linked_ns->add_soinfo(somain);
      somain->add_secondary_namespace(linked_ns);
    }
  }

  linker_setup_exe_static_tls(g_argv[0]);

  // Load ld_preloads and dependencies.
  std::vector<const char*> needed_library_name_list;
  size_t ld_preloads_count = 0;

  // 将预加载模块作为库的依赖最先添加到加载任务队列
  for (const auto& ld_preload_name : g_ld_preload_names) {
    needed_library_name_list.push_back(ld_preload_name.c_str());
    ++ld_preloads_count;
  }

  // 其次才添加可执行文件的依赖项
  for_each_dt_needed(si, [&](const char* name) {
    needed_library_name_list.push_back(name);
  });

  const char** needed_library_names = &needed_library_name_list[0];
  size_t needed_libraries_count = needed_library_name_list.size();

  // 调用find_libraries加载库
  if (needed_libraries_count > 0 &&
      !find_libraries(&g_default_namespace,
                      si,
                      needed_library_names,
                      needed_libraries_count,
                      nullptr,
                      &g_ld_preloads,
                      ld_preloads_count,
                      RTLD_GLOBAL,
                      nullptr,
                      true /* add_as_children */,
                      &namespaces)) {
    __linker_cannot_link(g_argv[0]);
  } 
  ...省略
}

上面分析LD_PRELOAD库最先添加到加载队列,然后调用find_libraries执行加载,此时needed_library_names最前面的元素就是预加载库,继续查看源码

/*
    ns                                加载的命名空间=调用者的命名空间
    start_with                        调用者的soinfo
    library_names                     所有加载库名称
    library_names_count               加载库数量
    soinfos                           保存加载完成的soinfo
    ld_preloads                       保存预加载库,没有可以为null
    ld_preloads_count                 预加载库数量
    extinfo                           Android调用附带
    add_as_children                   是否作为start_with的子库
    search_linked_namespaces          查询链接命名空间
    namespaces                        链接命名空间
*/
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,
                    std::vector<android_namespace_t*>* namespaces) {
  // Step 0: prepare.
  std::unordered_map<const soinfo*, ElfReader> readers_map;
  LoadTaskList load_tasks;

  for (size_t i = 0; i < library_names_count; ++i) {
    const char* name = library_names[i];
    load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map));
  }

  // If soinfos array is null allocate one on stack.
  // The array is needed in case of failure; for example
  // when library_names[] = {libone.so, libtwo.so} and libone.so
  // is loaded correctly but libtwo.so failed for some reason.
  // In this case libone.so should be unloaded on return.
  // See also implementation of failure_guard below.

  if (soinfos == nullptr) {
    size_t soinfos_size = sizeof(soinfo*)*library_names_count;
    soinfos = reinterpret_cast<soinfo**>(alloca(soinfos_size));
    memset(soinfos, 0, soinfos_size);
  }

  // list of libraries to link - see step 2.
  size_t soinfos_count = 0;

  auto scope_guard = android::base::make_scope_guard([&]() {
    for (LoadTask* t : load_tasks) {
      LoadTask::deleter(t);
    }
  });

  ZipArchiveCache zip_archive_cache;

  // Step 1: expand the list of load_tasks to include
  // all DT_NEEDED libraries (do not load them just yet)
  for (size_t i = 0; i<load_tasks.size(); ++i) {
    LoadTask* task = load_tasks[i];
    soinfo* needed_by = task->get_needed_by();

    bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
    task->set_extinfo(is_dt_needed ? nullptr : extinfo);
    task->set_dt_needed(is_dt_needed);

    LD_LOG(kLogDlopen, "find_libraries(ns=%s): task=%s, is_dt_needed=%d", ns->get_name(),
           task->get_name(), is_dt_needed);

    // Note: start from the namespace that is stored in the LoadTask. This namespace
    // is different from the current namespace when the LoadTask is for a transitive
    // dependency and the lib that created the LoadTask is not found in the
    // current namespace but in one of the linked namespace.
    if (!find_library_internal(const_cast<android_namespace_t*>(task->get_start_from()),
                               task,
                               &zip_archive_cache,
                               &load_tasks,
                               rtld_flags)) {
      return false;
    }

    soinfo* si = task->get_soinfo();

    if (is_dt_needed) {
      needed_by->add_child(si);
    }

    // When ld_preloads is not null, the first
    // ld_preloads_count libs are in fact ld_preloads.
    // 如果预加载库不为空则保存到ld_preloads中
    if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) {
      ld_preloads->push_back(si);
    }

    if (soinfos_count < library_names_count) {
      soinfos[soinfos_count++] = si;
    }
  }
  // 第一步已经完成so的加载,并都存在对应的soinfo

  // Step 2: Load libraries in random order (see b/24047022)
  LoadTaskList load_list;
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    auto pred = [&](const LoadTask* t) {
      return t->get_soinfo() == si;
    };

    // 库还未链接则添加到加载列表
    if (!si->is_linked() &&
        std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) {
      load_list.push_back(task);
    }
  }
  ...省略

  // Step 3: pre-link all DT_NEEDED libraries in breadth first order.
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    // 第三步预链接解析所有Dynamic Section
    if (!si->is_linked() && !si->prelink_image()) {
      return false;
    }
    register_soinfo_tls(si);
  }

  // Step 4: Construct the global group. Note: DF_1_GLOBAL bit of a library is
  // determined at step 3.

  // Step 4-1: DF_1_GLOBAL bit is force set for LD_PRELOADed libs because they
  // must be added to the global group
  // 第四步构造全局组,这里使用了上面的预加载库,设置全局标志
  if (ld_preloads != nullptr) {
    for (auto&& si : *ld_preloads) {
      si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
    }
  }

  // Step 4-2: Gather all DF_1_GLOBAL libs which were newly loaded during this
  // run. These will be the new member of the global group
  soinfo_list_t new_global_group_members;
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    if (!si->is_linked() && (si->get_dt_flags_1() & DF_1_GLOBAL) != 0) {
      // 本次加载的全局库作为新的全局组
      new_global_group_members.push_back(si);
    }
  }

  // Step 4-3: Add the new global group members to all the linked namespaces
  if (namespaces != nullptr) {
    for (auto linked_ns : *namespaces) {
        // 将新的全局组添加到所有链接命名空间
      for (auto si : new_global_group_members) {
        if (si->get_primary_namespace() != linked_ns) {
          linked_ns->add_soinfo(si);
          si->add_secondary_namespace(linked_ns);
        }
      }
    }
  }

  // Step 5: Collect roots of local_groups.
  // Whenever needed_by->si link crosses a namespace boundary it forms its own local_group.
  // Here we collect new roots to link them separately later on. Note that we need to avoid
  // collecting duplicates. Also the order is important. They need to be linked in the same
  // BFS order we link individual libraries.
  std::vector<soinfo*> local_group_roots;
  if (start_with != nullptr && add_as_children) {
    local_group_roots.push_back(start_with);
  } else {
    CHECK(soinfos_count == 1);
    local_group_roots.push_back(soinfos[0]);
  }

  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    soinfo* needed_by = task->get_needed_by();
    bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
    android_namespace_t* needed_by_ns =
        is_dt_needed ? needed_by->get_primary_namespace() : ns;

    if (!si->is_linked() && si->get_primary_namespace() != needed_by_ns) {
      auto it = std::find(local_group_roots.begin(), local_group_roots.end(), si);
      LD_LOG(kLogDlopen,
             "Crossing namespace boundary (si=%s@%p, si_ns=%s@%p, needed_by=%s@%p, ns=%s@%p, needed_by_ns=%s@%p) adding to local_group_roots: %s",
             si->get_realpath(),
             si,
             si->get_primary_namespace()->get_name(),
             si->get_primary_namespace(),
             needed_by == nullptr ? "(nullptr)" : needed_by->get_realpath(),
             needed_by,
             ns->get_name(),
             ns,
             needed_by_ns->get_name(),
             needed_by_ns,
             it == local_group_roots.end() ? "yes" : "no");

      if (it == local_group_roots.end()) {
        local_group_roots.push_back(si);
      }
    }
  }

  // Step 6: Link all local groups
  for (auto root : local_group_roots) {
    soinfo_list_t local_group;
    android_namespace_t* local_group_ns = root->get_primary_namespace();

    walk_dependencies_tree(root,
      [&] (soinfo* si) {
        if (local_group_ns->is_accessible(si)) {
          local_group.push_back(si);
          return kWalkContinue;
        } else {
          return kWalkSkip;
        }
      });

    // 获取全局组包含的soinfo,因为预加载库是一起加载的跟local_group_ns是同一个命名空间
    // 所有这里的全局组已经包含了预加载库
    soinfo_list_t global_group = local_group_ns->get_global_group();
    SymbolLookupList lookup_list(global_group, local_group);
    soinfo* local_group_root = local_group.front();

    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 (__libc_shared_globals()->load_hook) {
          __libc_shared_globals()->load_hook(si->load_bias, si->phdr, si->phnum);
        }
        lookup_list.set_dt_symbolic_lib(si->has_DT_SYMBOLIC ? si : nullptr);
        // 链接库使用上面的lookup_list
        if (!si->link_image(lookup_list, local_group_root, link_extinfo, &relro_fd_offset) ||
            !get_cfi_shadow()->AfterLoad(si, solist_get_head())) {
          return false;
        }
      }

      return true;
    });

    if (!linked) {
      return false;
    }
  }
  ...省略

  return true;
}

经上面代码注释了解到预加载库作为首先加载的库,且库调用si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);设置了DF_1_GLOBAL标志,后续再将全局库添加到它所有的链接命名空间内

  if (namespaces != nullptr) {
    for (auto linked_ns : *namespaces) {
        // 将新的全局组添加到所有链接命名空间
      for (auto si : new_global_group_members) {
        if (si->get_primary_namespace() != linked_ns) {
          linked_ns->add_soinfo(si);
          si->add_secondary_namespace(linked_ns);
        }
      }
    }
  }

  // 获取全局库就是根据 DF_1_GLOBAL 来获取
  soinfo_list_t android_namespace_t::get_global_group() {
  soinfo_list_t global_group;
  soinfo_list().for_each([&](soinfo* si) {
    if ((si->get_dt_flags_1() & DF_1_GLOBAL) != 0) {
      global_group.push_back(si);
    }
  });

  return global_group;
}

我们后面添加全局库就按照这个步骤来,后续查找符号时全局库集合调用local_group_ns->get_global_group();,因为我们本身加载的全局库跟待加载的库都在一个命名空间所以这里包含了我们的全局库,接下来开始链接库si->link_image(lookup_list, local_group_root, link_extinfo, &relro_fd_offset) ||!get_cfi_shadow()->AfterLoad(si, solist_get_head()) 其中lookup_list包含了全局组和本地组,其中link_image中调用了relocate(lookup_list)来重定向库的导入符号

bool soinfo::link_image(const SymbolLookupList& lookup_list, soinfo* local_group_root,
                        const android_dlextinfo* extinfo, size_t* relro_fd_offset) {
  if (is_image_linked()) {
    // already linked.
    return true;
  }

  if (g_is_ldd && !is_main_executable()) {
    async_safe_format_fd(STDOUT_FILENO, "\t%s => %s (%p)\n", get_soname(),
                         get_realpath(), reinterpret_cast<void*>(base));
  }

  local_group_root_ = local_group_root;
  if (local_group_root_ == nullptr) {
    local_group_root_ = this;
  }

  if ((flags_ & FLAG_LINKER) == 0 && local_group_root_ == this) {
    target_sdk_version_ = get_application_target_sdk_version();
  }

  ...省略

  if (!relocate(lookup_list)) {
    return false;
  }
  ...省略
}

符号重定向源码分析

bool soinfo::relocate(const SymbolLookupList& lookup_list) {

  VersionTracker version_tracker;

  if (!version_tracker.init(this)) {
    return false;
  }

  Relocator relocator(version_tracker, lookup_list);
  relocator.si = this;
  relocator.si_strtab = strtab_;
  relocator.si_strtab_size = has_min_version(1) ? strtab_size_ : SIZE_MAX;
  relocator.si_symtab = symtab_;
  relocator.tlsdesc_args = &tlsdesc_args_;
  relocator.tls_tp_base = __libc_shared_globals()->static_tls_layout.offset_thread_pointer();

  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());

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

      if (!packed_relocate<RelocMode::Typical>(relocator, sleb128_decoder(packed_relocs, packed_relocs_size))) {
        return false;
      }
    } else {
      DL_ERR("bad android relocation header.");
      return false;
    }
  }

  if (relr_ != nullptr) {
    DEBUG("[ relocating %s relr ]", get_realpath());
    if (!relocate_relr()) {
      return false;
    }
  }
// rela重定向
#if defined(USE_RELA)
  if (rela_ != nullptr) {
    DEBUG("[ relocating %s rela ]", get_realpath());

    if (!plain_relocate<RelocMode::Typical>(relocator, rela_, rela_count_)) {
      return false;
    }
  }
  if (plt_rela_ != nullptr) {
    DEBUG("[ relocating %s plt rela ]", get_realpath());
    if (!plain_relocate<RelocMode::JumpTable>(relocator, plt_rela_, plt_rela_count_)) {
      return false;
    }
  }
#else
// rel重定向
  if (rel_ != nullptr) {
    DEBUG("[ relocating %s rel ]", get_realpath());
    if (!plain_relocate<RelocMode::Typical>(relocator, rel_, rel_count_)) {
      return false;
    }
  }
  if (plt_rel_ != nullptr) {
    DEBUG("[ relocating %s plt rel ]", get_realpath());
    if (!plain_relocate<RelocMode::JumpTable>(relocator, plt_rel_, plt_rel_count_)) {
      return false;
    }
  }
#endif

  // Once the tlsdesc_args_ vector's size is finalized, we can write the addresses of its elements
  // into the TLSDESC relocations.
#if defined(__aarch64__)
  // Bionic currently only implements TLSDESC for arm64.
  for (const std::pair<TlsDescriptor*, size_t>& pair : relocator.deferred_tlsdesc_relocs) {
    TlsDescriptor* desc = pair.first;
    desc->func = tlsdesc_resolver_dynamic;
    desc->arg = reinterpret_cast<size_t>(&tlsdesc_args_[pair.second]);
  }
#endif

  return true;
}

最终调用 plain_relocate()

template <RelocMode OptMode, typename ...Args>
static bool plain_relocate(Relocator& relocator, Args ...args) {
  return needs_slow_relocate_loop(relocator) ?
      plain_relocate_impl<RelocMode::General>(relocator, args...) :
      plain_relocate_impl<OptMode>(relocator, args...);
}

template <RelocMode Mode>
__attribute__((noinline))
static bool plain_relocate_impl(Relocator& relocator, rel_t* rels, size_t rel_count) {
  for (size_t i = 0; i < rel_count; ++i) {
    if (!process_relocation<Mode>(relocator, rels[i])) {
      return false;
    }
  }
  return true;
}

template <RelocMode Mode>
__attribute__((always_inline))
static inline bool process_relocation(Relocator& relocator, const rel_t& reloc) {
  return Mode == RelocMode::General ?
      process_relocation_general(relocator, reloc) :
      process_relocation_impl<Mode>(relocator, reloc);
}

__attribute__((noinline))
static bool process_relocation_general(Relocator& relocator, const rel_t& reloc) {
  return process_relocation_impl<RelocMode::General>(relocator, reloc);
}

最终实现符号重定向是在process_relocation_impl方法

template <RelocMode Mode>
__attribute__((always_inline))
static bool process_relocation_impl(Relocator& relocator, const rel_t& reloc) {
  constexpr bool IsGeneral = Mode == RelocMode::General;

  void* const rel_target = reinterpret_cast<void*>(reloc.r_offset + relocator.si->load_bias);
  const uint32_t r_type = ELFW(R_TYPE)(reloc.r_info);
  const uint32_t r_sym = ELFW(R_SYM)(reloc.r_info);

  soinfo* found_in = nullptr;
  const ElfW(Sym)* sym = nullptr;
  const char* sym_name = nullptr;
  ElfW(Addr) sym_addr = 0;

  if (r_sym != 0) {
    // 获取重定向的符号名
    sym_name = relocator.get_string(relocator.si_symtab[r_sym].st_name);
  }

 ...省略

#if defined(USE_RELA)
  auto get_addend_rel   = [&]() -> ElfW(Addr) { return reloc.r_addend; };
  auto get_addend_norel = [&]() -> ElfW(Addr) { return reloc.r_addend; };
#else
  auto get_addend_rel   = [&]() -> ElfW(Addr) { return *static_cast<ElfW(Addr)*>(rel_target); };
  auto get_addend_norel = [&]() -> ElfW(Addr) { return 0; };
#endif
    // 通常符号都不是STT_TLS类型
  if (IsGeneral && is_tls_reloc(r_type)) {
    ... 省略
  } else {
    if (r_sym == 0) {
      // Do nothing.
    } else {
      // 这里具体查找符号
      if (!lookup_symbol<IsGeneral>(relocator, r_sym, sym_name, &found_in, &sym)) return false;
      if (sym != nullptr) {
        const bool should_protect_segments = handle_text_relocs &&
                                             found_in == relocator.si &&
                                             ELF_ST_TYPE(sym->st_info) == STT_GNU_IFUNC;
        if (should_protect_segments && !protect_segments()) return false;
        // 计算符号地址ElfW(Sym) 加上模块基址
        sym_addr = found_in->resolve_symbol_address(sym);
        if (should_protect_segments && !unprotect_segments()) return false;
      } else if constexpr (IsGeneral) {
        // A weak reference to an undefined symbol. We typically use a zero symbol address, but
        // use the relocation base for PC-relative relocations, so that the value written is zero.
        switch (r_type) {
#if defined(__x86_64__)
          case R_X86_64_PC32:
            sym_addr = reinterpret_cast<ElfW(Addr)>(rel_target);
            break;
#elif defined(__i386__)
          case R_386_PC32:
            sym_addr = reinterpret_cast<ElfW(Addr)>(rel_target);
            break;
#endif
        }
      }
    }
  }

  // 大部分符号类型都是 R_GENERIC_JUMP_SLOT 
  if constexpr (IsGeneral || Mode == RelocMode::JumpTable) {
    if (r_type == R_GENERIC_JUMP_SLOT) {
      count_relocation_if<IsGeneral>(kRelocAbsolute);
      const ElfW(Addr) result = sym_addr + get_addend_norel();
      trace_reloc("RELO JMP_SLOT %16p <- %16p %s",
                  rel_target, reinterpret_cast<void*>(result), sym_name);
        // 赋值
      *static_cast<ElfW(Addr)*>(rel_target) = result;
      return true;
    }
  }
  ...省略其它类型符号地址计算及赋值
}

其中最关键的符号查找调用lookup_symbol

template <bool DoLogging>
__attribute__((always_inline))
// relocator 就是前面传进来包含全局组和本地组的soinfos,全局组排在最前面
static inline bool lookup_symbol(Relocator& relocator, uint32_t r_sym, const char* sym_name,
                                 soinfo** found_in, const ElfW(Sym)** sym) {
  if (r_sym == relocator.cache_sym_val) {
    // 如果是上次查询到的符号则没有必要再次查询
    *found_in = relocator.cache_si;
    *sym = relocator.cache_sym;
    count_relocation_if<DoLogging>(kRelocSymbolCached);
  } else {
    // 判断版本是否匹配
    const version_info* vi = nullptr;
    if (!relocator.si->lookup_version_info(relocator.version_tracker, r_sym, sym_name, &vi)) {
      return false;
    }

    soinfo* local_found_in = nullptr;
    // 最终调用soinfo_do_lookup查询符号
    const ElfW(Sym)* local_sym = soinfo_do_lookup(sym_name, vi, &local_found_in, relocator.lookup_list);

    relocator.cache_sym_val = r_sym;
    relocator.cache_si = local_found_in;
    relocator.cache_sym = local_sym;
    *found_in = local_found_in;
    *sym = local_sym;
  }

  if (*sym == nullptr) {
    if (ELF_ST_BIND(relocator.si_symtab[r_sym].st_info) != STB_WEAK) {
      DL_ERR("cannot locate symbol \"%s\" referenced by \"%s\"...", sym_name, relocator.si->get_realpath());
      return false;
    }
  }

  count_relocation_if<DoLogging>(kRelocSymbol);
  return true;
}

const ElfW(Sym)* soinfo_do_lookup(const char* name, const version_info* vi,
                                  soinfo** si_found_in, const SymbolLookupList& lookup_list) {
  return lookup_list.needs_slow_path() ?
      soinfo_do_lookup_impl<true>(name, vi, si_found_in, lookup_list) :
      soinfo_do_lookup_impl<false>(name, vi, si_found_in, lookup_list);
}

template <bool IsGeneral>
__attribute__((noinline)) static const ElfW(Sym)*
// 最终查找符号的实现函数
soinfo_do_lookup_impl(const char* name, const version_info* vi,
                      soinfo** si_found_in, const SymbolLookupList& lookup_list) {
  const auto [ hash, name_len ] = calculate_gnu_hash(name);
  constexpr uint32_t kBloomMaskBits = sizeof(ElfW(Addr)) * 8;
  SymbolName elf_symbol_name(name);
  // 这里的lookup_list正是调用 link_image传入的SymbolLookupLib,全局库在最前面
  const SymbolLookupLib* end = lookup_list.end();
  const SymbolLookupLib* it = lookup_list.begin();

  while (true) {
    const SymbolLookupLib* lib;
    uint32_t sym_idx;

    // Iterate over libraries until we find one whose Bloom filter matches the symbol we're
    // searching for.
    // 每个库挨个查找有没有指定符号
    while (true) {
      if (it == end) return nullptr;
      lib = it++;

      if (IsGeneral && lib->needs_sysv_lookup()) {
        // 没有hash表则直接查找符号名
        if (const ElfW(Sym)* sym = lib->si_->find_symbol_by_name(elf_symbol_name, vi)) {
          *si_found_in = lib->si_;
          return sym;
        }
        continue;
      }

      if (IsGeneral) {
        TRACE_TYPE(LOOKUP, "SEARCH %s in %s@%p (gnu)",
                   name, lib->si_->get_realpath(), reinterpret_cast<void*>(lib->si_->base));
      }
     // 计算符号hash桶查询链
      const uint32_t word_num = (hash / kBloomMaskBits) & lib->gnu_maskwords_;
      const ElfW(Addr) bloom_word = lib->gnu_bloom_filter_[word_num];
      const uint32_t h1 = hash % kBloomMaskBits;
      const uint32_t h2 = (hash >> lib->gnu_shift2_) % kBloomMaskBits;

      if ((1 & (bloom_word >> h1) & (bloom_word >> h2)) == 1) {
        sym_idx = lib->gnu_bucket_[hash % lib->gnu_nbucket_];
        if (sym_idx != 0) {
          break;
        }
      }

      if (IsGeneral) {
        TRACE_TYPE(LOOKUP, "NOT FOUND %s in %s@%p",
                   name, lib->si_->get_realpath(), reinterpret_cast<void*>(lib->si_->base));
      }
    }

    // Search the library's hash table chain.
    ElfW(Versym) verneed = kVersymNotNeeded;
    bool calculated_verneed = false;

    uint32_t chain_value = 0;
    const ElfW(Sym)* sym = nullptr;
    // 根据符号hash快速查找
    do {
      sym = lib->symtab_ + sym_idx;
      chain_value = lib->gnu_chain_[sym_idx];
      if ((chain_value >> 1) == (hash >> 1)) {
        if (vi != nullptr && !calculated_verneed) {
          calculated_verneed = true;
          verneed = find_verdef_version_index(lib->si_, vi);
        }
        // 查找必须版本匹配且符号被定义为全局
        if (check_symbol_version(lib->versym_, sym_idx, verneed) &&
            static_cast<size_t>(sym->st_name) + name_len + 1 <= lib->strtab_size_ &&
            memcmp(lib->strtab_ + sym->st_name, name, name_len + 1) == 0 &&
            is_symbol_global_and_defined(lib->si_, sym)) {
          *si_found_in = lib->si_;
          if (IsGeneral) {
            TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd",
                       name, lib->si_->get_realpath(), reinterpret_cast<void*>(sym->st_value),
                       static_cast<size_t>(sym->st_size));
          }
          return sym;
        }
      }
      ++sym_idx;
    } while ((chain_value & 1) == 0);

    if (IsGeneral) {
      TRACE_TYPE(LOOKUP, "NOT FOUND %s in %s@%p",
                 name, lib->si_->get_realpath(), reinterpret_cast<void*>(lib->si_->base));
    }
  }
}

可以发现最终实现符号查找在soinfo_do_lookup_impl查找规则是先查找全局库后查找依赖库,有hash表则查询hash表没用则直接查询符号名称,hash表也分为elf hash(低版本)与gnu hash(高版本)

因此我们已经知道了添加全局库的步骤如下

  1. 将该全局库soinfo添加DF_1_GLOBAL标志si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL)
  2. 添加该soinfo到所有链接命名空间linked_ns->add_soinfo(si);si->add_secondary_namespace(linked_ns);

分析源码也可知只有在linker初始化执行时才会有全局库的加载,后续使用dlopen并未解析,那么后续创建的命名空间也会包含全局库吗

android_namespace_t* create_namespace(const void* caller_addr,
                                      const char* name,
                                      const char* ld_library_path,
                                      const char* default_library_path,
                                      uint64_t type,
                                      const char* permitted_when_isolated_path,
                                      android_namespace_t* parent_namespace) {
  // 创建命名空间肯定会有一个父命名空间                                       
  if (parent_namespace == nullptr) {
    // if parent_namespace is nullptr -> set it to the caller namespace
    soinfo* caller_soinfo = find_containing_library(caller_addr);

    parent_namespace = caller_soinfo != nullptr ?
                       caller_soinfo->get_primary_namespace() :
                       g_anonymous_namespace;
  }

  ProtectedDataGuard guard;
  std::vector<std::string> ld_library_paths;
  std::vector<std::string> default_library_paths;
  std::vector<std::string> permitted_paths;

  parse_path(ld_library_path, ":", &ld_library_paths);
  parse_path(default_library_path, ":", &default_library_paths);
  parse_path(permitted_when_isolated_path, ":", &permitted_paths);

  android_namespace_t* ns = new (g_namespace_allocator.alloc()) android_namespace_t();
  ns->set_name(name);
  ns->set_isolated((type & ANDROID_NAMESPACE_TYPE_ISOLATED) != 0);
  ns->set_greylist_enabled((type & ANDROID_NAMESPACE_TYPE_GREYLIST_ENABLED) != 0);
  ns->set_also_used_as_anonymous((type & ANDROID_NAMESPACE_TYPE_ALSO_USED_AS_ANONYMOUS) != 0);

  // Android Java加载库会附带ANDROID_NAMESPACE_TYPE_SHARED标志
  if ((type & ANDROID_NAMESPACE_TYPE_SHARED) != 0) {
    // append parent namespace paths.
    // 复制父命名空间的库加载路径,以及允许的路径
    std::copy(parent_namespace->get_ld_library_paths().begin(),
              parent_namespace->get_ld_library_paths().end(),
              back_inserter(ld_library_paths));

    std::copy(parent_namespace->get_default_library_paths().begin(),
              parent_namespace->get_default_library_paths().end(),
              back_inserter(default_library_paths));

    std::copy(parent_namespace->get_permitted_paths().begin(),
              parent_namespace->get_permitted_paths().end(),
              back_inserter(permitted_paths));

    // If shared - clone the parent namespace
    // Java调用完全继承所有soinfo
    add_soinfos_to_namespace(parent_namespace->soinfo_list(), ns);
    // and copy parent namespace links
    // 同时继承链接命名空间
    for (auto& link : parent_namespace->linked_namespaces()) {
      ns->add_linked_namespace(link.linked_namespace(), link.shared_lib_sonames(),
                               link.allow_all_shared_libs());
    }
  } else {
    // If not shared - copy only the shared group
    // 非Java调用只复制共享的库
    add_soinfos_to_namespace(parent_namespace->get_shared_group(), ns);
  }

  ns->set_ld_library_paths(std::move(ld_library_paths));
  ns->set_default_library_paths(std::move(default_library_paths));
  ns->set_permitted_paths(std::move(permitted_paths));

  if (ns->is_also_used_as_anonymous() && !set_anonymous_namespace(ns)) {
    DL_ERR("failed to set namespace: [name=\"%s\", ld_library_path=\"%s\", default_library_paths=\"%s\""
           " permitted_paths=\"%s\"] as the anonymous namespace",
           ns->get_name(),
           android::base::Join(ns->get_ld_library_paths(), ':').c_str(),
           android::base::Join(ns->get_default_library_paths(), ':').c_str(),
           android::base::Join(ns->get_permitted_paths(), ':').c_str());
    return nullptr;
  }

  return ns;
}

soinfo_list_t android_namespace_t::get_shared_group() {
  if (this == &g_default_namespace) {
    return get_global_group();
  }

  soinfo_list_t shared_group;
  soinfo_list().for_each([&](soinfo* si) {
    if ((si->get_rtld_flags() & RTLD_GLOBAL) != 0) {
      shared_group.push_back(si);
    }
  });

  return shared_group;
}

这里JAVA使用命名空间一般有两个,都是跟ClassLoader绑定在一起,包含系统ClassLoader和应用ClassLoader,关于命名空间介绍可以查看我的另一篇文章Android7.0以上命名空间详解(dlopen限制)get_shared_group查找了命名空间内soinfoRTLD_GLOBAL库查找,所以只要我们将全局库添加至所有命名空间并设置好标志后续创建的库都会默认包含我们的库。

分析到这里我们的目的已经很明确了:1.设置全局标志。2.将库添加到所有命名空间。当然Android7.0以下没有命名空间则直接省略第二步。

代码实现添加全局库  参考fake-linker源码

  1. 获取所有命名空间

    linker_main.cpp中有static soinfo* solist;全局静态变量,通过它可以获取到所有已加载的soinfo进而得到所有命名空间,执行时机越早则找到的命名空间越全,可能存在部分Android使用的命名空间无法找到,只需要在Hook模块特殊处理android_dlopen_ext确保添加全局库即可

  2. 设置全局库

    参考项目fake-linker源码

  3. 项目其它的一些处理

    1. 每个Android版本soinfo几乎都有变动,且不同版本使用STL版本可能还不一样,因此针对每个API等级一个库,目前支持Android 5.0~Android 11
    2. 源码涉及到一些linker内部符号查找,通过解析节区符号表来实现,通常来说linker都没有去除内部符号表,但是其它模块几乎都是去除掉的,因此对于某些特别手机linker去除掉内部符号表则无法使用
    3. 由于执行时机的问题某些模块("libjavacore.so", "libnativehelper.so", "libnativeloader.so", "libart.so", "libopenjdk.so",我们在Application执行其系统环境已经准备好了)已经链接过了,这时需要调用提供的接口重新链接符号
    4. 模块内部处理了dlopen,dlsym等函数,Hook模块可以拦截这些函数,然后调用提供的接口就可以恢复环境
    5. 内部提供手动重链接和系统重链接,手动重链接自己实现解析符号因此可以设置过滤项

总结

通过设置LD_PRELOAD环境变量一是执行时机问题,二是权限问题,三是某些符号无法拦截等缺陷,而我们手动更改设置全局库则不受这些影响,甚至可以控制指定模块,指定符号的Hook,只是内部解析符号和适配Android版本稍微麻烦一点。

上面代码分析可能有些乱,但是你只需要记住Android7.0以下全局库只需要设置soinfo的全局标志即可,Android7.0以上命名空间限制则还要要将全局库soinfo添加到所有命名空间即可,后续创建的命名空间必然继承全局库,然后因为执行时机问题一些库已经加载并链接了,因此我们要重新链接它的导入符号。

其它的更多使用请等待更新。


原文链接点击这里 原文

免费评分

参与人数 11吾爱币 +11 热心值 +11 收起 理由
yuanyxh + 1 + 1 用心讨论,共获提升!
fengbolee + 1 + 1 用心讨论,共获提升!
azcolf + 1 + 1 用心讨论,共获提升!
独行风云 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
5omggx + 1 + 1 用心讨论,共获提升!
qaz007 + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 谢谢@Thanks!
JMBQ + 1 + 1 鼓励转贴优秀软件安全工具和文档!
tomatobobot + 1 + 1 谢谢@Thanks!
_pan + 1 + 1 用心讨论,共获提升!
杨辣子 + 1 + 1 牛逼!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| beichen 发表于 2021-1-26 09:22
FraMeQ 发表于 2021-1-25 17:42
setprop wrap.app.package 不是更好用吗。整这么复杂

上面说了 setprop wrap.app.package需要root权限,且无法拦截dlsym函数,动态修改可以植入App内部无需权限
回不去的时光 发表于 2021-1-21 12:34
liujieboss 发表于 2021-1-21 13:16
Eaglecad 发表于 2021-1-21 19:17
好家伙,这么多,眼珠子都累了
FraMeQ 发表于 2021-1-25 17:42
setprop wrap.app.package 不是更好用吗。整这么复杂
澳洲烧卖 发表于 2021-1-25 22:23
感谢楼主分享
程月凛 发表于 2021-1-26 18:30
大佬值得让人尊敬
qaz007 发表于 2021-1-27 08:51
感谢楼主分享。
kakaka169 发表于 2021-1-28 15:25
收获很多了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-25 08:47

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表