JavaScript Fuzzing (模糊测试):如何利用 Grammar Fuzzing 或 Coverage-Guided Fuzzing 发现 JavaScript 引擎的漏洞?

JavaScript 引擎漏洞挖掘:Fuzzing 的艺术

各位靓仔靓女,老少爷们,欢迎来到今天的 JavaScript 引擎漏洞挖掘讲座!今天我们要聊聊如何用一些“不正经”的手段,来发现 JavaScript 引擎里那些藏得深深的 bug。

别害怕,我说的“不正经”指的是 Fuzzing (模糊测试)。这是一种让程序自己找 bug 的黑科技,简单来说,就是给程序喂各种各样的“奇怪”输入,看看它会不会崩溃、卡死或者做出一些“不正常”的行为。

今天,我们重点关注两种 Fuzzing 技术:Grammar Fuzzing 和 Coverage-Guided Fuzzing。它们就像两把不同的宝剑,可以帮助我们砍掉 JavaScript 引擎里的各种“妖魔鬼怪”。

一、Grammar Fuzzing:按剧本演出的“捣蛋鬼”

想象一下,你想测试一个编译器。如果随机生成一堆二进制数据喂给它,那效果肯定不好,因为编译器压根就看不懂。这时候,Grammar Fuzzing 就派上用场了。

Grammar Fuzzing 的核心思想是:按照目标语言的语法规则,生成有效的、但又可能包含各种边界情况的输入。 就像给演员一个剧本,让他们按照剧本演戏,但允许他们即兴发挥,加一些“捣蛋”的台词和动作。

1. 语法 (Grammar) 的定义

首先,我们需要定义 JavaScript 的语法。这听起来很吓人,但不用担心,我们不需要完全复刻 JavaScript 规范。只需要定义一些关键的语法规则,就足够进行有效的 Fuzzing 了。

例如,我们可以用 Backus-Naur Form (BNF) 来描述 JavaScript 的语法:

<program> ::= <statement>*
<statement> ::= <variable_declaration> | <expression_statement> | <if_statement>
<variable_declaration> ::= "var" <identifier> "=" <expression> ";"
<expression_statement> ::= <expression> ";"
<if_statement> ::= "if" "(" <expression> ")" "{" <statement>* "}"
<expression> ::= <literal> | <identifier> | <binary_expression>
<literal> ::= <number> | <string> | <boolean>
<binary_expression> ::= <expression> <operator> <expression>
<operator> ::= "+" | "-" | "*" | "/" | "==" | "!="
<identifier> ::= [a-zA-Z_][a-zA-Z0-9_]*
<number> ::= [0-9]+
<string> ::= """ [^"]* """
<boolean> ::= "true" | "false"

这个简单的语法描述了 JavaScript 程序的基本结构,包括变量声明、表达式语句和 if 语句。

2. Grammar Fuzzer 的实现

有了语法定义,我们就可以编写一个 Grammar Fuzzer,根据这些规则生成 JavaScript 代码。下面是一个简单的 Python 实现:

import random

class GrammarFuzzer:
    def __init__(self, grammar):
        self.grammar = grammar

    def generate(self, symbol):
        # 如果 symbol 是终结符,直接返回
        if symbol not in self.grammar:
            return symbol

        # 从 symbol 对应的产生式中随机选择一个
        production = random.choice(self.grammar[symbol])

        # 递归生成非终结符
        return ''.join([self.generate(s) for s in production])

# 定义 JavaScript 的语法 (简化版)
js_grammar = {
    '<program>': [['<statement>', '<program>'], []],
    '<statement>': [['<variable_declaration>'], ['<expression_statement>'], ['<if_statement>']],
    '<variable_declaration>': [['var', '<identifier>', '=', '<expression>', ';']],
    '<expression_statement>': [['<expression>', ';']],
    '<if_statement>': [['if', '(', '<expression>', ')', '{', '<statement>', '}']],
    '<expression>': [['<literal>'], ['<identifier>'], ['<binary_expression>']],
    '<literal>': [['<number>'], ['<string>'], ['<boolean>']],
    '<binary_expression>': [['<expression>', '<operator>', '<expression>']],
    '<operator>': [['+'], ['-'], ['*'], ['/'], ['=='], ['!=']],
    '<identifier>': [['x'], ['y'], ['z']],
    '<number>': [['1'], ['2'], ['3']],
    '<string>': [['"hello"'], ['"world"']],
    '<boolean>': [['true'], ['false']]
}

# 创建 Grammar Fuzzer
fuzzer = GrammarFuzzer(js_grammar)

# 生成 JavaScript 代码
for i in range(10):
    js_code = fuzzer.generate('<program>')
    print(js_code)

这段代码定义了一个 GrammarFuzzer 类,它接受一个语法定义作为输入,并可以根据这个语法生成代码。generate 函数递归地展开非终结符,直到生成最终的代码。

3. 高级技巧:增加“捣蛋”的概率

仅仅生成符合语法的代码是不够的,我们需要让生成的代码更“奇怪”,更有可能触发 bug。可以考虑以下技巧:

  • 边界值测试: 在生成数字和字符串时,可以尝试生成一些极值,比如 2**53 (JavaScript 的最大安全整数),Number.MAX_VALUENumber.MIN_VALUE,空字符串,超长字符串等等。
  • 类型混淆: JavaScript 是一种动态类型语言,可以尝试将不同类型的值进行运算,比如 "1" + 1true * "hello"
  • 特殊字符: 在字符串中插入特殊字符,比如 , n, r, t, uFFFF
  • 原型链污染: 尝试修改 Object.prototype,影响所有对象的行为。

例如,我们可以修改 js_grammar,增加一些生成 NaN 和 Infinity 的概率:

js_grammar = {
    # ... 之前的定义 ...
    '<number>': [['1'], ['2'], ['3'], ['NaN'], ['Infinity']],
    # ... 之后的定义 ...
}

4. 缺陷与优势

  • 优势: Grammar Fuzzing 可以生成结构化的输入,更容易触发语法和语义相关的 bug。可以针对特定的语言特性进行 Fuzzing。
  • 缺陷: 需要手动定义语法,比较耗时。生成的代码可能过于简单,无法覆盖到所有代码路径。

总结: Grammar Fuzzing 就像一个按照剧本演出的“捣蛋鬼”,它知道程序的规则,并利用这些规则来制造混乱,试图找到程序里的漏洞。

二、Coverage-Guided Fuzzing:拿着地图的“探险家”

Coverage-Guided Fuzzing 就像一个拿着地图的“探险家”,它会根据代码的覆盖率来调整 Fuzzing 的策略,尽可能地覆盖到更多的代码路径。

1. 代码覆盖率 (Code Coverage)

代码覆盖率是指测试用例执行的代码占总代码的比例。常见的代码覆盖率指标包括:

  • 语句覆盖率 (Statement Coverage): 每一行代码是否被执行到。
  • 分支覆盖率 (Branch Coverage): 每一个分支 (if/else) 是否被执行到。
  • 路径覆盖率 (Path Coverage): 所有可能的执行路径是否被执行到。

Coverage-Guided Fuzzing 的目标是最大化代码覆盖率,因为覆盖率越高,发现 bug 的概率就越大。

2. AFL (American Fuzzy Lop)

AFL 是一个非常流行的 Coverage-Guided Fuzzer,它通过以下步骤来最大化代码覆盖率:

  1. 种子输入 (Seed Input): AFL 首先需要一些种子输入,作为 Fuzzing 的起点。
  2. 变异 (Mutation): AFL 会对种子输入进行各种变异,比如位翻转、加减常数、插入删除字节等等。
  3. 执行 (Execution): AFL 会执行变异后的输入,并监测代码覆盖率。
  4. 反馈 (Feedback): 如果变异后的输入能够覆盖到新的代码路径,AFL 会将这个输入加入到种子队列中,作为后续 Fuzzing 的起点。

这个过程不断循环,直到达到预定的时间或覆盖率目标。

3. 使用 AFL Fuzz JavaScript 引擎

使用 AFL Fuzz JavaScript 引擎需要以下步骤:

  1. 编译目标程序: 使用 AFL 提供的编译器 (afl-gcc, afl-clang) 编译 JavaScript 引擎。需要注意的是,为了让 AFL 能够有效地监测代码覆盖率,需要在编译时插入一些插桩代码。

    例如,使用 afl-clang 编译 V8 引擎:

    ./tools/dev/gm.py x64.release
    export CC=/path/to/afl-clang-fast
    export CXX=/path/to/afl-clang-fast++
    gn gen out.gn/x64.release --args='target_cpu="x64" is_debug=false'
    ninja -C out.gn/x64.release d8
  2. 准备种子输入: 准备一些有效的 JavaScript 代码作为种子输入。可以从现有的 JavaScript 代码库中提取,也可以手动编写一些简单的测试用例。

  3. 运行 AFL: 使用 AFL 运行编译后的 JavaScript 引擎,并指定种子输入和输出目录。

    afl-fuzz -i seeds -o findings ./out.gn/x64.release/d8 @@
    • -i seeds: 指定种子输入目录。
    • -o findings: 指定输出目录,AFL 会将发现的崩溃和挂起的输入保存在这个目录中。
    • ./out.gn/x64.release/d8 @@: 指定要 Fuzz 的程序和输入文件,@@ 会被 AFL 替换为实际的输入文件名。
  4. 分析结果: AFL 会持续运行,并实时显示 Fuzzing 的状态,包括代码覆盖率、执行速度、发现的崩溃数量等等。如果 AFL 发现了崩溃,会在输出目录中生成相应的崩溃文件。需要分析这些崩溃文件,找出导致崩溃的原因。

4. AFL 的局限性与改进

AFL 擅长发现内存错误和控制流相关的 bug,但对于一些需要深入理解语言语义的 bug,效果可能不太好。

一些研究人员提出了改进 AFL 的方法,比如:

  • Context-Sensitive Fuzzing: 考虑代码的上下文信息,生成更有效的变异。
  • Type-Aware Fuzzing: 根据变量的类型信息,生成更合理的输入。
  • Hybrid Fuzzing: 结合 Grammar Fuzzing 和 Coverage-Guided Fuzzing 的优点,先用 Grammar Fuzzing 生成结构化的输入,再用 Coverage-Guided Fuzzing 优化这些输入。

5. 缺陷与优势

  • 优势: 可以自动化地探索代码路径,不需要手动编写大量的测试用例。可以发现一些意想不到的 bug。
  • 缺陷: 对于一些需要深入理解语言语义的 bug,效果可能不太好。需要大量的计算资源和时间。

总结: Coverage-Guided Fuzzing 就像一个拿着地图的“探险家”,它会根据代码的覆盖率来调整 Fuzzing 的策略,尽可能地覆盖到更多的代码路径,寻找隐藏的宝藏 (bug)。

三、实战案例:利用 Fuzzing 发现 V8 引擎漏洞

V8 引擎是 Google Chrome 浏览器使用的 JavaScript 引擎,也是一个非常流行的 JavaScript 引擎。有很多研究人员利用 Fuzzing 技术发现了 V8 引擎的漏洞。

案例 1:类型混淆漏洞

V8 引擎在处理一些类型转换时,可能会出现类型混淆的漏洞。例如,将一个浮点数转换为整数时,如果没有进行正确的类型检查,可能会导致整数溢出或者类型错误。

可以使用 Grammar Fuzzing 生成一些包含类型转换的代码,来测试 V8 引擎的类型转换逻辑。

案例 2:JIT 编译器漏洞

V8 引擎使用 JIT (Just-In-Time) 编译器来优化 JavaScript 代码的执行速度。JIT 编译器会将 JavaScript 代码编译成本地机器码,但在这个过程中,可能会出现一些编译错误,导致漏洞。

可以使用 Coverage-Guided Fuzzing 来覆盖 JIT 编译器的代码路径,寻找编译错误。

代码示例:利用 Grammar Fuzzing 发现类型混淆漏洞

import random

class GrammarFuzzer:
    def __init__(self, grammar):
        self.grammar = grammar

    def generate(self, symbol):
        if symbol not in self.grammar:
            return symbol

        production = random.choice(self.grammar[symbol])
        return ''.join([self.generate(s) for s in production])

js_grammar = {
    '<program>': [['<statement>', '<program>'], []],
    '<statement>': [['<expression_statement>']],
    '<expression_statement>': [['<expression>', ';']],
    '<expression>': [['<literal>'], ['<binary_expression>'], ['parseInt(<expression>)']],
    '<literal>': [['<number>'], ['<string>'], ['<boolean>']],
    '<binary_expression>': [['<expression>', '<operator>', '<expression>']],
    '<operator>': [['+'], ['-'], ['*'], ['/'], ['=='], ['!=']],
    '<number>': [['1'], ['2'], ['3'], ['1.1'], ['2.2'], ['3.3'], ['NaN'], ['Infinity'], ['-Infinity']],
    '<string>': [['"hello"'], ['"world"'], ['""']],
    '<boolean>': [['true'], ['false']]
}

fuzzer = GrammarFuzzer(js_grammar)

for i in range(100):
    js_code = fuzzer.generate('<program>')
    print(js_code)

    # 将生成的代码保存到文件
    with open('test.js', 'w') as f:
        f.write(js_code)

    # 使用 V8 引擎执行代码
    import subprocess
    try:
        subprocess.run(['./out.gn/x64.release/d8', 'test.js'], timeout=1, check=True, capture_output=True)
    except subprocess.TimeoutExpired:
        print("Timeout!")
    except subprocess.CalledProcessError as e:
        print("Crash!")
        print(e.stderr.decode())

这段代码会生成包含 parseInt 函数的 JavaScript 代码,并尝试使用 V8 引擎执行这些代码。如果 V8 引擎在执行过程中崩溃,说明可能存在类型混淆的漏洞。

四、Fuzzing 的注意事项

  • 选择合适的 Fuzzer: 不同的 Fuzzer 适用于不同的目标程序和漏洞类型。需要根据实际情况选择合适的 Fuzzer。
  • 准备充分的种子输入: 种子输入越多样化,Fuzzing 的效果就越好。
  • 监控 Fuzzing 的状态: 实时监控 Fuzzing 的状态,包括代码覆盖率、执行速度、发现的崩溃数量等等。
  • 分析 Fuzzing 的结果: 认真分析 Fuzzing 发现的崩溃,找出导致崩溃的原因,并修复漏洞。
  • 持续 Fuzzing: Fuzzing 是一个持续的过程,需要不断地改进 Fuzzing 的策略,才能发现更多的漏洞。

五、总结

今天我们聊了 JavaScript 引擎 Fuzzing 的一些基本概念和技术,包括 Grammar Fuzzing 和 Coverage-Guided Fuzzing。Fuzzing 是一种非常有效的漏洞挖掘技术,可以帮助我们发现 JavaScript 引擎里那些藏得深深的 bug。

希望今天的讲座对大家有所帮助。记住,Fuzzing 是一门艺术,需要不断地学习和实践,才能掌握其中的精髓。

祝大家 Fuzzing 愉快,早日发现 JavaScript 引擎的漏洞!

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注