前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
需求背景
前段时间搞一个东西用到了libsu
,不得不说用起来还是挺舒服的,直接代码全在一个Project里,然后只需要关心业务,不用管底层的实现,很方便。
后来在模拟器上跑发现跑不起来,情景就是APK没有提供x86_64
的so的情况下在root进程(RootService)中去加载arm64-v8a
的so,然后因为不同架构所以会加载失败,这个时候去调用so的接口就会寄掉,因为没有实现嘛。
当然能提供x86_64
的so最好,但是有些情况提供不了,比如我的情况就是用了Paddle Lite库,然后按照官方原话这个库仅支持arm64-v8a
,说是搞了很多优化,所以没得办法。
于是就产生了魔改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!
那就这样了,有缘再见~