index.html:
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
......
<div class="checker-box">
<label for="flag-input">Access code:</label>
<input id="flag-input" type="text" placeholder="CYBERLEAGUE{...}" size="40"
autocomplete="off" spellcheck="false">
<br>
<button id="check-btn" onclick="checkFlag()">Unlock</button>
<div id="status">Initialising vault...</div>
<div id="result" class="info">Loading...</div>
</div>
<script>
var _cfg = [0xCA, 0xFE, 0xBA, 0xBE];
var _c = (_cfg[0] << 24 | _cfg[1] << 16 | _cfg[2] << 8 | _cfg[3]) >>> 0;
var worker = new Worker('worker.js');
var statusEl = document.getElementById('status');
var resultEl = document.getElementById('result');
var checkBtn = document.getElementById('check-btn');
worker.onerror = function (e) {
statusEl.textContent = 'Error: ' + e.message;
resultEl.className = 'wrong';
resultEl.textContent = 'Failed to initialise vault.';
};
worker.onmessage = function (e) {
var msg = e.data;
if (msg.type === 'ready') {
statusEl.textContent = 'Vault ready.';
resultEl.className = 'info';
resultEl.textContent = 'Enter the access code and click Unlock.';
checkBtn.disabled = false;
} else if (msg.type === 'result') {
if (msg.success) {
resultEl.className = 'correct';
resultEl.textContent = 'Access granted.';
} else {
resultEl.className = 'wrong';
resultEl.textContent = msg.reason
? 'Access denied. (' + msg.reason + ')'
: 'Access denied. Try again.';
}
} else if (msg.type === 'error') {
statusEl.textContent = 'Error: ' + msg.message;
resultEl.className = 'wrong';
resultEl.textContent = 'Internal error.';
}
};
worker.postMessage({ type: 'init', cfg: _c });
function checkFlag() {
var raw = document.getElementById('flag-input').value.trim();
if (!raw) {
resultEl.className = 'info';
resultEl.textContent = 'Please enter an access code.';
return;
}
resultEl.className = 'info';
resultEl.textContent = 'Verifying...';
var enc = new TextEncoder();
var bytes = enc.encode(raw);
var masked = new Uint8Array(bytes.length);
for (var j = 0; j < bytes.length; j++) {
masked[j] = bytes[j] ^ 0x42;
}
worker.postMessage({ type: 'check', payload: Array.from(masked) });
}
document.getElementById('flag-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !checkBtn.disabled) checkFlag();
});
</script>
</body>
</html>
worker.js:
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
'use strict';
var DB_NAME = 'vault_store';
var DB_VERSION = 1;
var STORE_NAME = 'state';
var SK_KEY = 'sk';
var wasmInstance = null;
var _sk = null;
function openDB() {
return new Promise(function (resolve, reject) {
var req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = function (e) {
var db = e.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
req.onsuccess = function (e) { resolve(e.target.result); };
req.onerror = function (e) { reject(e.target.error); };
});
}
function storeSK(val) {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
var tx = db.transaction(STORE_NAME, 'readwrite');
var st = tx.objectStore(STORE_NAME);
var rq = st.put(val, SK_KEY);
rq.onsuccess = function () { resolve(); };
rq.onerror = function (e) { reject(e.target.error); };
});
});
}
function loadSK() {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
var tx = db.transaction(STORE_NAME, 'readonly');
var st = tx.objectStore(STORE_NAME);
var rq = st.get(SK_KEY);
rq.onsuccess = function (e) { resolve(e.target.result); };
rq.onerror = function (e) { reject(e.target.error); };
});
});
}
function loadWasm() {
return fetch('checker.wasm')
.then(function (resp) {
if (!resp.ok) throw new Error('fetch failed: ' + resp.status);
return resp.arrayBuffer();
})
.then(function (buf) {
return WebAssembly.instantiate(buf, {});
})
.then(function (result) {
return result.instance;
});
}
self.onmessage = function (e) {
var msg = e.data;
if (msg.type === 'init') {
loadWasm().then(function (inst) {
wasmInstance = inst;
var cfg = msg.cfg >>> 0;
// cfg = 0xCAFEBABE
var seed = (cfg ^ 0xFDB97531) >>> 0;
wasmInstance.exports.f0(seed);
_sk = (seed ^ 0xDEADBEEF) >>> 0;
return storeSK(_sk);
}).then(function () {
self.postMessage({ type: 'ready' });
}).catch(function (err) {
self.postMessage({ type: 'error', message: err.message });
});
} else if (msg.type === 'check') {
if (!wasmInstance) {
self.postMessage({ type: 'result', success: false, reason: 'not_ready' });
return;
}
loadSK().then(function (stored) {
if (stored !== _sk) {
self.postMessage({ type: 'result', success: false, reason: 'bad_state' });
return;
}
var payload = msg.payload;
var len = payload.length;
var mem = new Uint8Array(wasmInstance.exports.memory.buffer);
for (var i = 0; i < len; i++) {
mem[i] = payload[i];
}
// f1: check function
var ok = wasmInstance.exports.f1(0, len, stored);
self.postMessage({ type: 'result', success: ok === 1 });
}).catch(function (err) {
self.postMessage({ type: 'error', message: err.message });
});
} else {
self.postMessage({ type: 'error', message: 'unknown: ' + msg.type });
}
};
new Worker('worker.js'); this creates web worker in js, which will run in a sperate worker thread.
They will communicate through ‘message’. One side use postMessage, while the other side register onmessage handler.
We can see that the js script put in html starts with worker.postMessage({ type: 'init', cfg: _c });, handled by code after if (msg.type === 'init') in worker.js.
Onclicking the button will trigger the check code. In worker.js side, it will eventually call the wasm function.
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
(module
(type (;0;) (func (param i32)))
(type (;1;) (func (result i32)))
(type (;2;) (func (param i32 i32 i32) (result i32)))
(func (;0;) (type 0) (param i32)
local.get 0
global.set 0)
(func (;1;) (type 1) (result i32)
global.get 0
i32.const 1664525
i32.mul
i32.const 1013904223
i32.add
global.set 0
global.get 0)
(func (;2;) (type 2) (param i32 i32 i32) (result i32)
(local i32 i32 i32 i32 i32 i32 i32 i32)
local.get 1 ;; arg1, length
i32.const 33
i32.ne
if ;; label = @1 ;; if not equal, then execute this
i32.const 0
return
end
local.get 2 ;; arg2, initial key
i32.const -559038737
i32.xor
call 0 ;; set stat = key ^ 0xdeadbeef
i32.const 0
local.set 3
block ;; label = @1
loop ;; label = @2
local.get 3
i32.const 33
i32.ge_u
br_if 1 (;@1;) ;; breakout 1 layer, if the value on stack is not zero
call 1
i32.const 255
i32.and
local.set 5
......
local.get 3
i32.const 1
i32.add
local.set 3
br 0 (;@2;)
end
end
i32.const 1)
(memory (;0;) 1)
(global (;0;) (mut i32) (i32.const 0))
(export "memory" (memory 0))
(export "f0" (func 0))
(export "f1" (func 2))
(data (;0;) (i32.const 256) "\81\b1\8c\9f\c4\e07\bf\adE\e3\a9H`\df\1af\07\aa\c9\c5/q\bde\e7\04\96 \b0I\22_"))
;; i32.load/store 和memory相关.
1
2
3
(func (;0;) (type 0) (param i32)
local.get 0
global.set 0)
local.get 0 reads the param and put it into stack. Then global.set 0 pops it and set it as the value of registered global variable 0 - (global (;0;) (mut i32) (i32.const 0)) (type: i32, initial value: 0.)
Note that the global values are stored in wasm runtime global storage, sperated from memory.
data and memoryDefine a memory area and export it:
1
2
(memory (;0;) 1)
(export "memory" (memory 0))
So that worker.js can refer to it:
1
2
3
4
var mem = new Uint8Array(wasmInstance.exports.memory.buffer);
for (var i = 0; i < len; i++) {
mem[i] = payload[i];
}
And in wasm we can define an area of data, which is stored in memory too.
1
2
(data (;0;) (i32.const 256) "......")
;; data 0, which is stored in memory address 256 = 0x100.
Then we can access to memory data through load/store in wasm.
1
2
3
4
5
6
7
8
;; load an i32 value at memory[12] into stack
i32.const 12
i32.load
;; store 10 (i32) at the memory[12]
i32.const 12
i32.const 10
i32.store
The decrypt code is simple.