From 3696f6674f44d56d76fa53afaed097fef392873a Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 31 Jul 2026 19:43:05 +0200 Subject: [PATCH] Massive Flutter script update so it now works on release versions --- ...oid-disable-flutter-certificate-pinning.js | 546 +++++++++++++----- test/android/test.ts | 8 +- 2 files changed, 397 insertions(+), 157 deletions(-) diff --git a/android/android-disable-flutter-certificate-pinning.js b/android/android-disable-flutter-certificate-pinning.js index 3b34e78..0a4cc3b 100644 --- a/android/android-disable-flutter-certificate-pinning.js +++ b/android/android-disable-flutter-certificate-pinning.js @@ -8,8 +8,19 @@ * some cases, this goes further and uses larger functions as anchors - allowing us to find the * very short functions correctly, where the patterns would otherwise have false positives. * + * Flutter ships a separate engine build per mode: apps run with `flutter run` bundle the JIT + * 'debug' engine, while `flutter build --release` bundles a distinct AOT 'release' engine, built + * with LTO (and, on arm64, the LLVM machine outliner) so that the same functions compile to + * visibly different code. We therefore carry a separate set of patterns for each, and detect + * which engine is loaded at runtime. + * + * In release builds LTO also inlines X509_STORE_CTX_get_current_cert (a one-line accessor) into + * its callers, so there is no function left to call. Instead we locate the instruction that reads + * the field inside CertificateCallback and recover the struct offset from it, then read the + * certificate out of the store directly. + * * The patterns here have been generated from every non-patch release of Flutter from v2.0.0 - * to v3.32.0 (the latest at the time of writing). They may need updates for new versions + * to v3.44.0 (the latest at the time of writing). They may need updates for new versions * in future. * * Currently this is limited to just Android, but in theory this can be expanded to iOS and @@ -23,141 +34,263 @@ (() => { const PATTERNS = { - "android/x64": { - "dart::bin::SSLCertContext::CertificateCallback": { - "signatures": [ - "41 57 41 56 53 48 83 ec 10 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3", - "41 57 41 56 41 54 53 48 83 ec 18 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3" - ] + "android/x64": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "41 57 41 56 41 54 53 48 83 ec 18 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3", + "41 57 41 56 53 48 83 ec 10 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3" + ] + }, + "X509_STORE_CTX_get_current_cert": { + "signatures": [ + "48 8b 87 b8 00 00 00 c3", + "48 8b 47 60 c3", + "48 8b 87 a8 00 00 00 c3", + "48 8b 47 50 c3" + ], + "anchor": "dart::bin::SSLCertContext::CertificateCallback" + }, + "bssl::x509_to_buffer": { + "signatures": [ + "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", + "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", + "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", + "41 56 53 48 83 ec 18 48 89 f0 4? 89 f? 48 8d 74 24 08 48 83 26 00 48 89 c7 e8" + ] + }, + "i2d_X509": { + "signatures": [ + "48 8d 15 ?? ?? ?? ?? e9", + "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", + "55 41 57 41 56 53 48 83 ec 68 48 85 ff 0f 84 ?? ?? ?? ?? 48 89 f3 49 89 fe 4c 8d 7c 24 08", + "55 41 57 41 56 53 48 83 ec 68 48 85 ff 0f 84 ?? ?? ?? ?? 49 89 f6 49 89 ff 48 8d 5c 24 38" + ], + "anchor": "bssl::x509_to_buffer" + } }, - "X509_STORE_CTX_get_current_cert": { - "signatures": [ - "48 8b 47 50 c3", - "48 8b 47 60 c3", - "48 8b 87 a8 00 00 00 c3", - "48 8b 87 b8 00 00 00 c3" - ], - "anchor": "dart::bin::SSLCertContext::CertificateCallback" + "android/x86": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "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" + ] + }, + "X509_STORE_CTX_get_current_cert": { + "signatures": [ + "55 89 e5 83 e4 fc 8b 45 08 8b 40 64 89 ec 5d c3", + "55 89 e5 83 e4 fc 8b 45 08 8b 40 34 89 ec 5d c3", + "55 89 e5 83 e4 fc 8b 45 08 8b 40 5c 89 ec 5d c3", + "55 89 e5 83 e4 fc 8b 45 08 8b 40 2c 89 ec 5d c3" + ], + "anchor": "dart::bin::SSLCertContext::CertificateCallback" + }, + "bssl::x509_to_buffer": { + "signatures": [ + "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", + "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", + "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" + ] + }, + "i2d_X509": { + "signatures": [ + "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", + "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", + "55 89 e5 53 57 56 83 e4 f0 83 ec 40 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8b 75 08 83 ec 0c 85 f6 0f 84", + "55 89 e5 53 57 56 83 e4 f0 83 ec 40 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 83 ec 0c 83 7d 08 00 0f 84" + ], + "anchor": "bssl::x509_to_buffer" + } }, - "bssl::x509_to_buffer": { - "signatures": [ - "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", - "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", - "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", - "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", - "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" - ] + "android/arm64": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 ?0 ?? ?? 54 f3 03 01 aa ?? ?? ?? 94 e0 07 00 b4 e0 03 13 aa", + "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 ?0 ?? ?? 54 f3 03 01 aa ?? ?? ?? 94 c0 09 00 b4 e0 03 13 aa", + "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 ?0 ?? ?? 54 f3 03 01 aa ?? ?? ?? 94 00 0a 00 b4 e0 03 13 aa" + ] + }, + "X509_STORE_CTX_get_current_cert": { + "signatures": [ + "00 ?? ?? f9 c0 03 5f d6" + ], + "anchor": "dart::bin::SSLCertContext::CertificateCallback" + }, + "bssl::x509_to_buffer": { + "signatures": [ + "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 ?? ?? ?? 97 1f 04 00 71", + "f? ?? ?? ?? f? 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 0? 00 71 ?? ?? ?? 54 e8 ?? ?? f9", + "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 0? 00 71 ?? ?? ?? 54 e8 ?? ?? f9" + ] + }, + "i2d_X509": { + "signatures": [ + "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 01 08 80 52 ?? ?? ?? 97", + "?2 ?? ?? ?? 42 ?? ?? 91 ?? ?? ?? 17", + "ff 03 02 d1 fe 33 00 f9 f4 4f 07 a9 40 04 00 b4 ?? ?? ?? 94 e0 03 00 91 01 20 80 52 ?? ?? ?? 97 20 03 00 34", + "ff 43 02 d1 fe 57 07 a9 f4 4f 08 a9 00 06 00 b4 f4 03 00 aa e0 ?? ?? 91 f3 03 01 aa ?? ?? ?? 97 e0 ?? ?? 91" + ], + "anchor": "bssl::x509_to_buffer" + } }, - "i2d_X509": { - "signatures": [ - "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", - "48 8d 15 ?? ?? ?? ?? e9" - ], - "anchor": "bssl::x509_to_buffer" - } - }, - "android/x86": { - "dart::bin::SSLCertContext::CertificateCallback": { - "signatures": [ - "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" - ] + "android/arm": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "70 b5 84 b0 01 28 ?? d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 ?? d0 20 46 ?? f? ?? f? 0? 46 ??" + ] + }, + "X509_STORE_CTX_get_current_cert": { + "signatures": [ + "40 6b 70 47", + "40 6e 70 47", + "c0 6d 70 47", + "c0 6a 70 47" + ], + "anchor": "dart::bin::SSLCertContext::CertificateCallback" + }, + "bssl::x509_to_buffer": { + "signatures": [ + "?c b5 00 2? 0a 46 01 9? 01 a9 04 46 10 46 ?? f? ?? f? 0? 28 ?? d? 01 46 01 98 00 22 ?? f? ??" + ] + }, + "i2d_X509": { + "signatures": [ + "70 b5 8e b0 00 28 ?? d0 05 46 08 a8 0c 46 40 21 ?? f? ?? f? 00 28 ?? d0 ?? 4a 08 a8 02 a9 ?? f? ?? f? ?? b3", + "?? 4a 7a 44 ?? f? ??", + "70 b5 8e b0 ?? b3 08 ae 05 46 0c 46 30 46 ?? f? ?? f? 30 46 40 21 ?? f? ?? f? ?? b3 ?? 4a 08 a8 02 a9", + "70 b5 8e b0 ?? b3 02 ae 05 46 0c 46 30 46 ?? f? ?? f? 30 46 4f f4 80 71 ?? f? ?? f? ?? b3 ?? 4a 02 a8 08 a9" + ], + "anchor": "bssl::x509_to_buffer" + } }, - "X509_STORE_CTX_get_current_cert": { - "signatures": [ - "55 89 e5 83 e4 fc 8b 45 08 8b 40 2c 89 ec 5d c3", - "55 89 e5 83 e4 fc 8b 45 08 8b 40 34 89 ec 5d c3", - "55 89 e5 83 e4 fc 8b 45 08 8b 40 5c 89 ec 5d c3", - "55 89 e5 83 e4 fc 8b 45 08 8b 40 64 89 ec 5d c3" - ], - "anchor": "dart::bin::SSLCertContext::CertificateCallback" + "android-release/x64": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "41 57 41 56 53 48 83 ec 20 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3", + "41 57 41 56 41 54 53 48 83 ec 18 b? 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 4? 89", + "41 57 41 56 41 54 53 48 83 ec 18 41 b? 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 4? 89" + ] + }, + "bssl::x509_to_buffer": { + "signatures": [ + "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", + "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", + "41 57 41 56 53 48 83 ec 10 48 89 f0 48 89 fb 48 8d 74 24 08 48 83 26 00 48 89 c7", + "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" + ] + }, + "i2d_X509": { + "signatures": [ + "48 8d 15 ?? ?? ?? ?? e9", + "55 41 56 53 48 81 ec 80 00 00 00 48 85 ff 0f 84 ?? ?? ?? ?? 48 89 f3 49 89 fe", + "41 57 41 56 53 48 83 ec 60 48 85 ff 0f 84 ?? ?? ?? ?? 49 89 f6 49 89 ff 48 89 e7", + "55 41 57 41 56 41 54 53 48 81 ec a0 00 00 00 48 85 ff 0f 84 ?? ?? ?? ?? 48 89 f3 49 89 fe" + ], + "anchor": "bssl::x509_to_buffer" + }, + "X509_STORE_CTX::current_cert": { + "anchor": "dart::bin::SSLCertContext::CertificateCallback", + "anchorMode": "within", + "signatures": [ + "4c 8b bb b8 00 00 00", + "4d 8b 7e 60", + "4d 8b be a8 00 00 00", + "4d 8b 7e 50", + "49 8b 9e b8 00 00 00", + "4c 8b 73 50", + "4d 8b be b8 00 00 00" + ] + } }, - "bssl::x509_to_buffer": { - "signatures": [ - "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", - "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", - "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" - ] + "android-release/arm64": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "ff 03 01 d1 fe 0b 00 f9 f6 57 02 a9 f4 4f 03 a9 1f 04 00 71 ?0 ?? ?? 54" + ] + }, + "bssl::x509_to_buffer": { + "signatures": [ + "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 04 00 71 ?b ?? ?? 54 e8 ?? ?? f9", + "fe 0f 1e f8 f4 4f 01 a9 ?? ?? ?? 94 ff 07 00 f9 ?? ?? ?? 97 1f 04 00 71 ?b ?? ?? 54 e8 ?? ?? f9 e1 03 00 2a", + "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 ?? ?? ?? 94 ?? ?? ?? 94 ff 07 00 f9 ?? ?? ?? 97 1f 04 00 71 ?b ?? ?? 54", + "f? ?? ?? ?? f? 4f 01 a9 e8 03 01 aa f3 03 00 aa ?? ?? ?? 94 ff 07 00 f9 ?? ?? ?? 97 1f 0? 00 71 ?? ?? ?? 54", + "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 ?? ?? ?? 94 e1 ?? ?? 91 e0 03 08 aa ?? ?? ?? 97 1f 0? 00 71 ?? ?? ?? 54" + ] + }, + "i2d_X509": { + "signatures": [ + "?2 ?? ?? ?? 42 ?? ?? 91 ?? ?? ?? 17", + "ff 83 02 d1 fe 57 08 a9 f4 4f 09 a9 a0 06 00 b4 ?? ?? ?? 94 e0 ?? ?? 91 ?? ?? ?? 94 e0 05 00 34 02 02 80 52", + "ff 83 02 d1 fe 57 08 a9 f4 4f 09 a9 c0 06 00 b4 ?? ?? ?? 94 e0 ?? ?? 91 ?? ?? ?? 94 00 06 00 34 02 02 80 52", + "ff 03 02 d1 fe 33 00 f9 f4 4f 07 a9 c0 03 00 b4 ?? ?? ?? 94 e0 03 00 91 ?? ?? ?? 94 e0 02 00 34 02 02 80 52", + "ff 43 03 d1 fe 53 00 f9 f6 57 0b a9 f4 4f 0c a9 60 09 00 b4 00 e4 00 6f ?? ?? ?? 94 ?? ?? ?? 94 e0 09 00 34" + ], + "anchor": "bssl::x509_to_buffer" + }, + "X509_STORE_CTX::current_cert": { + "anchor": "dart::bin::SSLCertContext::CertificateCallback", + "anchorMode": "within", + "signatures": [ + "74 ?? ?? f9" + ] + } }, - "i2d_X509": { - "signatures": [ - "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", - "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" - ], - "anchor": "bssl::x509_to_buffer" - } - }, - - "android/arm64": { - "dart::bin::SSLCertContext::CertificateCallback": { - "signatures": [ - "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", - "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" - ] - }, - "X509_STORE_CTX_get_current_cert": { - "signatures": [ - "00 ?? ?? f9 c0 03 5f d6" - ], - "anchor": "dart::bin::SSLCertContext::CertificateCallback" - }, - "bssl::x509_to_buffer": { - "signatures": [ - "fe 0f 1e f8 f4 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 04 00 71", - "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", - "ff 83 00 d1 fe 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 00 00 71", - "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 00 00 71", - "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 04 00 71" - ] - }, - "i2d_X509": { - "signatures": [ - "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", - "?2 ?? ?? ?? 42 ?? ?? 91 ?? ?? ?? 17" - ], - "anchor": "bssl::x509_to_buffer" - } - }, - "android/arm": { - "dart::bin::SSLCertContext::CertificateCallback": { - "signatures": [ - "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", - "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", - "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" - ] - }, - "X509_STORE_CTX_get_current_cert": { - "signatures": [ - "c0 6a 70 47", - "40 6b 70 47", - "c0 6d 70 47", - "40 6e 70 47" - ], - "anchor": "dart::bin::SSLCertContext::CertificateCallback" - }, - "bssl::x509_to_buffer": { - "signatures": [ - "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", - "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", - "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", - "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", - "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" - ] - }, - "i2d_X509": { - "signatures": [ - "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", - "01 4a 7a 44 ?? f? ?? b" - ], - "anchor": "bssl::x509_to_buffer" + "android-release/arm": { + "dart::bin::SSLCertContext::CertificateCallback": { + "signatures": [ + "f0 b5 83 b0 01 28 ?? d1 01 20 03 b0 f0 bd ?? 48 0d 46 78 44 00 68 00 28 18 bf 82 f1 64 e9 ?? 48 78 44", + "f0 b5 83 b0 01 28 ?? d1 01 20 03 b0 f0 bd ?? 48 0? 46 78 44 00 68 00 28 18 bf ?0 f1 ?? eb ?? 48 78 44", + "f0 b5 83 b0 01 28 ?? d1 01 20 03 b0 f0 bd ?? 48 0c 46 78 44 00 68 00 28 18 bf 7? f1 ?? e? ?? 48 78 44", + "f0 b5 83 b0 01 28 ?? d1 01 20 03 b0 f0 bd ?? 48 0d 46 78 44 00 68 00 28 18 bf ?? f1 ?0 e? ?? 48 78 44", + "?0 b5 8? b0 01 28 ?? d1 01 20 0? b0 ?0 bd ?? 48 0? 46 78 44 ?? f? ?? e? 00 68 00 28 1c bf d0 f8 ?? 0? 00 28", + "f0 b5 83 b0 01 28 ?? d1 01 20 03 b0 f0 bd ?? 48 0d 46 78 44 00 68 00 28 18 bf 3? f1 ?? e? ?? 48 78 44" + ] + }, + "bssl::x509_to_buffer": { + "signatures": [ + "7c b5 00 2? 0a 46 01 9? 01 a9 04 46 10 46 ?? f? ?? f? 0? 28 ?? d? 01 ?? 01 ?? 00 22 ?? ?? ?? f? ?? ??", + "?c b5 00 2? 0a 46 01 9? 01 a9 04 46 10 46 ?? f? ?? f? 0? 28 ?? d? 01 46 01 98 00 22 ?? f? ?? f? ?? ??" + ] + }, + "i2d_X509": { + "signatures": [ + "?? 4a 7a 44 ?? f? ??", + "70 b5 90 b0 00 28 ?? d0 05 46 08 a8 0c 46 40 21 ?? f? ?? f? 00 28 ?? d0 ?? 4a 08 a8 02 a9 ?? f? ?? f? 00 28", + "b0 b5 8c b0 ?? b3 0c 46 05 46 68 46 4f f4 80 71 ?? f? ?? f? ?? b3 ?? 4a 06 a9 68 46 ?? f? ?? f? ?? b1 06 a8", + "f0 b5 95 b0 00 28 ?? d0 05 46 c0 ef 50 00 08 a8 0c 46 00 22 01 46 0d 92 41 f9 cd 0a 0a 60 40 21 ?? f? ??" + ], + "anchor": "bssl::x509_to_buffer" + }, + "X509_STORE_CTX::current_cert": { + "anchor": "dart::bin::SSLCertContext::CertificateCallback", + "anchorMode": "within", + "signatures": [ + "65 6e 40 68", + "6c 6b 40 68", + "ec 6d 40 68", + "ec 6a 40 68", + "6c 6e 40 68", + "e5 6a 40 68" + ] + } } } - } + // Not a function, but the instruction inside CertificateCallback that reads the field: + const CURRENT_CERT_FIELD = 'X509_STORE_CTX::current_cert'; + const MAX_ANCHOR_INSTRUCTIONS_TO_SCAN = 100; + // How much of CertificateCallback we scan to find the inlined field load. Every build + // we've seen compiles it to well under this. + const MAX_FUNCTION_BYTES_TO_SCAN = 0x400; + const CALL_MNEMONICS = ['call', 'bl', 'blx']; + // On ARM all of this code is Thumb, and both NativeFunction and Instruction.parse need the + // low bit set to treat an address as Thumb rather than A32. + const isArm32 = Process.arch === 'arm'; + const asCode = (address) => isArm32 ? address.or(1) : address; + function scanForSignature(base, size, patterns) { const results = []; for (const pattern of patterns) { @@ -167,6 +300,14 @@ return results; } + /** + * Finds a function that we're going to call or hook, so its address has to be exactly + * right: we require one unambiguous match, and fail loudly otherwise. + * + * Where the function is anchored, the anchor's call target is the function entry by + * definition, so we can confirm the address outright: we accept a signature only if it + * matches at the call target itself, never part-way into it. + */ function scanForFunction(moduleRXRanges, platformPatterns, functionName, anchorFn) { const patternInfo = platformPatterns[functionName]; const signatures = patternInfo.signatures; @@ -174,7 +315,7 @@ if (patternInfo.anchor) { const maxPatternByteLength = Math.max(...signatures.map(p => (p.length + 1) / 3)); - let addr = ptr(anchorFn); + let addr = asCode(ptr(anchorFn)); for (let i = 0; i < MAX_ANCHOR_INSTRUCTIONS_TO_SCAN; i++) { const instr = Instruction.parse(addr); @@ -182,11 +323,8 @@ if (CALL_MNEMONICS.includes(instr.mnemonic)) { const callTargetAddr = ptr(instr.operands[0].value); const results = scanForSignature(callTargetAddr, maxPatternByteLength, signatures); - if (results.length === 1) { - return results[0].address; - } else if (results.length > 1) { - console.log(`Found multiple matches for ${functionName} anchored by ${anchorFunction}:`, results); - throw new Error(`Found multiple matches for ${functionName}`); + if (results.some(result => result.address.equals(callTargetAddr))) { + return callTargetAddr; } } } @@ -194,15 +332,138 @@ throw new Error(`Failed to find any match for ${functionName} anchored by ${anchorFn}`); } else { const results = moduleRXRanges.flatMap((range) => scanForSignature(range.base, range.size, signatures)); - if (results.length !== 1 && signatures.length > 1) { - console.log(results); - throw new Error(`Found multiple matches for ${functionName}`); + + if (results.length !== 1) { + // Not necessarily a problem: we scan with each build's patterns in turn, so + // failing to match here is how we recognise the other kind of build. + if (DEBUG_MODE) console.log(`Matches for ${functionName}:`, results); + throw new Error(`Found ${results.length} matches for ${functionName}`); } return results[0].address; } } + /** + * Finds a function that's only used as a starting point to scan forwards from, never + * called. That means we don't need its exact entry point, which matters because + * signatures overlap here: one generated from a build with a shorter prologue also + * matches part-way into the same function in a build with a longer one. + * + * We only tolerate matches that fall inside the extent of the first match, which proves + * they cover the same code rather than a second, unrelated site. + */ + function scanForAnchor(moduleRXRanges, platformPatterns, functionName) { + const signatures = platformPatterns[functionName].signatures; + const results = moduleRXRanges + .flatMap((range) => scanForSignature(range.base, range.size, signatures)) + .sort((a, b) => a.address.compare(b.address)); + + if (results.length === 0) throw new Error(`Failed to find any match for ${functionName}`); + + const firstMatchEnd = results[0].address.add(results[0].size); + const overlapping = results.every(result => result.address.compare(firstMatchEnd) < 0); + + if (!overlapping) { + throw new Error(`Found ${results.length} separate matches for ${functionName}`); + } + + return results[0].address; + } + + /** + * Recovers the offset of X509_STORE_CTX->current_cert. In release builds the accessor is + * inlined, so we find the single instruction inside CertificateCallback that reads the + * field and take the displacement straight out of it. That way a future BoringSSL layout + * change is picked up automatically, rather than silently reading the wrong field. + */ + function findCurrentCertOffset(platformPatterns, certificateCallbackAddr) { + const patternInfo = platformPatterns[CURRENT_CERT_FIELD]; + + const results = scanForSignature( + certificateCallbackAddr, + MAX_FUNCTION_BYTES_TO_SCAN, + patternInfo.signatures + ); + + if (results.length !== 1) { + throw new Error(`Found ${results.length} matches for ${CURRENT_CERT_FIELD} - expected exactly one`); + } + + const instruction = Instruction.parse(asCode(results[0].address)); + const memoryOperand = instruction.operands.find(op => op.type === 'mem'); + + if (!memoryOperand) { + throw new Error(`No memory operand in ${CURRENT_CERT_FIELD} instruction: ${instruction}`); + } + + const offset = memoryOperand.value.disp; + if (!offset) { + throw new Error(`Implausible ${CURRENT_CERT_FIELD} offset ${offset} from: ${instruction}`); + } + + return offset; + } + + /** + * Resolves everything we need to hook, using one specific set of patterns. This has to + * succeed or fail as a whole: a pattern set for the wrong engine build can match one + * function by chance, and we want to fall through to the next set if it does, rather + * than hooking a half-resolved mixture. + */ + function resolveTargets(moduleRXRanges, patterns) { + const certificateCallbackAddr = scanForFunction(moduleRXRanges, patterns, 'dart::bin::SSLCertContext::CertificateCallback'); + + // Where the accessor still exists we call it; where LTO inlined it (all release + // builds) we recover the field offset and read the store directly. + let getCurrentCert; + if (patterns[CURRENT_CERT_FIELD]) { + const currentCertOffset = findCurrentCertOffset(patterns, certificateCallbackAddr); + if (DEBUG_MODE) console.log(`X509_STORE_CTX->current_cert at +0x${currentCertOffset.toString(16)}`); + getCurrentCert = (storeCtx) => storeCtx.add(currentCertOffset).readPointer(); + } else { + const x509GetCurrentCert = new NativeFunction( + asCode(scanForFunction(moduleRXRanges, patterns, 'X509_STORE_CTX_get_current_cert', certificateCallbackAddr)), + 'pointer', + ['pointer'] + ); + getCurrentCert = (storeCtx) => x509GetCurrentCert(storeCtx); + } + + // x509_to_buffer is just used as an anchor for searching: + const x509ToBufferAddr = scanForAnchor(moduleRXRanges, patterns, 'bssl::x509_to_buffer'); + const i2d_X509 = new NativeFunction( + asCode(scanForFunction(moduleRXRanges, patterns, 'i2d_X509', x509ToBufferAddr)), + 'int', + ['pointer', 'pointer'] + ); + + return { certificateCallbackAddr, getCurrentCert, i2d_X509 }; + } + + /** Works out which engine build is loaded, by seeing whose patterns actually match. */ + function findTargets(moduleRXRanges) { + // Frida calls 32-bit x86 'ia32', but our patterns are keyed by the name Flutter + // uses for the same architecture. + const arch = Process.arch === 'ia32' ? 'x86' : Process.arch; + const candidates = [`android-release/${arch}`, `android/${arch}`]; + + for (const key of candidates) { + if (!PATTERNS[key]) continue; + + try { + const targets = resolveTargets(moduleRXRanges, PATTERNS[key]); + if (DEBUG_MODE) console.log(`Matched Flutter ${key} patterns`); + return targets; + } catch (e) { + // Expected for whichever engine build isn't loaded - we just try the next. + if (DEBUG_MODE) console.log(`Flutter ${key} patterns don't apply here: ${e.message}`); + } + } + + throw new Error(`Could not match any known Flutter patterns for ${Process.arch}`); + } + function hookFlutter(moduleBase, moduleSize) { if (DEBUG_MODE) console.log('\n=== Disabling Flutter certificate pinning ==='); @@ -211,34 +472,17 @@ }); try { - const arch = Process.arch; - const patterns = PATTERNS[`android/${arch}`]; + const { certificateCallbackAddr, getCurrentCert, i2d_X509 } = findTargets(relevantRanges); // This callback is called for all TLS connections. It immediately returns 1 (success) if BoringSSL // trusts the cert, or it calls the configured BadCertificateCallback if it doesn't. Note that this // is called for every cert in the chain individually - not the whole chain at once. const dartCertificateCallback = new NativeFunction( - scanForFunction(relevantRanges, patterns, 'dart::bin::SSLCertContext::CertificateCallback'), + asCode(certificateCallbackAddr), 'int', ['int', 'pointer'] ); - // We inject code to check the certificate ourselves - getting the cert, converting to DER, and - // ignoring all validation results if the certificate matches our trusted cert. - const x509GetCurrentCert = new NativeFunction( - scanForFunction(relevantRanges, patterns, 'X509_STORE_CTX_get_current_cert', dartCertificateCallback), - 'pointer', - ['pointer'] - ); - - // Just used as an anchor for searching: - const x509ToBufferAddr = scanForFunction(relevantRanges, patterns, 'bssl::x509_to_buffer'); - const i2d_X509 = new NativeFunction( - scanForFunction(relevantRanges, patterns, 'i2d_X509', x509ToBufferAddr), - 'int', - ['pointer', 'pointer'] - ); - Interceptor.attach(dartCertificateCallback, { onEnter: function (args) { this.x509Store = args[1]; @@ -249,7 +493,7 @@ // This certificate isn't trusted by BoringSSL or the app's certificate callback. Check it ourselves // and override the result if it exactly matches our cert. try { - const x509Cert = x509GetCurrentCert(this.x509Store); + const x509Cert = getCurrentCert(this.x509Store); const derLength = i2d_X509(x509Cert, NULL); if (derLength <= 0) { diff --git a/test/android/test.ts b/test/android/test.ts index 940a308..e2a2586 100644 --- a/test/android/test.ts +++ b/test/android/test.ts @@ -338,16 +338,12 @@ describe('Test Android unpinning', function () { ]); }); - it("all buttons should succeed, except the known unsupported cases", async () => { + it("all buttons should succeed, except the raw custom-pinned request", async () => { await testAllButtons('Success', { exceptions: [ // This checks the certificate itself, by hand, at the lowest level. Unpinning // it requires reverse engineering the app - see the demo app's README. - 'RAW CUSTOM-PINNED REQUEST', - // The Flutter hooks find their targets by scanning for byte patterns, and - // those don't match the Flutter version this app now ships, so Flutter - // traffic is not unpinned at all here: - 'FLUTTER REQUEST' + 'RAW CUSTOM-PINNED REQUEST' ] }); });