《Lsposed hook 热更新dex中的算法》
2022-11-02 11:22:42   来源:DTA 大数据安全技术学习   评论:0 点击:

  《Lsposed hook 热更新dex中的算法》

  当前主流的加固和更新均是基于动态加载完成,而对于动态加载起来的类,我们使用常规的Hook方法是没有效果的。在Hook代码中,当我们把类名,方法名称,参数都补齐了后,运行Hook代码,总是出现class not found,那类都找不到,后面的操作还怎么玩。本章中,笔者将带领大家开发一个动态加载的案例,并把class not found的问题彻底带领大家解决了。这也对后面Hook带壳的应用程序打下坚实的基础。

  3.1 Dex 动态加载

  本章的核心在于对动态加载的Dex逆向操作,这里首先给大家普及一下动态加载在Android方面的应用。

  提到动态加载,这个技术并不算新颖,它的概念很早就被提出来了,并应用在实际的生产环境中。动态加载技术主要目的是为了达到让用户不用重新安装APK就能升级特定的应用功能。尤其是在SDK的开发项目中。使用这项技术不但可以应用新版本的覆盖率,同时也减少了服务器对应用APP旧版本的接口兼容压力。除此之外也可以快速的修复生产环境中的存在的BUG。

  3.1.1 Android系统动态加载

  APP的运行逻辑类似于一个Java程序,不同的是它们的底层虚拟机的类型,Android使用的是Dalvik/ART,而加载的对象也稍有不同,对象从Jar包换成了本章中的主角Dex。

  APP运行的时候,是否可以动态的从网络上下载一个APK或者是调用外部的Dex文件实现动态加载呢?逻辑上是行的通的,但是在Android上实现起来可没有那么容易,APK本质是一个压缩包,它不安装是运行不了的。如果把APK下载后,再让用户手动安装,那么也不叫动态加载了。本质就是用户重新安装了一个新的APP,这种做法在之前称之为静默安装。

  动态调用外部的Dex文件则是完全没有问题的。在APK文件中往往有一个或者多个Dex文件,我们写的每一句代码都会被编译到这些文件里面,Android应用运行的时候就是通过执行这些Dex文件完成应用的功能的。虽然一个APK一旦构建出来,我们是无法更换里面的Dex文件的,但是我们可以通过加载外部的Dex文件来实现动态加载,这个外部文件可以放在外部存储,或者从网络下载。

  3.1.2 Android系统动态加载的简要过程

  动态加载的核心原理是在程序运行的过程中加载一些外部的可执行文件,然后调用这些文件中的某个方法从而完成业务逻辑的执行。这里再补充一点:动态加载的对象不仅可以是Dex,也是可以so(动态链接共享库)。移动安全工程师出于安全的考虑,并不会让Android系统直接加载外部存储中的文件,他们对文件做了权限的隔离。

  Dex文件在被加载的时候,首先会把他们拷贝到文件目录下,如/data/app/packageName目录下,以此来确保这些三方文件被其它应用恶意的修改,然后再将他们加载到当前的运行环境中并调用相关的方法执行对应的逻辑,从而实现动态的调用。

  3.1.3 动态加载在加壳方面的应用

  这里以一代壳子为例:一代壳技术核心是Dex文件的整体加密,所以它在内存中是连续的一个内存区域。壳就相当于一把钥匙,通过壳来解密被加密的Dex文件。

  Dex存在的方式有两种,一种是以文件的方式存在于某个位置,一种是内存中的存在形态。针对这两种方式,可以引申出两种脱壳方式:

  .dex文件加载:监控文件读写操作,查找Dex的位置

  内存截取:一代壳在内存中是连续的存在,我们可以通过某些技术手段截取这段内存

  3.2 制作一个Dex

  我们知道APK文件本质上是一个压缩包,把应用的所有相关资源、代码、配置文件等都打包进去,最后供系统去调用。其中这个压缩包中还包括了.dex类型的文件,它是Java代码编译后的产物,通过Jadx或者GDA就能反编译查看其代码,反编译后的代码和开发代码在不混淆的情况下,基本一模一样。那么制作一个可被动态加载的Dex思路就出来了:我们单独建立一个project,写一段测试代码,build项目,然后解压提取Dex文件即可。

  3.2.1 新建项目

  我们只需要Dex文件,故只需要进行Java层的编码即可。新建Empty模板的项目,如图3-1所示:

  

  图3-1 新建项目

  3.2.2 编写测试代码

  接下来编写测试代码,如图3-2所示:

  

  图3-2 测试代码编写

  测试代码很简单:重新定义一个插件类,并编写一个测试方法。我们的目标就是编写一个APP并动态加载这个类并调用testMethod方法。通过Lsposed Hook掉插件中的内容,让他打印的内容更改为我们想要的内容。

  然后在MainActivity中调用这段代码,测试代码的正确性。代码调用如图3-3所示:

  

  图3-3 测试代码的正确性

  3.2.3 插件制作

  build项目后,APK产物在如图3-4所示的目录下,如图3-4所示:

  

  图3-4 APK产物

  然后再debug目录下,解压app-debug.apk文件,解压命令如下所示:

  unzip app-debug.apk

  Archive:  app-debug.apk

  inflating: res/animator/linear_indeterminate_line1_head_interpolator.xml

  inflating: res/color/material_on_surface_disabled.xml

  inflating: res/layout/test_toolbar.xml

  inflating: res/anim/design_snackbar_in.xml

  inflating: res/color/mtrl_navigation_bar_colored_item_tint.xml

  inflating: res/interpolator/btn_checkbox_checked_mtrl_animation_interpolator_0.xml

  解压后进入目录后,有如图3-5所示的文件:

  

  图3-5 解压APK产物

  此时我们会发现有有两个.dex文件,那么我们编写的测试代码到底在哪个里面呢?可以使用Jadx反编译查看一下。

  classes.dex

  反编译结果如图3-6所示:

  

  图3-6 classes.dex 反编译结果

  可以看到classes.dex反编译的结果是一些系统中的库,或三方的包。

  classes2.dex

  反编译结果如图3-7所示:

  

  图3-7 classes2.dex 反编译结果

  在classes2.dex中,看到了我们自己开发的代码。那么为什么要把代码拆分成两个Dex呢?能不能把他们合并为一个Dex呢?

  3.2.3.1 Android 分包技术

  Dex文件拆分是Android的分包技术。在应用程序开发的过程中,随着业务量的递增,应用程序中将会有很多的功能加入,随之而来的增加的就是代码的数量。在这个过程中不仅要加入自己写的逻辑代码,还要加入引入的三方包,最终编译的时候就会导致一个问题:

  Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

  在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。

  超过最大方法数限制的问题,是由于Dex文件格式限制,一个Dex文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于Dex文件,则是将工程所需全部class文件合并且压缩到一个Dex文件期间,也就是Android打包的Dex过程中, 单个Dex文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536。

  我们编写代码的软件是android studio,它的是由google开发,google对于超过最大方法数限制的问题,使用了multidex的技术。

  3.2.3.2 取消multidex

  我们就编写的几行代码同样也在编译的时候被分包处理了。为了不必要的麻烦,我们可以把dex合并起来。dex的合并可以在编译后,使用工具去合并,也可以在编译的时候指定参数去合并,我们这里采用第二种。

  android {

  compileSdkVersion 32

  buildToolsVersion "30.0.3"

  defaultConfig {

  applicationId "com.roysue.pluginsdex"

  minSdkVersion 22

  targetSdkVersion 32

  versionCode 1

  versionName "1.0"

  multiDexEnabled false  // 禁止dex拆分

  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

  }

  }

  再次build项目,并解压,得到的文件如图3-8所示:

  

  图3-8 禁用dex拆分后的解压结果

  3.2.3.3 插件上传

  插件可以上传的目录有很多这里提三个目录

  /sdcard

  可以放到sdcard中,但是要记得给权限

  /data/local/tmp

  Android开发的目录

  /data/app/packageName

  放到应用程序的目录下

  这里我们直接放在/sdcard中,存放操作如图3-9所示:

  

  图3-10 权限赋予

  这样一个插件Dex就制作完成了,我们可以通过正常的APP去编写加载代码加载这个Dex。

  3.3 如何加载自定义的Dex

  为了加载Dex,我们另外新建一个项目去加载插件Dex,新建的项目如图3-11所示:

  

  图3-11 新建项目

  3.3.1 DexClassLoader介绍

  想要加载Dex,我们需要了解ClassLoader相关知识,这里我们简单了解下类加载器相关的知识。不同的类由不同的类加载器加载。Android中的类分为项目开发的类,系统库中的类、还有动态加载起来的类。他们分别使用pathClassLoader、bootClassLoader、DexClassLoader,本章的知识需要用到DexClassLaoder,DexClassLoader是动态加载的核心组件,接下来就介绍下如何使用DexClassLoader。

  3.3.1.1 pathClassLoader && DexClassLoader

  pathClassLoader 和 DexClassLoader是BaseDexClassLoader派生出来的两个子类加载。

  PathClassLoader: 主要用于系统和app的类加载器,其中optimizedDirectory为null, 采用默认目录/data/dalvik-cache/。他的构造函数如下所示:

  public class PathClassLoader extends BaseDexClassLoader {

  public PathClassLoader(String dexPath, ClassLoader parent) {

  super(dexPath, null, null, parent);

  }

  public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {

  super(dexPath, null, librarySearchPath, parent);

  }

  }

  DexClassLoader: 可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载, 但必须是app私有可写目录来缓存odex文件. 能够加载系统没有安装的apk或者jar文件,

  因此很多热修复和插件化方案都是采用DexClassLoader;它的构造函数如下所示:

  public class

  DexClassLoader extends BaseDexClassLoader {

  public DexClassLoader(String dexPath, String optimizedDirectory,

  String librarySearchPath, ClassLoader parent) {

  super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);

  }

  }

  可以发现这两个类的构造函数最大的差别就是DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载。

  根据构造函数,可以知道如何使用DexClassLoader,下面是参数的含义:

  dexpath:dex的文件路径,可以是多个

  optimizedDirectory:用来缓存优化后的目录

  librarySearchPath:本地目录,一般为null

  parent 指定父加载器,指定父加载器是为了双亲委派机制,即让它的父亲先去加载,父亲加载不了,自己再来加载这个类。

  3.3.2 加载Dex

  Dex的加载根据DexClassLoader的构造函数,填入相关的参数即可。

  dexPath:/sdcard/plugin.dex

  optimizedDirectory: this.getCacheDir().getAbsolutePath() 获取当前应用的缓存目录

  librarySearchPath:null

  parent:this.getClassLoader() // 加载MainActivity的ClassLoader

  最终的加载代码如下:

  DexClassLoader loader = new DexClassLoader("/sdcard/plugin.dex",

  this.getCacheDir().getAbsolutePath(),null, this.getClassLoader());

  3.3.2.1 调用插件中的方法

  最后一步就是把插件中的方法调用起来,这里使用的是反射的方法做的调用,代码如下所示:

  public class MainActivity extends AppCompatActivity {

  @Override

  protected void onCreate(Bundle savedInstanceState) {

  super.onCreate(savedInstanceState);

  setContentView(R.layout.activity_main);

  DexClassLoader loader = new DexClassLoader("/sdcard/plugin.dex",

  this.getCacheDir().getAbsolutePath(),null, this.getClassLoader());

  try {

  load_dex_and_run(loader);

  } catch (ClassNotFoundException e) {

  e.printStackTrace();

  }

  }

  public static void load_dex_and_run(DexClassLoader loader) throws ClassNotFoundException {

  try {

  Class pluginClass = loader.loadClass("com.roysue.pluginsdex.PluginDex");

  Method demo = pluginClass.getDeclaredMethod("testMethod",String.class);

  demo.invoke(null,"roysue 666");

  } catch (NoSuchMethodException e) {

  e.printStackTrace();

  } catch (InvocationTargetException e) {

  e.printStackTrace();

  } catch (IllegalAccessException e) {

  e.printStackTrace();

  }

  }

  }

  编写完成代码后,在手机上运行此APP,此时会报出如下错误:

  2022-10-19 15:52:39.561 20588-20588/com.roysue.r0appchap03 E/System: Unable to load dex file: /sdcard/plugin.dex

  2022-10-19 15:52:39.562 20588-20588/com.roysue.r0appchap03 E/System: java.io.IOException: No original dex files found for dex location /sdcard/plugin.dex

  at dalvik.system.DexFile.openDexFileNative(Native Method)

  at dalvik.system.DexFile.openDexFile(DexFile.java:365)

  at dalvik.system.DexFile.<init>(DexFile.java:107)

  ...

  报错的内容是不能加载这个dex文件,这是由于没有给应用sdcard的权限。

  AndroidMainifest.xml中加入权限

  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

  手机上赋予应用权限

  AndroidMainifest.xml中加入权限仍报错,此时打开应用信息,如图3-12所示:

  

  图3-12 sdcard权限拒绝

  手机自动拒绝了应用的权限申请,此时,我们需要手动打开,如图3-13所示:

  

  图3-13 赋予权限

  Android 10 sdcard 权限新特性

  此时我们再次运行APP,发现还是会报错,无法加载插件Dex,这是Android 10的新特性。样例中用到的机器是pixel 3,系统是Android 10。对于Android的新特性,需要在AndroidMainifest.xml中加入如图3-14所示的代码:

  

  图3-14 增加配置

  所有的权限给齐之后,再次运行项目,可以正常打印插件Dex中的内容,结果如图3-15所示:

  

  图3-15 项目结果

  3.4 使用Lsposed直接对目标进行Hook

  接下来就是Hook代码的编写了,Lsposde环境搭建第一张已经详尽的说明,这里直接编写Hook代码。

  新建一个HookDex.java文件,并实现Xposed的接口,并在xposed_init中配置启动入口,如下所示:

  com.roysue.r0posed.HookDex

  同样,使用findAndHookMethod API对Dex中的testMethod进行Hook,测试代码如下所示:

  

 

  报错显示并没有找到这个类,这其实就是ClassLoader错误导致的。Lsposed当前注入的是r0appChap03的MainActivity.java类,而这个类是通过pathClassLoader加载起来的。而我们的插件在上面的开发代码中使用DexClassLoader加载起来。一般而言,只要报了Class not found错误,就是我们的ClassLoader没有找对。

  那么,我们可以编写一段代码验证一下上面讲解的内容。

  3.4.1 获取某个ClassLoader中的加载的类

  获取某个ClassLoader中的加载的类封装为一个函数,本章我们先不做讲解代码是如何开发出来的,下一张将会着重讲解,代码如下所示:

  public void find_classloader(ClassLoader loader) throws ClassNotFoundException,

  NoSuchFieldException, IllegalAccessException {

  // BaseDexClassLoader

  Class base_dex_class = loader.loadClass("dalvik.system.BaseDexClassLoader");

  Field path_list = base_dex_class.getDeclaredField("pathList");

  path_list.setAccessible(true);

  Object pathlist_obj = path_list.get(loader);

  // DexPathList

  Class path_list_class = loader.loadClass("dalvik.system.DexPathList");

  Field elements = path_list_class.getDeclaredField("dexElements");

  elements.setAccessible(true);

  Object[] elements_objs = (Object[]) elements.get(pathlist_obj);

  // 便利获取 dexfile

  for(Object obj:elements_objs){

  Class element_class = loader.loadClass("dalvik.system.DexPathList$Element");

  Field dexfile = element_class.getDeclaredField("dexFile");

  dexfile.setAccessible(true);

  DexFile dexfile_obj = (DexFile) dexfile.get(obj);

  // public Enumeration<String> entries()

  Enumeration<String> entry = dexfile_obj.entries();

  while(entry.hasMoreElements()){

  Log.i("HookDex_TAG","next element is => " + entry.nextElement());

  }

  }

  }

  在Hook代码中进行调用,代码如下所示:

  

  3.5 切换ClassLoader进行Hook

  那么如何去切换呢?既然APP可以加载插件Dex,并且使用的是DexClassLoader,在代码中也做了声明,那么我们是否可以Hook这个狗仔函数,在它切换的时候Hook呢?

  Hook DexClassLoader的代码如下所示:

  XposedHelpers.findAndHookConstructor("dalvik.system.DexClassLoader", lpparam.classLoader,

  String.class, String.class, String.class, ClassLoader.class, new XC_MethodHook() {

  @Override

  protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

  super.beforeHookedMethod(param);

  }

  @Override

  protected void afterHookedMethod(MethodHookParam param) throws Throwable {

  super.afterHookedMethod(param);

  }

  });

  这时候又有一个问题出现了,Hook代码有两个方法beforeHookedMethod和afterHookedMethod,选用哪个呢?想象是想不到的,我们直接在每个回调中编写一条日志,如下所示:

  // beforeHookedMethod

  Log.i("HookDex_TAG111", ((DexClassLoader) param.thisObject).toString());

  // afterHookedMethod

  Log.i("HookDex_TAG222", ((DexClassLoader) param.thisObject).toString());

  测试的结果如图3-18所示:

  

  图3-18 测试结果

  从测试结果来看,在进入DexClassLoader前并没有实例的存在,想想也能想到,都没去构造怎么生成呢?这个实例就是ClassLoader,拿到这个ClassLoader后,我们首先可以看下这个ClassLoader中加载了哪些类,调用如下所示:

  find_classloader((ClassLoader) param.thisObject);

  寻找ClassLoader中的类的结果如图3-19所示:

  

  图3-19 结果

  可以看到pluginsdex中的类都加载进来了,但是没有PluginDex类,这是因为PluginDex并没有被实例化,testMethod也是一个静态方法,所以没有显示。但是Hook还是可以正常进行的,Lsposed的Hook不分有没有被实例化。

  最终的Hook代码如下所示:

  XposedHelpers.findAndHookConstructor("dalvik.system.DexClassLoader", lpparam.classLoader,

  String.class, String.class, String.class, ClassLoader.class, new XC_MethodHook() {

  @Override

  protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

  super.beforeHookedMethod(param);

  // 在 before 中获取的只是系统默认的实例,所以要在 after 获取

  Log.i("HookDex_TAG111", ((DexClassLoader) param.thisObject).toString());

  }

  @Override

  protected void afterHookedMethod(MethodHookParam param) throws Throwable {

  super.afterHookedMethod(param);

  // display class

  Log.i("HookDex_TAG222", ((DexClassLoader) param.thisObject).toString());

  find_classloader((ClassLoader) param.thisObject);

  // hook class

  Class pluginClass = ((DexClassLoader)param.thisObject).loadClass("com.roysue.pluginsdex.PluginDex");

  XposedHelpers.findAndHookMethod(pluginClass, "testMethod",String.class, new XC_MethodHook() {

  @Override

  protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

  super.beforeHookedMethod(param);

  param.args[0] = "roysue 666";

  Log.i("HookDex_TAG","Enter testMethod func .....");

  }

  @Override

  protected void afterHookedMethod(MethodHookParam param) throws Throwable {

  super.afterHookedMethod(param);

  }

  });

  }

  });

  结果如图3-20所示:

  

  图3-20 Hook 结果

  3.7 本章小结

  本章中,我们学习了Hook动态加载的Dex,并对动态加载的Dex应用做了相关的普及。Hook动态加载的Dex难点在于切换ClassLoader,这需要大家对Android的ClassLoader有清晰的认识,文中只是介绍了用于动态加载的DexClassLoader,而类的加载是一个系统,有很多的ClassLoader针对不同种类的类会进行分类加载,后面的章节中,将会带领大家查看它的全貌。切换完ClassLoader后就可以使用Xposed的API对自定义中的Dex进行Hook了,顺利的解决了class not found的问题。

相关热词搜索:数据安全

上一篇:大数据产业链全解析
下一篇:大数据治理技术核心:元数据管理架构设计

本站内容除特别声明的原创文章之外,转载内容只为传递更多信息,并不代表本网站赞同其观点。转载的所有的文章、图片、音/视频文件等资料的版权归版权所有权人所有。本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如涉及作品内容、版权和其它问题,请及时通过电子邮件或电话通知我们,以便迅速采取适当措施,避免给双方造成不必要的经济损失。联系电话:010-82306116;邮箱:aet@chinaaet.com。
分享到: 收藏