前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

需求背景

前段时间搞一个东西用到了libsu,不得不说用起来还是挺舒服的,直接代码全在一个Project里,然后只需要关心业务,不用管底层的实现,很方便。

后来在模拟器上跑发现跑不起来,情景就是APK没有提供x86_64的so的情况下在root进程(RootService)中去加载arm64-v8a的so,然后因为不同架构所以会加载失败,这个时候去调用so的接口就会寄掉,因为没有实现嘛。

当然能提供x86_64的so最好,但是有些情况提供不了,比如我的情况就是用了Paddle Lite库,然后按照官方原话这个库仅支持arm64-v8a,说是搞了很多优化,所以没得办法。

一脸黑线.jpg

于是就产生了魔改libsu的想法。

项目地址和使用方法

libsu-nativebridge fork from libsu https://github.com/Bzi-Han/libsu-nativebridge

使用方法和原来的是一样的,只不过需要改一下包的路径

android {
    compileOptions {
        // The library uses Java 8 features
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
repositories {
    maven { url 'https://jitpack.io' }
}
dependencies {
    def libsuVersion = '5.2.2'

    // The core module that provides APIs to a shell
    implementation "com.github.Bzi-Han.libsu-nativebridge:core:${libsuVersion}"

    // Optional: APIs for creating root services. Depends on ":core"
    implementation "com.github.Bzi-Han.libsu-nativebridge:service:${libsuVersion}"

    // Optional: Provides remote file system support
    implementation "com.github.Bzi-Han.libsu-nativebridge:nio:${libsuVersion}"
}

其实我本来想发个PR,不过想到添加了额外的C++项目(劫持的so库),以及关键参数的生命不确定性(-Xforce-nb-testing)和我只做了x86_64的支持,所以暂时就先这样吧。

改造libsu

原理的话可以看上篇文章[编程] Android app_process 启动进程强行开启NativeBridge支持

总之我们先添加一个用于劫持nativebridge的so添加到项目中

defaultConfig {
    ndk {
        abiFilters += listOf("x86_64")
    }
}

externalNativeBuild {
    cmake {
        path = file("src/main/cpp/CMakeLists.txt")
    }
}

劫持的代码逻辑如下

bool android::native_bridge::v1::Initialize(const android::NativeBridgeRuntimeCallbacks *androidRuntimeCallbacks, const char *appCodeCacheDir, const char *isa)
{
    auto callbacks = detail::GetCallbacksInternal();

    if (nullptr == callbacks)
        return false;

    auto result = callbacks->initialize(androidRuntimeCallbacks, appCodeCacheDir, isa);
    if (!result)
    {
        isa = "arm64";

        LogError("[-] [Initialize] Tried default params failed, also hijack to %p %s %s", androidRuntimeCallbacks, appCodeCacheDir, isa);
        result = callbacks->initialize(androidRuntimeCallbacks, appCodeCacheDir, isa);
        if (!result)
            LogError("[-] [Initialize] Tried hijack params failed, unknow error");
        else
            LogInfo("[+] [Initialize] Tried hijack params succeeded");
    }

    return result;
}

我们的逻辑是先尝试用传入的参数去初始化,如果失败了,那么就尝试用arm64去初始化,如果还是失败了,那就无了。

一般来说就是arm64,因为我们的目的就是在x86_64的平台上转译arm64-v8a的so嘛。

然后就是Java层的修改。

我们添加一个函数用来判断当前是否支持NativeBridge

@SuppressLint("PrivateApi")
private boolean isSupportNativeBridge() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
        return false;
    // We only supported the x86_64 platform.
    if (!"x86_64".equals(Build.SUPPORTED_ABIS[0]))
        return false;

    try {
        Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
        Method get = SystemProperties.getMethod("get", String.class);
        String nativeBridgeProvider = (String)get.invoke(SystemProperties, "ro.dalvik.vm.native.bridge");

        return !"".equals(nativeBridgeProvider) && !"0".equals(nativeBridgeProvider);
    } catch (Exception e) {
        Utils.log(TAG, e.getMessage());
        return false;
    }
}

然后在startRootProcess函数返回的Task中修改启动逻辑和参数。

先初始化必要的变量

String setup = ""; // 用于设置启动root进程时的环境
boolean isNativeBridgeSupported = isSupportNativeBridge(); // 用于判断是否支持NativeBridge的变量
File nativeBridge = new File(ctx.getCacheDir(), "libnativebridgehijack.so"); // 释放assets里的so到本地的路径

然后释放assets里的so到本地

// Dump libnativebridgehijack.so as trampoline
if (isNativeBridgeSupported) {
    try (InputStream in = ctx.getResources().getAssets().open(Build.SUPPORTED_ABIS[0] + "/libnativebridgehijack.so");
            OutputStream out = new FileOutputStream(nativeBridge)) {
        Utils.pump(in, out);
    }
}

其中Build.SUPPORTED_ABIS[0]代表的是当前平台的ABI。

然后判断是否需要添加支持NativeBridge的启动参数

if (isNativeBridgeSupported) {
    setup += String.format(
            "cd %s && export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. &&",
            nativeBridge.getParent()
    );

    params += String.format(" -Xforce-nb-testing -XX:NativeBridge=%s", nativeBridge.getName());
}

其中setup的作用需要重点说一下,因为-XX:NativeBridge参数只能给范围[a-zA-Z0-9._-]内的字符并且只能以[a-zA-Z]开头,所以我们没法直接指定类似/data/local/tmp/xxx.so这样的路径,详情可以看
这里

所以这里我们通过cd来指定当前目录,然后通过export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.来指定LD_LIBRARY_PATH的值,这样就可以加载当前目录下的so了。

指定当前目录还有一个很重要的作用是code_cache目录的创建,因为-Xforce-nb-testing参数的逻辑是固定死当前目录下创建code_cache的,详情可以看这里

最后就是修改启动参数了

String cmd = String.format(Locale.ROOT,
        "%s (%s CLASSPATH=%s %s %s /system/bin %s " +
        "com.topjohnwu.superuser.internal.RootServerMain '%s' %d %s >/dev/null 2>&1)&",
        setup, env, mainJar, app_process, params, niceNameCmd,
        name.flattenToString(),   // args[0]
        Process.myUid(),          // args[1]
        action);                  // args[2]

至此,我们就大功告成了!

结语

I Got Smoke!

那就这样了,有缘再见~