Unsage类的使用

Java是以安全著称,但在Java中有一个类是一个Bug级别的存在,那就是Unsafe. 前面已经说过Unsafe在java中的使用,此处我们直接说用法:

避免初始化

当你想跳过对象初始化的阶段,或者绕过构造函数的检查,去实例化没有任何公共构造函数的类,可以使用allocateInstance:

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

class A {
private long a; // not initialized value

public A() {
this.a = 1; // initialization
}

public long a() { return this.a; }
}

```
使用构造函数、反射和unsafe初始化它,将得到不同的结果。

``` java
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
A o1 = new A(); // constructor
System.out.println(o1.a()); // prints 1

A o2 = A.class.newInstance(); // reflection
System.out.println(o2.a()); // prints 1

A o3 = (A) UnsafeUtils.getUnsafe().allocateInstance(A.class); // unsafe
System.out.println(o3.a()); // prints 0
}

内存崩溃(Memory corruption)

我们可以使用Unsafe去做一些绕过安全的技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   class Guard {
private int ACCESS_ALLOWED = 1;

public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
```


当客户端调用`giveAccess`代码是,始终返回的都是`false`.使用`Unsafe`可以绕过权限:

``` java
Guard guard=new Guard();
System.out.println("修改前:"+guard.giveAccess());
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption
System.out.println("修改后:"+guard.giveAccess());

计算对象的大小

sizeOf是返回对象的自身内存大小。

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
29
30
31
32
33
34
35
36
37
38
 public static long sizeOf(Object o) {
Unsafe u = UnsafeUtils.getUnsafe();
HashSet<Field> fields = new HashSet<Field>();
Class c = o.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if ((f.getModifiers() & Modifier.STATIC) == 0) {
fields.add(f);
}
}
c = c.getSuperclass();
}
// get offset
long maxSize = 0;
for (Field f : fields) {
long offset = u.objectFieldOffset(f);
if (offset > maxSize) {
maxSize = offset;
}
}

return ((maxSize/8) + 1) * 8; // padding
}

```

如果只是对象类的结构大小,那么可以更简单的实现:

```java
public static long sizeOf(Object object){
return UnsafeUtils.getUnsafe().getAddress(
normalize(UnsafeUtils.getUnsafe().getInt(object, 4L)) + 12L);
}
private static long normalize(int value) {
if(value >= 0) return value;
return (~0L >>> 32) & value;
}

浅拷贝(Shallow copy)

为了计算自身内存的大小,可以简单的添加拷贝的对象方法,标准的解决方案是使用Cloneable修改代码,或者实现自定义的拷贝方法.

浅拷贝:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

public class ShallowCopy {
static Unsafe unsafe = UnsafeUtils.getUnsafe();
public static Object shallowCopy(Object obj) {
long size = sizeOf(obj);
long start = toAddress(obj);
long address = unsafe.allocateMemory(size);
unsafe.copyMemory(start, address, size);
return fromAddress(address);
}
//toAddress和fromAddress将对象转换为其在内存中的地址,反之亦然。
static long toAddress(Object obj) {
Object[] array = new Object[] {obj};
long baseOffset = unsafe.arrayBaseOffset(Object[].class);
return normalize(unsafe.getInt(array, baseOffset));
}

static Object fromAddress(long address) {
Object[] array = new Object[] {null};
long baseOffset = unsafe.arrayBaseOffset(Object[].class);
unsafe.putLong(array, baseOffset, address);
return array[0];
}
public static long sizeOf(Object o) {

HashSet<Field> fields = new HashSet<Field>();
Class c = o.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if ((f.getModifiers() & Modifier.STATIC) == 0) {
fields.add(f);
}
}
c = c.getSuperclass();
}

// get offset
long maxSize = 0;
for (Field f : fields) {
long offset = unsafe.objectFieldOffset(f);
if (offset > maxSize) {
maxSize = offset;
}
}
return ((maxSize/8) + 1) * 8; // padding
}
//normalize是一个为了正确内存地址使用,将有符号的int类型强制转换成无符号的long类型的方法。
private static long normalize(int value) {
if(value >= 0) return value;
return (~0L >>> 32) & value;
}
}

```

### 隐藏密码(Hide Password)

在`Unsafe`类中可以删除内存中的对象,比如密码信息。用户的密码大多数都是byte[]或char[]数组,为什么要使用数组呢?

这个是出于安全的考虑,因为我们可以删除不需要的数组元素,如果是一个字符串对象的话,这可以像一个对象在内存中保存,删除该对象只是删除了引用,真正的数据还保存在内存中。

``` java

String password = new String("l00k@myHor$e");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // l00k@myHor$e
System.out.println(fake); // ????????????

unsafe.copyMemory(
fake, 0L, null, toAddress(password), sizeOf(password));

System.out.println(password); // ????????????
System.out.println(fake); // ????????????

```

<!-- ### 多继承(Multiple Inheritance)
Java中没有多继承。


``` java
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
```
这个代码片段将String类型添加到Integer超类中,因此我们可以强制转换,且没有运行时异常。

`(String) (Object) (new Integer(666))` -->

### 动态类(Dynamic classes)

我们可以在运行时创建一个类,比如从已编译的.class文件中。将类内容读取为字节数组,并正确地传递给defineClass方法。可以动态调用java的class文件

``` java
byte[] classContents = getClassContent();
Class c = unsafe.defineClass(null, classContents, 0, classContents.length,null,null);
Object res = c.getMethod("test").invoke(c.newInstance(), null);// 1
System.out.println(res);
1
2
3
4
5
6
7
8
9
10

private static byte[] getClassContent() throws Exception {
File f = new File("D:\\work\\open\\rzframe\\rz-common\\src\\test\\java\\com\\rz\\frame\\cas\\AppTest.class");
FileInputStream input = new FileInputStream(f);
byte[] content = new byte[(int)f.length()];
input.read(content);
input.close();
return content;
}

大数组(Big Arrays)

正如你所知,Java数组大小的最大值为Integer.MAX_VALUE。使用直接内存分配,我们创建的数组大小受限于堆大小。

class SuperArray {
    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }
}

简单的用法:

long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); // 4294967294
for (int i = 0; i < 100; i++) {
    array.set((long)Integer.MAX_VALUE + i, (byte)3);
    sum += array.get((long)Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum);  // 300

实际上,这是堆外内存(off-heap memory)技术,在java.nio包中部分可用。

这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用。它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃。

这可用于数学计算,代码可操作大数组的数据,可打破GC在大数组上延迟的限制。

作者

付威

发布于

2019-07-23

更新于

2020-08-10

许可协议

评论