首页
社区
课程
招聘
[分享][原创]frida中Java和js之间需要转换的数据
发表于: 23小时前 410

[分享][原创]frida中Java和js之间需要转换的数据

23小时前
410

一、说明

本文所使用的fria版本为17.9.6。如果文章有错误希望指出,不吝赐教。

在Frida中,Java层和Js层之间的数据并不是完全一致的,虽然frida-java-bridge会自动完成绝大多数类型转换,但有些类型需要手动转换或使用专门的API。下表列出了Java和js之间数据是否hi自动转换以及常用的API。

Java类型 JS中表现 是否自动转换 常用API
boolean Boolean
byte Number
short Number
int Number
float Number
double Number
long Int64对象(不是普通Number) ⚠️ .toString()valueOf()
char String(长度1)
String String 或 Java String Wrapper $new()
Object Java Wrapper ⚠️ Java.cast()
数组 JS数组 / PrimitiveArray ⚠️ Java.array()
List Java Wrapper 调用 .get() .size()
Map Java Wrapper 调用 Java API
Set Java Wrapper 调用 Java API
byte[] PrimitiveArray ⚠️ Java.array('byte', ...)
Object[] JS Array ⚠️ Java.array()
Enum Java Wrapper .name() .ordinal()
Class Java Wrapper Java.cast()
Throwable Java Wrapper .getMessage()

一、基本数类型

int
boolean
float
double
char
short
byte

这些在Java中式基本类型的变量,进入js后依然是普通变量:

Java.perform(function(){
    var Test Java.use("com.test.Test");
    
    Test.test.overload("int").implementation = function(i){
        console.log(typeof i);//number
        console.log(i);//123
        
        return this.test.call(this,i+1);
    }
})

这些基本类型几乎可以不考虑转换问题。

二、long类型

在Java中定义long id = 1234567890123L;,在js中输出:console.log(i);,输出的不是普通对象,而是一个Int64对象。例如:

console.log(id.toString());
console.log(id.valueOf());
console.log(id.equals(100));

因为JS的Number最大只能精确表示53位整数,而Java的long是64位,所以Frida使用专门的对象来避免精度丢失。

三、String

Java中String s = "Hello";,在js里通常可以直接当字符串使用:

console.log(s);
console.log(s.length);
console.log(s.toString());

至于第三段代码调用toString()方法的原因:

通常情况下,Java的String对象JS可以直接调用:

Java.perform(function() {
    var String = Java.use("java.lang.String");
    var str = String.$new("Hello Frida");
    
    console.log(str);                    // 输出: Hello Frida
    console.log("内容: " + str);          // 输出: 内容: Hello Frida
    
    console.log(str.toString());         // 输出: Hello Frida
});

1、避免对象引用输出

Java.perform(function() {
    var String = Java.use("java.lang.String");
    var str = String.$new("Hello");
    
    console.log(str);  // 可能输出: java.lang.String
    
    //  强制转换为 JavaScript 字符串
    console.log(str.toString());  // 输出: Hello
});

某些情况下,直接输出可能会是Java的对象引用

2、在复杂表达式中确保类型正确

Java.perform(function() {
    var String = Java.use("java.lang.String");
    var str = String.$new("Frida");
    
    if (str === "Frida") {
        console.log("相等");  // 可能不会执行!
    }
    
    if (str.toString() === "Frida") {
        console.log("相等");  // 会执行
    }
});

直接比较可能会出现问题,比如比较的是对象引用。

3、传递给需要纯字符串的API

Java.perform(function() {
    var String = Java.use("java.lang.String");
    var str = String.$new("test@example.com");
    
    //  某些 JavaScript API 可能不接受 Java 对象
    var parts = str.split("@");  // 可能报错!
    
    //  转换为纯 JavaScript 字符串
    var parts = str.toString().split("@");  // 正常工作
});

在JS中创建Java String对象:

var JString = Java.use("java.lang.String");
var str = JString.$new("hello")

四、Java Object

Java中User user,JS收到的是Java Wrapper,所以不能直接访问所有的字段,需要:

console.log(user.$className);
//$className的作用是打印user的完整包名+类名
console.log(user.getName());
console.log(user.getAge());

Frida访问Java类中字段需要按照特定格式:访问Java字段用.找到字段,再用.value获取或设置字段值

1、访问实例字段

假设有Java类:

public class User {
    public String name = "Alice";
    public int age = 20;
}

Frida:

Java.perform(function () {
    var User = Java.use("com.test.User");

    var obj = User.$new();

    console.log(obj.name.value);
    console.log(obj.age.value);
});
//Alice
//20

这里:

obj
 │
 ├── name   ---> Java Field Wrapper
 │               │
 │               └── value ---> "Alice"
 │
 └── age    ---> Java Field Wrapper
                 │
                 └── value ---> 20

也就是说obj.name得到的是Field Wrapper,而不是真正的字段值。真正的数据在obj.name.value中。

2、修改字段值

修改字段值只能通过下面的方式来修改:

user.age.value = 30;

3、访问静态字段

Java:

public class Config {
    public static int VERSION = 10;
}

Frida中需要定位到这个类,并且也需要使用.value:

var Config = Java.use("com.test.Config");

console.log(Config.VERSION.value);

Config.VERSION.value = 20;

4、如果字段是对象

例如:

public User user;

Frida:

var u = obj.user.value

得到的依然是一个Java Wrapper。

需要继续访问:

console.log(u.name.value);
console.log(u.age.value);
//也可以链式访问
console.log(obj.user.value.name.value);

5、如果字段和方法重名

这是Frida的特殊规则,例如Java中有“

class Test {
    public int m;

    public int m() {
        return 1;
    }
}

如果直接obj.m,那么frida默认认为你想要访问方法。

字段需要写成obj._m.value,也就是在字段名前加"_"。

五、数组

Java数组返回方法后,通常不需要主动转换。Frida已经帮助你转换成了一个可以直接在JS中操作的对象,只是不同类型的数组表现形式略有不同。

1、遍历数组

Frida可以获取所有从Java层得到的数组的length属性,也可以遍历这些数组,只不过根据数组形式的不同,遍历方式也不同。比如下面展示遍历从Java层获得的byte数组和Object数组:

//byte数组
for (var i = 0; i < result.length; i++){
    console.log(result[i]);
}
//Object数组
for (var j = 0; j < result.length; j++){
    console.log(result[i].age.value);
}

2、打印数组

Frida中直接打印数组的时候console.log(result),输出可能是[object Object]或类型的东西。这是因为它是一个PrimitiveArray对象,不是普通的JS Array。在这种情况下可以自己遍历这个数组,然后转移到JS的数组中:

var arr = [];
for (var i = 0; i < result.length; i++){
    arr.push(result[i]);
}

    console.log(arr);

3、转换byte数组

在Frida中真正常见的是byte[]转字符串和byte[]转十六进制:

例如Java中:byte[] = 12 5A FF 01 ...,那么JS中需要:

var hex = [];  
  
for (var i = 0; i < result.length; i++) {  
  
var b = result[i];  
  
if (b < 0)  
b += 256;  
  
hex.push(("0" + b.toString(16)).slice(-2));  
}  
  
console.log(hex.join(" "));
//输出0c 5a ff 01

因为Java的byte是有符号的(-128~127),所以遇到负数时通常先加256再格式化。

如果知道byte[]数组中存的是UTF-8文本,可以直接借助Java的String类输出:

var JString = Java.use("java.lang.String");

var str = JString.$new(result);

console.log(str);

如果不是UTF-8而是AES密文等二进制数据的时候,就不能这样转换,否则会出现乱码。

4、二进制数据转UTF-8

在Frida中也可以经常看到这样的工具函数:

var ByteString = Java.use("com.android.okhttp.okio.ByteString");
    //工具函数,用于将ASCII码转换为UTF8编码的字符串
    function toUtf8(data){
        console.log(ByteString.of(data).utf8());
    }

ByteString.of(data)ByteString类的一个静态方法。这个方法的作用是将原始的Java byte[]对象,包装成一个ByteString对象。

.uft8ByteString的一个实例方法,该方法的功能是尝试将包装的二进制数据用UTF-8字符集解码成一个Java字符串。

5、JS向Java传递数组

有时也需要在Frida中向Java层传递一个数组:

var User = Java.use("com.test.User");

var u1 = User.$new();
var u2 = User.$new();

var arr = Java.array(
    "com.test.User",
    [u1,u2]
);

Java.array(type, elements)是Frida创建Java数组的标准方式,支持基本类型数组和对象数组

6、一个实例

下面的例子几乎涵盖了实际逆向中的需求:

Java.perform(function () {

    var cls = Java.use("com.test.Test");

    cls.encrypt.implementation = function (input) {

        var result = this.encrypt(input);

        // Java byte[] -> JS Array
        var bytes = [];

        for (var i = 0; i < result.length; i++) {

            var b = result[i];

            if (b < 0)
                b += 256;

            bytes.push(b);
        }

        console.log(bytes);

        console.log(bytes.length);

        return result;
    };

});

六、List

例如Java代码:ArrayList<String>,不会自动变成JS Array,它仍然是一个Java对象。但是Frida也可以像操作Java数组那样,获取Java List的长度以及通过.get(下标)的方法得到对应的元素:

console.log(list.size());

console.log(list.get(0));

console.log(list.get(1));

var jsArray = [];

for (var i = 0; i < list.size(); i++) {
    jsArray.push(list.get(i));
}

七、Map

诸如Java的HashMap等,在Frida中获取到后依然是Java对象,但可以通过以下方法操作:

console.log(map.get("name"));

console.log(map.keySet());

console.log(map.values());

var iterator = map.entrySet().iterator();

while(iterator.hasNext()){
    var entry = iterator.next();

    console.log(entry.getKey());

    console.log(entry.getValue());
}

八、Class

Frida中可以这样操作:

console.log(cls.getName());

console.log(cls.getSuperclass());

var Class = Java.use("java.lang.Class");
var c = Java.cast(handle, Class);

九、Throwable

Frida中可以这样操作异常对象:

console.log(e.getMessage());

console.log(e.toString());

console.log(
    e.getStackTrace()
);

十、JNI Handle

有时 Native Hook拿到的是:

jobject
jstring
jclass

这些实际上JNI Handle,要在Java层使用,需要:Java.cast(handle, SomeClass);

1、JNI Handle本质

那么JNI Handle的本质是什么?在学习中,最容易建立正确模型的比喻是:“JNI Handle更像是native层一个指向Java层对象的指针”。但实际上来讲,JNI Handle是Native层保存的一个“Java对象引用”,它与指针的区别在于:JNI规范故意没有保证jobject一定就是对象的内存地址

可以试着这样理解:把JVM想象成一个仓库

Java Heap

┌────────────┐
│  User对象   │
│ name="Tom" │
│ age=18     │
└────────────┘

Native里面不会直接拿到它的首地址,而是拿到一个"Handle #25",或者"Reference #25",而JVM内部知道:

Handle #25
        │
        ▼
Java Heap
        │
        ▼
User对象

因此:Native->jobject->JVM->整整的Java对象。这也是为什么JNI官方文档一直使用“Reference(引用)”这个术语。

还有一部分原因是:Java有:

  • GC(垃圾回收机制)
  • Heap Compact(堆压缩机制)
  • 对象移动(Object Moving)

因此,Java对象在堆中的地址是可以改变的;而在 JNI 语境下,为了安全,不允许 C 语言代码直接保存或使用指向 Java 堆内存的真实指针地址。

这样看的话,它们之间的关系更像是:

操作系统 JNI
虚拟地址(Virtual Address) JNI Handlejobjectjstring 等)
MMU + 页表 JVM(准确地说是 JVM 的引用管理和 GC 系统)
物理页(Physical Page) Java Heap 中真正的 Java 对象

2、Frida中拿到JNI Handle后的转化

假设在Java中有:

package com.demo;

public class User {
    public String name = "Alice";

    public String getName() {
        return name;
    }

    public native void test();
}

可以看到在User类中注册了一个native函数test。对应JNI:

JNIEXPORT void JNICALL
Java_com_demo_User_test(JNIEnv *env, jobject thiz)
{
    // thiz 就是 JNI Handle(jobject)
}

那么就可以通过Frida代码得到JNI Handle:

Interceptor.attach(addr, {

    onEnter(args) {

        var jobjectHandle = args[1];

        console.log(jobjectHandle);
    }
});

拿到了JNI Handle以后就可以转换成Java对象:

Java.perform(function () {

    var User = Java.use("com.demo.User");

    Interceptor.attach(addr, {

        onEnter(args) {

            var handle = args[1];

            var obj = Java.cast(handle, User);

            console.log(obj);
        }
    });

});

至于为什么Java.cast()可以转化为Java对象是因为:

Java.cast()

        │

JNI Handle
        │
        ▼
JNI API
        │
        ▼
找到对应Java对象
        │
        ▼
创建Java Wrapper

所以Java.cast()并不是把一块内存“强制转换”成对象,而是告诉Frida:这个JNI 引用实际上对应的就是User类型,请为我创建一个对应的Java包装对象(Wrapper)“

3、转换后能做什么?

再此之前先看看Oracle官方JNI规范给出的完整的引用类型继承关系:

jobject
│
├── jclass
├── jstring
├── jthrowable
│
└── jarray
    │
    ├── jobjectArray
    ├── jbooleanArray
    ├── jbyteArray
    ├── jcharArray
    ├── jshortArray
    ├── jintArray
    ├── jlongArray
    ├── jfloatArray
    └── jdoubleArray

值得注意的是,所有的普通java实例在JNI函数中都会变为jobject。既然如此,jclass的存在意义是什么?

先说结论:jclass的存在不是因为JVM不能用jobject表示,而是因为java.lang.Class在Java中本身就是一中特殊现象,它代表类的元数据,而不是普通实例。换句话说:

  • jobject表示某个类的实例
  • jclass表示这个类本身。

(1)jobject转换后:

console.log(obj.name.value);//读取字段
obj.name.value = "Bob";//修改字段
obj.login();//调用实例方法
obj.getClass();//获取class

//强制类型转化
var ArrayList = Java.use("java.util.ArrayList");
var list = Java.cast(obj, ArrayList);
console.log(list.size());

(2)jstring转换后:

console.log(s.length());//返回字符串长度
console.log(s.charAt(0));//返回字符串索引0位置的字符,类型位char
console.log(s.contains("abc"));//判断字符s中是否包含子串"abc"
console.log(s.substring(1));//返回从索引1开始到结尾的字串
console.log(s.hashCode());//返回字符串的哈希码(一个int值)

(3)jbyteArray一般不转化

更多的是利用JNI API:

env.getByteArrayElements()
env.getArrayLength()
//或者自己遍历数组,然后转化为js数组

(4)jclass转换后:

既然它代表class对象,那么能否修改类呢?答案是:不能

原因很简单:因为java.lang.Class对象里面保存的是:类名、方法列表、字段列表、父类、接口、Annotation和Modifier。这些是类的元数据。所以转换后可以做很多的反射。

console.log(clazz.getName());//获取类名
clazz.getSuperclass();//获取父类
clazz.getInterfaces();//获取接口
clazz.getDeclaredFields();//获取字段
clazz.getDeclaredMethods();//获取方法
clazz.getDeclaredConstructors();//获取Constructor
clazz.isInterface();//判断是不是接口
clazz.isArray();//判断是不是数组
clazz.getModifiers();//获取modifier

[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

最后于 18小时前 被陈芸欣编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 347
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
可以可以,有时候想看某个值不知道怎么取,就得网上搜索半天脚本,这下直接总结完了,太棒了
20小时前
0
游客
登录 | 注册 方可回帖
返回