frida初探3:frida常见的hook用法

文章最后更新时间为:2022年05月13日 16:00:16

系列文章:

  1. frida初探1:认识frida
  2. frida初探2:obection常见用法
  3. frida初探3:frida常见的hook用法

本篇文章主要讲一下frida的常见用法。都是看着肉丝师傅的文章总结的,比较工具化,请大佬勿喷。

1. 智能补全

git clone https://github.com/oleavr/frida-agent-example.git
cd frida-agent-example/
npm install

然后使用VSCode等IDE打开此工程,在agent下编写typescript,会有智能提示。
npm run watch会监控代码修改自动编译生成js文件
frida -U -f com.example.android --no-pause -l _agent.js

2. frida数据类型

Frida中的基本类型全名Frida中的基本类型缩写(定义数组时使用)
booleanZ
byteB
charC
doubleD
floatF
intI
longJ
shortS

在Frida中用[表示数组。例如是int类型的数组,写法为:[I,如果是String类型的数组,则写法为:[java.lang.String; 注意:后面还有个;号。

2.1 char[]

const gson = Java.use('com.r0ysue.gson.Gson');
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
    var result = this.toString(charArray);
    console.log("charArray,result:",charArray,result)
    console.log("charArray Object Object:",gson.$new().toJson(charArray));
    return result;
}

gson的调用查看:https://bbs.pediy.com/thread-259186.htm

2.2byte[]

const gson = Java.use('com.r0ysue.gson.Gson');

Java.use("java.util.Arrays").toString.overload('[B').implementation = function(byteArray){
    var result = this.toString(byteArray);
    console.log("byteArray,result):",byteArray,result)
    console.log("byteArray Object Object:",gson.$new().toJson(byteArray));
    return result;
}

2.3 java array构造

Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
    var newCharArray = Java.array('char', [ '一','去','二','三','里' ]);
    var result = this.toString(newCharArray);
    console.log("newCharArray,result:",newCharArray,result)
    console.log("newCharArray Object Object:",gson.$new().toJson(newCharArray));
    var newResult = Java.use('java.lang.String').$new(Java.array('char', [ '烟','村','四','五','家']))
    return newResult;
}

3. 枚举所有的类

hook第一步是需要找到需要hook的类,可以通过以下方式枚举所有加载到内存中的类:

function main() {
  Java.perform(function () {
      Java.enumerateLoadedClasses({
          onMatch: function (name, handle){
             console.log("name:" + name + " handle:" + handle)
          },
          onComplete: function () {
              console.log("end")
          }
      })
  })
}
setImmediate(main)

一般情况下需要通过关键字来过滤一下类:

function main() {
  Java.perform(function () {
      Java.enumerateLoadedClasses({
          onMatch: function (name, handle){
            if (name.split(".")[1] == "bluetooth"){
              console.log("name:" + name + " handle:" + handle)
            }
            if (name.indexOf("android.bluetooth.BluetoothMap") != -1) {
              console.log("name:" + name + " handle:" + handle)
          }
            //  console.log("name:" + name + " handle:" + handle)
          },
          onComplete: function () {
              console.log("end")
          }
      })
  })
}
setImmediate(main)

4. 枚举类中的方法

function main() {
  var a = enumMethods("android.bluetooth.BluetoothDevice")
  a.forEach(function(s) {
      console.log(s);
  });
}

function enumMethods(targetClass)
{
    var hook = Java.use(targetClass);
    var ownMethods = hook.class.getDeclaredMethods();
    hook.$dispose;

    return ownMethods;
}

setImmediate(main)

5. 主动调用

  • 静态函数直接use class然后调用方法,非静态函数需要先choose实例然后调用。
  • 设置成员变量的值,写法是xx.value = yy,其他方面和函数一样。如果有一个成员变量和成员函数的名字相同,则在其前面加一个_,如_xx.value = yy
function main() {
  Java.perform(function () {
      //hook静态函数直接调用
      var demoClass = Java.use("com.example.demo.DemoClass")
      demoClass.setAge()

      //hook动态函数,找到instance实例,从实例调用函数方法
      Java.choose("com.example.demo.DemoClass", {
          onMatch: function (instance) {
              instance.setName()
          },
          onComplete: function () {
              console.log("end")
          }
      })
  })
}
setImmediate(main)

6. 打印栈回溯

  • java层

    function main() {
      Java.perform(function () {
        var demoClass = Java.use("com.example.demo.DemoClass")
    
        demoClass.isHook.implementation = function () {
            showStacks()
            printStack("com.example.demo.DemoClass.IsHook")
            var result = this.isHook()
            return false
            
        }})
    }
    
    function printStack(name) {
      Java.perform(function () {
          var Exception = Java.use("java.lang.Exception");
          var ins = Exception.$new("Exception");
          var straces = ins.getStackTrace();
          if (straces != undefined && straces != null) {
              var strace = straces.toString();
              var replaceStr = strace.replace(/,/g, "\n");
              console.log("=============================" + name + " Stack strat=======================");
              console.log(replaceStr);
              console.log("=============================" + name + " Stack end=======================\r\n");
              Exception.$dispose();
          }
      });
    }
    setImmediate(main)
  • so层
Interceptor.attach(Module.findExportByName("libnative-lib.so", 'Java_cn_hluwa_fridasamples_MainActivity_stringFromJNI'),
{
    onEnter: function (args) {
        console.log(Thread.backtrace(this.context, Backtracer.FUZZY)
        .map(DebugSymbol.fromAddress).join("\n"))
    },
    onLeave: function (retval) {
    }
});
/** output
 * 
 * 0xb38796ec base.odex!oatexec+0x596ec
 * 0x73fc33de system@framework@boot.oat!oatexec+0x104a3de
 * 0x73755a08 system@framework@boot.oat!oatexec+0x7dca08
 * 0x73fd47fc system@framework@boot.oat!oatexec+0x105b7fc
 * 0x73755a5c system@framework@boot.oat!oatexec+0x7dca5c
 * 0x73d31dee system@framework@boot.oat!oatexec+0xdb8dee
 * 0xb48febf0 libart.so!0xd9bf0
 * 0xb48fd332 libart.so!0xd8332
 * 0xb4c199ae libart.so!art_quick_invoke_static_stub+0xad
 * 0xb4b427f6 libart.so!0x31d7f6
 * 0xb4903992 libart.so!_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+0x125
 * 0xb4b45c62 libart.so!_ZN3art12InvokeMethodERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectS4_S4_j+0x2a5
 * 0x7425b0d8 system@framework@boot.oat!oatexec+0x12e20d8
 * 0x7425b0d8 system@framework@boot.oat!oatexec+0x12e20d8
 * 0xb4afb0c0 libart.so!0x2d60c0
 * 0x731cdeea system@framework@boot.oat!oatexec+0x254eea
 */
/** 官方文档: https://frida.re/docs/javascript-api/#thread
 * 
 * Thread.backtrace([context, backtracer]): generate a backtrace for the current thread, returned as an array of NativePointer objects.
 * If you call this from Interceptor’s onEnter or onLeave callbacks you should provide this.context for the optional context argument, 
 * as it will give you a more accurate backtrace. Omitting context means the backtrace will be generated from the current stack location, 
 * which may not give you a very good backtrace due to V8’s stack frames. The optional backtracer argument specifies the kind of backtracer to use, 
 * and must be either Backtracer.FUZZY or Backtracer.ACCURATE, where the latter is the default if not specified. 
 * The accurate kind of backtracers rely on debugger-friendly binaries or presence of debug information to do a good job, 
 * whereas the fuzzy backtracers perform forensics on the stack in order to guess the return addresses, which means you will get false positives, 
 * but it will work on any binary.
 */

7. hook构造函数

通过clazz.$init.implementation来构造函数。

function main() {
  Java.perform(function () {
    Java.use("com.tlamb96.kgbmessenger.b.a").$init.implementation = function (i, str1, str2, z) {
      this.$init(i, str1, str2, z)
      console.log(i, str1, str2, z)
  }
  })
}

setImmediate(main)

8. 方法的重载

对方法有重载时,直接hook该方法会报错。

void fun(int x , int y ){
    Log.d("ROYSUE.Sum" , String.valueOf(x+y));
}

String fun(String x){
    total +=x;
    return x.toLowerCase();
}

所以我们需要指定具体hook的是哪个重载的方法,用法如下:


my_class.fun.overload("int" , "int").implementation = function(x,y){
...
my_class.fun.overload("java.lang.String").implementation = function(x){

当我们要hook某个方法的所有重载时,可以这么做:

//目标类
var hook = Java.use(targetClass);
//重载次数
var overloadCount = hook[targetMethod].overloads.length;
//打印日志:追踪的方法有多少个重载
console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]");
//每个重载都进入一次
for (var i = 0; i < overloadCount; i++) {
//hook每一个重载
    hook[targetMethod].overloads[i].implementation = function() {
        console.warn("n*** entered " + targetClassMethod);
        // 打印参数
        if (arguments.length) console.log();
        for (var j = 0; j < arguments.length; j++) {
            console.log("arg[" + j + "]: " + arguments[j]);
        }

        //打印返回值
        var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?)
        console.log("nretval: " + retval);
        console.warn("n*** exiting " + targetClassMethod);
        return retval;
    }
}

9. rpc调用

也就是开放一个rpc接口,给别的程序或者平台去调用。

  • js代码
// s3.js
console.log("Script loaded successfully ");

function callSecretFun() { //定义导出函数
    Java.perform(function () { //找到隐藏函数并且调用
        Java.choose("com.roysue.demo02.MainActivity", {
            onMatch: function (instance) {
                console.log("Found instance: " + instance);
                console.log("Result of secret func: " + instance.secret());
            },
            onComplete: function () { }
        });
    });
}
rpc.exports = {
    callsecretfunction: callSecretFun //把callSecretFun函数导出为callsecretfunction符号,导出名不可以有大写字母或者下划线
};
  • python代码
# loader3.py
import time
import frida

def my_message_handler(message, payload):
    print message
    print payload

device = frida.get_usb_device()
pid = device.spawn(["com.roysue.demo02"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s3.js") as f:
    script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()

command = ""
while 1 == 1:
    command = raw_input("Enter command:\n1: Exit\n2: Call secret function\nchoice:")
    if command == "1":
        break
    elif command == "2": #在这里调用
        script.exports.callsecretfunction()

效果如下:

$ python loader3.py
Script loaded successfully
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:1

10. 枚举so以及其导入导出表

// 列举所有加载的module,也就是so
function listso() {
  Java.perform(function () {
    //枚举当前加载的模块
    var process_Obj_Module_Arr = Process.enumerateModules();
    for (var i = 0; i < process_Obj_Module_Arr.length; i++) {
      //包含"lib"字符串的
      if (process_Obj_Module_Arr[i].path.indexOf("lib") != -1) {
        console.log("模块名称:", process_Obj_Module_Arr[i].name);
        console.log("模块地址:", process_Obj_Module_Arr[i].base);
        console.log("大小:", process_Obj_Module_Arr[i].size);
        console.log("文件系统路径", process_Obj_Module_Arr[i].path);
      }
    }
  });
}
// 列举某个module的导入导出函数
function listsoinout(name) {
  Java.perform(function () {
    var imports = Module.enumerateImportsSync(name);
   
    var exports = Module.enumerateExportsSync(name);
  
    for (var i = 0; i < imports.length; i++) {
      console.log(imports[i].name + ": " + imports[i].address);
    }
    var exports = Module.enumerateExportsSync("libhello.so");
    for (var i = 0; i < exports.length; i++) {
      console.log(exports[i].name + ": " + exports[i].address);
    }
  })
}
// 列举某个module的Symbol函数
function frida_Module() {
    Java.perform(function () {
        const hooks = Module.load('libc.so');
        var Symbol = hooks.enumerateSymbols();
        for(var i = 0; i < Symbol.length; i++) {
            console.log("isGlobal:",Symbol[i].isGlobal);
            console.log("type:",Symbol[i].type);
            console.log("section:",JSON.stringify(Symbol[i].section));
            console.log("name:",Symbol[i].name);
            console.log("address:",Symbol[i].address);
         }
    });
}

11. hook native基本用法

  • 有导出函数时:

    function hook_native() {
      console.log("[*] Starting Hook Script.");
      var so_base_address = Module.findBaseAddress("libcyberpeace.so")
      console.log("so_base_address is: " + so_base_address)
    
      if (so_base_address) {
        var string_with_jni_addr = Module.findExportByName("libcyberpeace.so",
          "Java_com_testjava_jack_pingan2_cyberpeace_CheckString")
        console.log("string_with_jni_addr is: " + string_with_jni_addr)
        Interceptor.attach(string_with_jni_addr, {
          onEnter: function (args) {
            console.log("string_with_jni args: " + args[0], args[1], args[2])
            console.log(Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
          },
          onLeave: function (retval) {
            console.log("[*] 原始的So层函数返回值是:", retval)
            console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
            var newRetval = Java.vm.getEnv().newStringUtf("new retval from hook_native");
            retval.replace(ptr(newRetval));
          }
        })
      } else {
        console.log("find so base address fail", so_base_address)
      }
    }
  • 无导出函数时

需要手动定位要hook的函数地址,一般通过静态分析得出:so地址 + 函数偏移

function hook_native1() {
  console.log("[*] Starting Hook Script.");
  var so_base_address = Module.findBaseAddress("libcyberpeace.so")
  console.log("so_base_address is: " + so_base_address) // 32位需要加1,这里是64位

  if (so_base_address) {
    //要hook的函数在函数里面的偏移
    var n_addr_func_offset = 0x840;  
    
    //加载到内存后 函数地址 = so地址 + 函数偏移
    var n_addr_func = so_base_address.add(n_addr_func_offset)

    var ptr_func = new NativePointer(n_addr_func);
    Interceptor.attach(ptr_func, {
      onEnter: function (args) {
        console.log("string_with_jni args: " + args[0], args[1], args[2])
        console.log(Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
      },
      onLeave: function (retval) {
        console.log("[*] 原始的So层函数返回值是:", retval)
        console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
        var newRetval = Java.vm.getEnv().newStringUtf("new retval from hook_native");
        retval.replace(ptr(newRetval));
        retval.replace(1);
      }
    })
  } else {
    console.log("find so base address fail", so_base_address)
  }
}

或者也可以通过symbols符号来定位native方法地址:

function find_func_from_symbols() {
  var NewStringUTF_addr = null;
  var symbols = Process.findModuleByName("libart.so").enumerateSymbols();
  for (var i in symbols) {
      var symbol = symbols[i];
      if (symbol.name.indexOf("art") >= 0 &&
          symbol.name.indexOf("JNI") >= 0 &&
          symbol.name.indexOf("CheckJNI") < 0
      ){
          if (symbol.name.indexOf("NewStringUTF") >= 0) {
              console.log("find target symbols", symbol.name, "address is ", symbol.address);
              NewStringUTF_addr = symbol.address;
          }
      }
  }

  console.log("NewStringUTF_addr is ", NewStringUTF_addr);

  Interceptor.attach(NewStringUTF_addr, {
      onEnter: function (args) {
          console.log("args0",args[0])
          
      },
      onLeave: function (returnResult) {
          console.log("result: ", Java.cast(returnResult, Java.use("java.lang.String")));
          
      }
  })
}

关于so层的hook和逆向,后续单独再写。

参考

1 + 1 =
快来做第一个评论的人吧~