Open t2ym opened 6 years ago
__hook__('op', ThisObject, ['prop'], __context_mapper__[1])
$hook$.global(__hook__, __context_mapper__[24], 'o', 'let')[__context_mapper__[25]]
__hook__[Symbol()]
__context_mapper__
objects cannot be retrieved via window
object
for (let name in this) { /* name does not match with context mapper names */ }
$hook$.$ = function contextSymbolGenerator(symbolToContext, contexts) {
let result = [];
let contextToSymbol = {};
let hookGlobal = hook.global;
for (let i = 0; i < contexts.length; i++) {
symbolToContext[result[i] = _Symbol()] = contexts[i];
contextToSymbol[contexts[i]] = result[i];
hookGlobal[result[i]] = contextToSymbol;
}
return result;
}
__context_mapper__
is actually __
+ hex(sha256(context + code))
+ __
)const __context_mapper__ = $hook$.$(__hook__, [
'examples/example2.js,C',
'_p_C;examples/example2.js,C',
'examples/example2.js,C,add',
'examples/example2.js,C,add,plus'
]);
$hook$.global(__hook__, __context_mapper__[0], 'C', 'class')[__context_mapper__[1]] = class C {
add(a, b) {
return __hook__((a = 1, b = 2) => {
let plus = (...args) => __hook__((x, y) => x + y, null, args, __context_mapper__[3]);
return __hook__(plus, null, [
a,
b
], __context_mapper__[2], 0);
}, null, arguments, __context_mapper__[2]);
}
};
cache-automation.js
to automate cache collection for cache-bundle.json
cache-bundle.json
containing cache-automation.js
contents and serverSecret
, which is a random hex value as belowserverSecret
is lost, the application cannot accept any automation scripts from a special cache-bundle.json
since the matching sha256(serverSecret + code) === authorization
is checked against authorization
from <script src="cache-bundle.js?no-hook=true&authorization={VALUE}">
, which is embedded as a fixed value for the build.
// gulpfile.js (extracted)
const serverSecret = crypto.randomFillSync(Buffer.alloc(32)).toString('hex');
const cacheBundlePath = path.join('demo', 'cache-bundle.json');
const cacheAutomationScriptPath = path.join('demo', 'cache-automation.js');
const cacheAutomationScript = fs.readFileSync(cacheAutomationScriptPath, 'UTF-8');
let version = 'version_1';
let authorization; // sha256(serverSecret + cacheAutomationScript)
let hash = hook.utils.createHash('sha256');
hash.update(serverSecret + cacheAutomationScript);
authorization = hash.digest('hex');
gulp.task('get-version', (done) => { return gulp.src(['demo/original-index.html'], { base: 'demo' }) .pipe(through.obj((file, enc, callback) => { let html = String(file.contents); let versionIndex = html.indexOf('/hook.min.js?version=') + '/hook.min.js?version='.length; let versionIndexEnd = html.indexOf('&', versionIndex); version = 'version_' + html.substring(versionIndex, versionIndexEnd); callback(null, file); })) .pipe(through.obj((file, enc, callback) => { done(); })); });
// One-time special cache-bundle.json generation gulp.task('cache-bundle-automation-json', (done) => { fs.writeFileSync(cacheBundlePath, JSON.stringify({ "version": version, "https://thin-hook.localhost.localdomain/automation.json": JSON.stringify({ "state": "init", // update state in the script to perform operations including reloading "serverSecret": serverSecret, "script": cacheAutomationScript },null,0) },null,2)) done(); });
- Embed `hex(sha256(serverSecret + cacheAutomationScriptCode))` as `<script context-generator src="cache-bundle.js?no-hook=true&authorization={HERE}"></script>`
- In `cacheBundleGeneration.js`, wait for the global value `__{serverSecret}__` to obtain the raw cache-bundle JSON and save as `cache-bundle.json` after normalization. The one-time `serverSecret` is lost at this overwriting of `cache-bundle.json`
```javascript
// cacheBundleGeneration.js (extracted)
let rawCacheBundleJSON;
while (!rawCacheBundleJSON) {
try {
rawCacheBundleJSON = await page.evaluate(new Function(`return async function cacheBundle() {
try {
return __${serverSecret}__; // the variable disappears once read
}
catch (e) {
return [][0]; // undefined;
}
}`)());
}
catch (e) {
// try again
console.log(e.message);
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
console.log('cacheBundle raw length = ', rawCacheBundleJSON.length, ' bytes');
Example cache-automation.js
can be customized for the target application
cache-automation.js
is hooked
https://thin-hook.localhost.localdomain/automation.json
cache-automation.js
cache-automation.js
async function automationFunction()
has to come in the first line of the source code
async function automationFunction() {
/*
@license https://github.com/t2ym/thin-hook/blob/master/LICENSE.md
Copyright (c) 2018, Tetsuya Mori <t2y3141592@gmail.com>. All rights reserved.
*/
const timeoutForBundleSetFetched = 60000; // 60sec
// wait for bundle-set-fetched event
await new Promise((resolve, reject) => {
const start = Date.now();
let intervalId = setInterval(async () => {
const now = Date.now();
if (now - start > timeoutForBundleSetFetched) {
clearInterval(intervalId);
reject(new Error('timeout for bundle-set-fetched'));
}
try {
let model = document.querySelector('live-localizer').shadowRoot
.getElementById('main').shadowRoot
.getElementById('dialog')
.querySelector('live-localizer-panel').shadowRoot
.getElementById('model');
if (model) {
clearInterval(intervalId);
// Note: bundle-set-fetched is the load completion event for live-localizer widget and irrelevant to cache-bundle.json
model.addEventListener('bundle-set-fetched', (event) => {
resolve(event.type);
});
}
else {
// try again
}
}
catch (e) {
// try again
}
}, 1000);
});
await new Promise(async (resolve, reject) => { try { let menuItems = document.querySelector('my-app').shadowRoot .children[3] // app-drawer-layout .querySelector('app-drawer') .querySelector('iron-selector') .querySelectorAll('a'); let result = []; for (let i = menuItems.length - 1; i >= 0; i--) { menuItems[i].click(); result.push(menuItems[i].href); await new Promise(_resolve => { setTimeout(_resolve, 20000); // Note: It is better to wait for specific events or conditions than just for a fixed period }); } resolve(result); } catch (e) { reject(e.message); } }); }
puppeteer
are now performed AFTER the cache-bundle.json
generation
Block access via automation like puppeteer
Current Status
thin-hook@0.2.0
in stack branch merged to develop and master branchesnpm i thin-hook@0.2.0
ornpm i thin-hook
Update README.md for the next release
0.2.0
stack
branch<!-- end of mandatory no-hook scripts -->
const enableDebugging = false
inhook-callback.js
:true
to enable debuggingconst devToolsDisabled = true
indisable-devtools.js
:false
to enable debuggerconst wildcardWhitelist = []
inhook-callback.js
: for Chromenew Error().stack
formatnew RegExp('^at (.* [(])?' + origin + '/components/'), // trust the site contents including other components
new RegExp('^at ([^(]* [(])?' + 'https://cdnjs.cloudflare.com/ajax/libs/vis/4[.]18[.]1/vis[.]min[.]js'),
new RegExp('^at ([^(]* [(])?' + 'https://www.gstatic.com/charts/loader[.]js'),
const excludes = new Set() : { 'window.Math' }
undefined
for global objects unless they are in whitelistshook-native-api.js
deprecated and moved tohook-callback.js
Stack
class forcontextStack
instead ofArray
about:blank
for non-whitelisted global object accessconst URL = window.URL, RegExp = window.RegExp, ...
__hook__
arguments for global objects$hook$.$
context symbol generatorconst __context_mapper__
object__context_mapper__ = __ + hex(sha256(context + code)) + __
__hook__('op', ThisObject, ['prop'], __context_mapper__[1])
$hook$.global(__hook__, __context_mapper__[24], 'o', 'let')[__context_mapper__[25]]
__hook__[Symbol()]
-> contextcache-bundle.json
get-version
cache-bundle-automation-json
cacheBundleGeneration.js
cache-automation.js
https://thin-hook.localhost.localdomain/automation.json
cache-bundle.json
forcache-automation.js
serverSecret
- one-time build-time-only secret for validatingcache-automation.js
Proof of Concept Implementation
[x] Hook native global object access
DomContentLoaded
event<!-- end of mandatory no-hook scripts -->
tooriginal-index.html
caches
is inaccessible at the firstdomcontentloaded
eventPage.frameStartedLoading
event, there should be HOPEFULLY no chances of inspecting the main document untildomcontentloaded
. It is not verified whether any events betweenPage.frameStartedLoading
anddomcontentloaded
are fired or not.window
object propertiesObject.prototype.constructor
(=== Object
)EventTarget.prototype
object properties such asaddEventListener
Object.prototype
object properties such as__lookupGetter__
__proto__
window
object properties[ ] Block access
undefined
for any global object accessundefined
for own native properties ofwindow
undefined
for own non-native properties ofwindow
undefined
for globals from no-hook APIshook
,$hook$
, etc.) properly__hook__
by using Symbols for contextshook.eval('__hook__',...)('script')
by usingSymbol.for('__hook__')
hook.utils.createHash
andhook.utils.HTMLParser
via automationabout:blank
transitioncaches
[ ] Handle whitelists
[ ] Handle async callbacks
[ ] Allow
gulp cache-bundle
to fetch the bundle exclusively and securelycache-automation.js
to automate navigation for cache collectioncache-automation.js
and move cache operations tocache-bundle.js
cache-automation.js
performs only UI navigations for cache target UIs[x] Eliminate global object access without contextStack in hooked scripts
[ ] Update tests
$hook$.global(...)
in the expected hooked results[ ] Add puppeteer tests to
cacheBundleGeneration.js
caches
this.__proto__.__proto__.__proto__.__proto__.constructor
(=== Object
)Polymer
__lookupGetter__
originally fromObject.prototype
addEventListener
originally fromEventTarget.prototype
this.__proto__.__proto__.__proto__.__proto__.__lookupGetter__
Math.__lookupGetter__
Math.abs.__lookupGetter__
__hook__('.', this, ['navigator'], 'context')
__hook__('.', this, ['navigator'], Symbol.for('context'))
hook.eval('__hook__')('Object')
hook.utils.createHash.sha256
[ ] Reduce Overheads
hook.min.js
thin-hook/lib/*.js
scriptst2ym/espree
t2ym/escodegen
acorn
hook-callback.js
$hook$.global()
[ ] Issues
event
is mistreated as a global object insetAttribute('onXX', 'event.target')
{ event: true }
is missing in the initial scope of hookingenableDebugging
flag inhook-callback.js
Object.$__proto__$
is unexpectedly defined by__hook__()
globalObjectAccess.constructor
unexpectedly points toObject
while it is expected as a normal property of theglobalObjectAccess
objectObject['/components/polymer/lib/mixins/property-accessors.html,script@741,props']
is unexpectedly defined by__hook__()
hook-native-api.js
hook-prefix=_uNpREdiC4aB1e_
compact=false
is not supported by context symbols]);
must be looked up instead of']);
hook.encodeHtml()
,</head></html>
is inserted at a wrong position before<!-- end of mandatory no-hook scripts -->
Notes
(automated withnpm run cache-bundle
fails as expected for nowcache-automation.js
)demo/cache-bundle.json
can be generated via?cache-bundle=save
option, but can be automated withcache-automation.js