frida初探4:frida hook so

文章最后更新时间为:2023年08月24日 00:37:55

本篇文章主要记录frida hook so的常见用法,内容多参考自《frida 协议分析》

1. 枚举module

// 列举所有加载的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
var module = Process.findModuleByName("libxiaojianbang.so");
console.log(JSON.stringify(module));
//{"name":"libxiaojianbang.so","base":"0x7ad1ce6000","size":28672,"path":"/data/app/com.xiaojianbang.app-r_cD2g_EAJo-3V4FJEttXQ==/lib/arm64/libxiaojianbang.so"}
if(module != null){
    //do someting ... 
}

// module的定义
declare class Module {
    name: string;            //模块名
    base: NativePointer;    //模块基址
    size: number;            //模块大小
    path: string;            //模块所在路径
    enumerateImports(): ModuleImportDetails[];    //枚举导入表
    enumerateExports(): ModuleExportDetails[];    //枚举导出表
    enumerateSymbols(): ModuleSymbolDetails[];    //枚举符号表
    findExportByName(exportName: string): NativePointer | null;    //获取导出函数地址
    getExportByName(exportName: string): NativePointer;        //获取导出函数地址
    static load(name: string): Module;                            //加载指定模块
    static findBaseAddress(name: string): NativePointer | null;        //获取模块基址
    static getBaseAddress(name: string): NativePointer;            //获取模块基址
    //获取导出函数地址
static findExportByName(moduleName: string | null, exportName: string): NativePointer | null;    
//获取导出函数地址
    static getExportByName(moduleName: string | null, exportName: string): NativePointer;
}

2. 枚举符号

// 列举某个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");
    console.log(JSON.stringify(exports[0]));
        //{"type":"function","name":"JNI_OnLoad","address":"0xc68995f1"}

    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);
         }
    });
}

// 定位so中的函数
function findFuncInWitchSo(funcName) {
    var modules = Process.enumerateModules();
    for (let i = 0; i < modules.length; i++) {
        let module = modules[i];
        let _symbols = module.enumerateSymbols();
        for (let j = 0; j < _symbols.length; j++) {
            let _symbol = _symbols[i];
            if(_symbol.name == funcName){
                return module.name + " " + JSON.stringify(_symbol);
            }
        }
        let _exports = module.enumerateExports();
        for (let j = 0; j < _exports.length; j++) {
            let _export = _exports[j];
            if(_export.name == funcName){
                return module.name + " " + JSON.stringify(_export);
            }
        }
    }
    return null;
}
console.log(findFuncInWitchSo('strcat'));
//libc.so {"type":"function","name":"strcat","address":"0x7bc0e0322c"}

3. hook so

3.1 hook导出函数&symbols函数

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)
  }
}

onEnter是在原函数之前执行,然后执行原函数,最后执行onLeave函数中的代码。

onEnter接收的args,数组的前两个值是JNIENV和jclass/jobject,如果是静态方法则对应jclass,如果是实例方法则对应jobject,对于这类内存地址,可以通过console.log(hexdump(args[0])来打印内存,

或者也可以通过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")));
          
      }
  })
}

3.2 hook 任意函数

在so文件中,只需要得到函数的内存地址,就可以完成任意函数的hook,函数的地址=so文件基址+函数相对于so的偏移地址

  • 获取so文件基地址,可以通过findBaseAddress或者getBaseAddress来获取。
declare class Module {
......
    static findBaseAddress(name: string): NativePointer | null;
static getBaseAddress(name: string): NativePointer;
}
  • 获取函数相对于so的偏移地址,可以在IDA的汇编界面上查看,这里的偏移地址是函数定义部分的首地址,也就是.text段,不是在plt表的地址

需要注意的是,在32位so文件中的函数,需要在计算出的函数地址上+1,如果是64位则不需要

下面是

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)
  }
}

3.3 获取指针参数返回值

在c语言中,经常会将函数参数定义成指针,然后在执行过程中,改变传入的实参,也就是将函数参数当返回值用,这个时候去hook返回值就没用了,因为返回值通常是void。对于这种情况,需要在进入onEnter函数时,保存参数的内存地址,在onLeave时获取参数对应内存地址的内容:

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var MD5Final = soAddr.add(0x3A78);
Interceptor.attach(MD5Final, {
    onEnter: function (args) {
        this.args1 = args[1];
    }, onLeave: function (retval) {
        console.log(hexdump(this.args1));
    }
});
/*
7ffc689cc8  41 be f1 ce 7f dc 3e 42 c0 e5 d9 40 ad 74 ac 00  A.....>B...@.t..
//logcat中的输出结果
//CMD5 md5Result: 41bef1ce7fdc3e42c0e5d940ad74ac00
*/

3.4 inlineHook

inlineHook比较常用,可以获取到某一条指令的返回值,可以获取函数执行的中间结果。inlineHook和普通的hook函数在用法上没有区别,只需要改变偏移地址即可:

var hookAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1AF4);
Interceptor.attach(hookAddr, {
    onEnter: function (args) {
        console.log("onEnter x8: ", this.context.x8.toInt32());
        console.log("onEnter x9: ", this.context.x9.toInt32());
    }, onLeave: function (retval) {
        console.log("onLeave x0: ", this.context.x0.toInt32());
    }
});
/*
onEnter x8:  11
onEnter x9:  7
onLeave x0:  18
*/

this.context.x8是什么?this.context.x8 表示访问 ARM64 架构中的寄存器 X8 的值。关于 ARM64 架构的寄存器知识,这里先不做解释

3.5 修改函数参数和返回值

1. 数值参数

可以通过args[x].toInt32()、args[x].toUInt32()来分别打印有无符号的十进制,修改的话,可以通过ptr构造出NativePointer后赋值,也可以直接修改寄存器的值。

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var addFunc = soAddr.add(0x1ACC);
Interceptor.attach(addFunc, {
    onEnter: function (args) {
        args[2] = ptr(100);
//this.context.x2 = 100;
        console.log(args[2].toInt32());
    }, onLeave: function (retval) {
        console.log(retval.toInt32());
        retval.replace(100);
//this.context.x0 = 100;
    }
});
/*
args[2]:  100
retval:  113
//logcat中的输出为
//CADD addResult: 100
*/

2. 字符串参数

var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
var newStr = "xiaojianbang&liruyi";
var newStrAddr = Memory.allocUtf8String(newStr);
Interceptor.attach(MD5Update, {
    onEnter: function (args) {
        if(args[1].readCString() == "xiaojianbang"){
            args[1] = newStrAddr;
            console.log(hexdump(args[1]));
            args[2] = ptr(newStr.length);
            console.log(args[2].toInt32());
        }
    }, onLeave: function (retval) {
    }
});
/*
7b34a80060  78 69 61 6f 6a 69 61 6e 62 61 6e 67 26 6c 69 72  xiaojianbang&lir
7b34a80070  75 79 69 00 00 00 00 00 23 00 00 00 00 00 00 00  uyi.....#.......
19
//logcat中的输出结果
//CMD5 md5Result: 8f1968f06a1e62bb3d83119352cc26cc
*/

readCString用于从指定地址开始读取c语言字符串,Memory.allocUtf8String会将js的string写入内存,并以NativePointer的形式返回这段内存的首地址,需要在函数体外生成变量newStrAddr, 如果在onEnter中生成,则当onEnter函数执行完毕后,变量会被回收。

3.6 hook jni函数

有两种方式获取jni函数的地址:

  • 枚举libart的符号表,得到jni函数的地址
  • 先得到JNIenv结构体的地址,再通过偏移得到对应函数的地址

下面分别是这两种方式的demo

function hook_jni() {
    var _symbols = Process.getModuleByName("libart.so").enumerateSymbols();
    var newStringUtf = null;
    for (let i = 0; i < _symbols.length; i++) {
        var _symbol = _symbols[i];
        if(_symbol.name.indexOf("CheckJNI") == -1 && _symbol.name.indexOf("NewStringUTF") != -1){
            newStringUtf = _symbol.address;
        }
    }
    Interceptor.attach(newStringUtf, {
        onEnter: function (args) {
            console.log("newStringUtf  args: ", args[1].readCString());
        }, onLeave: function (retval) {
            console.log("newStringUtf  retval: ", retval);
        }
    });
}
hook_jni();
/*
newStringUtf args:  GB2312
newStringUtf retval:  0x81
newStringUtf args:  41bef1ce7fdc3e42c0e5d940ad74ac00
newStringUtf retval:  0xa9
*/


var envAddr = Java.vm.tryGetEnv().handle.readPointer();
var NewStringUTF = envAddr.add(167 * Process.pointerSize);
var NewStringUTFAddr = envAddr.add(167 * Process.pointerSize).readPointer();
console.log(hexdump(NewStringUTF));
console.log(hexdump(NewStringUTFAddr));
console.log(Instruction.parse(NewStringUTFAddr).toString());
/*
6fa4fde428  ec 30 d7 a4 6f 00 00 00 a0 38 d7 a4 6f 00 00 00  .0..o....8..o...
6fa4fde438  50 40 d7 a4 6f 00 00 00 70 40 d7 a4 6f 00 00 00  P@..o...p@..o...

6fa4d730ec  ff 43 03 d1 fc 6f 07 a9 fa 67 08 a9 f8 5f 09 a9  .C...o...g..._..
6fa4d730fc  f6 57 0a a9 f4 4f 0b a9 fd 7b 0c a9 fd 03 03 91  .W...O...{......

sub sp, sp, #0xd0
*/

function hook_jni2() {
    var envAddr = Java.vm.tryGetEnv().handle.readPointer();
    var NewStringUTFAddr = envAddr.add(167 * Process.pointerSize).readPointer();
    Interceptor.attach(NewStringUTFAddr, {
        onEnter: function (args) {
            console.log("FindClass args: ", args[1].readCString());
        }, onLeave: function (retval) {
            console.log("FindClass retval: ", retval);
        }
    });
}
hook_jni2();
/*
newStringUtf args:  GB2312
newStringUtf retval:  0x81
newStringUtf args:  41bef1ce7fdc3e42c0e5d940ad74ac00
newStringUtf retval:  0xa9
*/

4. 打印堆栈

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.
 */

5. 主动调用so函数

frida提供了new nativeFunction的方式来创建函数指针,其语法如下

new NativeFunction(address, returnType, argTypes[,abi])

returnType, argTypes支持很多种类型,比较常用的是void、pointe和int。这里的参数可以不用完全正确也可以调用函数。

下面以jstring2cstr函数为例实现主动调用,该函数接收的参数为(JNIEnv, jstring) ,返回char 类型。

Java.perform(function () {
    var soAddr = Module.findBaseAddress("libxiaojianbang.so");
    var funAddr = soAddr.add(0x16BC);
    var jstr2cstr = new NativeFunction(funAddr, 'pointer', ['pointer','pointer']);
    var env = Java.vm.tryGetEnv();
    //主动调用jni函数newStringUtf,将JavaScript的字符串转为Java字符串
    var jstring = env.newStringUtf("xiaojianbang");
    var retval = jstr2cstr(env.handle, jstring);
    //var retval = jstr2cstr(env, jstring);
    console.log(retval.readCString());
});
//xiaojianbang

下面是通过NativeFunction主动调用JNI函数

var symbols = Process.getModuleByName("libart.so").enumerateSymbols();
var NewStringUTFAddr = null;
var GetStringUTFCharsAddr = null;
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols[i];
    if(symbol.name.indexOf("CheckJNI") == -1 && symbol.name.indexOf("NewStringUTF") != -1){
        NewStringUTFAddr = symbol.address;
    }else if (symbol.name.indexOf("CheckJNI") == -1 && symbol.name.indexOf("GetStringUTFChars") != -1){
        GetStringUTFCharsAddr = symbol.address;
    }
}
var NewStringUTF = new NativeFunction(NewStringUTFAddr, 'pointer', ['pointer', 'pointer']);
var GetStringUTFChars = new NativeFunction(GetStringUTFCharsAddr, 'pointer', ['pointer', 'pointer', 'pointer']);

var jstring = NewStringUTF(Java.vm.tryGetEnv().handle, Memory.allocUtf8String("xiaojianbang"));
console.log(jstring);

var cstr = GetStringUTFChars(Java.vm.tryGetEnv(),  jstring,  ptr(0));
console.log(cstr.readCString());
/*
0x1
xiaojianbang
*/


var cstr = GetStringUTFChars(Java.vm.tryGetEnv(),  jstring,  Memory.alloc(1).writeS8(1));
console.log(cstr.readCString());
//xiaojianbang


//......
var GetStringUTFChars = new NativeFunction(GetStringUTFCharsAddr, 'pointer', ['pointer', 'pointer']);
var cstr = GetStringUTFChars(Java.vm.tryGetEnv(),  jstring);
console.log(cstr.readCString());
//xiaojianbang

6. 定位JNI函数

JNI函数一般分为动态和静态函数,有时候我们不知道它位于哪个so中,如果apk加了混淆,那么直接反编译是不能直接看到的,所以需要通过hook的方式快速定位Jni函数,jni函数分为动态注册和静态注册,不管哪一种都会调用相应的系统函数,所以只需要hook注册函数即可:

  • 静态注册hook dlsym,静态注册的native函数首次被调用才会经过dlsym,之后不再触发。

直接看代码即可

var dlsymAddr = Module.findExportByName("libdl.so", "dlsym");
Interceptor.attach(dlsymAddr, {
    onEnter: function (args) {
        this.args1 = args[1];
    }, onLeave: function (retval) {
        console.log(this.args1.readCString(),  retval);
    }
});
/*
//app以spawn的方式启动,得到如下输出
oatdata 0x7d360c3000
......
oatdatabimgrelro 0x0
HMI 0x7d33c94018
//点击CADD按钮后输出,之后不再触发
JNI_OnLoad 0x7d337abe10
Java_com_xiaojianbang_ndk_NativeHelper_add 0x7d337abacc
//点击CMD5按钮后输出,之后不再触发
Java_com_xiaojianbang_ndk_NativeHelper_md5 0x7d337abf2c
*/

var dlsymAddr = Module.findExportByName("libdl.so", "dlsym");
Interceptor.attach(dlsymAddr, {
    onEnter: function (args) {
        this.args1 = args[1];
    }, onLeave: function (retval) {
        var module = Process.findModuleByAddress(retval);
        if(module == null) return;
        console.log(this.args1.readCString(), module.name, retval, retval.sub(module.base));
    }
});
/*
......
JNI_OnLoad libxiaojianbang.so 0x7d91131e10 0x1e10
Java_com_xiaojianbang_ndk_NativeHelper_add libxiaojianbang.so 0x7d91131acc 0x1acc
Java_com_xiaojianbang_ndk_NativeHelper_md5 libxiaojianbang.so 0x7d91131f2c 0x1f2c
*/
  • 动态函数hook RegisterNatives,RegisterNatives函数有4个参数,第0个是JNIEnv*,第1个参数表示注册的参数是哪个类的,第2个参数是JNINativeMethod结构体地址,第3个参数是需要注册的函数个数
var RegisterNativesAddr = null;
var _symbols = Process.findModuleByName("libart.so").enumerateSymbols();
for (var i = 0; i < _symbols.length; i++) {
    var _symbol = _symbols[i];
    if (_symbol.name.indexOf("CheckJNI") == -1 && _symbol.name.indexOf("RegisterNatives") != -1) {
        RegisterNativesAddr = _symbols[i].address;
    }
}
console.log(RegisterNativesAddr);
// 0x7da0a0a158


Interceptor.attach(RegisterNativesAddr, {
    onEnter: function (args) {
        var env = Java.vm.tryGetEnv();
        // 主动调用jni函数,通过第1个参数可以获取类名
        var className = env.getClassName(args[1]);
        // 通过第三个参数可以获取注册的函数个数
        var methodCount = args[3].toInt32();

        for (let i = 0; i < methodCount; i++) {
            // 函数名
            var methodName = args[2].add(Process.pointerSize * 3 * i)
.readPointer().readCString();
            // 函数签名
            var signature = args[2].add(Process.pointerSize * 3 * i)
.add(Process.pointerSize).readPointer().readCString();
            // 真实函数地址
            var fnPtr = args[2].add(Process.pointerSize * 3 * i)
.add(Process.pointerSize * 2).readPointer();
            // 通过函数地址获取对应so
            var module = Process.findModuleByAddress(fnPtr);
            console.log(className, methodName, signature, fnPtr, module.name, fnPtr.sub(module.base));
        }
    }, onLeave: function (retval) {
    }
});

7. JNItrace工具的使用

pip install jnitrace

运行

jnitrace -m attach -l libxxx.so {processname}
1 + 1 =
快来做第一个评论的人吧~