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