0x00 前言

一直都知道Frida是一个非常强大的Hook框架,而且其Hook对象不仅限于安卓,连Windows应用的Hook他都可以搞定

然而受限于对JavaScript的不熟悉,一直拖着没有系统学习

借着《吾爱破解安卓逆向入门教程》提供的模板和介绍、对JS语言的相关语法的查阅以及利用其它框架的经验

还是系统地学学Frida比较好

参考:

《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)

《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)

《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)

《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)

0x01 Frida基础知识

Frida是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java层和NativeHook操作。

Frida的安装非常简单,不指定版本的话用pip就行(我的版本为16.4.3)

1
pip install frida-tools

之后再把Frida-server(需要与Frida版本对应)给push手机上(别用模拟器)

1
adb push frida-s /data/local/tmp/

之后启动手机上的Frida-server就好,这边提供两个脚本

一个快捷打开frida-sever

bat文件

1
2
3
@REM runfs.bat文件内容
adb shell < runfs.sh
pause

对应的sh文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# runfs.sh
su
cd /data/local/tmp

# ps -A:显示所有进程
# grep "frida-s":获取名为"frida-s"的进程名
# grep -v grep:过滤掉含有‘grep’字段的条目
# awk '{print $2}':awk是行处理器。按行,以空格分段的字符串,打印第二列信息;
pid=`ps -A | grep "frida-s" | grep -v grep | awk '{print $2}'`
# $(expression):获取表达式返回值,这里获取的是frida-s的PID
# -z:检查字符串变量的长度是否为 0
if [ -z "$pid" ]
then
echo "not running, starting"
./frida-s & # 加-l参数可以指定端口
ps -A | grep "frida-s" | grep -v grep
echo "started!"
else
echo "already runing!"
ps -A | grep "frida-s" | grep -v grep
fi

一个快捷杀死Frida-Server进程

1
2
3
@REM stopfs.bat文件内容
adb shell < stopfs.sh
pause

对应的sh文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# stopfs.sh
su
cd /data/local/tmp

# ps -A:显示所有进程
# grep "frida-s":获取名为"frida-s"的进程名
# grep -v grep:过滤掉含有‘grep’字段的条目
# awk '{print $2}':awk是行处理器。按行,以空格分段的字符串,打印第二列信息;
pid=`ps -A | grep "frida-s" | grep -v grep | awk '{print $2}'`
# $(expression):获取表达式返回值,这里获取的是frida-s的PID
# -z:检查字符串变量的长度是否为 0
if [ -z "$pid" ]
then
echo "not running, no need to do anything"
else
kill -9 $pid
echo "kill success!"
fi

可以通过

1
frida-ps -U

测试是否打开是否成功

之后就可以愉快使用Frida了

0x02 Frida操作模式

操作模式 描述 优点 主要用途
CLI(命令行)模式 通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 便于直接注入和操作 在较小规模的操作或者需求比较简单的场景中使用
RPC模式 使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 在大规模调用中更加普遍,特别是对于复杂数据处理的需求

0x03 Frida注入模式与启动命令

注入模式 描述 命令或参数 优点 主要用途
Spawn模式 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App 在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App 适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 当需要监控App从启动开始的所有行为时使用
Attach模式 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App 适合于已经运行的App,不会重新启动App,对用户体验影响较小 在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用

Spawn模式

1
frida -U -f 进程名 -l hook.js

Attach模式

1
frida -U 进程名 -l hook.js

0x04 Frida基础语法

API名称 描述
Java.use(className) 获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback) 确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

0x05 日志输出方法

日志方法 描述 区别
console.log() 使用JavaScript直接进行日志打印 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send() Frida的专有方法,用于发送数据或日志到外部Python脚本 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。

0x06 Hook模板

1
2
3
4
5
6
function main(){
Java.perform(function(){
hook();
});
}
setImmediate(main);

0x07 Frida在Java层

1.Hook普通方法、打印参数和修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义一个名为hook的函数
function hook(){
//获取一个名为"包名.类名"的Java类,并将其实例赋值给JavaScript变量'utils'
var utils = Java.use("包名.类名");
//修改"包名.类名"的"method"方法的实现(若方法名为'q'则下面所有'method'字串应为'q')。这个新的实现会接收两个参数(a和b)
//当然也可以用utils['method'].implementation这种方法
utils.method.implementation = function(a, b){
//将参数a和b的值改为123和456。
a = 123;
b = 456;
//调用原先的"method"方法,并将返回值存储在'retval'变量中
var retval = this.method(a, b);
//在控制台上打印参数a,b的值以及"method"方法的返回值
console.log(a, b, retval);
//返回"method"方法的返回值
return retval;
}
}

2.Hook重载函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// .overload()
// .overload('自定义参数')
// .overload('int')
function hook(){
var utils = Java.use("包名.类名");
//overload定义重载函数,根据函数的参数类型填,注意后面的function内的参数数量要对应上
//参数类型可以在smail代码里面找,也可以直接不定义任何参数然后在错误提示里面找
utils.Inner.overload('参数1','参数2').implementation = function(a,b){
b = "aaaaaaaaaa";
this.Inner(a,b);
console.log(b);
}
}

3.Hook构造函数

1
2
3
4
5
6
7
8
9
10
function hook(){
var utils = Java.use("包名.类名");
// 修改类的构造函数的实现,$init表示构造函数
// 只要有参数都要加上overload()
utils.$init.overload('参数').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}

4.Hook字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hook(){
Java.perform(function(){
//静态字段的修改
var utils = Java.use("包名.类名");
//修改类的静态字段"staticField"的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);

//非静态字段的修改
//使用`Java.choose()`枚举类的所有实例
Java.choose("包名.类名", {
// obj是一个类的实例的对象
onMatch: function(obj){
// 修改非静态字段"privateInt"的值为9999。
obj._privateInt.value = 9999; //字段名与函数名相同时,前面需要加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){

}
});
});
}

5.Hook内部类

1
2
3
4
5
6
7
8
9
10
function hook(){
Java.perform(function(){
//内部类
var innerClass = Java.use("包名.类名$内部类名");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}
});
}

6.枚举所有的类与类的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook(){
Java.perform(function(){
//枚举所有的类与类的所有方法,异步枚举
Java.enumerateLoadedClasses({
onMatch: function(name,handle){
//过滤类名
if(name.indexOf("包名.类名") !=-1){
console.log(name);
var clazz =Java.use(name);
console.log(clazz);
var methods = clazz.class.getDeclaredMethods();
console.log(methods);
}
},
onComplete: function(){}
})
})
}

7.枚举所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook(){
Java.perform(function(){
var Demo = Java.use("包名.类名");
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
}

8.主动调用

静态方法

1
2
3
4
5
6
function hook(){
Java.perform(function(){
var ClassName = Java.use("包名.类名");
var ret = ClassName.方法名("参数");
})
}

非静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook(){
Java.perform(function () {
var ret = null;
Java.choose("包名.类名",{ //要hook的类
onMatch:function(instance){
ret=instance.方法名("参数"); //要hook的方法
},
onComplete:function(){
console.log("result: " + ret);
}
});
})
}

0x08 Frida在Native层

在Native层

对于静态注册的函数,可以通过在导出表中搜索关键字java来找到(完整的是Java_包名_类名_方法名)

而动态注册的,则需要结合jadx找到函数名然后直接搜索函数名

需要注意的是,静态注册的函数既可以通过Java层hook也可以通过Native层hook,但是动态注册的只能在Native层hook

在IDA分析时,由于Native层的特性,函数的第一个参数一般为JNIEnv *env,分析时改改类型可能可以直接识别出一些函数。

其他类型参考jadx传值和下表对照进行修改

C++ 数据类型 Java 数据类型 JNI 数据类型签名
jint int “I”
jboolean boolean “Z”
jbyte byte “B”
jchar char “C”
jshort short “S”
jlong long “J”
jfloat float “F”
jdouble double “D”
jobject Object “Ljava/lang/Object;”
jstring String “Ljava/lang/String;”
jarray Array “[elementType”
jobjectArray Object[] “[Ljava/lang/Object;”
jbooleanArray boolean[] “[Z”
jbyteArray byte[] “[B”
jcharArray char[] “[C”
jshortArray short[] “[S”
jintArray int[] “[I”
jlongArray long[] “[J”
jfloatArray float[] “[F”
jdoubleArray double[] “[D”

1.Process、Module和Memory

Process 对象代表当前被Hook的进程,能获取进程的信息,枚举模块,枚举范围等

API 含义
Process.id 返回附加目标进程的 PID
Process.isDebuggerAttached() 检测当前是否对目标程序已经附加
Process.enumerateModules() 枚举当前加载的模块,返回模块对象的数组
Process.enumerateThreads() 枚举当前所有的线程,返回包含 id, state, context 等属性的对象数组

Module 对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件),能查询模块的信息,如模块的基址、名称、导入/导出的函数等

API 含义
Module.load() 加载指定so文件,返回一个Module对象
enumerateImports() 枚举所有Import库函数,返回Module数组对象
enumerateExports() 枚举所有Export库函数,返回Module数组对象
enumerateSymbols() 枚举所有Symbol库函数,返回Module数组对象
Module.findExportByName(exportName)、Module.getExportByName(exportName) 寻找指定so中export库中的函数地址
Module.findBaseAddress(name)、Module.getBaseAddress(name) 返回so的基地址

Memory是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等

方法 功能
Memory.copy() 复制内存
Memory.scan() 搜索内存中特定模式的数据
Memory.scanSync() 同上,但返回多个匹配的数据
Memory.alloc() 在目标进程的堆上申请指定大小的内存,返回一个NativePointer
Memory.writeByteArray() 将字节数组写入一个指定内存
Memory.readByteArray 读取内存

2.so基址的获取方式

1
2
3
4
5
6
7
8
9
10
function hook(){
Java.perform(function(){
var moduleAddr1 = Process.findModuleByName("so库名").base;
var moduleAddr2 = Process.getModuleByName("so库名").base;
var moduleAddr3 = Module.findBaseAddress("so库名");
console.log(moduleAddr1)
console.log(moduleAddr2)
console.log(moduleAddr3)
})
}

3.枚举导入导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook(){
Java.perform(function(){
//打印导入表
var imports = Module.enumerateImports("so库名");
for(var i =0; i < imports.length;i++){
if(imports[i].name == "导入函数名"){
console.log(JSON.stringify(imports[i])); //通过JSON.stringify打印object数据
console.log(imports[i].address);
}
}
//打印导出表
var exports = Module.enumerateExports("so库名");
for(var i =0; i < exports.length;i++){
console.log(JSON.stringify(exports[i]));
}

})
}

4.Native函数的基础Hook打印

(1)整数型、布尔值类型、char类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function hook(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("so库名","函数名(IDA中显示的函数)");
console.log(helloAddr);
if(helloAddr != null){
//Interceptor.attach是Frida里的一个拦截器
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
console.log(args[0]); //打印第一个参数的值
console.log(this.context.x1); // 打印寄存器内容
console.log(args[1].toInt32()); //toInt32()转十进制
console.log(args[2].readCString()); //读取字符串 char类型
console.log(hexdump(args[2])); //内存dump

},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
console.log(retval);
console.log("retval",retval.toInt32());
retval.replace(1); //修改返回值
}
})
}
})
}

(2)字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function hook(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("so库名","函数名");
if(helloAddr != null){
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
// 方法一
var jString = Java.cast(args[2], Java.use('java.lang.String'));
console.log("参数:", jString.toString());
// 方法二
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
}
})
}
})
}

5.Native函数的基础Hook修改

(1)整数型修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("so库名","函数名");
console.log(helloAddr);
if(helloAddr != null){
Interceptor.attach(helloAddr,{
onEnter: function(args){ //args参数
args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值
console.log(args[0]);

},
onLeave: function(retval){ //retval返回值
retval.replace(20000); //返回值修改
console.log("retval",retval.toInt32());
}
})
}
})
}

(2)字符串类型修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function hook(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("so库名","函数名");
if(helloAddr != null){
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();//获取原参数
console.log("参数:", originalStrPtr);
var modifiedContent = "至尊";
var newJString = JNIEnv.newStringUtf(modifiedContent); //创建JSting变量
args[2] = newJString;//赋值
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
var JNIEnv = Java.vm.getEnv();
var modifiedContent = "无敌";
var newJString = JNIEnv.newStringUtf(modifiedContent);
retval.replace(newJString);
}
})
}
})
}

6.Hook未导出函数与函数地址计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook(){
Java.perform(function(){
//根据导出函数名打印基址
var soAddr = Module.findBaseAddress("so库名");
console.log(soAddr);
var funcaddr = soAddr.add(相对地址);
console.log(funcaddr);
if(funcaddr != null){
Interceptor.attach(funcaddr,{
onEnter: function(args){ //args参数

},
onLeave: function(retval){ //retval返回值
console.log(retval.toInt32());
}
})
}
})
}

函数地址计算

  1. 安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令
  2. 通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)
  3. thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1
    arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移

7.Hook_dlopen

so库的加载如下(图片借用自《安卓逆向这档事》十二、大佬帮我分析一下):

对应的函数如下

函数名 描述
android_dlopen_ext()dlopen()do_dlopen() 这三个函数主要用于加载库文件。android_dlopen_ext 是系统的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。
find_library() find_library() 函数用于查找库,基本的用途是给定一个库的名字,然后查找并返回这个库的路径。
call_constructors() call_constructors() 是用于调用动态加载库中的构造函数的函数。
init 库的构造函数,用于初始化库中的静态变量或执行其他需要在库被加载时完成的任务。如果没有定义init函数,系统将不会执行任何动作。需要注意的是,init函数不应该有任何参数,并且也没有返回值。
init_array init_array是ELF(Executable and Linkable Format,可执行和可链接格式)二进制格式中的一个特殊段(section),这个段包含了一些函数的指针,这些函数将在main()函数执行前被调用,用于初始化静态局部变量和全局变量。
jni_onload 这是Android JNI(Java Native Interface)中的一个函数。当一个native库被系统加载时,该函数会被自动调用。JNI_OnLoad可以做一些初始化工作,例如注册你的native方法或者初始化一些数据结构。如果你的native库没有定义这个函数,那么JNI会使用默认的行为。JNI_OnLoad的返回值应该是需要的JNI版本,一般返回JNI_VERSION_1_6

dlopenandroid_dlopen_ext就是用于加载so库的,而且加载时机很早,方便进行其他操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("so库名") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook) hook();//为真则执行的逻辑
}
});
// 高版本Android系统使用android_dlopen_ext
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("so库名") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook) hook();//为真则执行的逻辑
}
});
}

注意一般配合frida的-f参数使用,因为要在非常早的时候hook,因此交由frida来启动会比较准确

0x09 Frida写数据

1
2
3
4
5
6
7
8
//一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足)
var file_path = "私有目录地址";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
file_handle.write(data); //写入数据
file_handle.flush(); //刷新
file_handle.close(); //关闭
}

0x0A Frida_inlineHook与读写汇编

Inline hook(内联钩子)是一种在程序运行时修改函数执行流程的技术。

它通过修改函数的原始代码,将目标函数的执行路径重定向到自定义的代码段,从而实现对目标函数的拦截和修改。

简单来说就是可以对任意地址的指令进行hook读写操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function inline_hook() {
var soAddr = Module.findBaseAddress("so库名");
if (soAddr) {
var func_addr = soAddr.add(0x10428);
Java.perform(function () {
Interceptor.attach(func_addr, {
onEnter: function (args) {
console.log(this.context.x22); //注意此时就没有args概念了
this.context.x22 = ptr(1); //给寄存器赋值
},
onLeave: function (retval) {
}
}
)
})
}
}

(1)地址解析成汇编

1
2
3
var soAddr = Module.findBaseAddress("so库名");
var codeAddr = Instruction.parse(soAddr.add(相对地址));
console.log(codeAddr.toString());

(2)FridaAPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var soAddr = Module.findBaseAddress("so库名");
var codeAddr = soAddr.add(相对地址);
Memory.patchCode(codeAddr, 4, function(code) {
const writer = new Arm64Writer(code, { pc: codeAddr });
writer.putBytes(hexToBytes("20008052"));
writer.flush();
});
function hexToBytes(str) {
var pos = 0;
var len = str.length;
if (len % 2 != 0) {
return null;
}
len /= 2;
var hexA = new Array();
for (var i = 0; i < len; i++) {
var s = str.substr(pos, 2);
var v = parseInt(s, 16);
hexA.push(v);
pos += 2;
}
return hexA;
}

0x0B 普通函数与jni函数的主动调用

官方文档规定了支持的类型以及调用约定

1
2
3
4
5
6
7
8
9
//查找函数地址
var funcAddr = Module.findBaseAddress("so库名").add(相对地址);
var funcAddr = Module.findExportByName("so库名", "函数名");
//声明函数指针
//NativeFunction的第一个参数是地址,第二个参数是返回值类型,第三个[]里的是传入的参数类型(有几个就填几个)
var aesAddr = new NativeFunction(funcAddr , 'pointer', ['pointer', 'pointer', 'pointer']);
var encry_text = Memory.allocUtf8String("OOmGYpk6s0qPSXEPp4X31g=="); //开辟一个内存存放字符串,返回值为指针
var key = Memory.allocUtf8String('wuaipojie0123456');
console.log(aesAddr(Java.vm.getEnv(), encry_text ,key).readCString()); // 调用函数,注意jni函数第一个值要传JNIEnv*