Frida获取Native方法的参数,NativeHook总结

3,887 阅读8分钟

Frida坑太多了,16总是卡死设备,要注意的事项也很多,由于Frida非常不稳定,这里比较建议使用frida15.2.2版本

通常来说, 我们使用以下方法就可以正常拿到变量结果

/*
 *  1.确定要Hook的函数
 *  原函数:
 *       status_t AudioPolicyManager::setDeviceConnectionStateInt(audio_devices_t deviceType,
 *        audio_policy_dev_state_t state,
 *        const char *device_address,
 *        const char *device_name,
 *        audio_format_t encodedFormat)
 *
 *  2.上面类型过于复杂,这里我们直接拿IDA查看函数对应的伪代码分析
 *  IDA伪代码:
 *        // attributes: thunk
 *         __int64 __fastcall android::AudioPolicyManager::setDeviceConnectionStateInt(int a1, int a2, int a3, char *a4)
 *         {
 *           return _ZN7android18AudioPolicyManager27setDeviceConnectionStateIntEj24audio_policy_dev_state_tPKcS3_14audio_format_t(
 *                   a1,
 *                   a2,
 *                   a3,
 *                   a4);
 *         }
 *
 *  3.从结果看IDA的伪代码参数是不正确的,所以我们不要太相信IDA的伪代码结果,但我们大致可以推断出5个参数的类型应该是
 *   int, int, char*, char*, int(推测,前4个参数基本是对的),知道具体参数类型后,接下来就方便读取参数值了
 */

//1.加载so文件
console.log("Loading Module => " + "libaudiopolicymanagerdefault.so");
let soModule = Process.getModuleByName("libaudiopolicymanagerdefault.so");
//2.定位Native函数
let funcName = soModule.getExportByName(
  "_ZN7android18AudioPolicyManager27setDeviceConnectionStateIntEj24audio_policy_dev_state_tPKcS3_14audio_format_t"
);
//3.Hook并打印参数
Interceptor.attach(funcName, {
  onEnter: function (args) {
    //要注意,目前根据测试,变量索引从1开始传递(怀疑0可能是什么对象,如jniEnv,1开始才是真正的参数)
    console.log("===============setDeviceConnectionStateInt===================");
    console.log("deviceType:" + args[1].readInt());
    console.log("deviceState:" + args[2].readInt());
    console.log("deviceAddress:" + args[3].readCString());
    console.log("deviceName:" + args[4].readCString());
    console.log("encodedFormat:" + args[5].readInt());
  },
  onLeave: function (retval) {
    console.log("retval:" + retval);
  },
});

不过不出意外的话,可能会出现报错,提示地址无法访问(询问GPT提示args可能不是简单的整数类型,那会不会是址类型的参数?如果是地址,我们这么直接拿不就是错的吗?试图以变量的方式访问地址)

Error: access violation accessing 0x4000000
    at <anonymous> (frida/runtime/core.js:141)
    at onEnter (agent/index.ts:312)

Error: access violation accessing 0x82000000
    at <anonymous> (frida/runtime/core.js:141)
    at onEnter (agent/index.ts:312)

试了很久,找到解决方案

//1.加载so文件
console.log("Loading Module => " + "libaudiopolicymanagerdefault.so");
let soModule = Process.getModuleByName("libaudiopolicymanagerdefault.so");
//2.定位Native函数
let funcName = soModule.getExportByName(
  "_ZN7android18AudioPolicyManager27setDeviceConnectionStateIntEj24audio_policy_dev_state_tPKcS3_14audio_format_t"
);
//3.Hook并打印参数
Interceptor.attach(funcName, {
  onEnter: function (args) {
    //使用Memory作为中介,读取地址里的参数(目前不知道什么原因,但确实可以了)
    console.log("===============setDeviceConnectionStateInt===================");
    var arg1Pointer: NativePointer = Memory.alloc(4);
    var arg2Pointer: NativePointer = Memory.alloc(4);
    var arg5Pointer: NativePointer = Memory.alloc(4);
    arg1Pointer.writePointer(args[1]);
    arg2Pointer.writePointer(args[2]);
    arg5Pointer.writePointer(args[5]);
    console.log("deviceType:" + arg1Pointer.readInt());
    console.log("deviceState:" + arg2Pointer.readInt());
    console.log("device_address:" + args[3].readCString());
    console.log("device_name:" + args[4].readCString());
    console.log("encodedFormat:" + arg5Pointer.readInt());
  },
  onLeave: function (retval) {
    console.log("retval:" + retval);
  },
});

运行结果,测试参数完全正确

===============setDeviceConnectionStateInt===================
deviceType:-2113929216
deviceState:1
device_address:card=1;device=0;
device_name:USB-Audio - 1080P USB Camera
encodedFormat:0
retval:0x0

其它使用技巧

1.传值和替换返回值

1.需要传值到onLeave中, 我们可以在onEnter中使用this.variable = value的方式传递和访问
2.替换函数返回值retval.replace(要替换的结果)

//这是一个替换函数返回值的一个技巧
Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            return 0;
        },
        onLeave: function (retval) {
            const dstAddr = Java.vm.getEnv().newStringUtf("1234");
            retval.replace(dstAddr);
            //如果是替换bool类型的返回值,我们可以调用ptr(0x1)或者prt(0x0)之类的方式
            //retval.replace(ptr(0x1));
        },
});

2.Hook原Native方法,并根据参数决定是否调用原函数

已踩坑:第一个参数切记使用指针,可能是JNIEnv指针,即使你看过源码是一个空参数方法,也需要声明一个指针

    //1.加载so文件
    console.log("Loading Module => " + "libaudiopolicymanagerdefault.so");
    let soModule = Process.getModuleByName("libaudiopolicymanagerdefault.so");
    //2.定位Native函数
    let funcName = soModule.getExportByName( "_ZN7android18AudioPolicyManager27setDeviceConnectionStateIntEj24audio_policy_dev_state_tPKcS3_14audio_format_t"
    );
    //3.根据原函数地址构建NativeFunction(这里要写在Interceptor.replace之前,留给之后调用)
    //**重点,请注意第一个参数可能是一个指针,所以用pointer,不用pointer后面可能出现莫名奇妙的问题,比如地址无法访问之类的**
    let nativeFunction = new NativeFunction(funcName, "int", ["pointer", "int", "int", "pointer", "pointer", "int"]);

    //4.替换原函数
    Interceptor.replace(
      funcName,
      new NativeCallback(
        function (arg1: any, deviceType: any, deviceState: any, deviceAddress: any, deviceName: any, encodedFormat: any) {
          console.log("===============setDeviceConnectionStateInt(Replacement)===================");
          console.log("arg1:" + arg1);
          console.log("deviceType:" + deviceType);
          console.log("deviceState:" + deviceState);
          console.log("deviceAddress:" + deviceAddress.readCString());
          console.log("deviceName:" + deviceName.readCString());
          console.log("encodedFormat:" + encodedFormat);
          //5.根据条件,是否调用原函数
          if (deviceType !== -2113929216) {
            //这一段可能会报错:access violation accessing 0x835a168,如果出现这种错误,那大概率是你声明的参数类型,或参结果不正确
            //(踩坑1天的经验)例如:
            //     1.原方法数据类型为pointer,你声明成int并且传入了int类型测错误
            //     2.要hook的方法有6个参数,但是你只声明和传入了5个(我就是这种,因为我看了原代码,明明没有参数,但却需要声明一个pointer并传入pointer的参数才行,就是上面说的,实际索引1才是我们的第1个参数值的问题)
            let original = nativeFunction(arg1, deviceType, 0, deviceAddress, deviceName, 0);
            console.error("原函数返回值:" + original);
            return original;
          }
          return 0;
        },
        "int",
        ["pointer", "int", "int", "pointer", "pointer", "int"]
      )
    );

3.根据模块和函数名称查找函数列表

    /**
     * 根据模块和函数名称查找函数列表
     * @param module so模块
     * @param funcName 函数名称
     * @returns        函数列表
     */
    function findFuncByModule(module: Module, funcName: string) {
      let moduleResult: any = [];
      module.enumerateExports().forEach(function (exp) {
        if (exp.name.indexOf(funcName) != -1) {
          moduleResult.push(exp);
        }
      });
      return moduleResult;
    }

    let soModule = Process.getModuleByName("libaudiopolicymanagerdefault.so");
    findFuncByModule(soModule, "setDeviceConnectionState").forEach(function (exp:any) {
      console.log("name:" + exp.name + " type:" + exp.type + " address:" + exp.address);
    });

4.如果native方法的参数是指针类型的,获取这个指针的数据

Interceptor.replace(
  setEngineDeviceConnectionStateFun,
  new NativeCallback(
    function (arg1: any, device: any, state: any) {
      console.log("===============setEngineDeviceConnectionState(Replacement)===================");
      console.log("arg1:" + arg1);
      console.log("device:" + device);
      console.log("state:" + state);
      //device其实就是参数对象的起始地址,想要得到对象内数据,我们必须知道数据类型,以及偏移,知道了之后,我们加上起始地址的偏移就可以得到参数内容
      var idOffset = parseInt(device + "", 16) + 98;
      var tagOffset = parseInt(device + "", 16) + 320;
      var idOffsetAddr = "0x" + "" + idOffset.toString(16);
      var tagOffsetAddr = "0x" + "" + tagOffset.toString(16);
      console.log("device对象地址 => " + device + ", device.id值 => " + ptr(idOffsetAddr).readUInt());
      console.log("device对象地址 => " + device + ", device.deviceTag值 => " + ptr(tagOffsetAddr).readCString());

      let original = setEngineDeviceConnectionState(arg1, arg2, arg3);
      console.log("original:" + original);
    },
    "void",
    ["pointer", "pointer", "int"]
  )
);

5.打印Java堆栈

/**
 * 打印java堆栈信息
 * @param showTag 显示标记
 */
function showJavaCallStack(showTag: string) {
  console.log("\n\n********************>JavaStack(" + showTag + ")");
  let thread = Java.use("java.lang.Thread").currentThread();
  thread.getStackTrace().forEach(function (element: string) {
    let lineInfo = element.toString();
    if (lineInfo.indexOf(packageName) >= 0) {
      console.log(lineInfo);
    } else {
      console.error(lineInfo);
    }
  });
  console.log("====================>JavaStackEnd(" + showTag + ")");
  console.log("\n\n");
}

6.打印Native堆栈(会导致脚本结束,目前原因未知)

/**
 * 打印native堆栈信息
 * @param context native堆栈上下文
 * @param showTag 显示标记
 */
function showNativeCallStack(context: any, showTag: string) {
  console.log("\n\n********************>NativeStack(" + showTag + ")");
  Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n");
  console.log("====================>NativeStackEnd(" + showTag + ")");
  console.log("\n\n");
}

//测试
Interceptor.replace(
  setInputDeviceFunc,
  new NativeCallback(
    function (arg1: any, ioHandle: any, deviceDesc: any, boolForce: any, patchCode: any) {
      //打印堆栈
      showNativeCallStack(this.context, "setInputDeviceFunc");
      console.log("arg1:" + arg1);
      console.log("ioHandle:" + ioHandle);
      console.log("deviceDesc:" + deviceDesc);
      console.log("boolForce:" + boolForce);
      console.log("patchCode:" + patchCode);
      let result = setInputDevice(arg1, ioHandle, deviceDesc, boolForce, patchCode);
      nextAudioPortGeneration(arg1);
      return result;
    },
    "void",
    ["pointer", "int", "pointer", "bool", "pointer"]
  )
);

7.获取java层传递object参数的值

  let scanImplCls = Java.use("com.android.server.wifi.scanner.WificondScannerImpl");
  let wifiNativeCls = Java.use("com.android.server.wifi.WifiNative");
  scanImplCls.pollLatestScanData.implementation = function () {
    console.log("pollLatestScanData");
    //当我们得到一个obj类型的对象时,可以通过.value的方式拿到这个对象的实际对象何值
    //比如this.mWifiNative是一个[object],但是this.mWifiNative.value却是com.android.server.wifi.WifiNative@aa9e215
    let wifiNativeObj = this.mWifiNative.value;
    console.log("mWifiNative => " + wifiNativeObj);
    let scanResult = wifiNativeObj.getScanResults();
    console.log("scanResult => " + scanResult);
    let originalResult = this.pollLatestScanData.call(this);
    return originalResult;
  };

8.如果native函数的参数是一个结构体,如何读取?

//例如:下面的代码中Size是一个结构体
status_t Parameters::getFilteredSizes(Size limit, Vector<Size> *sizes) {
struct Size {
        int32_t width;
        int32_t height;
};
//情况1:这种情况得到可能不是一个结构体,可能被拆分成多个参数,我们需要按照顺序读取数据
let targetAddress3 = soModule.getExportByName("_ZN7android7camera210Parameters16getFilteredSizesENS1_4SizeEPNS_6VectorIS2_EE");
Interceptor.attach(targetAddress3, {
  onEnter(args) {
    console.error("onEnter================getFilteredSizes");
    console.log("args[0] => " + args[0]);
    console.log("width => " + args[1].toInt32());
    console.log("height => " + args[2].toInt32());
  },
  onLeave(retval) {
    console.error("onLeave => " + retval.toInt32());
  }
});

9.Dump View的布局层次

/**
 * dump view的层级关系
 */
function dumpViewHierarchy(tag: any, rootView : any){
  dumpViewHierarchyInner(tag, rootView, false, 0);
}

/**
 * dump view的层级关系
 * @param tag 标记,当是递归时无效
 * @param rootView 根view
 * @param isRecursion 是否是递归
 */
function dumpViewHierarchyInner(tag: any, rootView : any, isRecursion: boolean, numIndex: number){
  let mTag = tag;;
  if(!isRecursion){
    mTag = "";
    console.error("\ndumpViewHierarchy>>>>>>>>>>>>>>>>>>>>>("+tag+")");
    console.log("("+numIndex+")"+rootView);
  }else {
    mTag = mTag + "--"
    console.log("("+numIndex+")"+mTag + rootView);
  }


  //遍历所有子View,包括递归
  let viewGroupCast = Java.use("android.view.ViewGroup");
  let javaCls = Java.use("java.lang.Class");
  let viewGroupCls = javaCls.forName("android.view.ViewGroup");
  let currentCls = javaCls.forName(rootView.$className);
  if(viewGroupCls.isAssignableFrom(currentCls)){
    let viewGroup = Java.cast(rootView, viewGroupCast);
    let childCount = viewGroup.getChildCount();
    for(let i = 0; i < childCount; i++){
      let childView = viewGroup.getChildAt(i);
      currentCls = javaCls.forName(childView.$className);
      if(viewGroupCls.isAssignableFrom(currentCls)){
        dumpViewHierarchyInner(mTag, childView, true, numIndex+1);
      }else{
        console.log("("+numIndex+")"+mTag+childView);
      }
    }
  }

  if(!isRecursion){
    console.error("dumpViewHierarchy<<<<<<<<<<<<<<<<<<<<<"+tag+"\n");
  }
}


//在需要的地方直接调用就好,需要传一个View的实例
 dumpViewHierarchy("VideoActivity", decorView);

Dump结果,我们同样可以自己改造这个方法,遍历所有View

image.png

10.递归所有的View, 上一个方法的拓展

/**
 * 遍历所有view
 * @param rootView 根view
 * @param callback 回调,自行实现回调方法onMatch(view:any)
 * @param isRecursion 是否是递归
 */
function eachAllViewInner(rootView: any, callback: any, isRecursion:any){
  if(!isRecursion){
    callback.onMatch(rootView);
  }else {
    callback.onMatch(rootView);
  }

  //遍历所有子View,包括递归
  let viewGroupCast = Java.use("android.view.ViewGroup");
  let javaCls = Java.use("java.lang.Class");
  let viewGroupCls = javaCls.forName("android.view.ViewGroup");
  let currentCls = javaCls.forName(rootView.$className);
  if(viewGroupCls.isAssignableFrom(currentCls)){
    let viewGroup = Java.cast(rootView, viewGroupCast);
    let childCount = viewGroup.getChildCount();
    for(let i = 0; i < childCount; i++){
      let childView = viewGroup.getChildAt(i);
      currentCls = javaCls.forName(childView.$className);
      if(viewGroupCls.isAssignableFrom(currentCls)){
        eachAllViewInner(childView, callback, true);
      }else{
        callback.onMatch(childView);
      }
    }
  }
}

11.打印java对象的所有字段信息和值

/**
 * 打印某个对象的所有字段
 * @param obj 要打印的对象
 */
function showAllFields(obj:any){
  console.error("\nshowAllFields=================>"+obj);
  let javaCls = Java.use("java.lang.Class");
  let currentCls = javaCls.forName(obj.$className);
  let fields = currentCls.getDeclaredFields();
  fields.forEach(function (field:any) {
    console.log("field => " + field);
    let AcccessibleCls = Java.use("java.lang.reflect.AccessibleObject");
    let AcccessibleObj = Java.cast(field, AcccessibleCls);
    AcccessibleObj.setAccessible(true);
    console.log("fieldValue => " + field.get(obj));
  });
  console.log("showAllFields=================<\n");
}

/**
 * 获取对象的某个字段名称
 * @param obj 要获取的对象
 * @param fieldName   字段名称
 * @returns 返回字段值
 */
function getFieldValue(obj:any, fieldName:any){
  let javaCls = Java.use("java.lang.Class");
  let currentCls = javaCls.forName(obj.$className);
  let field = currentCls.getDeclaredField(fieldName);
  let AcccessibleCls = Java.use("java.lang.reflect.AccessibleObject");
  let AcccessibleObj = Java.cast(field, AcccessibleCls);
  AcccessibleObj.setAccessible(true);
  return field.get(obj);
}

12.打印某个对象的所有Java方法

/**
 * 显示某个对象的所有方法
 * @param obj 要打印的对象
 */
function showAllMethods(obj:any){
  console.error("\nshowAllMethods=================>"+obj);
  let javaCls = Java.use("java.lang.Class");
  let currentCls = javaCls.forName(obj.$className);
  let methods = currentCls.getDeclaredMethods();
  methods.forEach(function (method:any) {
    console.log("method => " + method);
  });
  console.log("showAllMethods=================<\n");
}

13.获取时间输出

function getTimeStr(){
  //HH:mm:ss.SSS
  let date = new Date();
  let hour = date.getHours();
  let minute = date.getMinutes();
  let second = date.getSeconds();
  let millisecond = date.getMilliseconds();
  let timeStr = "";
  if(hour < 10){
    timeStr += "0";
  }
  timeStr += hour;
  timeStr += ":";
  if(minute < 10){
    timeStr += "0";
  }
  timeStr += minute;
  timeStr += ":";
  if(second < 10){
    timeStr += "0";
  }
  timeStr += second;
  timeStr += ".";
  if(millisecond < 10){
    timeStr += "00";
  }
  else if(millisecond < 100){
    timeStr += "0";
  }
  timeStr += millisecond;
  return "["+timeStr+"]";
}

文章如有错请指教,后续学到新的知识会再继续补充...