跨站脚本攻击(XSS)的变异形式:基于JavaScript引擎优化(JIT Spraying)的攻击详解
描述
JIT Spraying是一种高级的内存攻击技术,最初出现在2010年左右,主要针对支持即时编译(JIT)的JavaScript引擎,例如当时流行的Internet Explorer浏览器中的JScript引擎。它不是一种独立的XSS漏洞,而是一种针对某些XSS漏洞或内存破坏漏洞(如堆溢出)的辅助利用技术,旨在绕过数据执行保护(DEP)等现代内存保护机制,实现可靠的代码执行。
核心思想是:攻击者利用JavaScript引擎的JIT编译器特性,将大量精心构造的JavaScript代码注入到目标浏览器的内存中。当JIT编译器将这些代码编译成本地机器代码时,会在内存中生成可预测的字节模式。攻击者可以结合一个内存破坏漏洞(可能是由之前的XSS、堆溢出、释放后利用等触发),将程序的控制流重定向到这些JIT生成的、包含可执行指令的内存区域,从而执行shellcode,最终控制目标系统。
核心挑战:现代操作系统普遍采用DEP,它标记数据区域(如堆、栈)为不可执行。即使攻击者能注入代码,也无法执行。JIT Spraying通过让JIT编译器“合法”地生成可执行代码,绕过了DEP。
解题过程/原理解析
步骤1:理解JIT编译器的基本工作流程
- 解释执行:浏览器遇到JavaScript代码时,解释器逐行读取并执行,速度较慢。
- 热点识别:当解释器发现某段代码(如一个循环体、一个被频繁调用的函数)执行了非常多次(成为“热点”代码),为了提高性能,它会启动JIT编译器。
- 即时编译:JIT编译器将这段JavaScript代码动态编译成优化过的本地机器代码(汇编指令)。
- 代码缓存:编译后的本地代码被存储在内存的某个可执行区域(例如,一个专门为JIT代码分配的、具有“可执行”权限的内存页)。
- 直接执行:后续再执行这段代码时,直接运行已编译的本地代码,大幅提升速度。
关键点:JIT编译器生成的本地代码存储在具有“可执行”权限的内存中,这是DEP允许的。攻击者的目标就是控制JIT编译器生成的内容。
步骤2:利用JIT编译器生成可预测的机器码(Spraying)
- 构造JavaScript“喷射载荷”:攻击者编写一段看似正常的JavaScript代码,但其内容经过精心设计。例如,一个包含大量常数计算的循环:
function spray() { var shellcode = 0x90909090; // 例如,nop指令的机器码 for (var i = 0; i < 100000; i++) { var x = 0x1c0c1c0c; // 精心选择的数值A var y = 0x0c1c0c1c; // 精心选择的数值B // 执行一些包含这些常数的操作,确保JIT会编译它们 var z = x + y; // 可能还包含其他指令的编码 } } spray(); - 数值选择是关键:像
0x1c0c1c0c这样的32位整数并非随意。当JIT编译器将其编译为本地代码时,这个常数在内存中的表示(字节序列)恰好对应某些x86或x64汇编指令。例如:0x1C0C1C0C在内存中可能存储为字节序列0C 1C 0C 1C(小端序)。- 在x86指令集中,
0x0C是OR AL, imm8指令的操作码,0x1C是SBB AL, imm8指令的操作码。虽然单个这样的指令无意义,但当大量这类指令连续排列,攻击者可以通过计算偏移,使其组合成有意义的shellcode。
- 触发JIT编译:通过让这个
spray函数被重复执行成千上万次,使其成为“热点”,触发浏览器的JIT编译器。 - 填充内存:JIT编译器会为这段代码生成一个包含大量特定字节模式(由那些“常数”对应的指令序列组成)的可执行内存块。攻击者可能会调用多个这样的函数,在内存中“喷洒”出大片包含可预测模式的可执行区域。这个过程称为“喷射”(Spraying)。
步骤3:触发内存破坏漏洞,劫持控制流
- 前提存在另一个漏洞:攻击者必须已经通过XSS、堆溢出、释放后利用等漏洞,获得了在目标进程内存空间中进行某种程度的读写或破坏的能力。通常,这个漏洞能让攻击者覆盖一个函数指针、虚表指针、或栈上的返回地址。
- 重定向到JIT区域:利用这个内存破坏漏洞,攻击者将程序的执行流(例如EIP/RIP指令指针)重定向到之前通过JIT Spraying创建的那片可执行内存区域中的某个位置。由于JIT区域是可执行的,DEP不会阻止。
- 滑动到shellcode:由于攻击者精确计算了喷射载荷中特定字节序列对应的指令,他们知道当控制流跳转到这个区域的某个地址时,处理器会执行什么指令。通常,他们会让跳转地址指向一个包含大量无害指令(如NOP
0x90对应的模式)的“滑板区”(NOP Sled)。处理器会顺序执行这些无害指令,直到“滑”到精心放置的、由其他常数编码成的有效shellcode序列的起始点。
步骤4:执行shellcode,完成利用
- 有效载荷:shellcode通常是一段精简的机器代码,用于执行最终攻击目标,例如下载并执行恶意程序、开启后门、或进行权限提升。
- 完成攻击:处理器执行完shellcode,攻击者就完全控制了受影响的浏览器进程,进而可能控制整个系统。
防御措施
- JIT编译器加固:现代JavaScript引擎(如V8, SpiderMonkey, JavaScriptCore)已实施多项缓解措施:
- 常量加密/混淆:JIT编译器不再将嵌入代码中的大型整数常量直接作为字面量编译进机器码,而是通过加载指令从常量池读取,破坏了攻击者对生成机器码的精确预测。
- 控制流完整性:增加检查,确保执行流不会意外跳转到JIT代码区域的中间位置。
- 代码随机化:在JIT生成的代码中插入随机填充或对代码布局进行随机化,增加攻击者预测目标地址的难度。
- 启用地址空间布局随机化:ASLR使得JIT代码区域的内存地址在每次程序启动时都随机变化,攻击者难以预测跳转地址。虽然JIT Spraying理论上可以配合信息泄漏漏洞绕过ASLR,但增加了攻击复杂度。
- 数据执行保护:DEP本身是防御的基础,迫使攻击者必须通过JIT这样的“合法”代码生成途径。结合ASLR,防御效果更佳。
- 代码指针完整性:如控制流保护(CFG)等技术,严格限制程序可以跳转到的目标地址,防止跳转到不可信位置,包括JIT区域的中间位置。
- 及时更新浏览器:浏览器厂商持续改进JIT编译器的安全性,修复相关漏洞。
总结:JIT Spraying是一种将代码注入与内存破坏漏洞结合的高级利用技术,它巧妙地利用了浏览器性能优化机制(JIT)的副作用来绕过内存保护。虽然由于现代JavaScript引擎的强化,这种攻击在当今主流浏览器中已较难实现,但理解其原理对于深入认识浏览器安全、JIT编译器安全以及高级漏洞利用链的构建仍然至关重要。它体现了安全领域“性能优化可能引入安全风险”的经典权衡。