Bootstrap

基于frida的so-hook经验总结

日常笔记,水平有限,小白学习,大佬移步,有错误欢迎指出。

1.什么是静态注册 or 动态注册?

关于so文件当中的函数,分为导出函数未导出函数两种,导出函数打开IDA后能够在Exports导出表中找到的就是导出函数,通常是"Java_"开头的,未导出函数则在导出表中寻找不到。

一般来说静态编写的native函数都能在导出表中寻找到,而动态加载的则无法在导出表中找到。

静态注册的so函数命名格式是这样的:

Java_包名_类名_函数名

仔细观察下规律:

Java_com_rytong_hnair_HNASignature_getHNASignature

java层是这样的

在这里插入图片描述
ida-pro 导出表是这样的:

在这里插入图片描述

而动态注册的在导出表找不到对应的函数名,通常会看到JNI_OnLoad

在这里插入图片描述

2.咋看是Thumb指令 or Arm指令?

ida里依次找到Options ----> General

在这里插入图片描述

然后 Number of opcode bytes(non-graph) 设置为4 ,点击ok.

在这里插入图片描述
然后在IDA View中查看opcode 的长度, 如果出现 2 个字节和 4 个字节的, 说明为 thumb 指令集。

如果都是 4 个字节的, 说明是 arm 指令集。

在 Thumb 指令集下, inline hook 的需要进行偏移地址 +1 操作;

示例如图:

在这里插入图片描述

3.so函数地址的确认

这里分为两种情况

so基地址在/proc/<pid>/maps中可查看

情况1-导出函数: 导出函数地址 = so文件名 + 函数名

注: 函数名以汇编中出现的为准

let funcAddr = Module.findExportByName('libencryptlib.so', '_ZN7MD5_CTX11MakePassMD5EPhjS0_')
// 返回的是函数地址  第二个参数根据汇编中为准
console.log("so函数地址:", funcAddr);

// 通过Interceptor.attach来对函数进行hook
Interceptor.attach(funcAddr, {
  onEnter: function (args) {
    console.log('args[1]: ', hexdump(args[1])) // 打印参数的地址 通过hexdump打印16进制
    console.log(this.context.x1) // 打印寄存器内容
    console.log('args[2]: ', args[2].toInt32()) // 默认显示16进制,这里转为10进制
    this.args3 = args[3] // 将args[3]值保存到this上
  },
  onLeave: function (retval) {
    console.log('args[3]: ', hexdump(this.args3))
  },
})

情况2 - 非导出函数:非导出函数地址 = so基地址 + 函数偏移量

注意如果Thumb 指令, hook 的偏移地址需要进行 +1 操作

代码如下:

var libbili = Module.findBaseAddress("libbili.so");  # 获取so文件的基地址

var so_func = libbili.add(0x22B0 + 1); # 如果是Thumb指令下,执行 +1 操作
var so_func = libbili.add(0x22B0); # 如果是Arm指令下,直接操作即可,不再 +1 操作
console.log("so函数地址:", so_func);

总结一下就是:

安卓位数指令计算方式
32 位thumbso 基址 + 函数在 so 中的偏移 + 1
64 位armso 基址 + 函数在 so 中的偏移

可以看到libbili.so就是so文件的基地址,0x22B0 就是对应的偏移地址,而这个0x22B0在so文件对应的写法是sub_22B0,并且不区分大小写,也就是说0x22B0或者0x22b0都可以。

4.枚举导入表

let improts = Module.enumerateImports('libencryptlib.so')
for (let iterator of improts) {
  console.log(JSON.stringify(iterator))
  // {"type":"function","name":"__cxa_atexit","module":"/apex/com.android.runtime/lib64/bionic/libc.so","address":"0x778957bd34"}
}

5.枚举导出表

const exports = Module.enumerateExports('libencryptlib.so')
for (const iterator of exports) {
  console.log(JSON.stringify(iterator))
  // {"type":"letiable","name":"_ZTSx","address":"0x74d594b1c0"}
}

6.枚举符号表

// 列举 libart.so 中的所有导出函数(成员列表)
let symbols = Module.enumerateSymbolsSync("libart.so");
for (let i = 0; i < symbols.length; i++) {
     let symbol = symbols[i];
     console.log(symbol.name)

7.枚举进程中已加载的模块

const modules = Process.enumerateModules()
console.log(JSON.stringify(modules[0].enumerateExports()[0]))

8.常见的参数方法

// args[0] 是一个内存地址
hexdump(args[0]) // 打印参数的所在内存区域的字节数据
args[0].readCString() // 读取参数所对应的C字符串 (前提: 参数是一个可见字符串)
args[0].readPointer() // 用读地址方式去读取参数所对应的值 (如果参数是一个指针的话可能就需要使用)

9.内存读写

// 1. 读取指定地址的字符串
let baseAddr = Module.findBaseAddress('libxiaojianbang.so')
console.log(baseAddr.add(0x2c00).readCString())

// 2. dump指定地址的内存
console.log(hexdump(baseAddr.add(0x2c00)))

// 3. 读指定地址的内存
console.log(baseAddr.add(0x2c00).readByteArray(16))
console.log(Memory.readByteArray(baseAddr.add(0x2c00), 16)) //原先的API

// 4. 写指定地址的内存
baseAddr.add(0x2c00).writeByteArray(stringToBytes('xiaojianbang'))
console.log(hexdump(baseAddr.add(0x2c00)))

// 5. 申请新内存写入
Memory.alloc()
Memory.allocUtf8String()

// 6. 修改内存权限
Memory.protect(ptr(libso.base), libso.size, 'rwx')

10.确认 native 函数在哪个 so

静态分析查看静态代码块中加载的 so,但并不靠谱,因为 native 函数声明在一个类中,so 加载可以在其他的类中此外还可以在另外的类中,一次性加载所有的 so

hook 系统函数来得到绑定的 native 函数地址,然后再得到 so 地址

注册方式hook 点
jni 函数动态注册hook RegisterNatives
jni 函数静态注册hook dlsym

hook RegisterNatives(动态注册)

直接上yang神的hook 脚本(地址:https://github.com/lasting-yang/frida_hook_libart )来打印注册的native函数和so文件。

代码down下来,再稍微修改下:

function find_RegisterNatives(params) {
    // 列举 libart.so 中的所有导出函数(成员列表)
    let addrRegisterNatives = null;
    let symbols = Module.enumerateSymbolsSync("libart.so");
    for (let i = 0; i < symbols.length; i++) {
        let 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) {
            // 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives)
        }
    }

}
function hook_RegisterNatives(addrRegisterNatives) {

    if (addrRegisterNatives != null) {
        // RegisterNatives(env, 类型, Java和C的对应关系,个数)
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                // console.log("[RegisterNatives] method_count:", args[3]);
                let env = args[0];        // jni对象
                let java_class = args[1]; // 类
                let class_name = Java.vm.tryGetEnv().getClassName(java_class);
                console.log("java-类名:", class_name)
                let taget_class = "com.bilibili.nativelibrary.LibBili";

                if (class_name === taget_class) {
                    //只找我们自己想要类中的动态注册关系
                    console.log("\n[RegisterNatives] method_count:", parseInt(args[3]));
                    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);
                        //console.log("name:", name, "name:", sig, "module_name:", find_module.name, "offset:", offset);
                        console.log("java函数名:", name + ",", "参数和返回值:", sig + ",", "偏移地址offset:", offset);

                    }
                }
            }
        });
    }
}

setImmediate(find_RegisterNatives);

然后输入命令启动:

frida -U --no-pause -f package_name -l hook_RegisterNatives.js

hook_dlsym(静态注册)

function hook_dlsym() {
  let dlsymAddr = Module.findExportByName('libdl.so', 'dlsym')
  console.log(dlsymAddr)
  Interceptor.attach(dlsymAddr, {
    onEnter: function (args) {
      this.args1 = args[1]
    },
    onLeave: function (retval) {
      let module = Process.findModuleByAddress(retval)
      if (module == null) return
      console.log(this.args1.readCString(), module.name, retval, retval.sub(module.base))
    },
  })
}

1.so源码实例

#include <string.h>
#include <jni.h>
#include"test.h"

jstring JNICALL Java_com_example_mi_demoso_JNITest_getStringFromJNI(JNIEnv* env, jobject jo)
{
    char str[] = "x HelloWorld from JNI12345!";
    int c = test_add(97,1);
    str[0] = (char)c;
    return (*env)->NewStringUTF(env, str);
}

int test_add(int a,int b){
    return a+b;
}

这里将对test_add函数进行hook

2.寻找要hook函数的偏移地址

将要hook的so(libjnitest.so)拖进ida
在这里插入图片描述
上图可以看到:在导出函数窗口可以直接看到函数"test_add"偏移地址函数名(这里可以通过函数名或者地址进行hook),非导出函数只能通过地址hook;这里我们用地址hook的方法(图中表明hook的函数偏移为0x00000680函数地址 = so基地址 + 函数偏移
要注意:如果Thumb 指令, hook 的偏移地址需要进行 +1 操作, )

**!!

在这里插入图片描述

3.编写frida脚本

import frida
import sys

jscode = """
Java.perform(function(){
    var str_name_so = "libjnitest.so";    //需要hook的so名
    var n_addr_func_offset = 0x00000680;         //需要hook的函数的偏移
    var n_addr_so = Module.findBaseAddress(str_name_so); //加载到内存后 函数地址 = so地址 + 函数偏移
    var n_addr_func = parseInt(n_addr_so, 16) + n_addr_func_offset;
    var ptr_func = new NativePointer(n_addr_func);
    //var ptr_func = Module.findExportByName("libjnitest.so","test_add") //对函数名hook

    Interceptor.attach(ptr_func,{ 
        //onEnter: 进入该函数前要执行的代码,其中args是传入的参数,一般so层函数第一个参数都是JniEnv,第二个参数是jclass,从第三个参数开始是我们java层传入的参数
        onEnter: function(args) {
            send("Hook start");
            send("args[2]=" + args[2]); //第一个传入的参数
            send("args[3]=" + args[3]); //第二个参数
        },
        onLeave: function(retval){ //onLeave: 该函数执行结束要执行的代码,其中retval参数即是返回值
            send("return:"+retval); //返回值
            retval.replace(100); //替换返回值为100
        }
    });
});
"""
def printMessage(message,data):
    if message['type'] == 'send':
        print('[*] {0}'.format(message['payload']))
    else:
        print(message)

process = frida.get_remote_device().attach('com.example.testso') #进程名
script = process.create_script(jscode)
script.on('message',printMessage)
script.load()
sys.stdin.read()

4.效果验证

启动frida-server后进行,启动端口转发

adb forward tcp:27043 tcp:27043
adb forward tcp:27042 tcp:27042

然后执行python脚本(frida脚本),接着启动应用:
在这里插入图片描述
符合retval.replace(100); //替换返回值为100预期,即小写x处显示为d,即100.

参考:
https://www.cnblogs.com/aWxvdmVseXc0/p/12463319.html
https://refate.github.io/2019/01/13/rida_install/

;