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