起因
hook 框架每次修改hook代码都要重启手机,而且不方便测试,所以打算做一个动态加载的功能。
由于公司开发都是有java,而且我感觉frida那种开server的模式不太好持久化。感觉xposed模式就很合适,但是xposed也需要先运行 模块app,再运行目标app,同时运行两个app杀后台就不好了,而且我想简单点,安装既生效。
目标功能:xp打包模块apk的方式集成hook脚本到一个app里,但是一安装hook app就直接加载hook代码,并且不用启动包含hook脚本的app就可以直接hook。这样只需要执行被hook的app就可以实现rpc。
控制方法:目前做的是通过系统变量去控制(手机上有多个包含hook脚本的app时),hook哪个app这种东西直接在脚本里过滤一下就行
欧克,开始看lsposed 源码,然后再看怎么去实现我自己想要的逻辑
server端
获取模块
Main.java 中 可以看到代码
Startup.initXposed(isSystem, niceName, appDir, ILSPApplicationService.Stub.asInterface(binder));
if ((niceName.equals(BuildConfig.MANAGER_INJECTED_PKG_NAME) || niceName.equals(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME))
&& ParasiticManagerHooker.start()) {
Utils.logI("Loaded manager, skipping next steps");
return;
}
Utils.logI("Loading xposed for " + niceName + "/" + Process.myUid());
Startup.bootstrapXposed();
Startup.initXposed 是初始化,就不看了,直接看Startup.bootstrapXposed
public static void bootstrapXposed() {
// Initialize the Xposed framework
try {
startBootstrapHook(XposedInit.startsSystemServer);
XposedInit.loadLegacyModules();
} catch (Throwable t) {
Utils.logE("error during Xposed initialization", t);
}
}
XposedInit.loadLegacyModules(); 这里从名字看像加载模块的方法,就从这里开始吧
ps. 很明显正常的加载模块不在这里,但是,能加载模块就完了我又不是要再写一个lsposed,管那么多干啥
public static void loadLegacyModules() {
var moduleList = serviceClient.getLegacyModulesList();
moduleList.forEach(module -> {
var apk = module.apkPath;
var name = module.packageName;
var file = module.file;
loadedModules.put(name, Optional.of(apk)); // temporarily add it for XSharedPreference
if (!loadModule(name, apk, file)) {
loadedModules.remove(name);
}
});
}
这里就很明了了,获取moduleList,然后遍历moduleList,获取每一个module的apkPath、packageName、file
然后直接将 加载的模块put 到 loadedModules,再加载模块,加载失败就移除
由于这里探究的是lsposed如何加载模块的,我们就先看moduleList ,它的获取方式是
serviceClient.getLegacyModulesList();
从服务端传过来的,那就是说获取模块列表到这里就暂时结束了
但是在这里面,需要注意的是 module.file;这个参数,这个在后文实现getLegacyModulesList 再说吧
加载模块
然后来看加载模块 loadModule
private static boolean loadModule(String name, String apk, PreLoadedApk file) {
Log.i(TAG, "Loading legacy module " + name + " from " + apk);
var sb = new StringBuilder();
var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
for (String abi : abis) {
sb.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
}
var librarySearchPath = sb.toString();
var initLoader = XposedInit.class.getClassLoader();
var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader);
try {
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {
Log.e(TAG, " Cannot load module: " + name);
Log.e(TAG, " The Xposed API classes are compiled into the module's APK.");
Log.e(TAG, " This may cause strange issues and must be fixed by the module developer.");
Log.e(TAG, " For details, see: https://api.xposed.info/using.html");
return false;
}
} catch (ClassNotFoundException ignored) {
return false;
}
initNativeModule(file.moduleLibraryNames);
return initModule(mcl, apk, file.moduleClassNames);
}
没啥说的,就是通过模块名称在apk里进行加载,后面的initNativeModule,应该就是把java函数注册到native中
然后是initModule 这就就是真正的 Loading class 加载具体类了
private static boolean initModule(ClassLoader mcl, String apk, List<String> moduleClassNames) {
var count = 0;
for (var moduleClassName : moduleClassNames) {
try {
Log.i(TAG, " Loading class " + moduleClassName);
Class<?> moduleClass = mcl.loadClass(moduleClassName);
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it");
continue;
}
final Object moduleInstance = moduleClass.newInstance();
if (moduleInstance instanceof IXposedHookZygoteInit) {
IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
param.modulePath = apk;
param.startsSystemServer = startsSystemServer;
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
count++;
}
if (moduleInstance instanceof IXposedHookLoadPackage) {
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
count++;
}
if (moduleInstance instanceof IXposedHookInitPackageResources) {
hookResources();
XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
count++;
}
} catch (Throwable t) {
Log.e(TAG, " Failed to load class " + moduleClassName, t);
}
}
return count > 0;
}
客户端
直接看 package org.lsposed.manager;
它的 oncreate方法
public void onCreate() {
super.onCreate();
instance = this;
setCrashReport();
pref = PreferenceManager.getDefaultSharedPreferences(this);
if (!pref.contains("doh")) {
var name = "private_dns_mode";
if ("hostname".equals(Settings.Global.getString(getContentResolver(), name))) {
pref.edit().putBoolean("doh", false).apply();
} else {
pref.edit().putBoolean("doh", true).apply();
}
}
AppCompatDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme());
LocaleDelegate.setDefaultLocale(getLocale());
var res = getResources();
var config = res.getConfiguration();
config.setLocale(LocaleDelegate.getDefaultLocale());
//noinspection deprecation
res.updateConfiguration(config, res.getDisplayMetrics());
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("org.lsposed.manager.NOTIFICATION");
registerReceiver(new BroadcastReceiver() {
public void onReceive(Context context, Intent inIntent) {
var intent = (Intent) inIntent.getParcelableExtra(Intent.EXTRA_INTENT);
Log.d(TAG, "onReceive: " + intent);
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_FULLY_REMOVED, Intent.ACTION_UID_REMOVED -> {
var userId = intent.getIntExtra(Intent.EXTRA_USER, 0);
var packageName = intent.getStringExtra("android.intent.extra.PACKAGES");
var packageRemovedForAllUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false);
var isXposedModule = intent.getBooleanExtra("isXposedModule", false);
if (packageName != null) {
if (isXposedModule)
ModuleUtil.getInstance().reloadSingleModule(packageName, userId, packageRemovedForAllUsers);
else
App.getExecutorService().submit(() -> AppHelper.getAppList(true));
}
}
case ACTION_USER_ADDED, ACTION_USER_REMOVED, ACTION_USER_INFO_CHANGED -> App.getExecutorService().submit(() -> ModuleUtil.getInstance().reloadInstalledModules());
}
}
}, intentFilter, Context.RECEIVER_NOT_EXPORTED);
UpdateUtil.loadRemoteVersion();
}
里面判断了一条 isXposedModule
if (isXposedModule)
ModuleUtil.getInstance().reloadSingleModule(packageName, userId, packageRemovedForAllUsers);
else
App.getExecutorService().submit(() -> AppHelper.getAppList(true));
所以直接看 reloadSingleModule是个啥
public InstalledModule reloadSingleModule(String packageName, int userId, boolean packageFullyRemoved) {
if (packageFullyRemoved && isModuleEnabled(packageName)) {
enabledModules.remove(packageName);
listeners.forEach(ModuleListener::onModulesReloaded);
}
PackageInfo pkg;
try {
pkg = ConfigManager.getPackageInfo(packageName, PackageManager.GET_META_DATA, userId);
} catch (NameNotFoundException e) {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old));
return null;
}
ApplicationInfo app = pkg.applicationInfo;
var modernApk = getModernModuleApk(app);
if (modernApk != null || isLegacyModule(app)) {
InstalledModule module = new InstalledModule(pkg, modernApk);
installedModules.put(Pair.create(packageName, userId), module);
listeners.forEach(i -> i.onSingleModuleReloaded(module));
return module;
} else {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old));
return null;
}
}
这里就能很明显地看到 模块列表如何产生的了 installedModules
把这个方法改改,就是上文的 getLegacyModulesList。下面是我改好的
public static List<InstalledModule> getLegacyModulesList(List<PackageInfo> pks) {
List<InstalledModule> modules = new ArrayList<>();
// for (PackageInfo pkg : getInstalledPackagesFromAllUsers()) {
for (PackageInfo pkg : pks) {
ApplicationInfo app = pkg.applicationInfo;
// Log.e("Lychow666", "apk -----> " + pkg.packageName);
if (pkg.packageName.contains("android") || pkg.packageName.contains("com.google")|| pkg.packageName.contains("com.junge"))
continue;
var modernApk = getModernModuleApk(app);
if (modernApk != null || isLegacyModule(app)) {
modules.add(new InstalledModule(pkg, modernApk));
}
}
Log.e("Lychow666", "modules -----> " + modules);
// installedModules = modules;
return modules;
}
回到上面说有问题的地方
这里可以看到我返回的是一个 List
找file这个东西哪儿来的,我比较笨,直接整个项目所有出现 file 的地方都看一遍,还真让我找到了,直接给结果
daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java
它里面的方法
static PreLoadedApk loadModule(String path, boolean obfuscate) {
if (path == null) return null;
var file = new PreLoadedApk();
var preLoadedDexes = new ArrayList<SharedMemory>();
var moduleClassNames = new ArrayList<String>(1);
var moduleLibraryNames = new ArrayList<String>(1);
try (var apkFile = new ZipFile(toGlobalNamespace(path))) {
readDexes(apkFile, preLoadedDexes, obfuscate);
readName(apkFile, "META-INF/xposed/java_init.list", moduleClassNames);
if (moduleClassNames.isEmpty()) {
file.legacy = true;
readName(apkFile, "assets/xposed_init", moduleClassNames);
readName(apkFile, "assets/native_init", moduleLibraryNames);
} else {
file.legacy = false;
readName(apkFile, "META-INF/xposed/native_init.list", moduleLibraryNames);
}
} catch (IOException e) {
Log.e(TAG, "Can not open " + path, e);
return null;
}
if (preLoadedDexes.isEmpty()) return null;
if (moduleClassNames.isEmpty()) return null;
if (obfuscate) {
var signatures = ObfuscationManager.getSignatures();
for (int i = 0; i < moduleClassNames.size(); i++) {
var s = moduleClassNames.get(i);
for (var entry : signatures.entrySet()) {
if (s.startsWith(entry.getKey())) {
moduleClassNames.add(i, s.replace(entry.getKey(), entry.getValue()));
}
}
}
}
file.preLoadedDexes = preLoadedDexes;
file.moduleClassNames = moduleClassNames;
file.moduleLibraryNames = moduleLibraryNames;
return file;
}
var file = new PreLoadedApk(); 返回的就是file
这没啥好说的,代码很清晰明了,就是把dex加载到共享内存里,然后读了下"assets/xposed_init" 这里面保存了开发xp时的目标类。
这里面有个坑点,在读dex的时候 readDexes 这个方法
private static void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes,
boolean obfuscate) {
int secondary = 2;
for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null;
dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) {
try (var is = apkFile.getInputStream(dexFile)) {
preLoadedDexes.add(readDex(is, obfuscate));
} catch (IOException | ErrnoException e) {
Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e);
}
}
}
遍历apk里的所有dex,然后preLoadedDexes.add(readDex(is, obfuscate));
通过调用readDex,把结果放进preLoadedDexes,看看 readDex,坑点来了
private static SharedMemory readDex(InputStream in, boolean obfuscate) throws IOException, ErrnoException {
var memory = SharedMemory.create(null, in.available());
var byteBuffer = memory.mapReadWrite();
Channels.newChannel(in).read(byteBuffer);
SharedMemory.unmap(byteBuffer);
if (obfuscate) {
var newMemory = ObfuscationManager.obfuscateDex(memory);
if (memory != newMemory) {
memory.close();
memory = newMemory;
}
}
memory.setProtect(OsConstants.PROT_READ);
return memory;
}
第三个参数没理解错的话是否要混淆dex,ObfuscationManager.obfuscateDex 的代码是
public class ObfuscationManager {
// static {
// System.loadLibrary("core");
// }
// For module dexes
public static native SharedMemory obfuscateDex(SharedMemory memory);
// generates signature
public static native HashMap<String, String> getSignatures();
}
这里我Android studio ctrl+鼠标左键 能正常跳转到native代码去,编译也没问题,但是一运行就报错
java.lang.UnsatisfiedLinkError: No implementation found for android.os.SharedMemory org.lychow.example.util.ObfuscationManager.obfuscateDex(android.os.SharedMemory) (tried Java_org_lsposed_lspd_util_ObfuscationManager_obfuscateDex and Java_org_lychow_example_util_ObfuscationManager_obfuscateDex__Landroid_os_SharedMemory_2) - is the library loaded, e.g. System.loadLibrary?
搜索半天无果,直接逃避问题,不混淆dex就完了,然后就可以了
跟之前感觉有点意思,跟完感觉其实挺简单的,就是获取路径加载dex到共享内存,然后读取一下apk里是否含有xposed_init文件,通过这个文件内容去获取类。。。
虽然是照着模式抄作业,但在开发的时候还是遇到了几个坑(原谅我是小白)
-
拿不到所有app的全路径
-
拿到全路径后发现,没有读取的权限,只有system server进程里有权限,但是不想再写那个b c++了
-
混淆dex的方法,java调用不到native函数(逃避未解决)
原文始发于微信公众号(逆向成长日记):Lsposed 加载模块源码分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论