学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

2万

积分

41

好友

1157

主题

[转载图文] Hook梦幻旅途之Frida

发表于 2020-9-11 18:25:02 | 查看: 1574| 回复: 0


一、基础知识

Frida是全世界最好的Hook框架。在此我们详细记录各种各样常用的代码套路,它可以帮助逆向人员对指定的进程的so模块进行分析。它主要提供了功能简单的python接口和功能丰富的js接口,使得hook函数和修改so编程化,值得一提的是接口中包含了主控端与目标进程的交互接口,由此我们可以即时获取信息并随时进行修改。使用frida可以获取进程的信息(模块列表,线程列表,库导出函数),可以拦截指定函数和调用指定函数,可以注入代码,总而言之,使用frida我们可以对进程模块进行手术刀式剖析。
1.1 Frida安装
需要安装Python Frida库以及对应手机架构的Frida server,Frida如果安装极慢或者失败,原因在于国内网络状况。
1.1.1 启动进程
启动手机Frida server进程
  adb shell
  su
  cd /data/local/tmp
  chmod 777 frida-server
  ./frida-server
  

  PS:/data/local/tmp是一个放置frida server的常见位置。
  
1.1.2 混合运行Frida
以Python+Javascript混合脚本方式运行Frida(两种模式)。
// 以附加模式启动(Attach)
  // 要求待测试App正在运行
  run.py文件
  // 导入frida库,sys系统库用于让脚本持续运行
  import sys
  import frida
  # 找寻手机frida server
  device = frida.get_usb_device()
  # 选择应用进程(一般为包名)
  appPackageName =""
  # 附加
  session = device.attach(appPackageName)
  # 加载脚本,填入脚本路径
  with open("script.js", encoding="utf-8")as f:
  script = session.create_script(f.read())
  script.load()
  sys.stdin.read()    //也可以不依赖sys库,使用time.sleep(10000000);
  script.js文件
  setImmediate(function() {
  //prevent timeout
  console.log("
  • Starting script");   Java.perform(function() {   // 具体逻辑   })   })   ####################################################################################   // 启动新的进程(Spawn)   // 不要求待测试App正在运行,Frida会启动一个新的App进程并挂起   // 优点:因为是Frida启动的进程,在启动的同时注入frida代码,所以Hook的时机很早。   // 适用于在进程启动前的一些hook,如hook RegisterNative、较早进行的加解密等,注入完成后调用resume恢复进程。   // 缺点:会Hook到从App启动→想要分析的界面和逻辑的内容,干扰项多,且容易卡死。   run.py文件   import sys   import frida   # 找寻手机frida server   device = frida.get_usb_device()   # 选择应用进程(一般为包名)   appPackageName =""   # 启动新进程   pid = device.spawn([appPackageName])   device.resume(pid)   session = device.attach(pid)   # 加载脚本,填入脚本路径   with open("script.js", encoding="utf-8")as f:   script = session.create_script(f.read())   script.load()   sys.stdin.read()//也可以不依赖sys库,使用time.sleep(10000000);   script.js文件   setImmediate(function() {   //prevent timeout   console.log("
  • Starting script");   Java.perform(function() {   // 具体逻辑   })   })   

  • PS:脚本的第一步总是通过get_usb_device用于寻找USB连接的手机设备,这是因为Frida是一个跨平台的Hook框架,它也可以Hook Windows、mac等PC设备,命令行输入frida-ls-devices可以展示当前环境所有可以插桩的设备,输入frida-ps展示当前PC所有进程(一个进程往往意味着一个应用),frida-ps -U即意味着展示usb所连接设备的进程信息。你可以通过Python+Js混合脚本的方式操作Frida,但其体验远没有命令行运行Frida Js脚本丝滑。
    1.1.3 获取前端进程
    获取最前端Activity所在的进程,进程名。
    // 可以省去填写包名的困扰
      device = frida.get_usb_device()
      front_app = device.get_frontmost_application()
      print(front_app)
      front_app_name = front_app.identifier
      print(front_app_name)
      输出1:Application(identifier="com.xxxx.xxx", name="xxxx", pid=xxxx)
      输出2: com.xxxx.xxxx
      

    1.1.4 命令行调用
    命令行方式使用:
    Spawn方式
      frida -U --no-pause -f packageName -l scriptPath
      Attach方式
      frida -U --no-pause packageName -l scriptPath
      输出内容太多时,可以将输出导出至文件
      frida -U --no-pause -f packageName -l scriptPath -o savePath
      

    可以自行查看所有的可选参数。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    通过CLI 进行hook有诸多优势,列举两个:
    1) 当脚本出错时,会提供很好的错误提示;

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    2)Frida进程注入后和原JS脚本保持同步,只需要修改原脚本并保存,进程就会自动使用修改后的脚本,这会让出错→修复,调试→修改调试目标 的过程更迅捷。
    1.2 Frida In Java
    1.Frida hook 无重载Java方法;
    2.Frida hook 有重载Java方法;
    3.Frida hook Java方法的所有重载。
    1.2.1 Hook导入导出表函数地址
    对So的Hook第一步就是找到对应的指针(内存地址),Frida提供了各式各样的API帮助我们完成这一工作。
    获得一个存在于导出表的函数的地址:
    // 方法一
      var so_name = "";
      var function_name = "";
      var this_addr = Module.findExportByName(so_name, function_name);
      // 方法二
      var so_name = "";
      var function_name = "";
      var this_addr = Module.getExportByName(so_name, function_name);
      // 区别在于当找不到该函数时findExportByName返回null,而getExportByName抛出异常。
      // 方法三
      var so_name = "";
      var function_name = "";
      var this_addr = "";
      var i = undefined;
      var exports = Module.enumerateExportsSync(so_name);
      for(i=0; i<exports.length; i++){
      if(exports[i].name == function_name){
      var this_addr = exports[i].address;
      break;
      }
      }
      

    1.2.2 枚举进程模块/导出函数
    枚举某个进程的所有模块/某个模块的所有导出函数。
    Frida与IDA交互:
    1.内存地址和IDA地址相互转换;
    function memAddress(memBase, idaBase, idaAddr) {
      var offset = ptr(idaAddr).sub(idaBase);
      var result = ptr(memBase).add(offset);
      return result;
      }
      function idaAddress(memBase, idaBase, memAddr) {
      var offset = ptr(memAddr).sub(memBase);
      var result = ptr(idaBase).add(offset);
      return result;
      }
      

    二、Hook JNI函数
    JNI很多概念十分模糊,我们做如下定义,后续的阐述都依照此定义。
    ·native:特指Java语言中的方法修饰符native。
    ·Native方法:特指Java层中声明的、用native修饰的方法。
    ·JNI实现方法:特指Native方法对应的JNI层的实现方法。
    ·JNI函数:特指JNIEnv提供的函数。
    ·Native函数:泛指C/C++层的本地库/自写函数等。
    2.1 JNI编程模型
    如果对JNI以及NDK开发了解较少,务必阅读如下资料。(我不要你觉得,听我  的,下面都是精挑细选的。)
    ·《深入理解Android 卷1》——第二章:深入理解JNI 作者邓凡平
    ·《Android的设计与实现 卷1》——第二章:框架基础JNI 作者杨云君
    除此之外,你可能还会想了解一些其他的知识,我们回顾一下JNI编程模型。
    步骤1:Java层声明Native方法。
    步骤2:JNI层实现Java层声明的Native方法,在JNI层可以调用底层库/回调Java方法。这部分将被编译为动态库(SO文件)供系统加载。
    步骤3:加载JNI层代码编译后生成的SO文件。
    这其中有一个额外的关键点,SO文件的架构。
    C/C++等Native语言直接运行在操作系统上,由CPU执行代码,所以编译后的文件既和操作系统有关,也和CPU相关。So是C/C++代码在Linux系统中编译后的文件,Window系统中为dll格式文件。
    Android手机的CPU型号千千万,但CPU架构主要有七种,Mips,Mips64位,x86,x86_64,armeabi,armv7-a,armv8,编译时我们需要生成这七种架构的so文件以适配各种各样的手机。
    2.2 armv7a架构成因
    在反编译过程中,我们需要选择某种CPU架构的so文件,得到特定架构的汇编代码。一般情况下我们选择armv7a架构,这涉及到一系列连环的原因。
    2.2.1 通用情况
    七种架构可以简单分为Mips,X86,ARM三家,前两者的在Android处理器市场占比极小。Arm架构几乎成为了Android处理器的行业标准,IOS和Android都采用ARM架构处理器。
    2.2.2 Apk臃肿考虑
    Apk的包体积对下载转化率、分发费直接挂钩,所以Apk一旦度过初创时期,就要考虑Apk的包体积优化,而So文件往往占据1/3-1/2的包体积,不提供市场占有率极小的Mips以及X86系列的So,可以瞬间解决Apk臃肿。
    2.2.3 形势考虑
    形势比人强,ARM如日中天,无奈之下Mips和X86都设计了用于转换ARM汇编的中间层,即使Apk只提供了ARM的So库文件,这两种CPU架构的手机也可以以较慢速度运行APK。
    2.2.4 ARM兼容性
    ARM有armeabi,armv7a,armv8a这三个系列,系列之间是不断发展和完善的升级关系。目前主流手机的CPU都是armv8a,即64位的ARM设备,而armeabi甚至只用在Android 4.0以下的手机,但好在Arm是向下兼容的,如果Apk不需要用到一些高性能的东西,完全可以只提供armeabi的So,这样几乎可以支持所有架构的手机。
    2.3 Hook JNI函数
    通过上述的学习我们了解到,JNIEnv提供给了我们两百多个函数,帮助我们将Java中的对象和数据转换成C/C++的类型,帮助我们调用Java函数、帮助我们将C中生成的结果转换回Java中的对象和数据并返回,因此,如果能Hook JNI函数,会对我们逆向与分析So产生帮助。
    使用Frida Hook Native函数十分简单,只需要我们提供地址即可。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    Frida提供了一种非常方便优雅的方式获得JNIEnv的地址,需要注意的是必须在Java.perform中调用。
    var jnienv_addr = 0x0;
      Java.perform(function(){
      jnienv_addr = Java.vm.getEnv().handle.readPointer();
      });
      console.log("JNIEnv base adress get by Java.vm.getEnv().handle.readPointer():" + jnienv_addr);
      

    JNIEnv指针指向JNINativeInterface这个数组,里面包含两百多个指针,即各种各样的JNI函数。
    我们可以查看一下Jni.h头文件

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    假设JNIEnv地址为0x1000,一个指针长4,那么reversed0地址即为0x1000,reversed1为0x1004,之后我们读取这个指针,就可以得到JNI函数的地址,从而实现Hook。
    在我们上述的JNINativeInterface数组中,它排在第七个,那么偏移就是4*(7-1)=24。
    function hook_native_findclass() {
      var jnienv_addr = Java.vm.getEnv().handle.readPointer();
      var FindClassPtr = Memory.readPointer(jnienv_addr.add(24));
      // 注意,Frida提供了add(+),sub(-)等函数供我们做加减乘除,你也可以通过add(0x12)这种形式加一个十六进制数。
      console.log("FindClassPtr addr: " + FindClassPtr);
      Interceptor.attach(FindClassPtr, {
      onEnter: function (args) {
      ...
      }
      });
      }
      

    接下来我们以IDA为例,加深理解。在我们使用IDA逆向和分析SO时,如果单纯导入SO,会有大量“无法识别”的函数。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    所以惯例上,我们会导入Jni.h头文件,再设置方法的第一个参数为JNIEnv类型,这样IDA就能顺利将形如*(a1+xxx)这种指针识别为JNI函数 ,但可能很多人没有想过为什么这样可以成功。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    事实上,导入Jni.h头文件是为了引入JNINativeInterface与JNIInvokeInterface结构体信息,而转换参数一为JNIEnv类型,就是在提醒IDA,将*(env+704)映射成对应的JNIEnv函数。
    而我们现在所做的是一种相反的操作,已知各个JNI函数的名字和他们在数组中的位置,希望得到其地址。
    不知道大家是否发现,由于JNI实现方法的第一个参数总是JNIEnv,所以我们也可以通过Hook一个JNI实现方法作为跳板,从而获得JNIEnv的地址。
    function hook_jni(){
      var so_name = ""; // 请选择目标Apk SO
      var function_name = "";  //请选择目标SO中一个JNI实现方法
      var open_addr = Module.findExportByName(so_name, function_name);
      Interceptor.attach(open_addr, {
      onEnter: function (args) {
      var jnienv_addr = 0x0;
      console.log("get by args[0].readPointer():" + args[0].readPointer());
      Java.perform(function () {
      jnienv_addr = Java.vm.getEnv().handle.readPointer();
      });
      console.log("get by Java.vm.getEnv().handle.readPointer():" + jnienv_addr);
      },
      onLeave: function (retval) {
      }
      });
      }
      hook_jni();
      

    结果完全正确,但这种方法流程明显更加复杂,不够优雅,不建议使用。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    好了,我们回归到主线上来,上面我们Hook了FindClass这个函数,想一下我们Hook一个JNI函数需要做的工作,一是找到这个函数对应的偏移,二是在onEnter和onLeave中编写具体的逻辑,因为每个JNI函数的参数和返回值都不一样。
    有没有办法简化这两个步骤呢?比如只需要输入JNI函数名,而不需要手动计算偏移?这个好办,我们看一下代码。
    var jni_struct_array = [
      "reserved0",
      "reserved1",
      "reserved2",
      "reserved3",
      "GetVersion",
      "DefineClass",
      "FindClass",
      *******此处省略两百多个JNI函数**********
      "FromReflectedMethod",
      "FromReflectedField",
      "ExceptionCheck",
      "NewDirectByteBuffer",
      "GetDirectBufferAddress",
      "GetDirectBufferCapacity",
      "GetObjectRefType",
      ]
      function getJNIFunctionAdress(jnienv_addr,func_name){
      var offset = jni_struct_array.indexOf(func_name) * 4;
      return Memory.readPointer(jnienv_addr.add(offset))
      }
      

    代码很简单,将JNI函数罗列在数组中,通过Js中indexOf这个数组处理函数得到目标数组的索引,乘4就是偏移了,除此之外,你可以选择乘Process.pointerSize,这是Frida提供给我们的Api,返回当前平台指针所占用的内存大小,这样做可以增加脚本的移植性(其实没啥区别)。
    我们进一步希望,能不能不用在onEnter和onLeave中编写具体的逻辑,反正JNI函数的参数和返回值类型都在Jni.h中定义好了,也不会有什么更多的变化了。
    需要注意的是,它在理论上实现了Hook 所有JNI函数,并提供了人性化的筛选等功能,但在我的测试机上并没有很顺利或者正确的打印出全部JNI调用,更多精彩需要读者自己去挖掘喽。
    三、Hook动态注册函数

    在第二部分我们将尝试Hook JNIEnv提供的RegisterNatives函数,在上面我们已经讲过JNI函数的Hook,为什么要花同样的篇幅去讲解呢?当然是因为这个函数比较常用,而且可以给分析带来很大帮助。
    3.1 反编译so文件
    在逆向时,静态注册的函数只需要找到对应的So,函数导出表中搜索即可定位。而动态注册的函数会复杂一些,下面列一下流程。
    1.在导出函数中搜索JNI_OnLoad,点击进入。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    2.Tab或者f5键反汇编arm指令。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    3.之前我们已经知道,凡是*(指针变量+xxx)这种形式都是在使用JNI函数,所以导入Jni.h头文件,在a1,v5,v2等变量上右键如图。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    这个时候JNI函数都正确展示出来,如果大家反编译的是自己的Apk,对照着看源码和反汇编代码,仍然会感觉“不太舒服”,我们还有一些额外的工作可以做。
    4.IDA由于不确定参数的数目,常常会不显示函数的参数,用如下的方式强制展示参数(findclass显然不可能无参)。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    在几个jni函数上都试一下,结果如下,需要注意的是,自己写的App可能不会有这些问题。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    5.接下来我们隐藏掉类型转换,这样代码会更加可读。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    反编译的工作顺利完成了,接下来找动态注册的函数。
    3.2 寻找关键函数
    看一下RegisterNatives这个函数的原型。
    jint RegisterNatives(JNIEnv *env,jclass clazz, const JNINativeMethod *methods, jnint nMethods);
      

    第一个参数是JNIEnv指针,所有的JNI函数第一个参数都是它。
    第二个参数jclasss是类对象,通过 JNI FindClass函数得来。
    第三个参数是一个数组,数组中包含了若干个结构体,每个结构体存储了Java Native方法到JNI实现方法的映射关系。
    第四个参数代表了数组中结构体的数量,或者可以说此次动态注册了多少个native方法。
    我们仔细品一下这个结构体,内容为Java层方法名+签名+JNI层对应的函数指针,Java层方法名并不携带包的路径,包的信息由第二个参数,也就是jclass类对象提供。签名的写法和Smali语法类似,想必大家不陌生。JNI层对应的函数指针也似乎没啥问题。
    接下来我们阅读一下截图中的RegisterNatives函数,v3即类对象,“com/m4399/……”即Java native函数所声明的类,第四个参数为16,即off_20044这个数组中有十六个结构体,或者说十六组java native函数与jni实现函数的映射。
    我想你应该不会对off_20044这个命名感到恐慌,这是IDA生成的假名字,详细内容见下表。off_20044即代表了这是一个数据,位于20044这个偏移位置,我们双击进去试试。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    data:00020044证实了我们的想法,可以发现,IDA反汇编的效果还不错,我们从上往下划分,每三行代表一个完整的映射。只要两个地方让人不太舒服。
    1.第一个结构体为什么占那么多行?
    这是因为作为内容的起始部分,IDA会在右方用注释的方式展示它的交叉引用状况,交叉引用占用了正常的两行,JNI_Onload+46 以及.textL0ff_14C10这两个位置引用了这份数据,正是交叉引用的注释导致第一个结构体,或者说第一行下面平白空了两行。我们可以在off_20044上按快捷键x查看其交叉引用,验证我们的观点。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    2.我们之前说过,每个结构体里三块内容,Java层方法名+签名+JNI层对应的函数指针,而IDA结果正确吗?aGetmd5并不像方法名,aLjavaLangStrin_0也不像正确的签名,第三个sub_xxx,根据我们上表,它代表了一个函数的起点,这倒是和“JNI层对应的函数指针”不谋而合。可是方法名和签名是怎么回事?
    这是因为IDA给方法名以及签名二次取了名字。
    #原代码
      a = 3
      #IDA反编译后
      a1 = 3 #a
      a = a1
      

    IDA用注释的形式给出了真正的值,因此我们可以直接看右边注释,这结果明显就正确了,除此之外,IDA在命名时会参考原值,因此才会有aLjavaLangStrin_0这种似是而非的名字。
    3.3 应用的场景
    至此,我们已经搞懂了动态注册,也称函数注册的定位,那么为什么还需要用Hook registernative函数呢?直接用IDA查看一下不就得了?
    有多方面的考虑,考虑一下这两个情景
    ·找不到某个Native声明的Java函数是哪个SO加载来的。
    ·IDA反编译时遇到了防护,JNI_Onload无法顺利反编译(常见)。
    这个时候Hook动态注册函数就能一把尖刀,直刺So中函数所在的位置。为了理解上更通顺,我们不考虑一步到位,而是一步步去优化Hook代码,希望对大家有所帮助。
    var RevealNativeMethods = function() {
      // 为了可移植性,选择使用Frida 提供的Process.pointerSize来计算指针所占用内存,也可以直接var pSize  = 4
      var pSize = Process.pointerSize;
      // 获取当前线程的JNIEnv
      var env = Java.vm.getEnv();
      // 我们所需要Hook的函数是在JNIEnv指针数组的第215位,因为我们这里只是Hook单个函数,所以没有引入包含全体JNI函数的数组
      var RegisterNatives = 215;
      // 将通过位置计算函数地址这一步骤封装为函数
      function getNativeAddress(idx) {
      var nativrAddress = env.handle.readPointer().add(idx * pSize).readPointer();
      console.log("nativrAddress:"+nativrAddress);
      return nativrAddress;
      }
      // 开始Hook
      Interceptor.attach(getNativeAddress(RegisterNatives), {
      onEnter: function(args) {
      console.log("Already enter getNativeAddress Function!");
      // 遍历数组中每一个结构体,需要注意的是,参数4即代表了结构体数量,我们这里使用了它
      for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
      var methodsPtr = ptr(args[2]);
      var structSize = pSize * 3;
      var methodName = methodsPtr.add(i * structSize).readPointer();
      var signature = methodsPtr.add(i * structSize + pSize).readPointer();
      var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer();
      /*
      typedef struct {
      const char* name;
      const char* signature;
      void* fnPtr;
      } JNINativeMethod;
      */
      var ret = {
      // methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
      // 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
      methodName:methodName.readCString(),
      signature:signature.readCString(),
      address:fnPtr,
      };
      // 使用JSON.stringfy()打印内容通常是好的选择
      console.log(JSON.stringify(ret))
      }
      }
      });
      };
      Java.perform(RevealNativeMethods);
      

    由于registerNatives发生的时机往往很早,建议采用Spawn方式注入,否则可能毫无收获。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    3.3.1 代码优化
    似乎很不错的样子,但是自己看一下内容,却不大如人意。
    Hook输出了Java方法名,但我们之前说过,Java层方法名并不携带包的路径,包的信息由第二个参数,所以方法名提供不了什么信息,第二个信息是参数签名,和我们预期一致,第三个信息是函数地址,有一个很大的问题,输出的地址是内存中的真正地址,而我们分析SO时需要用到IDA,IDA 加载模块的时候,会以基址 0 加载分析 so 模块,但是 SO运行在 Android 上的时候,每次的加载地址不是固定的,有没有办法解决这个问题呢?
    办法是很多的,我们查看Frida官方文档可以发现,Frida提供了两个根据地址得到所在SO文件等信息的函数。
    我们对照一下结果,修改代码输出如下:
    var ret = {
      // methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
      // 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
      // 只需要新增如下两行代码
      module1: DebugSymbol.fromAddress(fnPtr),
      module2: Process.findModuleByAddress(fnPtr),
      methodName:methodName.readCString(),
      signature:signature.readCString(),
      address:fnPtr,
      };
      查看任意一条输出结果,此Native方法名为tokenDecrypt
      {"module1":{"address":"0x8a339267","name":"0x17267","moduleName":"libm4399.so","fileName":"","lineNumber":0},
      "module2":{"name":"libm4399.so","base":"0x8a322000","size":135168,"path":"/data/app/com.m4399.gamecenter-1/lib/arm/libm4399.so"},
      "methodName":"tokenDecrypt",
      "signature":"(Ljava/lang/String;)Ljava/lang/String;",
      "address":"0x8a339267"}
      

    可以发现,两个API侧重点不同,地址为0x8a339267,函数1返回自身地址,符号名(0x17267),所属SO名,具体文件名和行数(这两个字段似乎无效),符号名name可能有些不理解,我们待会儿再讲。函数2返回所属SO,base字段,即为基址,表示此SO在内存中起始的位置,size字段代表了SO的大小,path即为SO在手机中的真实路径。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    图中可以看出,如果想得到IDA中的虚拟地址,两个函数都可以做到。使用函数一的name字段,或者address减去函数二提供给我们的So基址。我们先通过IDA来验证tokenDecrypt这个函数结果是否准确。0x17266+1即0x17267,name字段被验证。0x8a339267-0x8a322000=0x17267,两种方法都OK。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    通过Frida提供的Api,我们得到了地址对应的SO文件以及它在IDA中的位置,这真是可喜的事儿。除此之外,我们补充另外一种方式来定义地址,即修改IDA中SO的基址。

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    效果如下:

    Hook梦幻旅途之Frida

    Hook梦幻旅途之Frida
    在我们这个场景下,这样处理并不方便, 但在IDA动态调试时,通过Rebease 基址,让其与运行时 so 的基址相同,可以极大的方便静态分析。
    需要注意的是,我们使用此Hook脚本时,目的不是印证IDA中反编译的地址和Frida hook得到的地址是否相同,而是为了定位。IDA中使用快捷键G可以迅速进行地址跳转。
    接下来我们需要进一步优化脚本,参数2是jclass对象,可以让我们获得这个方法所在类的信息,它是JNI方法Findclass的结果,因此我们要Hook 这个JNI方法。Findclass的结果需要和对应的RegisterNative函数匹配,这涉及到JNIEnv线程的问题,我们使用集合的方式处理。来看一下完整的代码吧。
    var RevealNativeMethods = function() {
      // 为了移植性,选择使用Frida API来计算指针所占用内存,也可以直接var pSize  = 4
      var pSize = Process.pointerSize;
      // 获取当前线程的JNIEnv
      var env = Java.vm.getEnv();
      // 我们所需要Hook的函数是在JNIEnv指针数组的第6和第215位
      var RegisterNatives = 215;
      var FindClassIndex = 6;
      // 将通过位置计算函数地址这一步骤封装为函数
      function getNativeAddress(idx) {
      var nativrAddress = env.handle.readPointer().add(idx * pSize).readPointer();
      return nativrAddress;
      }
      // 初始化集合,用于处理两个JNI函数之间的同步关系
      var jclassAddress2NameMap = {};
      // Hook 两个JNI函数
      Interceptor.attach(getNativeAddress(FindClassIndex), {
      onEnter: function (args) {
      // 设置一个集合,不同的JNIEnv线程对应不同的class
      jclassAddress2NameMap[args[0]] = args[1].readCString();
      }
      });
      Interceptor.attach(getNativeAddress(RegisterNatives), {
      onEnter: function(args) {
      console.log("Already enter getNativeAddress Function!");
      // 遍历数组中每一个结构体,需要注意的是,参数4即代表了结构体数量,我们这里使用了它
      for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
      var methodsPtr = ptr(args[2]);
      var structSize = pSize * 3;
      var methodName = methodsPtr.add(i * structSize).readPointer();
      var signature = methodsPtr.add(i * structSize + pSize).readPointer();
      var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer();
      /*
      typedef struct {
      const char* name;
      const char* signature;
      void* fnPtr;
      } JNINativeMethod;
      */
      var ret = {
      // methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
      // 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
      moduleName: DebugSymbol.fromAddress(fnPtr)["moduleName"],
      jClass:jclassAddress2NameMap[args[0]],
      methodName:methodName.readCString(),
      signature:signature.readCString(),
      address:fnPtr,
      IdaAddress: DebugSymbol.fromAddress(fnPtr)["name"],
      };
      // 使用JSON.stringfy()打印内容通常是好的选择
      console.log(JSON.stringify(ret))
      }
      }
      });
      };
      Java.perform(RevealNativeMethods);
      





    温馨提示:
    1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
    2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
    3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
    论坛交流群:672619046

    小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

    GMT+8, 2024-4-20 05:41 , Processed in 0.308652 second(s), 41 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表