frida使用

基于frida对代码进行验证,判断当前请求是否会执行此方法

一、python解释器

  • 安装python 3.7.9

    • https://www.python.org/downloads/release/python-379/
  • 利用python3.7.9创建一个虚拟环境+项目

二、frida客户端

pip install frida==16.0.1
pip install frida-tools==12.0.1

三、frida-server

  • 查看手机cpu架构

    adb shell getprop ro.product.cpu.abi
  • 下载对应cpu架构和版本的frida-server

    • https://github.com/frida/frida/releases
    • 解压并上传文件至手机
      • 上传
        adb push Path\to\frida-server-16.0.1-arm64 /data/local/tmp/
      • 赋予可执行权限
        >>>adb shell
        >>>su
        >>>cd /data/local/tmp/
        >>>chmod 755 frida-server-16.0.1-android-arm64

四、启动和Hook

  • 手机端启动frida-server

    >>>adb shell
    >>>su
    >>>cd /data/local/tmp
    >>>./frida-server-16.0.1-android-arm64

    如果客户端通过-p参数指定了其它端口号,那么服务端也必须通过-l参数指定该端口号:frida-server -l 

    • frida支持两种连接模式:
      • USB数据线模式
        • 手机一定要通过adb协议与计算机连接,注入app时要加上-U参数,比如: frida -U -f com.xx.yy -l xxx.js 
      • 网络模式(以自定义端口8888为例)
        • 无需保证手机和计算机通过adb协议连接
        • 服务端:需要【-l】参数(--listen=Address)指定监听ip和端口,比如 ./frida-server-16.0.1-android-arm64 -l 0.0.0.0:8888 
        • 端口转发
          adb forward tcp:8888 tcp:8888
        • 客户端:
          • 需要【-H】参数指定手机的IP和端口,比如: 
            frida -H 192.168.0.110:8888 -f com.xx.yy -l xxx.js 
          • python脚本形式,则:
            rdev = frida.get_device_manager().add_remote_device('192.168.0.110:8888') # 通过网络连接方式,并指定端口号为8888
  • 电脑端Hook

    • 1、端口转发

      import subprocess
      
      # 如果连接多台设备,则需要通过-s指定设备,使电脑上的端口和设备上的端口建立连接,前面是电脑上的端口号
          # 比如:adb -s 127.0.0.1:62001 forward tcp:27042 tcp:27042
          # 比如:adb -s 127.0.0.1:62025 forward tcp:27042 tcp:27042
      
      subprocess.getoutput('adb forward tcp:27042 tcp:27042')
      subprocess.getoutput('adb forward tcp:27043 tcp:27043')
      View Code
    • 2、获取进程(先打开app)

      # 枚举手机上的所有进程 & 前台进程
      import frida
      
      # 获取设备信息
      rdev = frida.get_remote_device()
      
      # 枚举所有的进程
      processes = rdev.enumerate_processes()
      for process in processes:
          print(process)
      
      # 获取在前台运行的APP
      front_app = rdev.get_frontmost_application()
      print(front_app)
      View Code
    • 3、Hook

      • python脚本

        • attach模式:需要先运行App,再执行脚本
          '''
          attach:需要先手动打开并运行app,再运行Hook脚本
          '''
          
          import frida
          import sys
          
          # 连接手机设备
          rdev = frida.get_remote_device()
          session = rdev.attach("app名")  # 应用名
          
          scr = """
          Java.perform(function () {
              // 包.类
              var 类名 = Java.use("包名.类名");
              
              // 注意构造方法名:$init
              // 注意有重载的方法:类名.方法名.overload('参数类型').implementation;参数类型示例:boolean.class, int.class, '[B'(表示字节数组)
              类名.方法名.implementation = function(参数){
                  // 调用方法前可以打印日志信息
                  var res = this.方法名.apply(this, arguments)  调用原有方法
                  // 调用原方法后可以打印日志信息
                  return res
              }
          });
          """
          
          script = session.create_script(scr)
          def on_message(message, data):
              print(f'{message} => {data}')
          
          script.on("message", on_message)
          script.load()
          sys.stdin.read()
          View Code
        • spawn模式:脚本自动重启App并进行Hook
          '''
          spawn:脚本自动重启APP并进行Hook
          '''
          
          import frida
          import sys
          
          # 连接手机设备
          rdev = frida.get_remote_device()
          pid = rdev.spawn(["app包名"]) # 自动重启应用,并返回进程id
          session = rdev.attach(pid)
          
          scr = """
          Java.perform(function () {
              // 包.类
              var 类名 = Java.use("包名.类名");
              
              // 注意构造方法名:$init
              // 注意有重载的方法:类名.方法名.overload('参数类型'),implementation xxx;参数类型示例:'java.lang.String', 'java.util.TreeMap', '[B'(表示字节数组)
              类名.方法名.implementation = function(参数){
                  // 调用方法前可以打印日志信息
                  var res = this.方法名.apply(this, arguments)  调用原有方法
                  // 调用原方法后可以打印日志信息
                  return res
              }
          });
          """
          script = session.create_script(scr)
          def on_message(message, data):
              print(f'{message} => {data}')
          script.on("message", on_message)
          script.load()
          rdev.resume(pid)
          sys.stdin.read()
          View Code
      • JavaScript脚本

        • demo.js

          Java.perform(function () {
              // 包.类
              var 类名 = Java.use("包名.类名");
              类名.方法名.implementation = function(参数){
                  // 调用原方法前可以打印日志信息
                  // console.log(`明文:${参数}`)
                  //调用原有方法
                  var res = this.方法名.apply(this, arguments)
                  // 调用原方法后可以打印日志信息
                  // console.log(`密文:${参数}`)
                  return res
              }
          });
          View Code
        • attach:先启动app,然后再在终端执行

          frida -UF -l demo.js 
        • spawn:脚本自动重启APP并进行Hook

          frida -U -f 包名 -l demo.js
          注意:输入q + 再点击回车则退出
        • 命令行参数说明:
          -U:表示使用 USB 连接方式与目标设备建立通信
          -F:表示强制重新加载脚本。这样可以在脚本发生变化后不重启目标应用的情况下更新脚本,以便及时应用新的修改
          -f:指定要监控的应用程序的包名或进程名,以便 frida 在启动时可以自动启动并监控该应用程序
          -l:指定要加载的 JavaScript 脚本文件
          -o: 将输出结果保存到指定文件中这样可以方便地对输出结果进行分析和保存,提高工作效率。
          -l:指定监听ip和端口,比如 ./frida-server-16.0.1-android-arm64 -l 0.0.0.0:8888
          --no-pause:注入frida后,目标进程不会暂停,而是继续执行

五、过frida检测(反调试) 

  • 1、so文件中进行的监测

    • hook安卓系统底层加载了哪些so文件
      import frida
      import sys
      
      rdev = frida.get_remote_device()
      pid = rdev.spawn(["com.xxx.xxx"])
      session = rdev.attach(pid)
      
      scr = """
      Java.perform(function () {
      
          var dlopen = Module.findExportByName(null, "dlopen");
          var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
      
          Interceptor.attach(dlopen, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  var path = ptr(path_ptr).readCString();
                  console.log("[dlopen:]", path);
              },
              onLeave: function (retval) {
      
              }
          });
      
          Interceptor.attach(android_dlopen_ext, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  var path = ptr(path_ptr).readCString();
                  console.log("[dlopen_ext:]", path);
              },
              onLeave: function (retval) {
      
              }
          });
      
      
      });
      """
      script = session.create_script(scr)
      
      
      def on_message(message, data):
          print(message, data)
      
      
      script.on("message", on_message)
      script.load()
      rdev.resume(pid)
      sys.stdin.read()
      View Code
    • 尝试删除闪退前的那个so文件(需要忽略系统本身的so文件,app中so文件所在目录:/data/app/包名开头的文件/lib/arm64/libxxx.so
  • 2、使用加强版frida

    • 本质是frida-server运行时,会在同级目录下生成re.frida.server这个目录,该目录及其内部的文件会被检测
    • 下载地址:https://github.com/hzzheyang/strongR-frida-android/releases
    • 注意和自己frida版本相一致,上传到手机后赋予可执行权限
  • 使用其它版本frida

    • 比如frida-12.1.18,相应的服务端也得一致

六、常见参数hook脚本参数类型转换

  • so层
    Interceptor.attach中的onEnter及onLeave方法中参数:
    常见形式:
    //1、指针类型:
        需要先通过ptr(xx)进行转换,再进行后续操作,比如ptr(path_ptr).readCString()
    //2、字节数组转16进制:
        hexdump(args[0], {length: 16}),每行16个字节展示,【对于字节数组类型,建议转16进制显示,因为字符串可能是乱码形式】
    //3、字符串类型
            Memory.readCString(args[0])
            args[0].readCString()
            args[0].readUtf8String()    读取utf8编码字符串
    // 4、整形:args[2].toInt32()
  • java层
    // 一、引入字节处理类
    var ByteString = Java.use("com.android.okhttp.okio.ByteString"); //
    // 字节转16进制
    var hex_str = ByteString.of(bArr).hex(); // 字节转16进制
    // 字节转字符串
    var str = ByteString.of(bArr).utf8(); // 字节转字符串
    
    // 二、TreeMap类型
    // 获取对象信息
    console.log('map=>', JSON.stringify(map));
    // 根据对象信息,转成其真实的类
    var obj = Java.cast(map, Java.use('java.util.TreeMap'));
    // 获取字符串形式内容
    console.log("参数=", obj.toString());
    
    // 三、数组转字节数组
    var bs = Java.array('byte',bArr);

七、常见hook脚本

  • so层hook

    • hook所有加载的so文件

      Java.perform(function () {
          // 针对不同安卓版本,所以有以下两个系统函数
          var dlopen = Module.findExportByName(null, "dlopen");
          var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
      
          Interceptor.attach(dlopen, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  // 常见形式:
                  // 1、指针类型参数,需要通过ptr(xx)进行转换
                  // 2、转16进制:hexdump(args[0], {length: 16}),每行16个字节展示,【对于字节数组类型,建议转16进制显示,因为字符串可能是乱码形式】
                  // 3、Memory.readCString(args[0])
                  // 4、args[0].readUtf8String()
                  // 5、args[2].toInt32()  整型
                  var path = ptr(path_ptr).readCString();
                  console.log("[dlopen:]", path);
              },
              onLeave: function (retval) {
      
              }
          });
      
          Interceptor.attach(android_dlopen_ext, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  var path = ptr(path_ptr).readCString();
                  console.log("[dlopen_ext:]", path);
              },
              onLeave: function (retval) {
      
              }
          });
      });
      
      // frida -Uf 包名 xxx.js
      View Code
    • hook导出函数

      Java.perform(function () {
      
          var addr_func = Module.findExportByName("libJNIEncrypt.so", "AES_128_ECB_PKCS5Padding_Encrypt");
      
          Interceptor.attach(addr_func, {
              onEnter: function(args){
                  console.log("--------------------------执行函数--------------------------");
                  // console.log("参数1:", args[0].readUtf8String());
                  console.log("参数1:", Memory.readCString(args[0])); //
                  console.log("参数2:", args[1].readUtf8String());
              },
              onLeave: function(retValue){
                  console.log("返回值:", retValue.readUtf8String());
              }
          })
      });
      View Code
    • 根据偏移量hook指定函数(比如so中函数名sub_22B0)

      // 1、找基地址
      var soAddr = Module.findBaseAddress("libbili.so");
      
      // 2、根据偏移量找函数的地址(32位系统,偏移量需要 +1,64位系统则不需要)
      var s_func = soAddr.add(0x22b0 + 1);
      console.log(s_func);
      
      Interceptor.attach(s_func, {
          onEnter: function (args) {
              // args[0]
              // args[1],明文字符串
              // args[2],明文字符串长度
      
              // console.log("执行update,长度是:",args[2], args[2].toInt32());
              // console.log( hexdump(args[1], {length: args[2].toInt32()})  );
              // console.log(args[0])
              console.log(args[1].readUtf8String())
          },
          onLeave: function (args) {
              console.log("=======================结束===================");
          }
      });
      View Code
    • 定位so文件名(静态注册)

      /*   只需要修改native方法名即可     */
      
      
      Java.perform(function () {
          var dlsymadd = Module.findExportByName("libdl.so", 'dlsym');
          Interceptor.attach(dlsymadd, {
              onEnter: function (args) {
                  this.info = args[1];
      
              }, onLeave: function (retval) {
                  //那个so文件 module.name
                  var module = Process.findModuleByAddress(retval);
                  if (module == null) {
                      return retval;
                  }
                  // native方法
                  var funcName = this.info.readCString();
                  if (funcName.indexOf("getHNASignature") !== -1) {
                      console.log(module.name);
                      console.log('\t', funcName);
                  }
                  return retval;
              }
          })
      });
      
      
      // Application(identifier="com.rytong.hnair", name="海南航空", pid=14958, parameters={})
      // frida -U -f  com.rytong.hnair  -l static_find_so.js
      View Code
    • hook系统函数RegisterNatives + 定位so文件名(动态注册)

      //------------------需要修改target_class:具体的java类名--------------------
      
      // 列举 libart.so 中的所有系统导出函数(成员列表)
      var symbols = Module.enumerateSymbolsSync("libart.so");
      
      // 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
      var addrRegisterNatives = null;
      
      for (var i = 0; i < symbols.length; i++) {
          var symbol = symbols[i];
      
          console.log(symbol.name)
          //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
          if (symbol.name.indexOf("art") >= 0 &&
              symbol.name.indexOf("JNI") >= 0 &&
              symbol.name.indexOf("RegisterNatives") >= 0 &&
              symbol.name.indexOf("CheckJNI") < 0) {
      
              addrRegisterNatives = symbol.address;
              console.log("RegisterNatives is at ", symbol.address, symbol.name);
          }
      }
      
      
      Interceptor.attach(addrRegisterNatives, {
          onEnter: function (args) {
              var env = args[0];        // jni对象
              var java_class = args[1]; //
              var class_name = Java.vm.tryGetEnv().getClassName(java_class);
      
              // 只有类名为com.bilibili.nativelibrary.LibBili,才打印输出
              var taget_class = "com.bilibili.nativelibrary.LibBili";
      
              if(class_name === taget_class){
                  console.log("\n[RegisterNatives] method_count:", args[3]);
      
                  // args[2] 就是动态注册的对应关系。
                  // ptr是new NativePointer(s) 的缩写。(C语言中的指针)
                  var methods_ptr = ptr(args[2]);
                  var method_count = parseInt(args[3]);
      
                  for (var i = 0; i < method_count; i++) {
                      // Java中函数名字的
                      var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                      // 参数和返回值类型
                      var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                      // C中的函数内存地址
                      var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
      
                      var name = Memory.readCString(name_ptr);
                      var sig = Memory.readCString(sig_ptr);
                      var find_module = Process.findModuleByAddress(fnPtr_ptr);
                      var offset = ptr(fnPtr_ptr).sub(find_module.base) // fnPtr_ptr - 模块基地址
                      console.log("name:", name, "  sig:", sig, "  module_name:", find_module.name, "  offset:", offset);
                  }
              }
          }
      });
      
      // so文件只会在一开始进行加载,所以需要以spawn的方式运行
      // frida -Uf tv.danmaku.bili -l 4.工具-hook_系统函数RegisterNatives.js
      View Code
    • hook系统函数NewStringUTF

      //------------------需要修改target_class:具体的java类名--------------------
      
      // 列举 libart.so 中的所有系统导出函数(成员列表)
      var symbols = Module.enumerateSymbolsSync("libart.so");
      
      // 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
      var addrRegisterNatives = null;
      
      for (var i = 0; i < symbols.length; i++) {
          var symbol = symbols[i];
      
          console.log(symbol.name)
          //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
          if (symbol.name.indexOf("art") >= 0 &&
              symbol.name.indexOf("JNI") >= 0 &&
              symbol.name.indexOf("RegisterNatives") >= 0 &&
              symbol.name.indexOf("CheckJNI") < 0) {
      
              addrRegisterNatives = symbol.address;
              console.log("RegisterNatives is at ", symbol.address, symbol.name);
          }
      }
      
      
      Interceptor.attach(addrRegisterNatives, {
          onEnter: function (args) {
              var env = args[0];        // jni对象
              var java_class = args[1]; //
              var class_name = Java.vm.tryGetEnv().getClassName(java_class);
      
              // 只有类名为com.bilibili.nativelibrary.LibBili,才打印输出
              var taget_class = "com.bilibili.nativelibrary.LibBili";
      
              if(class_name === taget_class){
                  console.log("\n[RegisterNatives] method_count:", args[3]);
      
                  // args[2] 就是动态注册的对应关系。
                  // ptr是new NativePointer(s) 的缩写。(C语言中的指针)
                  var methods_ptr = ptr(args[2]);
                  var method_count = parseInt(args[3]);
      
                  for (var i = 0; i < method_count; i++) {
                      // Java中函数名字的
                      var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                      // 参数和返回值类型
                      var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                      // C中的函数内存地址
                      var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
      
                      var name = Memory.readCString(name_ptr);
                      var sig = Memory.readCString(sig_ptr);
                      var find_module = Process.findModuleByAddress(fnPtr_ptr);
                      var offset = ptr(fnPtr_ptr).sub(find_module.base) // fnPtr_ptr - 模块基地址
                      console.log("name:", name, "  sig:", sig, "  module_name:", find_module.name, "  offset:", offset);
                  }
              }
          }
      });
      
      // so文件只会在一开始进行加载,所以需要以spawn的方式运行
      // frida -Uf tv.danmaku.bili -l 4.工具-hook_系统函数RegisterNatives.js
      View Code
    • 延迟hook

      //------------------需要2处so文件名+1处函数名--------------------
      
      function do_hook(so_file_name) {
          setTimeout(function(){
              var addr = Module.findExportByName(so_file_name, "getByteHash");
              console.log(addr); //0xb696387d
      
              Interceptor.attach(addr, {
                  onEnter: function (args) {
                      this.x1 = args[2];
                  },
                  onLeave: function (retval) {
                      console.log("--------------------")
                      console.log(Memory.readCString(this.x1));
                      console.log(Memory.readCString(retval));
                  }
              })
           },10);
      
      }
      
      function load_so_and_hook(so_file_name) {
          var dlopen = Module.findExportByName(null, "dlopen");
          var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
      
          Interceptor.attach(dlopen, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  var path = ptr(path_ptr).readCString();
                  // console.log("[dlopen:]", path);
                  this.path = path;
              }, onLeave: function (retval) {
                  if (this.path.indexOf(so_file_name) !== -1) {
                      console.log("[dlopen:]", this.path);
                      do_hook(so_file_name);
      
                  }
              }
          });
      
          Interceptor.attach(android_dlopen_ext, {
              onEnter: function (args) {
                  var path_ptr = args[0];
                  var path = ptr(path_ptr).readCString();
      
                  this.path = path;
              }, onLeave: function (retval) {
                  if (this.path.indexOf(so_file_name) !== -1) {
                      console.log("\nandroid_dlopen_ext加载:", this.path);
                      do_hook(so_file_name);
      
                  }
              }
          });
      }
      
      load_so_and_hook("libkeyinfo.so");
      
      // frida -U -f com.achievo.vipshop -l delay_hook.js
      View Code
  • Java层hook

    • Map相关

      Java.perform(function () {
          var Map = Java.use("java.util.Map");
          var HashMap = Java.use('java.util.HashMap');
          var TreeMap = Java.use('java.util.TreeMap');
      
          TreeMap.put.implementation = function (key,value) {
              if(key.toString() === "data"){
                  console.log('key::',key.toString());
                  console.log('value::',value.toString());
              }
              var res = this.put(key,value);
              return res;
          }
      });
      // frida -UF -l hook_Map.js
      View Code
    • StringBuider

      Java.perform(function () {
        var StringBuilder = Java.use('java.lang.StringBuilder');
      
        StringBuilder.toString.implementation = function () {
          var result = this.toString();
          console.log('StringBuilder.toString() => ', result);
          return result;
        };
      });
      
      // frida -UF -l hook_StringBuilder.js -o output_string.txt
      View Code
    • Base64

      Java.perform(function () {
        var Base64 = Java.use('android.util.Base64');
      
        Base64.encodeToString.overload('[B','int').implementation = function (bArr, val) {
          var result = this.encodeToString(input, flags);
          console.log('Base64加密结果 =>:', result);
          // console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
          return result;
        };
      });
      
      // frida -UF -l hook_Base64.js
      View Code
    • okhttp3中所有注册拦截器

      Java.perform(function () {
          var Builder = Java.use('okhttp3.OkHttpClient$Builder');
      
          Builder.addInterceptor.implementation = function (inter) {
              console.log(JSON.stringify(inter) );
              return this.addInterceptor(inter);
          };
      })
      View Code

      需要以spawn的形式运行,然后最好命令行的形式输出,通过-o参数输出内容到指定文件内

      • 对上述拦截器可以挨个继续hook,根据请求前后变化来判断哪个拦截器对请求进行了处理
        Java.perform(function () {
            var a1 = Java.use('cn.shihuo.modulelib.utils.f1.a$a');
            a1.intercept.implementation = function (chain) {
                var request = chain.request();
                var urlString = request.url().toString();
                if(urlString.indexOf("https://sh-gateway.shihuo.cn/v4/services/sh-goodsapi/app_swoole_zone/getAttributes/v")!= -1){
                    console.log("拦截器1-->", urlString);
                }
                // 跳过当前拦截器的处理,直接交给后续拦截器进行处理
                var response = chain.proceed(request);
                return response;
        
                //var res = this.intercept(chain);
                //return res;
            };
        })
        View Code
    • 暴力反射调用私有静态方法

      Java.perform(function () {
          // 获取类
          var aClass = Java.use('com.bilibili.lib.biliid.internal.fingerprint.a.a');
      
          // 返回单个方法对象
          var hMethod = aClass.class.getDeclaredMethod('h', [Java.use('java.lang.String').class, Java.use('int').class]);
      
          // 没有参数时,传入空数组
          // var hMethod = aClass.class.getDeclaredMethod('h', []);
      
          // 取消访问检查(暴力反射)
          hMethod.setAccessible(true);
      
          // 1、 主动调用
          var result = hMethod.invoke(null, 'hello', 5);
          // 没有参数时
          // var result = hMethod.invoke(null);
          console.log('Result:', result);
      
          // 2、hook监听
          // 注意:这里使用原有的方法名:h
          aClass.h.implementation = function (arg1, arg2) {
              console.log("---------------------")
              console.log('arg1=', arg1)
              console.log('arg2=', JSON.stringify(arg2))
              var res = this.h(arg1, arg2);
              console.log('res=', res)
              return res
          }
      });
      
      // frida -Uf 包名 xxx.js
      View Code
    • hook函数调用栈

      // hook方法内部添加如下代码行即可
      console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
posted @ 2023-09-26 03:08  eliwang  阅读(688)  评论(0编辑  收藏  举报