From ac99f9e293d4ba9dca276da7061123941f1604bd Mon Sep 17 00:00:00 2001 From: Libor Polčák Date: May 24 2020 18:16:23 +0000 Subject: Add wrapping of arrays similarly to JS Zero See #43 --- diff --git a/common/levels.js b/common/levels.js index 578c83f..091dd59 100644 --- a/common/levels.js +++ b/common/levels.js @@ -99,9 +99,20 @@ var level_3 = { ["navigator.deviceMemory"], // HTML-LS ["navigator.hardwareConcurrency"], + // ARRAY + see the insert_array_wrappings() below + ["window.DataView", true], ] }; +// ARRAY +(function insert_array_wrappings() { + let arrays = ["Uint8Array", "Int8Array", "Uint8ClampedArray", "Int16Array", + "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array"]; + for (let a of arrays) { + level_3.wrappers.push([`window.${a}`, true]); + } +})(); + // default extension_settings_data setting. used on install var extension_settings_data = level_0; diff --git a/common/options.js b/common/options.js index a717292..97925c4 100644 --- a/common/options.js +++ b/common/options.js @@ -5,6 +5,7 @@ // // Copyright (C) 2019 Libor Polcak // Copyright (C) 2019 Martin Timko +// Copyright (C) 2020 Peter Hornak // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,7 +23,7 @@ if ((typeof chrome) !== "undefined") { - var browser = chrome; + var browser = chrome; } function prepare_level_config(action_descr, params = { @@ -36,6 +37,8 @@ function prepare_level_config(action_descr, params = { xhr_checked: false, xhr_block_checked: false, xhr_ask_checked: false, + arrays_checked: false, + mapping_checked: false, }) { var configuration_area_el = document.getElementById("configuration_area"); configuration_area_el.textContent = ""; @@ -117,6 +120,20 @@ function prepare_level_config(action_descr, params = { Ask before executing an XHR request. + + +
+ + Protect against ArrayBuffer exploitation. +
+
+ + Use random mapping of array indexing to memory. +
+
+
+
+
`); @@ -134,6 +151,7 @@ function prepare_level_config(action_descr, params = { } connect_options_group("time_precision"); connect_options_group("xhr"); + connect_options_group("arrays"); document.getElementById("save").addEventListener("click", function(e) { e.preventDefault(); new_level = { @@ -177,6 +195,18 @@ function prepare_level_config(action_descr, params = { ["navigator.hardwareConcurrency"], ); } + + if (document.getElementById("arrays_main_checkbox").checked) { + let doMapping = document.getElementById("mapping_checkbox").checked; + let arrays = ["Uint8Array", "Int8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array"]; + for (let a of arrays) { + new_level.wrappers.push([`window.${a}`, doMapping]); + } + new_level.wrappers.push( + ["window.DataView", doMapping], + ); + } + if (new_level.level_id.length > 0 && new_level.level_text.length > 0 && new_level.level_description.length) { if (new_level.level_id.length > 3) { alert("Level ID too long, provide 3 characters or less"); @@ -232,6 +262,8 @@ function edit_level(id) { xhr_checked: "window.XMLHttpRequest" in lev, xhr_block_checked: "window.XMLHttpRequest" in lev ? lev["window.XMLHttpRequest"][0] : false, xhr_ask_checked: "window.XMLHttpRequest" in lev ? lev["window.XMLHttpRequest"][1] : false, + arrays_checked: "Uint8Array" in lev, + mapping_checked: (lev["Uint8Array"][0]) }); } diff --git a/common/wrappingS-ECMA-ARRAY.js b/common/wrappingS-ECMA-ARRAY.js new file mode 100644 index 0000000..b115b99 --- /dev/null +++ b/common/wrappingS-ECMA-ARRAY.js @@ -0,0 +1,679 @@ +// +// JavaScript Restrictor is a browser extension which increases level +// of security, anonymity and privacy of the user while browsing the +// internet. +// +// Copyright (C) 2020 Peter Hornak +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +/* + * Create private namespace + */ + + +/// This function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function packIEEE754(v, ebits, fbits) { + var bias = (1 << (ebits - 1)) - 1; + + function roundToEven(n) { + var w = Math.floor(n), f = n - w; + if (f < 0.5) + return w; + if (f > 0.5) + return w + 1; + return w % 2 ? w + 1 : w; + } + + // Compute sign, exponent, fraction + var s, e, f; + if (v !== v) { + // NaN + // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping + e = (1 << ebits) - 1; + f = Math.pow(2, fbits - 1); + s = 0; + } else if (v === Infinity || v === -Infinity) { + e = (1 << ebits) - 1; + f = 0; + s = (v < 0) ? 1 : 0; + } else if (v === 0) { + e = 0; + f = 0; + s = (1 / v === -Infinity) ? 1 : 0; + } else { + s = v < 0; + v = Math.abs(v); + + if (v >= Math.pow(2, 1 - bias)) { + // Normalized + e = Math.min(Math.floor(Math.log(v) / Math.LN2), 1023); + var significand = v / Math.pow(2, e); + if (significand < 1) { + e -= 1; + significand *= 2; + } + if (significand >= 2) { + e += 1; + significand /= 2; + } + var d = Math.pow(2, fbits); + f = roundToEven(significand * d) - d; + e += bias; + if (f / d >= 1) { + e += 1; + f = 0; + } + if (e > 2 * bias) { + // Overflow + e = (1 << ebits) - 1; + f = 0; + } + } else { + // Denormalized + e = 0; + f = roundToEven(v / Math.pow(2, 1 - bias - fbits)); + } + } + + // Pack sign, exponent, fraction + var bits = [], i; + for (i = fbits; i; i -= 1) { + bits.push(f % 2 ? 1 : 0); + f = Math.floor(f / 2); + } + for (i = ebits; i; i -= 1) { + bits.push(e % 2 ? 1 : 0); + e = Math.floor(e / 2); + } + bits.push(s ? 1 : 0); + bits.reverse(); + var str = bits.join(''); + + // Bits to bytes + var bytes = []; + while (str.length) { + bytes.unshift(parseInt(str.substring(0, 8), 2)); + str = str.substring(8); + } + return bytes; +} + +/// This function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function unpackIEEE754(bytes, ebits, fbits) { + // Bytes to bits + var bits = [], i, j, b, str, + bias, s, e, f; + + for (i = 0; i < bytes.length; ++i) { + b = bytes[i]; + for (j = 8; j; j -= 1) { + bits.push(b % 2 ? 1 : 0); + b = b >> 1; + } + } + bits.reverse(); + str = bits.join(''); + + // Unpack sign, exponent, fraction + bias = (1 << (ebits - 1)) - 1; + s = parseInt(str.substring(0, 1), 2) ? -1 : 1; + e = parseInt(str.substring(1, 1 + ebits), 2); + f = parseInt(str.substring(1 + ebits), 2); + + // Produce number + if (e === (1 << ebits) - 1) { + return f !== 0 ? NaN : s * Infinity; + } else if (e > 0) { + // Normalized + return s * Math.pow(2, e - bias) * (1 + f / Math.pow(2, fbits)); + } else if (f !== 0) { + // Denormalized + return s * Math.pow(2, -(bias - 1)) * (f / Math.pow(2, fbits)); + } else { + return s < 0 ? -0 : 0; + } +} + +/// Function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function unpackF64(b) { + return unpackIEEE754(b, 11, 52); +} + +/// Function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function packF64(v) { + return packIEEE754(v, 11, 52); +} + +/// Function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function unpackF32(b) { + return unpackIEEE754(b, 8, 23); +} + +/// Function was adopted from https://github.com/inexorabletash/polyfill/blob/master/typedarray.js under MIT licence. +function packF32(v) { + return packIEEE754(v, 8, 23); +} + +function constructDecorator(wrapped) { + return function () { + const res = wrapped.apply(originalF, arguments); + return replacementF(res); + } +} + +function offsetDecorator(wrapped, type, proxyRef, offsetF) { + return function () { + var ref = proxyRef; + let newArr = []; + // Create a copy of array + for (let i = 0; i < this.length; i++) { + newArr[i] = this[offsetF(i)] + } + // Shift to original + for (let i = 0; i < this.length; i++) { + this[i] = newArr[i]; + } + // Do func + let res; + if (type === 3) { + res = new this.__proto__.constructor(this)[wrapped.name.split(' ')[1]]() + } else { + res = wrapped.apply(this, arguments); + } + // Create copy of new arr + let secArr = []; + for (let i = 0; i < this.length; i++) { + secArr[i] = this[i]; + } + for (let i = 0; i < this.length; i++) { + this[offsetF(i)] = secArr[i]; + } + switch (type) { + case 0: + return ref; + case 1: + return replacementF(res); + case 2: + return res; + default: + return res + } + } +} + +function redefineNewArrayFunctions(target, offsetF) { + target['sort'] = offsetDecorator(target['sort'], 0, target, offsetF); + target['reverse'] = offsetDecorator(target['reverse'], 0, target, offsetF); + target['fill'] = offsetDecorator(target['fill'], 0, target, offsetF); + target['copyWithin'] = offsetDecorator(target['copyWithin'], 0, target, offsetF); + target['subarray'] = offsetDecorator(target['subarray'], 0, target, offsetF); + target['slice'] = offsetDecorator(target['slice'], 1, target, offsetF); + target['map'] = offsetDecorator(target['map'], 1, target, offsetF); + target['filter'] = offsetDecorator(target['filter'], 1, target, offsetF); + target['set'] = offsetDecorator(target['set'], 2, target, offsetF); + target['reduce'] = offsetDecorator(target['reduce'], 2, target, offsetF); + target['reduceRight'] = offsetDecorator(target['reduceRight'], 2, target, offsetF); + target['lastIndexOf'] = offsetDecorator(target['lastIndexOf'], 2, target, offsetF); + target['indexOf'] = offsetDecorator(target['indexOf'], 2, target, offsetF); + target['forEach'] = offsetDecorator(target['forEach'], 2, target, offsetF); + target['find'] = offsetDecorator(target['find'], 2, target, offsetF); + target['join'] = offsetDecorator(target['join'], 2, target, offsetF); + target['entries'] = offsetDecorator(target['entries'], 3, target, offsetF); + target['keys'] = offsetDecorator(target['keys'], 3, target, offsetF); + target['values'] = offsetDecorator(target['values'], 3, target, offsetF); +} + +function redefineNewArrayConstructors(target) { + target['from'] = constructDecorator(originalF['from']); + target['of'] = constructDecorator(originalF['of']); +} + +/// Default proxy handler for Typed Arrays +var proxyHandler = `{ + get(target, key, receiver) { + var random_idx = Math.floor(Math.random() * target['length']); + // Load random index from array + var rand_val = target[random_idx]; + let proto_keys = ['buffer', 'byteLength', 'byteOffset', 'length']; + if (proto_keys.indexOf(key) >= 0) { + return target[key]; + } + // offsetF argument needs to be in array range + if (typeof key !== 'symbol' && Number(key) >= 0 && Number(key) < target.length) { + key = offsetF(key) + } + let value = Reflect.get(...arguments); + return typeof value == 'function' ? value.bind(target) : value; + }, + set(target, key, value) { + var random_idx = Math.floor(Math.random() * (target['length'])); + // Load random index from array + var rand_val = target[random_idx]; + rand_val = rand_val; + if (typeof key !== 'symbol' && Number(key) >= 0 && Number(key) < target.length) { + key = offsetF(key) + } + return Reflect.set(...arguments); + } +}`; + +function getByteDecorator(wrapped, offsetF, name, doMapping) { + return function () { + const originalIdx = arguments[0]; + const endian = arguments[1]; + if (name === 'getUint8') { + // Random access + let ran = wrapped.apply(this, [Math.floor(Math.random() * (this.byteLength))]); + // Call original func + arguments[0] = offsetF(originalIdx); + return wrapped.apply(this, arguments); + } + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const byteNumber = (parseInt(name[name.length - 2] + name[name.length - 1]) || parseInt(name[name.length - 1])) / 8; + let res = 0; + let swapNumber = byteNumber - 1; + for (let i = 0; i < byteNumber; i++) { + if (endian) { + // Shift starting with 0,1,2 + swapNumber = i * 2; + } + res += this.getUint8(originalIdx + i) << ((swapNumber - i) * 8); + } + return res; + } +} + +function setByteDecorator(wrapped, offsetF, name, doMapping) { + function toNBitBin(n, bits) { + if (n < 0) { + n = 0xFFFFFFFF + n + 1; + } + function makeString(n) { + let s = ""; + for (let i = 0; i < n; i++) { + s += "0"; + } + return s; + } + return (makeString(bits) + parseInt(n, 10).toString(2)).substr(-bits); + } + + return function () { + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const originalIdx = arguments[0]; + const value = arguments[1]; + const endian = arguments[2]; + if (name === 'setUint8') { + // Random access + this.getUint8(0); + // Call original func + arguments[0] = offsetF(originalIdx); + return wrapped.apply(this, arguments); + } + const byteNumber = (parseInt(name[name.length - 2] + name[name.length - 1]) || parseInt(name[name.length - 1])) / 8; + const binNumber = toNBitBin(value, byteNumber * 8); + let numberPart; + for (let i = 0; i < byteNumber; i++) { + numberPart = binNumber.substr(i * 8, 8); + numberPart = parseInt(numberPart, 2); + if (endian) { + this.setUint8(originalIdx + byteNumber - i - 1, numberPart); + } else { + this.setUint8(originalIdx + i, numberPart); + } + } + return undefined; + } +} + +function getFloatDecorator(wrapped, name, doMapping) { + return function () { + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const originalIdx = arguments[0]; + if (originalIdx === undefined) { + wrapped.apply(this, arguments) + } + const endian = arguments[1]; + const byteNumber = (parseInt(name[name.length - 2] + name[name.length - 1]) || parseInt(name[name.length - 1])) / 8; + let binArray = []; + // Random access + this.getUint8(0); + for (let i = 0; i < byteNumber; i++) { + binArray[binArray.length] = this.getUint8(originalIdx + i); + } + if (endian) { + binArray = binArray.reverse() + } + if (byteNumber === 4) { + return unpackF32(binArray); + } else { + return unpackF64(binArray); + } + } +} + +function setFloatDecorator(wrapped, name, doMapping) { + return function () { + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const originalIdx = arguments[0]; + const value = arguments[1]; + if (originalIdx === undefined || value === undefined) { + wrapped.apply(this, arguments) + } + const endian = arguments[2]; + const byteNumber = (parseInt(name[name.length - 2] + name[name.length - 1]) || parseInt(name[name.length - 1])) / 8; + let binArray; + + // Random access + this.getUint8(0); + if (byteNumber === 4) { + binArray = packF32(value); + } else { + binArray = packF64(value); + } + for (let i = 0; i < binArray.length; i++) { + if (endian) { + this.setUint8(originalIdx + byteNumber - i - 1, binArray[i]); + } else { + this.setUint8(originalIdx + i, binArray[i]); + } + } + return undefined; + } +} + +function getBigIntDecorator(wrapped, doMapping) { + return function () { + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const originalIdx = arguments[0]; + if (originalIdx === undefined) { + wrapped.apply(this, arguments) + } + const endian = arguments[1]; + let hex = []; + let binArray = []; + for (let i = 0; i < 8; i++) { + binArray[binArray.length] = this.getUint8(originalIdx + i); + } + if (endian) { + binArray = binArray.reverse(); + } + for (let i of binArray) { + let h = i.toString(16); + if (h.length % 2) { + h = '0' + h; + } + hex.push(h); + } + let result = BigInt('0x' + hex.join('')); + if (binArray[0] >= 128) { + return result - 18446744073709551615n - 1n; + } + return result + } +} + +function setBigIntDecorator(wrapped, doMapping) { + return function () { + if (!doMapping){ + this.getUint8(0); + return wrapped.apply(this, arguments); + } + const originalIdx = arguments[0]; + let value = arguments[1]; + if (originalIdx === undefined || value === undefined || typeof value !== 'bigint') { + return wrapped.apply(this, arguments) + } + const endian = arguments[2]; + if (value < 0n) { + value = 18446744073709551615n + value + 1n; + } + let hex = BigInt(value).toString(16); + if (hex.length % 2) { + hex = '0' + hex; + } + + const len = hex.length / 2; + let binArray = []; + let j = 0; + // Random access + this.getUint8(0); + for (let i = 0; i < 8; i++) { + if (i < 8 - len) { + binArray[binArray.length] = 0; + } else { + binArray[binArray.length] = parseInt(hex.slice(j, j + 2), 16); + j += 2; + } + } + if (endian) { + binArray.reverse(); + } + for (let i in binArray) { + this.setUint8(originalIdx + parseInt(i), binArray[i]) + } + return undefined; + } +} + +function redefineDataViewFunctions(target, offsetF, doMapping) { + // Replace functions working with Ints + var dataViewTypes = ['getInt8', 'getInt16', 'getInt32', 'getUint8', 'getUint16', 'getUint32']; + for (type of dataViewTypes) { + target[type] = getByteDecorator(target[type], offsetF, type, doMapping); + type = 's' + type.substr(1); + target[type] = setByteDecorator(target[type], offsetF, type, doMapping); + } + + var dataViewTypes2 = ['getFloat32', 'getFloat64']; + for (type of dataViewTypes2) { + target[type] = getFloatDecorator(target[type], type, doMapping); + type = 's' + type.substr(1); + target[type] = setFloatDecorator(target[type], type, doMapping); + } + var dataViewTypes3 = ['getBigInt64', 'getBigUint64']; + for (type of dataViewTypes3) { + target[type] = getBigIntDecorator(target[type], doMapping); + type = 's' + type.substr(1); + target[type] = setBigIntDecorator(target[type], doMapping); + } + +}; + +(function () { + var common_function_body = ` + let _data; + if (typeof target === 'object' && target !== null) { + if (is_proxy in target){ + // If already Proxied array is passed as arg return it + return target; + } + } + _data = new originalF(...arguments); + // No offset + var offsetF = function(x) { + return x; + }; + if (doMapping) { + // Random numbers for offset function + let n = _data.length; + let a; + while (true){ + a = Math.floor(Math.random() * 4096); + if (gcd(a,n) === 1){ + break; + } + } + let b = Math.floor(Math.random() * 4096); + // Define function to calculate offset; + offsetF = function(x) { + if (x === undefined){ + return x; + } + x = x < 0 ? n + x : x; + return (a*x + b) % n ; + }; + let arr = [] + for (let i = 0; i < _data.length; i++) { + arr[i] = _data[i]; + } + + for (let i = 0; i < _data.length; i++) { + _data[offsetF(i)] = arr[i]; + } + } + let _target = target; + var proxy = new newProxy(_data, ${proxyHandler}); + // Proxy has to support all methods, original object supports. + ${offsetDecorator}; + ${redefineNewArrayFunctions}; + if (doMapping) { + // Methods have to work with offsets; + redefineNewArrayFunctions(proxy, offsetF); + } + // Preload array + let j; + for (let i = 0; i < _data['length']; i++) { + j = _data[i]; + } + return proxy; + `; + + var wrappers = [ + { + parent_object: 'window', + parent_object_property: 'DataView', + original_function: 'window.DataView', + wrapped_objects: [], + wrapping_function_args: 'buffer, byteOffset, byteLength', + helping_code: packIEEE754 + unpackIEEE754 + packF32 + unpackF32 + packF64 + unpackF64 + ` + function gcd(x, y) { + while(y) { + var t = y; + y = x % y; + x = t; + } + return x; + } + var doMapping = args[0]; + `, + wrapping_function_body: ` + let _data = new originalF(...arguments); + let n = _data.byteLength; + let a; + while (true){ + a = Math.floor(Math.random() * 4096); + if (gcd(a,n) === 1){ + break; + } + } + let b = Math.floor(Math.random() * 4096); + // Define function to calculate offset; + offsetF = function(x) { + if (x === undefined){ + return x; + } + x = x < 0 ? n + x : x; + return (a*x + b) % n ; + }; + ${getByteDecorator} + ${setByteDecorator} + ${getFloatDecorator} + ${setFloatDecorator} + ${getBigIntDecorator} + ${setBigIntDecorator} + ${redefineDataViewFunctions} + for (let i = 0; i < n; i++) { + let random = _data.getUint8(i); + } + if (!doMapping){ + offsetF = function(x) { + return x; + } + } + redefineDataViewFunctions(_data, offsetF, doMapping); + return _data; + `, + }, + ]; + + + let DEFAULT_TYPED_ARRAY_WRAPPER = { + parent_object: 'window', + parent_object_property: '_PROPERTY_', + original_function: 'window._PROPERTY_', + wrapped_objects: [], + helping_code:` + let doMapping = args[0]; + var proxyHandler = ${proxyHandler}; + function gcd(x, y) { + while(y) { + var t = y; + y = x % y; + x = t; + } + return x; + } + + const is_proxy = Symbol('is_proxy'); + const originalProxy = Proxy; + var proxyHandler = { + has (target, key) { + return (is_proxy === key) || (key in target); + } + }; + let newProxy = new Proxy(Proxy, { + construct(target, args) { + return new originalProxy(new target(...args), proxyHandler); + } + }); + `, + wrapping_function_args: `target`, + wrapping_function_body: common_function_body, + post_replacement_code: ` + ${constructDecorator} + ${redefineNewArrayConstructors} + redefineNewArrayConstructors(window._PROPERTY_); + ` + }; + + var typedTypes = ['Uint8Array', 'Int8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']; + for (let p of typedTypes) { + let wrapper = {...DEFAULT_TYPED_ARRAY_WRAPPER}; + wrapper.parent_object_property = wrapper.parent_object_property.replace('_PROPERTY_', p); + wrapper.original_function = wrapper.original_function.replace('_PROPERTY_', p); + wrapper.post_replacement_code = wrapper.post_replacement_code.split('_PROPERTY_').join(p); + wrapper.wrapping_function_body += `// ${p};`; + wrappers.push(wrapper); + } + + add_wrappers(wrappers); +})();