Lsposed 加载模块源码分析

admin 2024年7月15日13:04:20评论91 views字数 11346阅读37分49秒阅读模式

起因

   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 源码,然后再看怎么去实现我自己想要的逻辑

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方法

  @Override  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() {          @Override          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;  }

回到上面说有问题的地方

Lsposed 加载模块源码分析

这里可以看到我返回的是一个 List,正常来说他应该返回的是Module类的列表但是我懒索性直接InstalledModule,但是InstalledModule这个类里面没有file这个属性,这个就需要我们自己去构建了

找file这个东西哪儿来的,我比较笨,直接整个项目所有出现 file 的地方都看一遍,还真让我找到了,直接给结果

daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java

它里面的方法

    @Nullable    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.UnsatisfiedLinkErrorNo 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 loadede.gSystem.loadLibrary?

搜索半天无果,直接逃避问题,不混淆dex就完了,然后就可以了

跟之前感觉有点意思,跟完感觉其实挺简单的,就是获取路径加载dex到共享内存,然后读取一下apk里是否含有xposed_init文件,通过这个文件内容去获取类。。。

虽然是照着模式抄作业,但在开发的时候还是遇到了几个坑(原谅我是小白)

  1. 拿不到所有app的全路径

  2. 拿到全路径后发现,没有读取的权限,只有system server进程里有权限,但是不想再写那个b c++了

  3. 混淆dex的方法,java调用不到native函数(逃避未解决)

Lsposed 加载模块源码分析

原文始发于微信公众号(逆向成长日记):Lsposed 加载模块源码分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月15日13:04:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Lsposed 加载模块源码分析https://cn-sec.com/archives/2953213.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息