Skip to content

Commit 5677fda

Browse files
committed
Update Android Frida script with Flutter & detection bypasses + fixes
1 parent 59ef86a commit 5677fda

File tree

5 files changed

+524
-69
lines changed

5 files changed

+524
-69
lines changed

overrides/frida/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ The scripts can automatically handle:
4141
-l ./android/android-certificate-unpinning.js \
4242
-l ./android/android-certificate-unpinning-fallback.js \
4343
-l ./android/android-disable-root-detection.js \
44+
-l ./android/android-disable-flutter-certificate-pinning.js \
4445
-f $PACKAGE_ID
4546
```
4647
7. Explore, examine & modify all the traffic you're interested in! If you have any problems, please [open an issue](https://github.com/httptoolkit/frida-interception-and-unpinning/issues/new) and help make these scripts even better.
@@ -140,6 +141,10 @@ Each script includes detailed documentation on what it does and how it works in
140141
141142
It blocks suspicious behavior like file existence checks and shell command execution, helping evade detection in apps using both standard and advanced root checks.
142143
144+
* `android-disable-flutter-certificate-pinning.js`
145+
146+
Ensures that Flutter-based applications (which generally ignore the system certificate configuration) trust your CA certificate, even in most cases of explicit certificate pinning.
147+
143148
* `ios/`
144149
145150
* `ios-connect-hook.js`
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**************************************************************************************************
2+
*
3+
* This script hooks Flutter internal certificate handling, to trust our certificate (and ignore
4+
* any custom certificate validation - e.g. pinning libraries) for all TLS connections.
5+
*
6+
* Unfortunately Flutter is shipped as native code with no exported symbols, so we have to do this
7+
* by matching individual function signatures by known patterns of assembly instructions. In
8+
* some cases, this goes further and uses larger functions as anchors - allowing us to find the
9+
* very short functions correctly, where the patterns would otherwise have false positives.
10+
*
11+
* The patterns here have been generated from every non-patch release of Flutter from v2.0.0
12+
* to v3.32.0 (the latest at the time of writing). They may need updates for new versions
13+
* in future.
14+
*
15+
* Currently this is limited to just Android, but in theory this can be expanded to iOS and
16+
* desktop platforms in future.
17+
*
18+
* Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
19+
* SPDX-License-Identifier: AGPL-3.0-or-later
20+
* SPDX-FileCopyrightText: Tim Perry <[email protected]>
21+
*
22+
*************************************************************************************************/
23+
24+
(() => {
25+
const PATTERNS = {
26+
"android/x64": {
27+
"dart::bin::SSLCertContext::CertificateCallback": {
28+
"signatures": [
29+
"41 57 41 56 53 48 83 ec 10 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3",
30+
"41 57 41 56 41 54 53 48 83 ec 18 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3"
31+
]
32+
},
33+
"X509_STORE_CTX_get_current_cert": {
34+
"signatures": [
35+
"48 8b 47 50 c3",
36+
"48 8b 47 60 c3",
37+
"48 8b 87 a8 00 00 00 c3",
38+
"48 8b 87 b8 00 00 00 c3"
39+
],
40+
"anchor": "dart::bin::SSLCertContext::CertificateCallback"
41+
},
42+
"bssl::x509_to_buffer": {
43+
"signatures": [
44+
"41 56 53 50 48 89 f0 48 89 fb 48 89 e6 48 83 26 00 48 89 c7 e8 ?? ?? ?? ?? 85 c0 7e 1b",
45+
"53 48 83 ec 10 48 89 f0 48 89 fb 48 8d 74 24 08 48 83 26 00 48 89 c7 e8 ?? ?? ?? ?? 85 c0",
46+
"41 56 53 48 83 ec 18 48 89 f0 48 89 fb 48 8d 74 24 08 48 83 26 00 48 89 c7 e8",
47+
"41 56 53 48 83 ec 18 48 89 f0 49 89 fe 48 8d 74 24 08 48 83 26 00 48 89 c7 e8",
48+
"41 57 41 56 53 48 83 ec 10 48 89 f0 49 89 fe 48 89 e6 48 83 26 00 48 89 c7 e8"
49+
]
50+
},
51+
"i2d_X509": {
52+
"signatures": [
53+
"55 41 56 53 48 83 ec 70 48 85 ff 0f 84 ?? ?? ?? ?? 48 89 f3 49 89 fe 48 8d 7c 24 40 6a 40",
54+
"48 8d 15 ?? ?? ?? ?? e9"
55+
],
56+
"anchor": "bssl::x509_to_buffer"
57+
}
58+
},
59+
"android/x86": {
60+
"dart::bin::SSLCertContext::CertificateCallback": {
61+
"signatures": [
62+
"55 89 e5 53 57 56 83 e4 f0 83 ec 30 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? bf 01 00 00 00 83 7d 08 01 0f 84"
63+
]
64+
},
65+
"X509_STORE_CTX_get_current_cert": {
66+
"signatures": [
67+
"55 89 e5 83 e4 fc 8b 45 08 8b 40 2c 89 ec 5d c3",
68+
"55 89 e5 83 e4 fc 8b 45 08 8b 40 34 89 ec 5d c3",
69+
"55 89 e5 83 e4 fc 8b 45 08 8b 40 5c 89 ec 5d c3",
70+
"55 89 e5 83 e4 fc 8b 45 08 8b 40 64 89 ec 5d c3"
71+
],
72+
"anchor": "dart::bin::SSLCertContext::CertificateCallback"
73+
},
74+
"bssl::x509_to_buffer": {
75+
"signatures": [
76+
"55 89 e5 53 57 56 83 e4 f0 83 ec 10 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 08 83 20 00 83 ec 08 50 52",
77+
"55 89 e5 53 56 83 e4 f0 83 ec 10 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 0c 83 20 00 83 ec 08 50 52",
78+
"55 89 e5 53 57 56 83 e4 f0 83 ec 20 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 14 83 20 00 89 44 24 04 89 14 24"
79+
]
80+
},
81+
"i2d_X509": {
82+
"signatures": [
83+
"55 89 e5 53 57 56 83 e4 f0 83 ec 40 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8b 7d 08 85 ff 0f 84 ?? ?? ?? ?? 83 ec 08",
84+
"55 89 e5 53 83 e4 f0 83 ec 10 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 83 ec 04 8d 83 ?? ?? ?? ?? 50 ff 75 0c ff 75 08"
85+
],
86+
"anchor": "bssl::x509_to_buffer"
87+
}
88+
},
89+
90+
"android/arm64": {
91+
"dart::bin::SSLCertContext::CertificateCallback": {
92+
"signatures": [
93+
"ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 c0 07 00 54 f3 03 01 aa ?? ?? ?? 94",
94+
"ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 c0 02 00 54 f3 03 01 aa ?? ?? ?? 94"
95+
]
96+
},
97+
"X509_STORE_CTX_get_current_cert": {
98+
"signatures": [
99+
"00 ?? ?? f9 c0 03 5f d6"
100+
],
101+
"anchor": "dart::bin::SSLCertContext::CertificateCallback"
102+
},
103+
"bssl::x509_to_buffer": {
104+
"signatures": [
105+
"fe 0f 1e f8 f4 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 04 00 71",
106+
"fe 0f 1e f8 f4 4f 01 a9 e8 03 01 aa f3 03 00 aa e1 ?? ?? 91 e0 03 08 aa ff 07 00 f9",
107+
"ff 83 00 d1 fe 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 00 00 71",
108+
"ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 00 00 71",
109+
"ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 04 00 71"
110+
]
111+
},
112+
"i2d_X509": {
113+
"signatures": [
114+
"ff 43 02 d1 fe 57 07 a9 f4 4f 08 a9 a0 06 00 b4 f4 03 00 aa f3 03 01 aa e0 ?? ?? 91",
115+
"?2 ?? ?? ?? 42 ?? ?? 91 ?? ?? ?? 17"
116+
],
117+
"anchor": "bssl::x509_to_buffer"
118+
}
119+
},
120+
"android/arm": {
121+
"dart::bin::SSLCertContext::CertificateCallback": {
122+
"signatures": [
123+
"70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 4d d0 20 46 ?? f? ?? f? 05 46 ?? f? ?? f",
124+
"70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 52 d0 20 46 ?? f? ?? f? 06 46 ?? f? ?? f",
125+
"70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 50 d0 20 46 ?? f? ?? f? 06 46 ?? f? ?? f"
126+
]
127+
},
128+
"X509_STORE_CTX_get_current_cert": {
129+
"signatures": [
130+
"c0 6a 70 47",
131+
"40 6b 70 47",
132+
"c0 6d 70 47",
133+
"40 6e 70 47"
134+
],
135+
"anchor": "dart::bin::SSLCertContext::CertificateCallback"
136+
},
137+
"bssl::x509_to_buffer": {
138+
"signatures": [
139+
"bc b5 00 25 0a 46 01 95 01 a9 04 46 10 46 ?? f? ?? f? 01 28 08 db 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98",
140+
"bc b5 00 25 0a 46 01 95 01 a9 04 46 10 46 ?? f? ?? f? 00 28 09 dd 01 46 01 98 00 22 ?? f? ?? f? 20 60 01 98",
141+
"7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 00 28 0e dd 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98",
142+
"7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 01 28 0d db 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98",
143+
"7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 01 28 0e db 01 46 01 98 00 22 ?? f? ?? f? 05 46 00 90"
144+
]
145+
},
146+
"i2d_X509": {
147+
"signatures": [
148+
"70 b5 8e b0 00 28 4f d0 05 46 08 a8 0c 46 40 21 ?? f? ?? f? 00 28 43 d0 2a 4a 08 a8 02 a9 ?? f? ?? f? e8 b3",
149+
"01 4a 7a 44 ?? f? ?? b"
150+
],
151+
"anchor": "bssl::x509_to_buffer"
152+
}
153+
}
154+
}
155+
156+
157+
const MAX_ANCHOR_INSTRUCTIONS_TO_SCAN = 100;
158+
159+
const CALL_MNEMONICS = ['call', 'bl', 'blx'];
160+
161+
function scanForSignature(base, size, patterns) {
162+
const results = [];
163+
for (const pattern of patterns) {
164+
const result = Memory.scanSync(base, size, pattern);
165+
results.push(...result);
166+
}
167+
return results;
168+
}
169+
170+
function scanForFunction(moduleRXRanges, platformPatterns, functionName, anchorFn) {
171+
const patternInfo = platformPatterns[functionName];
172+
const signatures = patternInfo.signatures;
173+
174+
if (patternInfo.anchor) {
175+
const maxPatternByteLength = Math.max(...signatures.map(p => (p.length + 1) / 3));
176+
177+
let addr = ptr(anchorFn);
178+
179+
for (let i = 0; i < MAX_ANCHOR_INSTRUCTIONS_TO_SCAN; i++) {
180+
const instr = Instruction.parse(addr);
181+
addr = instr.next;
182+
if (CALL_MNEMONICS.includes(instr.mnemonic)) {
183+
const callTargetAddr = ptr(instr.operands[0].value);
184+
const results = scanForSignature(callTargetAddr, maxPatternByteLength, signatures);
185+
if (results.length === 1) {
186+
return results[0].address;
187+
} else if (results.length > 1) {
188+
console.log(`Found multiple matches for ${functionName} anchored by ${anchorFunction}:`, results);
189+
throw new Error(`Found multiple matches for ${functionName}`);
190+
}
191+
}
192+
}
193+
194+
throw new Error(`Failed to find any match for ${functionName} anchored by ${anchorFn}`);
195+
} else {
196+
const results = moduleRXRanges.flatMap((range) => scanForSignature(range.base, range.size, signatures));
197+
if (results.length !== 1 && signatures.length > 1) {
198+
console.log(results);
199+
throw new Error(`Found multiple matches for ${functionName}`);
200+
}
201+
202+
return results[0].address;
203+
}
204+
}
205+
206+
function hookFlutter(moduleBase, moduleSize) {
207+
if (DEBUG_MODE) console.log('\n=== Disabling Flutter certificate pinning ===');
208+
209+
const relevantRanges = Process.enumerateRanges('r-x').filter(range => {
210+
return range.base >= moduleBase && range.base < moduleBase.add(moduleSize);
211+
});
212+
213+
try {
214+
const arch = Process.arch;
215+
const patterns = PATTERNS[`android/${arch}`];
216+
217+
// This callback is called for all TLS connections. It immediately returns 1 (success) if BoringSSL
218+
// trusts the cert, or it calls the configured BadCertificateCallback if it doesn't. Note that this
219+
// is called for every cert in the chain individually - not the whole chain at once.
220+
const dartCertificateCallback = new NativeFunction(
221+
scanForFunction(relevantRanges, patterns, 'dart::bin::SSLCertContext::CertificateCallback'),
222+
'int',
223+
['int', 'pointer']
224+
);
225+
226+
// We inject code to check the certificate ourselves - getting the cert, converting to DER, and
227+
// ignoring all validation results if the certificate matches our trusted cert.
228+
const x509GetCurrentCert = new NativeFunction(
229+
scanForFunction(relevantRanges, patterns, 'X509_STORE_CTX_get_current_cert', dartCertificateCallback),
230+
'pointer',
231+
['pointer']
232+
);
233+
234+
// Just used as an anchor for searching:
235+
const x509ToBufferAddr = scanForFunction(relevantRanges, patterns, 'bssl::x509_to_buffer');
236+
const i2d_X509 = new NativeFunction(
237+
scanForFunction(relevantRanges, patterns, 'i2d_X509', x509ToBufferAddr),
238+
'int',
239+
['pointer', 'pointer']
240+
);
241+
242+
Interceptor.attach(dartCertificateCallback, {
243+
onEnter: function (args) {
244+
this.x509Store = args[1];
245+
},
246+
onLeave: function (retval) {
247+
if (retval.toInt32() === 1) return; // Ignore successful validations
248+
249+
// This certificate isn't trusted by BoringSSL or the app's certificate callback. Check it ourselves
250+
// and override the result if it exactly matches our cert.
251+
try {
252+
const x509Cert = x509GetCurrentCert(this.x509Store);
253+
254+
const derLength = i2d_X509(x509Cert, NULL);
255+
if (derLength <= 0) {
256+
throw new Error('Failed to get DER length for X509 cert');
257+
}
258+
259+
// We create our own target buffer (rather than letting BoringSSL do so, which would
260+
// require more hooks to handle cleanup).
261+
const derBuffer = Memory.alloc(derLength)
262+
const outPtr = Memory.alloc(Process.pointerSize);
263+
outPtr.writePointer(derBuffer);
264+
265+
const certDataLength = i2d_X509(x509Cert, outPtr)
266+
const certData = new Uint8Array(derBuffer.readByteArray(certDataLength));
267+
268+
if (certData.every((byte, j) => CERT_DER[j] === byte)) {
269+
retval.replace(1); // We trust this certificate, return success
270+
}
271+
} catch (error) {
272+
console.error('[!] Internal error in Flutter certificate unpinning:', error);
273+
}
274+
}
275+
});
276+
277+
console.log('=== Flutter certificate pinning disabled ===');
278+
} catch (error) {
279+
console.error('[!] Error preparing Flutter certificate pinning hooks:', error);
280+
throw error;
281+
}
282+
}
283+
284+
let flutter = Process.findModuleByName('libflutter.so');
285+
if (flutter) {
286+
hookFlutter(flutter.base, flutter.size);
287+
} else {
288+
waitForModule('libflutter.so', function (module) {
289+
hookFlutter(module.base, module.size);
290+
});
291+
}
292+
})();

0 commit comments

Comments
 (0)