3.frida Native层Hook

测试用例

本次的hook代码都用 frida-tools方式 书写。首先写一个简单的程序用来测试。后续的测试就在这个程序上小修小改,不做赘述。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_print"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="print()" />

    <Button
        android:id="@+id/btn_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="addThreeNum()" />

</LinearLayout>
package com.zyc.fridasodemo;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button btnPrint;
    private Button btnAddThreeNum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnPrint = findViewById(R.id.btn_print);
        btnPrint.setOnClickListener(this);
        btnAddThreeNum = findViewById(R.id.btn_add);
        btnAddThreeNum.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_print:
                String print = Calc.print();
                Toast.makeText(this, print, Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_add:
                int add = Calc.addThreeNum(1, 2, 3);
                Toast.makeText(this, String.valueOf(add), Toast.LENGTH_SHORT).show();
                break;
        }
    }
}
package com.zyc.fridasodemo;

public class Calc {
    static {
        System.loadLibrary("native-lib");
    }

    public native static String print();

    public native static int addThreeNum(int a, int b, int c);
}
#include <jni.h>
#include <string>

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Tag", __VA_ARGS__)

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zyc_fridasodemo_Calc_print(JNIEnv *env, jclass clazz) {
    const char *str = "Hello c++";
    return env->NewStringUTF(str);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_zyc_fridasodemo_Calc_addThreeNum(JNIEnv *env, jclass clazz, jint a, jint b, jint c) {
    return a + b + c;
}

运行:
在这里插入图片描述
 

遍历导入/导出函数

先通过ida查看so的导入函数:
在这里插入图片描述
导出函数:
在这里插入图片描述
hook so的导入/导出函数需要分别用到 Module.enumerateImports()Module.enumerateExports() 两个函数。

function hookImEx() {
    console.log("以下是导入函数:");
    var importMethods = Module.enumerateImports("libnative-lib.so");
    for (let index = 0; index < importMethods.length; index++) {
        const element = importMethods[index];
        console.log(JSON.stringify(element));
    }

    console.log("以下是导出函数:");
    var exportMethods = Module.enumerateExports("libnative-lib.so");
    for (let index = 0; index < exportMethods.length; index++) {
        const element = exportMethods[index];
        console.log(JSON.stringify(element));
    }
}

待so加载好后运行,可以从下列对象的name、address取到函数名和地址:
在这里插入图片描述
 

通过函数名Hook导出函数

Hook so中函数,必须先获取其地址,可以用 Module.findExportByName() 方法传入指定函数名获取地址。

function hookByName() {
    const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
    if (address) {
        Interceptor.attach(address, {
            onEnter(args) {
                console.log("onEnter...");
                console.log("args[0]:",args[0]);
                console.log("args[1]:",args[1]);
                console.log("args[2]:",args[2]);
                console.log("args[3]:",args[3]);
                console.log("args[4]:",args[4]);
            },
            onLeave(retval) { 
                console.log("onLeave...");
                console.log("retval:",retval); // 函数返回值
            }
        });
    }
}

待so加载好后运行,其中arg[0]为 JNIEnv * 地址,arg[1]为 jclass地址(如果是非静态函数,则是jobject)。后面为三个int,使用 .toInt32 可以转为10进制。
在这里插入图片描述
 

Hook参数

根据上面获取的 args 我们可以修改参数,但注意参数得是 NativePointer,直接用 “=” 赋值会报错“expected a pointer”。

function hookParam(){
    const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
    if (address) {
        Interceptor.attach(address, {
            onEnter(args) {
                args[2] = ptr(1000); // 写成 args[2]=1000 会报错
            },
            onLeave(retval) {}
        });
    }
}

运行,点击可以看到返回值已经改变:
在这里插入图片描述
 

Hook返回值

修改返回值时,用 “=” 并不会报错,但这样的修改不会影响程序变量,如:

onLeave(retval) {
    retval = 888;
    console.log("retval:",retval);
}

效果:
在这里插入图片描述
正确写法需要使用 replace() 函数:

function hookReturn(){
    const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
    if (address) {
        Interceptor.attach(address, {
            onEnter(args) {},
            onLeave(retval) {
                retval.replace(32); //用 replace() 不要用 =
                console.log("retval:",retval);
                
                // 如果返回值是jstring,得用下面方式替换
               //var env = Java.vm.getEnv(); //获取env对象,即第一个参数
               //var jstrings = env.newStringUtf("xxxx"); //返回的是字符串指针,使用jni函数构造一个newStringUtf对象用来代替这个指针
               //retval.replace(jstrings); 
            }
        });
    }
}

运行:
在这里插入图片描述
 

Hook引用传递返回值

引用传递是C++常见函数写法,这样是没有返回值的。

//char* str = "hello"; //这样写会报错,str指向静态存储区不允许修改
char str[] = "hello";

void change(char* str){
    cs[0] = '1';
    cs[1] = '2';
    cs[2] = '3';
}

只要Hook onEnter()onLeave() 时的指针(参数),就能知道函数的作用:

function hookPoint() {
    const address = Module.findExportByName("libnative-lib.so", "_Z6changePc");
    if (address) {
        console.log("\r\n函数地址:" + address);
        Interceptor.attach(address, {
            onEnter(args) {
                console.log("onEnter...");
                console.log("引用参数:" + args[0].readCString()); //打印cstring
                console.log("参数处内存:\r\n" + hexdump(args[0]));
                this.args0 = args[0]; //保存参数,给onLeave()中使用
            },
            onLeave(retval) {
                console.log("onLeave...");
                console.log("引用参数:" + this.args0.readCString());
                console.log("参数处内存:\r\n" + hexdump(this.args0));
            }
        });
    }
}

运行:
在这里插入图片描述
 

获取so基址

使用 findBaseAddress() 可以hook到so的基址:

function hookBaseAddress() {
    const address = Module.findBaseAddress("libnative-lib.so");
    if (address) {
        console.log(address); //这里我获取到的是 0xb8f91000
    }
}

使用 cat /proc/(进程pid)/maps 命令可以验证libnative-lib.so基址。
在这里插入图片描述
 

Hook未导出函数

如果遇到so中动态注册函数的情况,又该如何Hook?在测试用例中增加一个动态注册的printDynamic()。

// public native static String printDynamic(String a); //com.zyc.fridasodemo.Calc类增加此方法

// static + JNI_OnLoad动态注册,ida看不到导出
static jstring print_Dynamic(JNIEnv *env, jclass clazz, jstring a){
    return a;
}

static JNINativeMethod methods[] = {
        {"printDynamic","(Ljava/lang/String;)Ljava/lang/String;",(void*)print_Dynamic},
};

static int registerNatives(JNIEnv *env) {
    //找到声明native方法的类
    const char* className  = "com/zyc/fridasodemo/Calc";
    jclass clazz = env->FindClass(className);
    if(clazz == NULL){
        return JNI_FALSE;
    }

    //注册函数 参数:java类 所要注册的函数数组 注册函数的个数
    int methodsNum = sizeof(methods)/ sizeof(methods[0]);
    if(env->RegisterNatives(clazz,methods,methodsNum) < 0){
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
    //获取JNIEnv
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    assert(env != NULL);

    if(!registerNatives(env)){
        return -1;
    }
    //返回jni 的版本
    return JNI_VERSION_1_6;
}

打release包后用ida检查so的导出函数发现并没有get_two_num()。
在这里插入图片描述
通过 JNI_OnLoad() 找到其地址:
在这里插入图片描述
然后就可以用 基址+偏移 方式进行Hook了:

function hookDynamic() {
    const soAddress = Module.findBaseAddress("libnative-lib.so");
    console.log("so基址:" + soAddress);
    if (soAddress) {
        const methodAddress = soAddress.add(0x0690); // thrumb指令集要+1
        if (methodAddress) {
            console.log("函数地址:" + methodAddress);
            Interceptor.attach(methodAddress, {
                onEnter(args) {
                    console.log("hook动态注册函数的参数:" + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()); //打印jstring要用getStringUtfChars
                },
                onLeave(retval) {
                    console.log("hook动态注册函数的返回值:" + Java.vm.getEnv().getStringUtfChars(retval, null).readCString());
                }
            });
        }
    }
}

运行成功:
在这里插入图片描述
 

Hook so加载(dlopen)

当遇到“等so加载完后进行某Hook操作时”,可以Hook dlopen(有的是android_dlopen_ext)方法来确定so的加载。PS:不推荐Hook System.loadLibrary(),因为加载so的方式并不止这一种,而且最终都会调用 dlopen()

function hookDlopen() {
    let dlopenAddr = Module.findExportByName(null, "dlopen");
    if (dlopenAddr) {
        Interceptor.attach(dlopenAddr, {
            onEnter(args) { // dlopen(const char* filename, int flags)
                let soName = args[0].readCString(); // "/data/app/com.zyc.fridasodemo-1/lib/x86/libnative-lib.so"
                if (soName.indexOf("libnative-lib.so") != -1) {
                    console.log("成功加载到了-->" + soName);
                    this.hasloaded = true;
                }
            },
            onLeave(retval) { //onLeave()中才是dlopen()加载完成后
                if (this.hasloaded) {
                    hooImEx(); // 等so加载完成就执行导入/导出函数打印
                }
            }
        });
    }
}

运行,点击按钮导致so加载,Hook到加载触发导入/导出函数打印:
在这里插入图片描述
 

读写内存

通过内存地址我们可以直接操作内存数据,以下面字符串为例,这是用例中print()返回的字符串:
在这里插入图片描述
直接Hook操作这段内存修改:

function hookMem() {
    const soAddress = Module.findBaseAddress("libnative-lib.so");
    console.log("so基址:" + soAddress);
    if (soAddress) {
        console.log("读取...");
        const strAddress = soAddress.add(0x0D40);
        console.log("打印这段字符串:" + strAddress.readCString());
        console.log("读16字节:");
        console.log(strAddress.readByteArray(16));

        console.log("写入...");
        Memory.protect(strAddress, Process.pageSize, "rw-"); //修改内存页属性后再写入,不然可能报access violation accessing
        strAddress.writeUtf8String("123") //内存写入字符串,该方法末尾会自动添加'\0'
        console.log("\r\n打印这段字符串:" + strAddress.readCString());
        console.log("读16字节:\r\n");
        console.log(strAddress.readByteArray(16));
    }
}

运行:
在这里插入图片描述
也可以按字节修改:

strAddress.writeByteArray([0x41,0x41,0x41]);

运行:
在这里插入图片描述
 

Hook JNI函数

很多时候我们需要通过hook JNI函数来达到目的,比如Hook RegisterNatives()拿到动态注册的函数,或是NewStringUTF()查看加解密字符串的构建等,实现JNI的hook可以使用下面两种方法:

  • 偏移计算:拿到JNIEnv结构体的地址,加上函数在结构体中的偏移即可。
  • libart.so:从libart.so中遍历出要找到的JNINativeMethod。

通过偏移计算

首先把jni.h中JNINativeInterface结构体声明格式化成单行形式(点此下载),如果要hook NewStringUTF(),从文件中找到其在168行 :
在这里插入图片描述
则其偏移地址为 env指向地址 + (168-1) x 指针大小 ,以此写出:

function hookJni() {
    Java.perform(function () { //这样才能取到env,否则为null
        const envAddr = ptr(Java.vm.tryGetEnv().handle);
        console.log("env地址:" + envAddr);
        const envPointAddr = envAddr.readPointer();
        console.log("env指向地址:" + envPointAddr);

        const envPointAddr168 = envPointAddr.add((168 - 1) * Process.pointerSize);
        console.log("env偏移168地址:" + envPointAddr168);
        const newStringUtfAddr = envPointAddr168.readPointer();
        console.log("jni函数newStringUtf地址:" + newStringUtfAddr);

        Interceptor.attach(newStringUtfAddr, {
            onEnter(args) {
                console.log("附加到newStringUtf函数...");

                let arg1 = args[1].readCString();
                console.log("NewStringUTF的args[1]:" + arg1);
            },
            onLeave(retval) {
                console.log("附加到newStringUtf函数返回...");

                let ret = Java.vm.getEnv().getStringUtfChars(retval, null).readCString();
                console.log("返回值:" + ret);
            }
        });
    });
}

效果:
在这里插入图片描述

通过libart.so

使用 Module.enumerateSymbols() 遍历JNI 函数,假设我们要hook RegisterNatives():

/*
* typedef struct {
*    const char* name;
*     const char* signature;
*     void*       fnPtr;
* } JNINativeMethod;
*/

function hookJniByArtSym() {
    var artSymbol = Module.enumerateSymbols("libart.so");
    if (artSymbol) {
        for (let i = 0; i < artSymbol.length; i++) {
            const element = artSymbol[i];
            if (element.name.indexOf("RegisterNatives") != -1) { //函数名有干扰字符,所以用indexOf而不是==,同时要排除CheckJNI函数
                console.log("拿到了函数:" + element.name);
                console.log("函数地址:" + element.address);

                Interceptor.attach(element.address, {
                    onEnter(args) {
                        console.log("附加到RegisterNatives()...");

                        let JNINativeMethod = args[2]; // jstring print_Dynamic(JNIEnv *env, jclass clazz, jstring a)
                        console.log("动态注册的函数名称:" + JNINativeMethod.readPointer().readCString());
                        console.log("动态注册的函数签名:" + JNINativeMethod.add(Process.pointerSize).readPointer().readCString());
                        console.log("动态注册的函数地址:" + JNINativeMethod.add(Process.pointerSize*2).readPointer().readCString());
                    },
                    onLeave(retval) { }
                });
            }
        }
    }
}

运行发现 Module.enumerateSymbols() 一直获取不到值,查看官网才知道:
在这里插入图片描述
换个环境来运行就OK了:
在这里插入图片描述
 

主动调用

主动调用Native函数需要用到 NativeFunction(address, returnType, argTypes[, abi]) ,其中returnType类型如下:

void
pointer
int
uint
long
ulong
char
uchar
size_t
ssize_t
float
double
int8
uint8
int16
uint16
int32
uint32
int64
uint64
bool

写一个测试函数方便frida调用:

jstring go(JNIEnv *env,jstring a){
    const char *str1 = "Hello c++ ";
    const char *str2 = env->GetStringUTFChars(a,0);
    const char * ret = (std::string(str1) + std::string(str2)).c_str();
    LOGE("go %s",ret);
    return env->NewStringUTF(ret);
}

通过函数地址和签名构建 NativeFunction

function hookForward() {
    Java.perform(function () {
        const soAddr = Module.findBaseAddress("libnative-lib.so");
        const methodAddr = soAddr.add(0x9540); //ida静态分析查看到go()偏移0x9540
        if (methodAddr) {
            const fun = new NativeFunction(methodAddr, "pointer", ["pointer", "pointer"]); //jstring , env* 都是pointer
            const env = Java.vm.tryGetEnv();
            let jstr = env.newStringUtf("zyc zyc");
            let ret = fun(env, jstr);
            console.log("返回值:", env.getStringUtfChars(ret, null).readCString());
        }
    });
}

运行:
在这里插入图片描述
 

写入文件

写入文件其实就是主动调用 libc.so 中文件操作相关函数(记得留意apk的读写权限):

FILE *fopen(const char *filename, const char *mode);
int fputs(const char *str, FILE *stream);
int fclose(FILE *stream)

Hook代码:

function hookWriteFile() {
    //拿到文件相关函数地址
    const addr_fopen = Module.findExportByName("libc.so", "fopen");
    const addr_fputs = Module.findExportByName("libc.so", "fputs");
    const addr_fclose = Module.findExportByName("libc.so", "fclose");
    if (addr_fopen && addr_fputs && addr_fclose) {
        //通过地址构建函数
        const fopen = new NativeFunction(addr_fopen, "pointer", ["pointer", "pointer"]);
        const fputs = new NativeFunction(addr_fputs, "int", ["pointer", "pointer"]);
        const fclose = new NativeFunction(addr_fclose, "int", ["pointer"]);

        //打开文件
        console.log("打开文件...");
        let filename = Memory.allocUtf8String("/data/local/tmp/zyc.txt");
        let open_mode = Memory.allocUtf8String("w");
        let file = fopen(filename, open_mode);

        //写入内容
        console.log("写入内容...");
        let content = Memory.allocUtf8String("zyc zyc\n");
        let retval = fputs(content, file);

        //关闭文件
        console.log("关闭文件...");
        fclose(file);
    }
}

运行:
在这里插入图片描述
 

打印堆栈

Native中增加几个函数:

void c() {}
void b() { c(); }
void a() { b(); }

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zyc_fridasodemo_Calc_print(JNIEnv *env, jclass clazz) {
    a();
    ...
}

onEnter()使用 Thread.backtrace() 获取堆栈信息:

function hookBacktrace() {
    const address = Module.findExportByName("libnative-lib.so", "_Z1cv"); //内存中 c() -- _Z1cv()
    if (address) {
        Interceptor.attach(address, {
            onEnter(args) { 
                console.log('c() called from:\n' + 
                Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress).join('\n') + '\n'); //map与join用于格式化
            },
            onLeave(retval) {}
        });
    }
}

运行,hook后点击print():
在这里插入图片描述
 

参考资料

frida框架hook常用字符串模板总结
Android so加载深入分析

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你好!Frida是一个强大的动态代码注入和调试工具,可以用于hook和修改应用程序的行为。如果你想要hook Native代码,可以使用Frida的JavaScript API来实现。 首先,你需要在设备上安装Frida,并确保设备已经越狱(iOS)或者已经root(Android)。 接下来,你需要编写一个JavaScript脚本来进行hook。在脚本中,你可以使用Frida提供的一些函数来定位和修改Native函数。 例如,下面的代码可以用来hook一个Native函数并修改它的行为: ```javascript // 导入Frida模块 const frida = require('frida'); // 目标进程的名称 const targetProcessName = 'your_target_process_name'; // 要hook的函数名称 const targetFunctionName = 'your_target_function_name'; // Frida attach到目标进程 frida.attach(targetProcessName) .then(session => { // 创建一个脚本对象 const script = session.createScript(` // 找到目标函数 const targetFunction = Module.findExportByName(null, "${targetFunctionName}"); // 替换目标函数的实现 Interceptor.replace(targetFunction, new NativeCallback(() => { // 修改函数的行为,这里可以写你想要的逻辑 console.log("Function ${targetFunctionName} hooked!"); // 调用原始函数 const originalFunction = new NativeFunction(targetFunction, 'void', []); originalFunction.call(); // 可以在这里添加你的自定义代码 }, 'void', [])); }); // 加载并运行脚本 script.load() .then(() => { console.log("Script loaded successfully!"); }) .catch(error => { console.log(`Script error: ${error}`); }); }) .catch(error => { console.log(`Attach error: ${error}`); }); ``` 在上面的代码中,我们首先导入了Frida模块,然后指定了目标进程的名称和要hook的函数名称。然后我们使用`frida.attach()`函数来连接到目标进程,并创建一个脚本对象。在脚本中,我们使用`Module.findExportByName()`函数来找到目标函数,然后使用`Interceptor.replace()`函数替换目标函数的实现。在替换的实现中,我们可以添加一些自定义的逻辑以修改函数的行为。 最后,我们使用`script.load()`函数加载并运行脚本。如果一切顺利,你应该能看到"Script loaded successfully!"的输出。 这只是一个简单的例子,你可以根据你的需求进行更复杂的hook和修改。希望对你有所帮助!如果有任何问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值