crucible 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +102 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +366 -0
- data/Rakefile +23 -0
- data/TESTING.md +319 -0
- data/config.sample.yml +48 -0
- data/crucible.gemspec +48 -0
- data/exe/crucible +122 -0
- data/lib/crucible/configuration.rb +212 -0
- data/lib/crucible/server.rb +123 -0
- data/lib/crucible/session_manager.rb +209 -0
- data/lib/crucible/stealth/evasions/chrome_app.js +75 -0
- data/lib/crucible/stealth/evasions/chrome_csi.js +33 -0
- data/lib/crucible/stealth/evasions/chrome_load_times.js +44 -0
- data/lib/crucible/stealth/evasions/chrome_runtime.js +190 -0
- data/lib/crucible/stealth/evasions/iframe_content_window.js +101 -0
- data/lib/crucible/stealth/evasions/media_codecs.js +65 -0
- data/lib/crucible/stealth/evasions/navigator_hardware_concurrency.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_languages.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_permissions.js +53 -0
- data/lib/crucible/stealth/evasions/navigator_plugins.js +261 -0
- data/lib/crucible/stealth/evasions/navigator_vendor.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_webdriver.js +16 -0
- data/lib/crucible/stealth/evasions/webgl_vendor.js +43 -0
- data/lib/crucible/stealth/evasions/window_outerdimensions.js +18 -0
- data/lib/crucible/stealth/utils.js +266 -0
- data/lib/crucible/stealth.rb +213 -0
- data/lib/crucible/tools/cookies.rb +206 -0
- data/lib/crucible/tools/downloads.rb +273 -0
- data/lib/crucible/tools/extraction.rb +335 -0
- data/lib/crucible/tools/helpers.rb +46 -0
- data/lib/crucible/tools/interaction.rb +355 -0
- data/lib/crucible/tools/navigation.rb +181 -0
- data/lib/crucible/tools/sessions.rb +85 -0
- data/lib/crucible/tools/stealth.rb +167 -0
- data/lib/crucible/tools.rb +42 -0
- data/lib/crucible/version.rb +5 -0
- data/lib/crucible.rb +60 -0
- metadata +201 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: chrome.runtime
|
|
3
|
+
* Mock the chrome.runtime object on secure origins.
|
|
4
|
+
*/
|
|
5
|
+
(function() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const utils = window._stealthUtils;
|
|
9
|
+
if (!utils) return;
|
|
10
|
+
|
|
11
|
+
if (!window.chrome) {
|
|
12
|
+
Object.defineProperty(window, 'chrome', {
|
|
13
|
+
writable: true,
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: false,
|
|
16
|
+
value: {}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const existsAlready = 'runtime' in window.chrome;
|
|
21
|
+
const isNotSecure = !window.location.protocol.startsWith('https');
|
|
22
|
+
if (existsAlready || isNotSecure) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STATIC_DATA = {
|
|
27
|
+
OnInstalledReason: {
|
|
28
|
+
CHROME_UPDATE: 'chrome_update',
|
|
29
|
+
INSTALL: 'install',
|
|
30
|
+
SHARED_MODULE_UPDATE: 'shared_module_update',
|
|
31
|
+
UPDATE: 'update'
|
|
32
|
+
},
|
|
33
|
+
OnRestartRequiredReason: {
|
|
34
|
+
APP_UPDATE: 'app_update',
|
|
35
|
+
OS_UPDATE: 'os_update',
|
|
36
|
+
PERIODIC: 'periodic'
|
|
37
|
+
},
|
|
38
|
+
PlatformArch: {
|
|
39
|
+
ARM: 'arm',
|
|
40
|
+
ARM64: 'arm64',
|
|
41
|
+
MIPS: 'mips',
|
|
42
|
+
MIPS64: 'mips64',
|
|
43
|
+
X86_32: 'x86-32',
|
|
44
|
+
X86_64: 'x86-64'
|
|
45
|
+
},
|
|
46
|
+
PlatformNaclArch: {
|
|
47
|
+
ARM: 'arm',
|
|
48
|
+
MIPS: 'mips',
|
|
49
|
+
MIPS64: 'mips64',
|
|
50
|
+
X86_32: 'x86-32',
|
|
51
|
+
X86_64: 'x86-64'
|
|
52
|
+
},
|
|
53
|
+
PlatformOs: {
|
|
54
|
+
ANDROID: 'android',
|
|
55
|
+
CROS: 'cros',
|
|
56
|
+
LINUX: 'linux',
|
|
57
|
+
MAC: 'mac',
|
|
58
|
+
OPENBSD: 'openbsd',
|
|
59
|
+
WIN: 'win'
|
|
60
|
+
},
|
|
61
|
+
RequestUpdateCheckStatus: {
|
|
62
|
+
NO_UPDATE: 'no_update',
|
|
63
|
+
THROTTLED: 'throttled',
|
|
64
|
+
UPDATE_AVAILABLE: 'update_available'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
window.chrome.runtime = {
|
|
69
|
+
...STATIC_DATA,
|
|
70
|
+
get id() {
|
|
71
|
+
return undefined;
|
|
72
|
+
},
|
|
73
|
+
connect: null,
|
|
74
|
+
sendMessage: null
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
|
|
78
|
+
NoMatchingSignature: new TypeError(preamble + 'No matching signature.'),
|
|
79
|
+
MustSpecifyExtensionID: new TypeError(
|
|
80
|
+
preamble + `${method} called from a webpage must specify an Extension ID (string) for its first argument.`
|
|
81
|
+
),
|
|
82
|
+
InvalidExtensionID: new TypeError(preamble + `Invalid extension id: '${extensionId}'`)
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const isValidExtensionID = str =>
|
|
86
|
+
str.length === 32 && str.toLowerCase().match(/^[a-p]+$/);
|
|
87
|
+
|
|
88
|
+
// Mock sendMessage
|
|
89
|
+
const sendMessageHandler = {
|
|
90
|
+
apply: function(target, ctx, args) {
|
|
91
|
+
const [extensionId, options, responseCallback] = args || [];
|
|
92
|
+
const errorPreamble = 'Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ';
|
|
93
|
+
const Errors = makeCustomRuntimeErrors(errorPreamble, 'chrome.runtime.sendMessage()', extensionId);
|
|
94
|
+
|
|
95
|
+
const noArguments = args.length === 0;
|
|
96
|
+
const tooManyArguments = args.length > 4;
|
|
97
|
+
const incorrectOptions = options && typeof options !== 'object';
|
|
98
|
+
const incorrectResponseCallback = responseCallback && typeof responseCallback !== 'function';
|
|
99
|
+
|
|
100
|
+
if (noArguments || tooManyArguments || incorrectOptions || incorrectResponseCallback) {
|
|
101
|
+
throw Errors.NoMatchingSignature;
|
|
102
|
+
}
|
|
103
|
+
if (args.length < 2) {
|
|
104
|
+
throw Errors.MustSpecifyExtensionID;
|
|
105
|
+
}
|
|
106
|
+
if (typeof extensionId !== 'string') {
|
|
107
|
+
throw Errors.NoMatchingSignature;
|
|
108
|
+
}
|
|
109
|
+
if (!isValidExtensionID(extensionId)) {
|
|
110
|
+
throw Errors.InvalidExtensionID;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
utils.mockWithProxy(
|
|
117
|
+
window.chrome.runtime,
|
|
118
|
+
'sendMessage',
|
|
119
|
+
function sendMessage() {},
|
|
120
|
+
sendMessageHandler
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Mock connect
|
|
124
|
+
const connectHandler = {
|
|
125
|
+
apply: function(target, ctx, args) {
|
|
126
|
+
const [extensionId, connectInfo] = args || [];
|
|
127
|
+
const errorPreamble = 'Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ';
|
|
128
|
+
const Errors = makeCustomRuntimeErrors(errorPreamble, 'chrome.runtime.connect()', extensionId);
|
|
129
|
+
|
|
130
|
+
const noArguments = args.length === 0;
|
|
131
|
+
const emptyStringArgument = args.length === 1 && extensionId === '';
|
|
132
|
+
if (noArguments || emptyStringArgument) {
|
|
133
|
+
throw Errors.MustSpecifyExtensionID;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const tooManyArguments = args.length > 2;
|
|
137
|
+
const incorrectConnectInfoType = connectInfo && typeof connectInfo !== 'object';
|
|
138
|
+
if (tooManyArguments || incorrectConnectInfoType) {
|
|
139
|
+
throw Errors.NoMatchingSignature;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const extensionIdIsString = typeof extensionId === 'string';
|
|
143
|
+
if (extensionIdIsString && extensionId === '') {
|
|
144
|
+
throw Errors.MustSpecifyExtensionID;
|
|
145
|
+
}
|
|
146
|
+
if (extensionIdIsString && !isValidExtensionID(extensionId)) {
|
|
147
|
+
throw Errors.InvalidExtensionID;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof extensionId === 'object') {
|
|
151
|
+
if (args.length > 1) throw Errors.NoMatchingSignature;
|
|
152
|
+
if (Object.keys(extensionId).length === 0) throw Errors.MustSpecifyExtensionID;
|
|
153
|
+
throw Errors.MustSpecifyExtensionID;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return utils.patchToStringNested(makeConnectResponse());
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
utils.mockWithProxy(
|
|
161
|
+
window.chrome.runtime,
|
|
162
|
+
'connect',
|
|
163
|
+
function connect() {},
|
|
164
|
+
connectHandler
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
function makeConnectResponse() {
|
|
168
|
+
const onSomething = () => ({
|
|
169
|
+
addListener: function addListener() {},
|
|
170
|
+
dispatch: function dispatch() {},
|
|
171
|
+
hasListener: function hasListener() {},
|
|
172
|
+
hasListeners: function hasListeners() { return false; },
|
|
173
|
+
removeListener: function removeListener() {}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: '',
|
|
178
|
+
sender: undefined,
|
|
179
|
+
disconnect: function disconnect() {},
|
|
180
|
+
onDisconnect: onSomething(),
|
|
181
|
+
onMessage: onSomething(),
|
|
182
|
+
postMessage: function postMessage() {
|
|
183
|
+
if (!arguments.length) {
|
|
184
|
+
throw new TypeError('Insufficient number of arguments.');
|
|
185
|
+
}
|
|
186
|
+
throw new Error('Attempting to use a disconnected port object');
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: iframe.contentWindow
|
|
3
|
+
* Fix for the HEADCHR_IFRAME detection (iframe.contentWindow.chrome).
|
|
4
|
+
* Only srcdoc powered iframes cause issues due to a chromium bug.
|
|
5
|
+
*/
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const utils = window._stealthUtils;
|
|
10
|
+
if (!utils) return;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Adds a contentWindow proxy to the provided iframe element
|
|
14
|
+
const addContentWindowProxy = iframe => {
|
|
15
|
+
const contentWindowProxy = {
|
|
16
|
+
get(target, key) {
|
|
17
|
+
// Make this thing behave like a regular iframe window
|
|
18
|
+
if (key === 'self') {
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
if (key === 'frameElement') {
|
|
22
|
+
return iframe;
|
|
23
|
+
}
|
|
24
|
+
// Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy
|
|
25
|
+
if (key === '0') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return Reflect.get(target, key);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (!iframe.contentWindow) {
|
|
33
|
+
const proxy = new Proxy(window, contentWindowProxy);
|
|
34
|
+
Object.defineProperty(iframe, 'contentWindow', {
|
|
35
|
+
get() {
|
|
36
|
+
return proxy;
|
|
37
|
+
},
|
|
38
|
+
set(newValue) {
|
|
39
|
+
return newValue; // contentWindow is immutable
|
|
40
|
+
},
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: false
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Handles iframe element creation, augments srcdoc property
|
|
48
|
+
const handleIframeCreation = (target, thisArg, args) => {
|
|
49
|
+
const iframe = target.apply(thisArg, args);
|
|
50
|
+
|
|
51
|
+
const _iframe = iframe;
|
|
52
|
+
const _srcdoc = _iframe.srcdoc;
|
|
53
|
+
|
|
54
|
+
// Add hook for the srcdoc property
|
|
55
|
+
Object.defineProperty(iframe, 'srcdoc', {
|
|
56
|
+
configurable: true,
|
|
57
|
+
get: function() {
|
|
58
|
+
return _srcdoc;
|
|
59
|
+
},
|
|
60
|
+
set: function(newValue) {
|
|
61
|
+
addContentWindowProxy(this);
|
|
62
|
+
// Reset property, the hook is only needed once
|
|
63
|
+
Object.defineProperty(iframe, 'srcdoc', {
|
|
64
|
+
configurable: false,
|
|
65
|
+
writable: false,
|
|
66
|
+
value: _srcdoc
|
|
67
|
+
});
|
|
68
|
+
_iframe.srcdoc = newValue;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return iframe;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Adds a hook to intercept iframe creation events
|
|
75
|
+
const addIframeCreationSniffer = () => {
|
|
76
|
+
const createElementHandler = {
|
|
77
|
+
get(target, key) {
|
|
78
|
+
return Reflect.get(target, key);
|
|
79
|
+
},
|
|
80
|
+
apply: function(target, thisArg, args) {
|
|
81
|
+
const isIframe =
|
|
82
|
+
args && args.length && `${args[0]}`.toLowerCase() === 'iframe';
|
|
83
|
+
if (!isIframe) {
|
|
84
|
+
return target.apply(thisArg, args);
|
|
85
|
+
} else {
|
|
86
|
+
return handleIframeCreation(target, thisArg, args);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
utils.replaceWithProxy(
|
|
91
|
+
document,
|
|
92
|
+
'createElement',
|
|
93
|
+
createElementHandler
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
addIframeCreationSniffer();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Silently fail
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: media.codecs
|
|
3
|
+
* Fix Chromium not reporting "probably" to proprietary codecs.
|
|
4
|
+
* Chromium doesn't support proprietary codecs, only Chrome does.
|
|
5
|
+
*/
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const utils = window._stealthUtils;
|
|
10
|
+
if (!utils) return;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse media type input to extract mime type and codecs.
|
|
14
|
+
* @param {String} arg - Input like 'video/mp4; codecs="avc1.42E01E"'
|
|
15
|
+
*/
|
|
16
|
+
const parseInput = arg => {
|
|
17
|
+
const [mime, codecStr] = arg.trim().split(';');
|
|
18
|
+
let codecs = [];
|
|
19
|
+
if (codecStr && codecStr.includes('codecs="')) {
|
|
20
|
+
codecs = codecStr
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(`codecs="`, '')
|
|
23
|
+
.replace(`"`, '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split(',')
|
|
26
|
+
.filter(x => !!x)
|
|
27
|
+
.map(x => x.trim());
|
|
28
|
+
}
|
|
29
|
+
return { mime, codecStr, codecs };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const canPlayType = {
|
|
33
|
+
apply: function(target, ctx, args) {
|
|
34
|
+
if (!args || !args.length) {
|
|
35
|
+
return target.apply(ctx, args);
|
|
36
|
+
}
|
|
37
|
+
const { mime, codecs } = parseInput(args[0]);
|
|
38
|
+
|
|
39
|
+
// This specific mp4 codec is missing in Chromium
|
|
40
|
+
if (mime === 'video/mp4') {
|
|
41
|
+
if (codecs.includes('avc1.42E01E')) {
|
|
42
|
+
return 'probably';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// This mimetype is only supported if no codecs are specified
|
|
46
|
+
if (mime === 'audio/x-m4a' && !codecs.length) {
|
|
47
|
+
return 'maybe';
|
|
48
|
+
}
|
|
49
|
+
// This mimetype is only supported if no codecs are specified
|
|
50
|
+
if (mime === 'audio/aac' && !codecs.length) {
|
|
51
|
+
return 'probably';
|
|
52
|
+
}
|
|
53
|
+
// Everything else as usual
|
|
54
|
+
return target.apply(ctx, args);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (typeof HTMLMediaElement !== 'undefined') {
|
|
59
|
+
utils.replaceWithProxy(
|
|
60
|
+
HTMLMediaElement.prototype,
|
|
61
|
+
'canPlayType',
|
|
62
|
+
canPlayType
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: navigator.hardwareConcurrency
|
|
3
|
+
* Set the hardwareConcurrency to a reasonable value (default: 4).
|
|
4
|
+
*/
|
|
5
|
+
(function(opts) {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const utils = window._stealthUtils;
|
|
9
|
+
if (!utils) return;
|
|
10
|
+
|
|
11
|
+
const hardwareConcurrency = opts.hardwareConcurrency || 4;
|
|
12
|
+
|
|
13
|
+
utils.replaceGetterWithProxy(
|
|
14
|
+
Object.getPrototypeOf(navigator),
|
|
15
|
+
'hardwareConcurrency',
|
|
16
|
+
utils.makeHandler().getterValue(hardwareConcurrency)
|
|
17
|
+
);
|
|
18
|
+
})({ hardwareConcurrency: null }); // Will be replaced by Ruby
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: navigator.languages
|
|
3
|
+
* Override navigator.languages to match Accept-Language header.
|
|
4
|
+
*/
|
|
5
|
+
(function(opts) {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const utils = window._stealthUtils;
|
|
9
|
+
if (!utils) return;
|
|
10
|
+
|
|
11
|
+
const languages = opts.languages || ['en-US', 'en'];
|
|
12
|
+
|
|
13
|
+
utils.replaceGetterWithProxy(
|
|
14
|
+
Object.getPrototypeOf(navigator),
|
|
15
|
+
'languages',
|
|
16
|
+
utils.makeHandler().getterValue(Object.freeze([...languages]))
|
|
17
|
+
);
|
|
18
|
+
})({ languages: null }); // Will be replaced by Ruby
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: navigator.permissions
|
|
3
|
+
* Fix Notification.permission behaving weirdly in headless mode.
|
|
4
|
+
* On secure origins the permission should be "default", not "denied".
|
|
5
|
+
*/
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const utils = window._stealthUtils;
|
|
10
|
+
if (!utils) return;
|
|
11
|
+
|
|
12
|
+
const isSecure = document.location.protocol.startsWith('https');
|
|
13
|
+
|
|
14
|
+
// In headful on secure origins the permission should be "default", not "denied"
|
|
15
|
+
if (isSecure) {
|
|
16
|
+
if (typeof Notification !== 'undefined') {
|
|
17
|
+
utils.replaceGetterWithProxy(Notification, 'permission', {
|
|
18
|
+
apply() {
|
|
19
|
+
return 'default';
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// On insecure origins in headful the state is "denied",
|
|
26
|
+
// whereas in headless it's "prompt"
|
|
27
|
+
if (!isSecure) {
|
|
28
|
+
if (typeof Permissions !== 'undefined' && typeof PermissionStatus !== 'undefined') {
|
|
29
|
+
const handler = {
|
|
30
|
+
apply(target, ctx, args) {
|
|
31
|
+
const param = (args || [])[0];
|
|
32
|
+
|
|
33
|
+
const isNotifications =
|
|
34
|
+
param && param.name && param.name === 'notifications';
|
|
35
|
+
if (!isNotifications) {
|
|
36
|
+
return utils.cache.Reflect.apply(...arguments);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Promise.resolve(
|
|
40
|
+
Object.setPrototypeOf(
|
|
41
|
+
{
|
|
42
|
+
state: 'denied',
|
|
43
|
+
onchange: null
|
|
44
|
+
},
|
|
45
|
+
PermissionStatus.prototype
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
utils.replaceWithProxy(Permissions.prototype, 'query', handler);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evasion: navigator.plugins
|
|
3
|
+
* In headless mode navigator.mimeTypes and navigator.plugins are empty.
|
|
4
|
+
* This plugin emulates both with functional mocks to match regular headful Chrome.
|
|
5
|
+
*/
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const utils = window._stealthUtils;
|
|
10
|
+
if (!utils) return;
|
|
11
|
+
|
|
12
|
+
// That means we're running headful
|
|
13
|
+
const hasPlugins = 'plugins' in navigator && navigator.plugins.length;
|
|
14
|
+
if (hasPlugins) {
|
|
15
|
+
return; // Nothing to do here
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Plugin and MimeType data (matches Chrome)
|
|
19
|
+
const mimeTypesData = [
|
|
20
|
+
{
|
|
21
|
+
type: 'application/pdf',
|
|
22
|
+
suffixes: 'pdf',
|
|
23
|
+
description: '',
|
|
24
|
+
__pluginName: 'Chrome PDF Viewer'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'application/x-google-chrome-pdf',
|
|
28
|
+
suffixes: 'pdf',
|
|
29
|
+
description: 'Portable Document Format',
|
|
30
|
+
__pluginName: 'Chrome PDF Plugin'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'application/x-nacl',
|
|
34
|
+
suffixes: '',
|
|
35
|
+
description: 'Native Client Executable',
|
|
36
|
+
__pluginName: 'Native Client'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'application/x-pnacl',
|
|
40
|
+
suffixes: '',
|
|
41
|
+
description: 'Portable Native Client Executable',
|
|
42
|
+
__pluginName: 'Native Client'
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const pluginsData = [
|
|
47
|
+
{
|
|
48
|
+
name: 'Chrome PDF Plugin',
|
|
49
|
+
filename: 'internal-pdf-viewer',
|
|
50
|
+
description: 'Portable Document Format',
|
|
51
|
+
__mimeTypes: ['application/x-google-chrome-pdf']
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Chrome PDF Viewer',
|
|
55
|
+
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
|
|
56
|
+
description: '',
|
|
57
|
+
__mimeTypes: ['application/pdf']
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Native Client',
|
|
61
|
+
filename: 'internal-nacl-plugin',
|
|
62
|
+
description: '',
|
|
63
|
+
__mimeTypes: ['application/x-nacl', 'application/x-pnacl']
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Helper to define properties with vanilla descriptors
|
|
68
|
+
const defineProp = (obj, prop, value) =>
|
|
69
|
+
Object.defineProperty(obj, prop, {
|
|
70
|
+
value,
|
|
71
|
+
writable: false,
|
|
72
|
+
enumerable: false,
|
|
73
|
+
configurable: true
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Generate function mocks for item/namedItem/refresh
|
|
77
|
+
const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
|
|
78
|
+
item: utils.createProxy(proto.item, {
|
|
79
|
+
apply(target, ctx, args) {
|
|
80
|
+
if (!args.length) {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`Failed to execute 'item' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const isInteger = args[0] && Number.isInteger(Number(args[0]));
|
|
86
|
+
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
namedItem: utils.createProxy(proto.namedItem, {
|
|
90
|
+
apply(target, ctx, args) {
|
|
91
|
+
if (!args.length) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`Failed to execute 'namedItem' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return dataArray.find(mt => mt[itemMainProp] === args[0]) || null;
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
refresh: proto.refresh
|
|
100
|
+
? utils.createProxy(proto.refresh, {
|
|
101
|
+
apply(target, ctx, args) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
: undefined
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Generate a magic array (MimeTypeArray or PluginArray)
|
|
109
|
+
const generateMagicArray = (
|
|
110
|
+
dataArray,
|
|
111
|
+
proto,
|
|
112
|
+
itemProto,
|
|
113
|
+
itemMainProp
|
|
114
|
+
) => {
|
|
115
|
+
const makeItem = data => {
|
|
116
|
+
const item = {};
|
|
117
|
+
for (const prop of Object.keys(data)) {
|
|
118
|
+
if (prop.startsWith('__')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
defineProp(item, prop, data[prop]);
|
|
122
|
+
}
|
|
123
|
+
return patchItem(item, data);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const patchItem = (item, data) => {
|
|
127
|
+
let descriptor = Object.getOwnPropertyDescriptors(item);
|
|
128
|
+
|
|
129
|
+
// Plugins have a magic length property
|
|
130
|
+
if (itemProto === Plugin.prototype) {
|
|
131
|
+
descriptor = {
|
|
132
|
+
...descriptor,
|
|
133
|
+
length: {
|
|
134
|
+
value: data.__mimeTypes.length,
|
|
135
|
+
writable: false,
|
|
136
|
+
enumerable: false,
|
|
137
|
+
configurable: true
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const obj = Object.create(itemProto, descriptor);
|
|
143
|
+
const blacklist = [...Object.keys(data), 'length', 'enabledPlugin'];
|
|
144
|
+
|
|
145
|
+
return new Proxy(obj, {
|
|
146
|
+
ownKeys(target) {
|
|
147
|
+
return Reflect.ownKeys(target).filter(k => !blacklist.includes(k));
|
|
148
|
+
},
|
|
149
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
150
|
+
if (blacklist.includes(prop)) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const magicArray = [];
|
|
159
|
+
|
|
160
|
+
dataArray.forEach(data => {
|
|
161
|
+
magicArray.push(makeItem(data));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Add direct property access based on types
|
|
165
|
+
magicArray.forEach(entry => {
|
|
166
|
+
defineProp(magicArray, entry[itemMainProp], entry);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const magicArrayObj = Object.create(proto, {
|
|
170
|
+
...Object.getOwnPropertyDescriptors(magicArray),
|
|
171
|
+
length: {
|
|
172
|
+
value: magicArray.length,
|
|
173
|
+
writable: false,
|
|
174
|
+
enumerable: false,
|
|
175
|
+
configurable: true
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const functionMocks = generateFunctionMocks(proto, itemMainProp, magicArray);
|
|
180
|
+
|
|
181
|
+
const magicArrayObjProxy = new Proxy(magicArrayObj, {
|
|
182
|
+
get(target, key = '') {
|
|
183
|
+
if (key === 'item') {
|
|
184
|
+
return functionMocks.item;
|
|
185
|
+
}
|
|
186
|
+
if (key === 'namedItem') {
|
|
187
|
+
return functionMocks.namedItem;
|
|
188
|
+
}
|
|
189
|
+
if (proto === PluginArray.prototype && key === 'refresh') {
|
|
190
|
+
return functionMocks.refresh;
|
|
191
|
+
}
|
|
192
|
+
return utils.cache.Reflect.get(...arguments);
|
|
193
|
+
},
|
|
194
|
+
ownKeys(target) {
|
|
195
|
+
const keys = [];
|
|
196
|
+
const typeProps = magicArray.map(mt => mt[itemMainProp]);
|
|
197
|
+
typeProps.forEach((_, i) => keys.push(`${i}`));
|
|
198
|
+
typeProps.forEach(propName => keys.push(propName));
|
|
199
|
+
return keys;
|
|
200
|
+
},
|
|
201
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
202
|
+
if (prop === 'length') {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return magicArrayObjProxy;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Generate MimeTypeArray
|
|
213
|
+
const mimeTypes = generateMagicArray(
|
|
214
|
+
mimeTypesData,
|
|
215
|
+
MimeTypeArray.prototype,
|
|
216
|
+
MimeType.prototype,
|
|
217
|
+
'type'
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Generate PluginArray
|
|
221
|
+
const plugins = generateMagicArray(
|
|
222
|
+
pluginsData,
|
|
223
|
+
PluginArray.prototype,
|
|
224
|
+
Plugin.prototype,
|
|
225
|
+
'name'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Cross-reference plugins and mimeTypes
|
|
229
|
+
for (const pluginData of pluginsData) {
|
|
230
|
+
pluginData.__mimeTypes.forEach((type, index) => {
|
|
231
|
+
plugins[pluginData.name][index] = mimeTypes[type];
|
|
232
|
+
|
|
233
|
+
Object.defineProperty(plugins[pluginData.name], type, {
|
|
234
|
+
value: mimeTypes[type],
|
|
235
|
+
writable: false,
|
|
236
|
+
enumerable: false,
|
|
237
|
+
configurable: true
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(mimeTypes[type], 'enabledPlugin', {
|
|
240
|
+
value:
|
|
241
|
+
type === 'application/x-pnacl'
|
|
242
|
+
? mimeTypes['application/x-nacl'].enabledPlugin
|
|
243
|
+
: new Proxy(plugins[pluginData.name], {}),
|
|
244
|
+
writable: false,
|
|
245
|
+
enumerable: false,
|
|
246
|
+
configurable: true
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Patch navigator
|
|
252
|
+
const patchNavigator = (name, value) =>
|
|
253
|
+
utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
|
|
254
|
+
get() {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
patchNavigator('mimeTypes', mimeTypes);
|
|
260
|
+
patchNavigator('plugins', plugins);
|
|
261
|
+
})();
|