-
Notifications
You must be signed in to change notification settings - Fork 280
Expand file tree
/
Copy pathconfig.js
More file actions
255 lines (212 loc) · 9.55 KB
/
Copy pathconfig.js
File metadata and controls
255 lines (212 loc) · 9.55 KB
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/**************************************************************************************************
*
* This file defines various config parameters, used later within the other scripts.
*
* In all cases, you'll want to set CERT_PEM and likely PROXY_HOST and PROXY_PORT.
*
* Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-FileCopyrightText: Tim Perry <tim@httptoolkit.com>
*
*************************************************************************************************/
// Put your CA certificate data here in PEM format:
const CERT_PEM = `-----BEGIN CERTIFICATE-----
[!! Put your CA certificate data here, in PEM format !!]
-----END CERTIFICATE-----`;
// Put your intercepting proxy's address here:
const PROXY_HOST = '127.0.0.1';
const PROXY_PORT = 8000;
// If you like, set to to true to enable extra logging:
const DEBUG_MODE = false;
// If you find issues with non-HTTP traffic being captured (due to the
// native connect hook script) you can add ports here to exempt traffic
// on that port from being redirected. Note that this will only affect
// traffic captured by the raw connection hook - for apps using the
// system HTTP proxy settings, traffic on these ports will still be
// sent via the proxy and intercepted despite this setting.
const IGNORED_NON_HTTP_PORTS = [];
// As HTTP/3 is often not well supported by MitM proxies, by default it
// is blocked entirely, so all outgoing UDP connections to port 443
// will fail. If this is set to false, they will instead be left unintercepted.
const BLOCK_HTTP3 = true;
// Set this to true if your proxy supports SOCKS5 connections.
// This makes it possible for native-connect-hook to redirect
// non-HTTP traffic through your proxy (to view it raw, and
// avoid breaking non-HTTP traffic en route).
const PROXY_SUPPORTS_SOCKS5 = false;
// ----------------------------------------------------------------------------
// You don't need to modify any of the below, it just checks and applies some
// of the configuration that you've entered above.
// ----------------------------------------------------------------------------
if (DEBUG_MODE) {
// Add logging just for clean output & to separate reloads:
console.log('\n*** Starting scripts ***');
if (globalThis.Java?.available) {
Java.perform(() => {
setTimeout(() => console.log('*** Scripts completed ***\n'), 5);
// (We assume that nothing else will take more than 5ms, but app startup
// probably will, so this should separate script & runtime logs)
});
} else {
setTimeout(() => console.log('*** Scripts completed ***\n'), 5);
// (We assume that nothing else will take more than 5ms, but app startup
// probably will, so this should separate script & runtime logs)
}
} else {
console.log(''); // Add just a single newline, for minimal clarity
}
// Check the certificate (without literally including the instruction phrasing
// here, as that can be confusing for some users):
if (CERT_PEM.match(/\[!!.* CA certificate data .* !!\]/)) {
throw new Error('No certificate was provided' +
'\n\n' +
'You need to set CERT_PEM in the Frida config script ' +
'to the contents of your CA certificate.'
);
}
// ----------------------------------------------------------------------------
// Don't modify any of the below unless you know what you're doing!
// This section defines various utilities & calculates some constants which may
// be used by later scripts elsewhere in this project.
// ----------------------------------------------------------------------------
// As web atob & Node.js Buffer aren't available, we need to reimplement base64 decoding
// in pure JS. This is a quick rough implementation without much error handling etc!
// Base64 character set (plus padding character =) and lookup:
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const BASE64_LOOKUP = new Uint8Array(123);
for (let i = 0; i < BASE64_CHARS.length; i++) {
BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
}
/**
* Take a base64 string, and return the raw bytes
* @param {string} input
* @returns Uint8Array
*/
function decodeBase64(input) {
// Calculate the length of the output buffer based on padding:
let outputLength = Math.floor((input.length * 3) / 4);
if (input[input.length - 1] === '=') outputLength--;
if (input[input.length - 2] === '=') outputLength--;
const output = new Uint8Array(outputLength);
let outputPos = 0;
// Process each 4-character block:
for (let i = 0; i < input.length; i += 4) {
const a = BASE64_LOOKUP[input.charCodeAt(i)];
const b = BASE64_LOOKUP[input.charCodeAt(i + 1)];
const c = BASE64_LOOKUP[input.charCodeAt(i + 2)];
const d = BASE64_LOOKUP[input.charCodeAt(i + 3)];
// Assemble into 3 bytes:
const chunk = (a << 18) | (b << 12) | (c << 6) | d;
// Add each byte to the output buffer, unless it's padding:
output[outputPos++] = (chunk >> 16) & 0xff;
if (input.charCodeAt(i + 2) !== 61) output[outputPos++] = (chunk >> 8) & 0xff;
if (input.charCodeAt(i + 3) !== 61) output[outputPos++] = chunk & 0xff;
}
return output;
}
/**
* Take a single-certificate PEM string, and return the raw DER bytes
* @param {string} input
* @returns Uint8Array
*/
function pemToDer(input) {
const pemLines = input.split('\n');
if (
pemLines[0] !== '-----BEGIN CERTIFICATE-----' ||
pemLines[pemLines.length- 1] !== '-----END CERTIFICATE-----'
) {
throw new Error(
'Your certificate should be in PEM format, starting & ending ' +
'with a BEGIN CERTIFICATE & END CERTIFICATE header/footer'
);
}
const base64Data = pemLines.slice(1, -1).map(l => l.trim()).join('');
if ([...base64Data].some(c => !BASE64_CHARS.includes(c))) {
throw new Error(
'Your certificate should be in PEM format, containing only ' +
'base64 data between a BEGIN & END CERTIFICATE header/footer'
);
}
return decodeBase64(base64Data);
}
const CERT_DER = pemToDer(CERT_PEM);
// Right now this API is a bit funky - the callback will be called with a Frida Module instance
// if the module is properly detected, but may be called with just { name, path, base, size }
// in some cases (e.g. shared libraries loaded from inside an APK on Android). Works OK right now,
// as it's not widely used but needs improvement in future if we extend this.
function waitForModule(moduleName, callback) {
if (Array.isArray(moduleName)) {
moduleName.forEach(module => waitForModule(module, callback));
}
try {
const module = Process.getModuleByName(moduleName)
module.ensureInitialized();
callback(module);
return;
} catch (e) {
try {
const module = Module.load(moduleName);
callback(module);
return;
} catch (e) {}
}
MODULE_LOAD_CALLBACKS[moduleName] = callback;
}
const getModuleName = (nameOrPath) => {
const endOfPath = nameOrPath.lastIndexOf('/');
return nameOrPath.slice(endOfPath + 1);
};
const MODULE_LOAD_CALLBACKS = {};
new ApiResolver('module').enumerateMatches('exports:linker*!*dlopen*').forEach((dlopen) => {
Interceptor.attach(dlopen.address, {
onEnter(args) {
const moduleArg = args[0].readCString();
if (moduleArg) {
this.path = moduleArg;
this.moduleName = getModuleName(moduleArg);
}
},
onLeave(retval) {
if (!this.path || !retval || retval.isNull()) return;
if (!MODULE_LOAD_CALLBACKS[this.moduleName]) return;
let module = Process.findModuleByName(this.moduleName)
?? Process.findModuleByAddress(retval);
if (!module) {
// Some modules are loaded in ways that mean Frida can't detect them, and
// can't look them up by name (notably when loading libraries from inside an
// APK on Android). To handle this, we can use dlsym to look up an example
// symbol and find the underlying module details directly, where possible.
module = getAnonymousModule(this.moduleName, this.path, retval);
if (!module) return;
}
Object.keys(MODULE_LOAD_CALLBACKS).forEach((key) => {
if (this.moduleName === key) {
if (module) {
MODULE_LOAD_CALLBACKS[key](module);
delete MODULE_LOAD_CALLBACKS[key];
}
}
});
}
});
});
const getAnonymousModule = (name, path, handle) => {
const dlsymAddr = Module.findGlobalExportByName('dlsym');
if (!dlsymAddr) {
console.error(`[!] Cannot find dlsym, cannot get anonymous module info for ${name}`);
return;
}
const dlsym = new NativeFunction(dlsymAddr, 'pointer', ['pointer', 'pointer']);
// Handle here is the return value from dlopen - but in this scenario, it's just an
// opaque handle into to 'soinfo' data that other methods can use to get the
// real pointer to parts of the module, like so:
const onLoadPointer = dlsym(handle, Memory.allocUtf8String('JNI_OnLoad'));
// Once we have an actual pointer, we can get the range that holds it:
const range = Process.getRangeByAddress(onLoadPointer);
return {
base: range.base,
size: range.size,
name,
path,
}
};