n0n3mi1y

Frida Script SystemCallAndroidMonitoring

Dec 6th, 2024
50
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. //#region CONFIG
  2.  
  3. const PROXY_HOST = '{-Variable.scam_proxyIp-}';
  4. const PROXY_PORT = {-Variable.scam_proxyPort-};
  5. const DEBUG_MODE = true;
  6.  
  7. const CERT_PEM = `{-Variable.scam_certPEM-}`;
  8.  
  9. // If you find issues with non-HTTP traffic being captured (due to the
  10. // native connect hook script) you can add ports here to exempt traffic
  11. // on that port from being redirected. Note that this will only affect
  12. // traffic captured by the raw connection hook - for apps using the
  13. // system HTTP proxy settings, traffic on these ports will still be
  14. // sent via the proxy and intercepted despite this setting.
  15. const IGNORED_NON_HTTP_PORTS = [];
  16.  
  17.  
  18. // ----------------------------------------------------------------------------
  19. // You don't need to modify any of the below, it just checks and applies some
  20. // of the configuration that you've entered above.
  21. // ----------------------------------------------------------------------------
  22.  
  23.  
  24. if (DEBUG_MODE) {
  25.     // Add logging just for clean output & to separate reloads:
  26.     console.log('\n*** Starting scripts ***');
  27.     if (Java.available) {
  28.         Java.perform(() => {
  29.             setTimeout(() => console.log('*** Scripts completed ***\n'), 5);
  30.             // (We assume that nothing else will take more than 5ms, but app startup
  31.             // probably will, so this should separate script & runtime logs)
  32.         });
  33.     } else {
  34.         setTimeout(() => console.log('*** Scripts completed ***\n'), 5);
  35.         // (We assume that nothing else will take more than 5ms, but app startup
  36.         // probably will, so this should separate script & runtime logs)
  37.     }
  38. } else {
  39.     console.log(''); // Add just a single newline, for minimal clarity
  40. }
  41.  
  42. // Check the certificate (without literally including the instruction phrasing
  43. // here, as that can be confusing for some users):
  44. if (CERT_PEM.match(/\[!!.* CA certificate data .* !!\]/)) {
  45.     throw new Error('No certificate was provided' +
  46.         '\n\n' +
  47.         'You need to set CERT_PEM in the Frida config script ' +
  48.         'to the contents of your CA certificate.'
  49.     );
  50. }
  51.  
  52.  
  53.  
  54. // ----------------------------------------------------------------------------
  55. // Don't modify any of the below unless you know what you're doing!
  56. // This section defines various utilities & calculates some constants which may
  57. // be used by later scripts elsewhere in this project.
  58. // ----------------------------------------------------------------------------
  59.  
  60.  
  61.  
  62. // As web atob & Node.js Buffer aren't available, we need to reimplement base64 decoding
  63. // in pure JS. This is a quick rough implementation without much error handling etc!
  64.  
  65. // Base64 character set (plus padding character =) and lookup:
  66. const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  67. const BASE64_LOOKUP = new Uint8Array(123);
  68. for (let i = 0; i < BASE64_CHARS.length; i++) {
  69.     BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
  70. }
  71.  
  72.  
  73. /**
  74.  * Take a base64 string, and return the raw bytes
  75.  * @param {string} input
  76.  * @returns Uint8Array
  77.  */
  78. function decodeBase64(input) {
  79.     // Calculate the length of the output buffer based on padding:
  80.     let outputLength = Math.floor((input.length * 3) / 4);
  81.     if (input[input.length - 1] === '=') outputLength--;
  82.     if (input[input.length - 2] === '=') outputLength--;
  83.  
  84.     const output = new Uint8Array(outputLength);
  85.     let outputPos = 0;
  86.  
  87.     // Process each 4-character block:
  88.     for (let i = 0; i < input.length; i += 4) {
  89.         const a = BASE64_LOOKUP[input.charCodeAt(i)];
  90.         const b = BASE64_LOOKUP[input.charCodeAt(i + 1)];
  91.         const c = BASE64_LOOKUP[input.charCodeAt(i + 2)];
  92.         const d = BASE64_LOOKUP[input.charCodeAt(i + 3)];
  93.  
  94.         // Assemble into 3 bytes:
  95.         const chunk = (a << 18) | (b << 12) | (c << 6) | d;
  96.  
  97.         // Add each byte to the output buffer, unless it's padding:
  98.         output[outputPos++] = (chunk >> 16) & 0xff;
  99.         if (input.charCodeAt(i + 2) !== 61) output[outputPos++] = (chunk >> 8) & 0xff;
  100.         if (input.charCodeAt(i + 3) !== 61) output[outputPos++] = chunk & 0xff;
  101.     }
  102.  
  103.     return output;
  104. }
  105.  
  106. /**
  107.  * Take a single-certificate PEM string, and return the raw DER bytes
  108.  * @param {string} input
  109.  * @returns Uint8Array
  110.  */
  111. function pemToDer(input) {
  112.     const pemLines = input.split('\n');
  113.     if (
  114.         pemLines[0] !== '-----BEGIN CERTIFICATE-----' ||
  115.         pemLines[pemLines.length- 1] !== '-----END CERTIFICATE-----'
  116.     ) {
  117.         throw new Error(
  118.             'Your certificate should be in PEM format, starting & ending ' +
  119.             'with a BEGIN CERTIFICATE & END CERTIFICATE header/footer'
  120.         );
  121.     }
  122.  
  123.     const base64Data = pemLines.slice(1, -1).map(l => l.trim()).join('');
  124.     if ([...base64Data].some(c => !BASE64_CHARS.includes(c))) {
  125.         throw new Error(
  126.             'Your certificate should be in PEM format, containing only ' +
  127.             'base64 data between a BEGIN & END CERTIFICATE header/footer'
  128.         );
  129.     }
  130.  
  131.     return decodeBase64(base64Data);
  132. }
  133.  
  134. const CERT_DER = pemToDer(CERT_PEM);
  135.  
  136. function waitForModule(moduleName, callback) {
  137.     if (Array.isArray(moduleName)) {
  138.         moduleName.forEach(module => waitForModule(module, callback));
  139.     }
  140.  
  141.     try {
  142.         Module.ensureInitialized(moduleName);
  143.         callback(moduleName);
  144.         return;
  145.     } catch (e) {
  146.         try {
  147.             Module.load(moduleName);
  148.             callback(moduleName);
  149.             return;
  150.         } catch (e) {}
  151.     }
  152.  
  153.     MODULE_LOAD_CALLBACKS[moduleName] = callback;
  154. }
  155.  
  156. const getModuleName = (nameOrPath) => {
  157.     const endOfPath = nameOrPath.lastIndexOf('/');
  158.     return nameOrPath.slice(endOfPath + 1);
  159. };
  160.  
  161. const MODULE_LOAD_CALLBACKS = {};
  162. new ApiResolver('module').enumerateMatches('exports:linker*!*dlopen*').forEach((dlopen) => {
  163.     Interceptor.attach(dlopen.address, {
  164.         onEnter(args) {
  165.             const moduleArg = args[0].readCString();
  166.             if (moduleArg) {
  167.                 this.moduleName = getModuleName(moduleArg);
  168.             }
  169.         },
  170.         onLeave() {
  171.             if (!this.moduleName) return;
  172.  
  173.             Object.keys(MODULE_LOAD_CALLBACKS).forEach((key) => {
  174.                 if (this.moduleName === key) {
  175.                     MODULE_LOAD_CALLBACKS[key](this.moduleName);
  176.                     delete MODULE_LOAD_CALLBACKS[key];
  177.                 }
  178.             });
  179.         }
  180.     });
  181. });
  182.  
  183. //#endregion CONFIG
  184.  
  185.  
  186. //#region NATIVE_CONNECT_HOOK
  187. /**
  188.  * In some cases, proxy configuration by itself won't work. This notably includes Flutter apps (which ignore
  189.  * system/JVM configuration entirely) and plausibly other apps intentionally ignoring proxies. To handle that
  190.  * we hook native connect() calls directly, to redirect traffic on all ports to the target.
  191.  *
  192.  * This handles all attempts to connect an outgoing socket, and for all TCP connections opened it will
  193.  * manually replace the connect() parameters so that the socket connects to the proxy instead of the
  194.  * 'real' destination.
  195.  *
  196.  * This doesn't help with certificate trust (you still need some kind of certificate setup) but it does ensure
  197.  * the proxy receives all connections (and so will see if connections don't trust its CA). It's still useful
  198.  * to do proxy config alongside this, as applications may behave a little more 'correctly' if they're aware
  199.  * they're using a proxy rather than doing so unknowingly.
  200.  *
  201.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  202.  * SPDX-License-Identifier: AGPL-3.0-or-later
  203.  * SPDX-FileCopyrightText: Tim Perry <[email protected]>
  204.  */
  205.  
  206. const PROXY_HOST_IPv4_BYTES = PROXY_HOST.split('.').map(part => parseInt(part, 10));
  207. const IPv6_MAPPING_PREFIX_BYTES = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff];
  208. const PROXY_HOST_IPv6_BYTES = IPv6_MAPPING_PREFIX_BYTES.concat(PROXY_HOST_IPv4_BYTES);
  209.  
  210. const connectFn = (
  211.     Module.findExportByName('libc.so', 'connect') ?? // Android
  212.     Module.findExportByName('libc.so.6', 'connect') ?? // Linux
  213.     Module.findExportByName('libsystem_kernel.dylib', 'connect') // iOS
  214. );
  215.  
  216. if (!connectFn) { // Should always be set, but just in case
  217.     console.warn('Could not find libc connect() function to hook raw traffic');
  218. } else {
  219.     Interceptor.attach(connectFn, {
  220.         onEnter(args) {
  221.             const fd = this.sockFd = args[0].toInt32();
  222.             const sockType = Socket.type(fd);
  223.  
  224.             const addrPtr = ptr(args[1]);
  225.             const addrLen = args[2].toInt32();
  226.             const addrData = addrPtr.readByteArray(addrLen);
  227.  
  228.             if (sockType === 'tcp' || sockType === 'tcp6' || sockType === 'udp6') {
  229.                 const portAddrBytes = new DataView(addrData.slice(2, 4));
  230.                 const port = portAddrBytes.getUint16(0, false); // Big endian!
  231.  
  232.                 const shouldBeIntercepted = !IGNORED_NON_HTTP_PORTS.includes(port);
  233.  
  234.                 const isIPv6 = sockType === 'tcp6' || sockType === 'udp6';
  235.  
  236.                 const hostBytes = isIPv6
  237.                     // 16 bytes offset by 8 (2 for family, 2 for port, 4 for flowinfo):
  238.                     ? new Uint8Array(addrData.slice(8, 8 + 16))
  239.                     // 4 bytes, offset by 4 (2 for family, 2 for port)
  240.                     : new Uint8Array(addrData.slice(4, 4 + 4));
  241.  
  242.                 const isIntercepted = port === PROXY_PORT && areArraysEqual(hostBytes,
  243.                     isIPv6
  244.                         ? PROXY_HOST_IPv6_BYTES
  245.                         : PROXY_HOST_IPv4_BYTES
  246.                 );
  247.  
  248.                 if (isIntercepted) return;
  249.  
  250.                 if (!shouldBeIntercepted) {
  251.                     // Not intercecpted, sent to unrecognized port - probably not HTTP(S)
  252.                     if (DEBUG_MODE) {
  253.                         console.debug(`Allowing unintercepted connection to port ${port}`);
  254.                     }
  255.                     return;
  256.                 }
  257.  
  258.                 // Otherwise, it's an unintercepted connection that should be captured:
  259.  
  260.                 console.log(`Manually intercepting connection to ${getReadableAddress(hostBytes, isIPv6)}:${port}`);
  261.  
  262.                 // Overwrite the port with the proxy port:
  263.                 portAddrBytes.setUint16(0, PROXY_PORT, false); // Big endian
  264.                 addrPtr.add(2).writeByteArray(portAddrBytes.buffer);
  265.  
  266.                 // Overwrite the address with the proxy address:
  267.                 if (isIPv6) {
  268.                     // Skip 8 bytes: 2 family, 2 port, 4 flowinfo
  269.                     addrPtr.add(8).writeByteArray(PROXY_HOST_IPv6_BYTES);
  270.                 } else {
  271.                     // Skip 4 bytes: 2 family, 2 port
  272.                     addrPtr.add(4).writeByteArray(PROXY_HOST_IPv4_BYTES);
  273.                 }
  274.             } else if (DEBUG_MODE) {
  275.                 console.log(`Ignoring ${sockType} connection`);
  276.                 this.ignored = true;
  277.             }
  278.  
  279.             // N.b. we ignore all non-TCP connections: both UDP and Unix streams
  280.         },
  281.         onLeave: function (result) {
  282.             if (!DEBUG_MODE || this.ignored) return;
  283.  
  284.             const fd = this.sockFd;
  285.             const sockType = Socket.type(fd);
  286.             const address = Socket.peerAddress(fd);
  287.             console.debug(
  288.                 `Connected ${sockType} fd ${fd} to ${JSON.stringify(address)} (${result.toInt32()})`
  289.             );
  290.         }
  291.     });
  292.  
  293.     console.log(`== Redirecting ${
  294.         IGNORED_NON_HTTP_PORTS.length === 0
  295.         ? 'all'
  296.         : 'all unrecognized'
  297.     } TCP connections to ${PROXY_HOST}:${PROXY_PORT} ==`);
  298. }
  299.  
  300. const getReadableAddress = (
  301.     /** @type {Uint8Array} */ hostBytes,
  302.     /** @type {boolean} */ isIPv6
  303. ) => {
  304.     if (!isIPv6) {
  305.         // Return simple a.b.c.d IPv4 format:
  306.         return [...hostBytes].map(x => x.toString()).join('.');
  307.     }
  308.  
  309.     if (
  310.         hostBytes.slice(0, 10).every(b => b === 0) &&
  311.         hostBytes.slice(10, 12).every(b => b === 255)
  312.     ) {
  313.         // IPv4-mapped IPv6 address - print as IPv4 for readability
  314.         return '::ffff:'+[...hostBytes.slice(12)].map(x => x.toString()).join('.');
  315.     }
  316.  
  317.     else {
  318.         // Real IPv6:
  319.         return `[${[...hostBytes].map(x => x.toString(16)).join(':')}]`;
  320.     }
  321. };
  322.  
  323. const areArraysEqual = (arrayA, arrayB) => {
  324.     if (arrayA.length !== arrayB.length) return false;
  325.     return arrayA.every((x, i) => arrayB[i] === x);
  326. };
  327.  
  328. //#endregion NATIVE_CONNECT_HOOK
  329.  
  330.  
  331. //#region NATIVE_TLS_HOOK
  332. /**************************************************************************************************
  333.  *
  334.  * Once we have captured traffic (once it's being sent to our proxy port) the next step is
  335.  * to ensure any clients using TLS (HTTPS) trust our CA certificate, to allow us to intercept
  336.  * encrypted connections successfully.
  337.  *
  338.  * This script does this, by defining overrides to hook BoringSSL (used by iOS 11+) and Cronet
  339.  * (the Chromium network stack, used by some Android apps including TikTok). This is the primary
  340.  * certificate trust mechanism for iOS, and only a niche addition for Android edge cases.
  341.  *
  342.  * The hooks defined here ensure that normal certificate validation is skipped, and instead any
  343.  * TLS connection using our trusted CA is always trusted. In general use this disables both
  344.  * normal & certificate-pinned TLS/HTTPS validation, so that all connections which use your CA
  345.  * should always succeed.
  346.  *
  347.  * This does not completely disable TLS validation, but it does significantly relax it - it's
  348.  * intended for use with the other scripts in this repo that ensure all traffic is routed directly
  349.  * to your MitM proxy (generally on your local network). You probably don't want to use this for
  350.  * any sensitive traffic sent over public/untrusted networks - it is difficult to intercept, and
  351.  * any attacker would need a copy of the CA certificate you're using, but by its nature as a messy
  352.  * hook around TLS internals it's probably not 100% secure.
  353.  *
  354.  * Since iOS 11 (2017) Apple has used BoringSSL internally to handle all TLS. This code
  355.  * hooks low-level BoringSSL calls, to override all custom certificate validation completely.
  356.  * https://nabla-c0d3.github.io/blog/2019/05/18/ssl-kill-switch-for-ios12/ to the general concept,
  357.  * but note that this script goes further - reimplementing basic TLS cert validation, rather than
  358.  * just returning OK blindly for all connections.
  359.  *
  360.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  361.  * SPDX-License-Identifier: AGPL-3.0-or-later
  362.  * SPDX-FileCopyrightText: Tim Perry <[email protected]>
  363.  *
  364.  *************************************************************************************************/
  365.  
  366. const TARGET_LIBS = [
  367.     { name: 'libboringssl.dylib', hooked: false }, // iOS primary TLS implementation
  368.     { name: 'libsscronet.so', hooked: false }, // Cronet on Android
  369.     { name: 'boringssl', hooked: false }, // Bundled by some apps e.g. TikTok on iOS
  370.     { name: 'libssl.so', hooked: false }, // Native OpenSSL in Android
  371. ];
  372.  
  373. TARGET_LIBS.forEach((targetLib) => {
  374.     waitForModule(targetLib.name, (moduleName) => {
  375.         patchTargetLib(moduleName);
  376.         targetLib.hooked = true;
  377.     });
  378.  
  379.     if (
  380.         targetLib.name === 'libboringssl.dylib' &&
  381.         Process.platform === 'darwin' &&
  382.         !targetLib.hooked
  383.     ) {
  384.         // On iOS, we expect this to always work immediately, so print a warning if we
  385.         // ever have to skip this TLS patching process.
  386.         console.log(`\n !!! --- Could not load ${targetLib.name} to hook TLS --- !!!`);
  387.     }
  388. });
  389.  
  390. function patchTargetLib(targetLib) {
  391.     // Get the peer certificates from an SSL pointer. Returns a pointer to a STACK_OF(CRYPTO_BUFFER)
  392.     // which requires use of the next few methods below to actually access.
  393.     // https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get0_peer_certificates
  394.     const SSL_get0_peer_certificates = new NativeFunction(
  395.         Module.findExportByName(targetLib, 'SSL_get0_peer_certificates'),
  396.         'pointer', ['pointer']
  397.     );
  398.  
  399.     // Stack methods:
  400.     // https://commondatastorage.googleapis.com/chromium-boringssl-docs/stack.h.html
  401.     const sk_num = new NativeFunction(
  402.         Module.findExportByName(targetLib, 'sk_num'),
  403.         'size_t', ['pointer']
  404.     );
  405.  
  406.     const sk_value = new NativeFunction(
  407.         Module.findExportByName(targetLib, 'sk_value'),
  408.         'pointer', ['pointer', 'int']
  409.     );
  410.  
  411.     // Crypto buffer methods:
  412.     // https://commondatastorage.googleapis.com/chromium-boringssl-docs/pool.h.html
  413.     const crypto_buffer_len = new NativeFunction(
  414.         Module.findExportByName(targetLib, 'CRYPTO_BUFFER_len'),
  415.         'size_t', ['pointer']
  416.     );
  417.  
  418.     const crypto_buffer_data = new NativeFunction(
  419.         Module.findExportByName(targetLib, 'CRYPTO_BUFFER_data'),
  420.         'pointer', ['pointer']
  421.     );
  422.  
  423.     const SSL_VERIFY_OK = 0x0;
  424.     const SSL_VERIFY_INVALID = 0x1;
  425.  
  426.     // We cache the verification callbacks we create. In general (in testing, 100% of the time) the
  427.     // 'real' callback is always the exact same address, so this is much more efficient than creating
  428.     // a new callback every time.
  429.     const verificationCallbackCache = {};
  430.  
  431.     const buildVerificationCallback = (realCallbackAddr) => {
  432.         if (!verificationCallbackCache[realCallbackAddr]) {
  433.             const realCallback = (!realCallbackAddr || realCallbackAddr.isNull())
  434.                 ? new NativeFunction(realCallbackAddr, 'int', ['pointer','pointer'])
  435.                 : () => SSL_VERIFY_INVALID; // Callback can be null - treat as invalid (=our validation only)
  436.  
  437.             let pendingCheckThreads = new Set();
  438.  
  439.             const hookedCallback = new NativeCallback(function (ssl, out_alert) {
  440.                 let realResult = false; // False = not yet called, 0/1 = call result
  441.  
  442.                 const threadId = Process.getCurrentThreadId();
  443.                 const alreadyHaveLock = pendingCheckThreads.has(threadId);
  444.  
  445.                 // We try to have only one thread running these checks at a time, as parallel calls
  446.                 // here on the same underlying callback seem to crash in some specific scenarios
  447.                 while (pendingCheckThreads.size > 0 && !alreadyHaveLock) {
  448.                     Thread.sleep(0.01);
  449.                 }
  450.                 pendingCheckThreads.add(threadId);
  451.  
  452.                 if (targetLib !== 'libboringssl.dylib') {
  453.                     // Cronet assumes its callback is always called, and crashes if not. iOS's BoringSSL
  454.                     // meanwhile seems to use some negative checks in its callback, and rejects the
  455.                     // connection independently of the return value here if it's called with a bad cert.
  456.                     // End result: we *only sometimes* proactively call the callback.
  457.                     realResult = realCallback(ssl, out_alert);
  458.                 }
  459.  
  460.                 // Extremely dumb certificate validation: we accept any chain where the *exact* CA cert
  461.                 // we were given is present. No flexibility for non-trivial cert chains, and no
  462.                 // validation beyond presence of the expected CA certificate. BoringSSL does do a
  463.                 // fair amount of essential validation independent of the certificate comparison
  464.                 // though, so some basics may be covered regardless (see tls13_process_certificate_verify).
  465.  
  466.                 // This *intentionally* does not reject certs with the wrong hostname, expired CA
  467.                 // or leaf certs, and lots of other issues. This is significantly better than nothing,
  468.                 // but it is not production-ready TLS verification for general use in untrusted envs!
  469.  
  470.                 const peerCerts = SSL_get0_peer_certificates(ssl);
  471.  
  472.                 // Loop through every cert in the chain:
  473.                 for (let i = 0; i < sk_num(peerCerts); i++) {
  474.                     // For each cert, check if it *exactly* matches our configured CA cert:
  475.                     const cert = sk_value(peerCerts, i);
  476.                     const certDataLength = crypto_buffer_len(cert).toNumber();
  477.  
  478.                     if (certDataLength !== CERT_DER.byteLength) continue;
  479.  
  480.                     const certPointer = crypto_buffer_data(cert);
  481.                     const certData = new Uint8Array(certPointer.readByteArray(certDataLength));
  482.  
  483.                     if (certData.every((byte, j) => CERT_DER[j] === byte)) {
  484.                         if (!alreadyHaveLock) pendingCheckThreads.delete(threadId);
  485.                         return SSL_VERIFY_OK;
  486.                     }
  487.                 }
  488.  
  489.                 // No matched peer - fallback to the provided callback instead:
  490.                 if (realResult === false) { // Haven't called it yet
  491.                     realResult = realCallback(ssl, out_alert);
  492.                 }
  493.  
  494.                 if (!alreadyHaveLock) pendingCheckThreads.delete(threadId);
  495.                 return realResult;
  496.             }, 'int', ['pointer','pointer']);
  497.  
  498.             verificationCallbackCache[realCallbackAddr] = hookedCallback;
  499.         }
  500.  
  501.         return verificationCallbackCache[realCallbackAddr];
  502.     };
  503.  
  504.     const customVerifyAddrs = [
  505.         Module.findExportByName(targetLib, "SSL_set_custom_verify"),
  506.         Module.findExportByName(targetLib, "SSL_CTX_set_custom_verify")
  507.     ].filter(Boolean);
  508.  
  509.     customVerifyAddrs.forEach((set_custom_verify_addr) => {
  510.         const set_custom_verify_fn = new NativeFunction(
  511.             set_custom_verify_addr,
  512.             'void', ['pointer', 'int', 'pointer']
  513.         );
  514.  
  515.         // When this function is called, ignore the provided callback, and
  516.         // configure our callback instead:
  517.         Interceptor.replace(set_custom_verify_fn, new NativeCallback(function(ssl, mode, providedCallbackAddr) {
  518.             set_custom_verify_fn(ssl, mode, buildVerificationCallback(providedCallbackAddr));
  519.         }, 'void', ['pointer', 'int', 'pointer']));
  520.     });
  521.  
  522.     if (customVerifyAddrs.length) {
  523.         if (DEBUG_MODE) {
  524.             console.log(`[+] Patched ${customVerifyAddrs.length} ${targetLib} verification methods`);
  525.         }
  526.         console.log(`== Hooked native TLS lib ${targetLib} ==`);
  527.     } else {
  528.         console.log(`\n !!! Hooking native TLS lib ${targetLib} failed - no verification methods found`);
  529.     }
  530.  
  531.     const get_psk_identity_addr = Module.findExportByName(targetLib, "SSL_get_psk_identity");
  532.     if (get_psk_identity_addr) {
  533.         // Hooking this is apparently required for some verification paths which check the
  534.         // result is not 0x0. Any return value should work fine though.
  535.         Interceptor.replace(get_psk_identity_addr, new NativeCallback(function(ssl) {
  536.             return "PSK_IDENTITY_PLACEHOLDER";
  537.         }, 'pointer', ['pointer']));
  538.     } else if (customVerifyAddrs.length) {
  539.         console.log(`Patched ${customVerifyAddrs.length} custom_verify methods, but couldn't find get_psk_identity`);
  540.    }
  541. }
  542.  
  543. //#endregion NATIVE_TLS_HOOK
  544.  
  545. //#region  PROXY_OVERRIDE
  546. /**************************************************************************************************
  547. *
  548. * The first step in intercepting HTTP & HTTPS traffic is to set the default proxy settings,
  549. * telling the app that all requests should be sent via our HTTP proxy.
  550. *
  551. * In this script, we set that up via a few different mechanisms, which cumulatively should
  552. * ensure that all connections are sent via the proxy, even if they attempt to use their
  553. * own custom proxy configurations to avoid this.
  554. *
  555. * Despite that, this still only covers well behaved apps - it's still possible for apps
  556.  * to send network traffic directly if they're determined to do so, or if they're built
  557.  * with a framework that does not do this by default (Flutter is notably in this category).
  558.  * To handle those less tidy cases, we manually capture traffic to recognized target ports
  559.  * in the native connect() hook script.
  560.  *
  561.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  562.  * SPDX-License-Identifier: AGPL-3.0-or-later
  563.  * SPDX-FileCopyrightText: Tim Perry <tim@httptoolkit.com>
  564.  *
  565.  *************************************************************************************************/
  566.  
  567. Java.perform(() => {
  568.     // Set default JVM system properties for the proxy address. Notably these are used
  569.     // to initialize WebView configuration.
  570.     Java.use('java.lang.System').setProperty('http.proxyHost', PROXY_HOST);
  571.     Java.use('java.lang.System').setProperty('http.proxyPort', PROXY_PORT.toString());
  572.     Java.use('java.lang.System').setProperty('https.proxyHost', PROXY_HOST);
  573.     Java.use('java.lang.System').setProperty('https.proxyPort', PROXY_PORT.toString());
  574.  
  575.     Java.use('java.lang.System').clearProperty('http.nonProxyHosts');
  576.     Java.use('java.lang.System').clearProperty('https.nonProxyHosts');
  577.  
  578.     // Some Android internals attempt to reset these settings to match the device configuration.
  579.     // We block that directly here:
  580.     const controlledSystemProperties = [
  581.         'http.proxyHost',
  582.         'http.proxyPort',
  583.         'https.proxyHost',
  584.         'https.proxyPort',
  585.         'http.nonProxyHosts',
  586.         'https.nonProxyHosts',
  587.     ];
  588.     Java.use('java.lang.System').clearProperty.implementation = function (property) {
  589.         if (controlledSystemProperties.includes(property)) {
  590.             if (DEBUG_MODE) console.log(`Ignoring attempt to clear ${property} system property`);
  591.             return this.getProperty(property);
  592.         }
  593.         return this.clearProperty(...arguments);
  594.     }
  595.     Java.use('java.lang.System').setProperty.implementation = function (property) {
  596.         if (controlledSystemProperties.includes(property)) {
  597.             if (DEBUG_MODE) console.log(`Ignoring attempt to override ${property} system property`);
  598.             return this.getProperty(property);
  599.         }
  600.         return this.setProperty(...arguments);
  601.     }
  602.  
  603.     // Configure the app's proxy directly, via the app connectivity manager service:
  604.     const ConnectivityManager = Java.use('android.net.ConnectivityManager');
  605.     const ProxyInfo = Java.use('android.net.ProxyInfo');
  606.     ConnectivityManager.getDefaultProxy.implementation = () => ProxyInfo.$new(PROXY_HOST, PROXY_PORT, '');
  607.     // (Not clear if this works 100% - implying there are ConnectivityManager subclasses handling this)
  608.  
  609.     console.log(`== Proxy system configuration overridden to ${PROXY_HOST}:${PROXY_PORT} ==`);
  610.  
  611.     // Configure the proxy indirectly, by overriding the return value for all ProxySelectors everywhere:
  612.     const Collections = Java.use('java.util.Collections');
  613.     const ProxyType = Java.use('java.net.Proxy$Type');
  614.     const InetSocketAddress = Java.use('java.net.InetSocketAddress');
  615.     const ProxyCls = Java.use('java.net.Proxy'); // 'Proxy' is reserved in JS
  616.  
  617.     const targetProxy = ProxyCls.$new(
  618.         ProxyType.HTTP.value,
  619.         InetSocketAddress.$new(PROXY_HOST, PROXY_PORT)
  620.     );
  621.     const getTargetProxyList = () => Collections.singletonList(targetProxy);
  622.  
  623.     const ProxySelector = Java.use('java.net.ProxySelector');
  624.  
  625.     // Find every implementation of ProxySelector by quickly scanning method signatures, and
  626.     // then checking whether each match actually implements java.net.ProxySelector:
  627.     const proxySelectorClasses = Java.enumerateMethods('*!select(java.net.URI): java.util.List/s')
  628.         .flatMap((matchingLoader) => matchingLoader.classes
  629.             .map((classData) => Java.use(classData.name))
  630.             .filter((Cls) => ProxySelector.class.isAssignableFrom(Cls.class))
  631.         );
  632.  
  633.     // Replace the 'select' of every implementation, so they all send traffic to us:
  634.     proxySelectorClasses.forEach(ProxySelectorCls => {
  635.         if (DEBUG_MODE) {
  636.             console.log('Rewriting', ProxySelectorCls.toString());
  637.         }
  638.         ProxySelectorCls.select.implementation = () => getTargetProxyList()
  639.     });
  640.  
  641.     console.log(`== Proxy configuration overridden to ${PROXY_HOST}:${PROXY_PORT} ==`);
  642. });
  643.  
  644. //#endregion PROXY_OVERRIDE
  645.  
  646. //#region CERTIFICATE_INJECTION
  647. /**************************************************************************************************
  648.  *
  649.  * Once we have captured traffic (once it's being sent to our proxy port) the next step is
  650.  * to ensure any clients using TLS (HTTPS) trust our CA certificate, to allow us to intercept
  651.  * encrypted connections successfully.
  652.  *
  653.  * This script does so by attaching to the internals of Conscrypt (the Android SDK's standard
  654.  * TLS implementation) and pre-adding our certificate to the 'already trusted' cache, so that
  655.  * future connections trust it implicitly. This ensures that all normal uses of Android APIs
  656.  * for HTTPS & TLS will allow interception.
  657.  *
  658.  * This does not handle all standalone certificate pinning techniques - where the application
  659.  * actively rejects certificates that are trusted by default on the system. That's dealt with
  660.  * in the separate certificate unpinning script.
  661.  *
  662.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  663.  * SPDX-License-Identifier: AGPL-3.0-or-later
  664.  * SPDX-FileCopyrightText: Tim Perry <[email protected]>
  665.  *
  666.  *************************************************************************************************/
  667.  
  668. Java.perform(() => {
  669.     // First, we build a JVM representation of our certificate:
  670.     const String = Java.use("java.lang.String");
  671.     const ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream');
  672.     const CertFactory = Java.use('java.security.cert.CertificateFactory');
  673.  
  674.     let cert;
  675.     try {
  676.         const certFactory = CertFactory.getInstance("X.509");
  677.         const certBytes = String.$new(CERT_PEM).getBytes();
  678.         cert = certFactory.generateCertificate(ByteArrayInputStream.$new(certBytes));
  679.     } catch (e) {
  680.         console.error('Could not parse provided certificate PEM!');
  681.         console.error(e);
  682.         Java.use('java.lang.System').exit(1);
  683.     }
  684.  
  685.     // Then we hook TrustedCertificateIndex. This is used for caching known trusted certs within Conscrypt -
  686.     // by prepopulating all instances, we ensure that all TrustManagerImpls (and potentially other
  687.     // things) automatically trust our certificate specifically (without disabling validation entirely).
  688.     // This should apply to Android v7+ - previous versions used SSLContext & X509TrustManager.
  689.     [
  690.         'com.android.org.conscrypt.TrustedCertificateIndex',
  691.         'org.conscrypt.TrustedCertificateIndex', // Might be used (com.android is synthetic) - unclear
  692.         'org.apache.harmony.xnet.provider.jsse.TrustedCertificateIndex' // Used in Apache Harmony version of Conscrypt
  693.     ].forEach((TrustedCertificateIndexClassname, i) => {
  694.         let TrustedCertificateIndex;
  695.         try {
  696.             TrustedCertificateIndex = Java.use(TrustedCertificateIndexClassname);
  697.         } catch (e) {
  698.             if (i === 0) {
  699.                 throw new Error(`${TrustedCertificateIndexClassname} not found - could not inject system certificate`);
  700.             } else {
  701.                 // Other classnames are optional fallbacks
  702.                 if (DEBUG_MODE) {
  703.                     console.log(`[ ] Skipped cert injection for ${TrustedCertificateIndexClassname} (not present)`);
  704.                 }
  705.                 return;
  706.             }
  707.         }
  708.  
  709.         TrustedCertificateIndex.$init.overloads.forEach((overload) => {
  710.             overload.implementation = function () {
  711.                 this.$init(...arguments);
  712.                 // Index our cert as already trusted, right from the start:
  713.                 this.index(cert);
  714.             }
  715.         });
  716.  
  717.         TrustedCertificateIndex.reset.overloads.forEach((overload) => {
  718.             overload.implementation = function () {
  719.                 const result = this.reset(...arguments);
  720.                 // Index our cert in here again, since the reset removes it:
  721.                 this.index(cert);
  722.                 return result;
  723.             };
  724.         });
  725.  
  726.         if (DEBUG_MODE) console.log(`[+] Injected cert into ${TrustedCertificateIndexClassname}`);
  727.     });
  728.  
  729.     // This effectively adds us to the system certs, and also defeats quite a bit of basic certificate
  730.     // pinning too! It auto-trusts us in any implementation that uses TrustManagerImpl (Conscrypt) as
  731.     // the underlying cert checking component.
  732.  
  733.     console.log('== System certificate trust injected ==');
  734. });
  735. //#endregion CERTIFICATE_INJECTION
  736.  
  737. //#region CERTIFICATE_UNPINNING
  738. /**************************************************************************************************
  739.  *
  740.  * This script defines a large set of targeted certificate unpinning hooks: matching specific
  741.  * methods in certain classes, and transforming their behaviour to ensure that restrictions to
  742.  * TLS trust are disabled.
  743.  *
  744.  * This does not disable TLS protections completely - each hook is designed to disable only
  745.  * *additional* restrictions, and to explicitly trust the certificate provided as CERT_PEM in the
  746.  * config.js configuration file, preserving normal TLS protections wherever possible, even while
  747.  * allowing for controlled MitM of local traffic.
  748.  *
  749.  * The file consists of a few general-purpose methods, then a data structure declaratively
  750.  * defining the classes & methods to match, and how to transform them, and then logic at the end
  751.  * which uses this data structure, applying the transformation for each found match to the
  752.  * target process.
  753.  *
  754.  * For more details on what was matched, and log output when each hooked method is actually used,
  755.  * enable DEBUG_MODE in config.js, and watch the Frida output after running this script.
  756.  *
  757.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  758.  * SPDX-License-Identifier: AGPL-3.0-or-later
  759.  * SPDX-FileCopyrightText: Tim Perry <[email protected]>
  760.  *
  761.  *************************************************************************************************/
  762.  
  763. function buildX509CertificateFromBytes(certBytes) {
  764.     const ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream');
  765.     const CertFactory = Java.use('java.security.cert.CertificateFactory');
  766.     const certFactory = CertFactory.getInstance("X.509");
  767.     return certFactory.generateCertificate(ByteArrayInputStream.$new(certBytes));
  768. }
  769.  
  770. function getCustomTrustManagerFactory() {
  771.     // This is the one X509Certificate that we want to trust. No need to trust others (we should capture
  772.     // _all_ TLS traffic) and risky to trust _everything_ (risks interception between device & proxy, or
  773.     // worse: some traffic being unintercepted & sent as HTTPS with TLS effectively disabled over the
  774.     // real web - potentially exposing auth keys, private data and all sorts).
  775.     const certBytes = Java.use("java.lang.String").$new(CERT_PEM).getBytes();
  776.     const trustedCACert = buildX509CertificateFromBytes(certBytes);
  777.  
  778.     // Build a custom TrustManagerFactory with a KeyStore that trusts only this certificate:
  779.  
  780.     const KeyStore = Java.use("java.security.KeyStore");
  781.     const keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
  782.     keyStore.load(null);
  783.     keyStore.setCertificateEntry("ca", trustedCACert);
  784.  
  785.     const TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
  786.     const customTrustManagerFactory = TrustManagerFactory.getInstance(
  787.         TrustManagerFactory.getDefaultAlgorithm()
  788.     );
  789.     customTrustManagerFactory.init(keyStore);
  790.  
  791.     return customTrustManagerFactory;
  792. }
  793.  
  794. function getCustomX509TrustManager() {
  795.     const customTrustManagerFactory = getCustomTrustManagerFactory();
  796.     const trustManagers = customTrustManagerFactory.getTrustManagers();
  797.  
  798.     const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
  799.  
  800.     const x509TrustManager = trustManagers.find((trustManager) => {
  801.         return trustManager.class.isAssignableFrom(X509TrustManager.class);
  802.     });
  803.  
  804.     // We have to cast it explicitly before Frida will allow us to use the X509 methods:
  805.     return Java.cast(x509TrustManager, X509TrustManager);
  806. }
  807.  
  808. // Some standard hook replacements for various cases:
  809. const NO_OP = () => {};
  810. const RETURN_TRUE = () => true;
  811. const CHECK_OUR_TRUST_MANAGER_ONLY = () => {
  812.     const trustManager = getCustomX509TrustManager();
  813.     return (certs, authType) => {
  814.         trustManager.checkServerTrusted(certs, authType);
  815.     };
  816. };
  817.  
  818. const PINNING_FIXES = {
  819.     // --- Native HttpsURLConnection
  820.  
  821.     'javax.net.ssl.HttpsURLConnection': [
  822.         {
  823.             methodName: 'setDefaultHostnameVerifier',
  824.             replacement: () => NO_OP
  825.         },
  826.         {
  827.             methodName: 'setSSLSocketFactory',
  828.             replacement: () => NO_OP
  829.         },
  830.         {
  831.             methodName: 'setHostnameVerifier',
  832.             replacement: () => NO_OP
  833.         },
  834.     ],
  835.  
  836.     // --- Native SSLContext
  837.  
  838.     'javax.net.ssl.SSLContext': [
  839.         {
  840.             methodName: 'init',
  841.             overload: ['[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'],
  842.             replacement: (targetMethod) => {
  843.                 const customTrustManagerFactory = getCustomTrustManagerFactory();
  844.  
  845.                 // When constructor is called, replace the trust managers argument:
  846.                 return function (keyManager, _providedTrustManagers, secureRandom) {
  847.                     return targetMethod.call(this,
  848.                         keyManager,
  849.                         customTrustManagerFactory.getTrustManagers(), // Override their trust managers
  850.                         secureRandom
  851.                     );
  852.                 }
  853.             }
  854.         }
  855.     ],
  856.  
  857.     // --- Native Conscrypt CertPinManager
  858.  
  859.     'com.android.org.conscrypt.CertPinManager': [
  860.         {
  861.             methodName: 'isChainValid',
  862.             replacement: () => RETURN_TRUE
  863.         },
  864.         {
  865.             methodName: 'checkChainPinning',
  866.             replacement: () => NO_OP
  867.         }
  868.     ],
  869.  
  870.     // --- Native pinning configuration loading (used for configuration by many libraries)
  871.  
  872.     'android.security.net.config.NetworkSecurityConfig': [
  873.         {
  874.             methodName: '$init',
  875.             overload: '*',
  876.             replacement: (targetMethod) => {
  877.                 const PinSet = Java.use('android.security.net.config.PinSet');
  878.                 const EMPTY_PINSET = PinSet.EMPTY_PINSET.value;
  879.                 return function () {
  880.                     // Always ignore the 2nd 'pins' PinSet argument entirely:
  881.                     arguments[2] = EMPTY_PINSET;
  882.                     targetMethod.call(this, ...arguments);
  883.                 }
  884.             }
  885.         }
  886.     ],
  887.  
  888.     // --- Native HostnameVerification override (n.b. Android contains its own vendored OkHttp v2!)
  889.  
  890.     'com.android.okhttp.internal.tls.OkHostnameVerifier': [
  891.         {
  892.             methodName: 'verify',
  893.             overload: [
  894.                 'java.lang.String',
  895.                 'javax.net.ssl.SSLSession'
  896.             ],
  897.             replacement: (targetMethod) => {
  898.                 // Our trust manager - this trusts *only* our extra CA
  899.                 const trustManager = getCustomX509TrustManager();
  900.  
  901.                 return function (hostname, sslSession) {
  902.                     try {
  903.                         const certs = sslSession.getPeerCertificates();
  904.  
  905.                         // https://stackoverflow.com/a/70469741/68051
  906.                         const authType = "RSA";
  907.  
  908.                         // This throws if the certificate isn't trusted (i.e. if it's
  909.                         // not signed by our extra CA specifically):
  910.                         trustManager.checkServerTrusted(certs, authType);
  911.  
  912.                         // If the cert is from our CA, great! Skip hostname checks entirely.
  913.                         return true;
  914.                     } catch (e) {} // Ignore errors and fallback to default behaviour
  915.  
  916.                     // We fallback to ensure that connections with other CAs (e.g. direct
  917.                     // connections allowed past the proxy) validate as normal.
  918.                     return targetMethod.call(this, ...arguments);
  919.                 }
  920.             }
  921.         }
  922.     ],
  923.  
  924.     'com.android.okhttp.Address': [
  925.         {
  926.             methodName: '$init',
  927.             overload: [
  928.                 'java.lang.String',
  929.                 'int',
  930.                 'com.android.okhttp.Dns',
  931.                 'javax.net.SocketFactory',
  932.                 'javax.net.ssl.SSLSocketFactory',
  933.                 'javax.net.ssl.HostnameVerifier',
  934.                 'com.android.okhttp.CertificatePinner',
  935.                 'com.android.okhttp.Authenticator',
  936.                 'java.net.Proxy',
  937.                 'java.util.List',
  938.                 'java.util.List',
  939.                 'java.net.ProxySelector'
  940.             ],
  941.             replacement: (targetMethod) => {
  942.                 const defaultHostnameVerifier = Java.use("com.android.okhttp.internal.tls.OkHostnameVerifier")
  943.                     .INSTANCE.value;
  944.                 const defaultCertPinner = Java.use("com.android.okhttp.CertificatePinner")
  945.                     .DEFAULT.value;
  946.  
  947.                 return function () {
  948.                     // Override arguments, to swap any custom check params (widely used
  949.                     // to add stricter rules to TLS verification) with the defaults instead:
  950.                     arguments[5] = defaultHostnameVerifier;
  951.                     arguments[6] = defaultCertPinner;
  952.  
  953.                     targetMethod.call(this, ...arguments);
  954.                 }
  955.             }
  956.         },
  957.         // Almost identical patch, but for Nougat and older. In these versions, the DNS argument
  958.         // isn't passed here, so the arguments to patch changes slightly:
  959.         {
  960.             methodName: '$init',
  961.             overload: [
  962.                 'java.lang.String',
  963.                 'int',
  964.                 // No DNS param
  965.                 'javax.net.SocketFactory',
  966.                 'javax.net.ssl.SSLSocketFactory',
  967.                 'javax.net.ssl.HostnameVerifier',
  968.                 'com.android.okhttp.CertificatePinner',
  969.                 'com.android.okhttp.Authenticator',
  970.                 'java.net.Proxy',
  971.                 'java.util.List',
  972.                 'java.util.List',
  973.                 'java.net.ProxySelector'
  974.             ],
  975.             replacement: (targetMethod) => {
  976.                 const defaultHostnameVerifier = Java.use("com.android.okhttp.internal.tls.OkHostnameVerifier")
  977.                     .INSTANCE.value;
  978.                 const defaultCertPinner = Java.use("com.android.okhttp.CertificatePinner")
  979.                     .DEFAULT.value;
  980.  
  981.                 return function () {
  982.                     // Override arguments, to swap any custom check params (widely used
  983.                     // to add stricter rules to TLS verification) with the defaults instead:
  984.                     arguments[4] = defaultHostnameVerifier;
  985.                     arguments[5] = defaultCertPinner;
  986.  
  987.                     targetMethod.call(this, ...arguments);
  988.                 }
  989.             }
  990.         }
  991.     ],
  992.  
  993.     // --- OkHttp v3
  994.  
  995.     'okhttp3.CertificatePinner': [
  996.         {
  997.             methodName: 'check',
  998.             overload: ['java.lang.String', 'java.util.List'],
  999.             replacement: () => NO_OP
  1000.         },
  1001.         {
  1002.             methodName: 'check',
  1003.             overload: ['java.lang.String', 'java.security.cert.Certificate'],
  1004.             replacement: () => NO_OP
  1005.         },
  1006.         {
  1007.             methodName: 'check',
  1008.             overload: ['java.lang.String', '[Ljava.security.cert.Certificate;'],
  1009.             replacement: () => NO_OP
  1010.         },
  1011.         {
  1012.             methodName: 'check$okhttp',
  1013.             replacement: () => NO_OP
  1014.         },
  1015.     ],
  1016.  
  1017.     // --- SquareUp OkHttp (< v3)
  1018.  
  1019.     'com.squareup.okhttp.CertificatePinner': [
  1020.         {
  1021.             methodName: 'check',
  1022.             overload: ['java.lang.String', 'java.security.cert.Certificate'],
  1023.             replacement: () => NO_OP
  1024.         },
  1025.         {
  1026.             methodName: 'check',
  1027.             overload: ['java.lang.String', 'java.util.List'],
  1028.             replacement: () => NO_OP
  1029.         }
  1030.     ],
  1031.  
  1032.     // --- Trustkit (https://github.com/datatheorem/TrustKit-Android/)
  1033.  
  1034.     'com.datatheorem.android.trustkit.pinning.PinningTrustManager': [
  1035.         {
  1036.             methodName: 'checkServerTrusted',
  1037.             replacement: CHECK_OUR_TRUST_MANAGER_ONLY
  1038.         }
  1039.     ],
  1040.  
  1041.     // --- Appcelerator (https://github.com/tidev/appcelerator.https)
  1042.  
  1043.     'appcelerator.https.PinningTrustManager': [
  1044.         {
  1045.             methodName: 'checkServerTrusted',
  1046.             replacement: CHECK_OUR_TRUST_MANAGER_ONLY
  1047.         }
  1048.     ],
  1049.  
  1050.     // --- PhoneGap sslCertificateChecker (https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin)
  1051.  
  1052.     'nl.xservices.plugins.sslCertificateChecker': [
  1053.         {
  1054.             methodName: 'execute',
  1055.             overload: ['java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext'],
  1056.             replacement: () => (_action, _args, context) => {
  1057.                 context.success("CONNECTION_SECURE");
  1058.                 return true;
  1059.             }
  1060.             // This trusts _all_ certs, but that's fine - this is used for checks of independent test
  1061.             // connections, rather than being a primary mechanism to secure the app's TLS connections.
  1062.         }
  1063.     ],
  1064.  
  1065.     // --- IBM WorkLight
  1066.  
  1067.     'com.worklight.wlclient.api.WLClient': [
  1068.         {
  1069.             methodName: 'pinTrustedCertificatePublicKey',
  1070.             getMethod: (WLClientCls) => WLClientCls.getInstance().pinTrustedCertificatePublicKey,
  1071.             overload: '*'
  1072.         }
  1073.     ],
  1074.  
  1075.     'com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning': [
  1076.         {
  1077.             methodName: 'verify',
  1078.             overload: '*',
  1079.             replacement: () => NO_OP
  1080.         }
  1081.         // This covers at least 4 commonly used WorkLight patches. Oddly, most sets of hooks seem
  1082.         // to return true for 1/4 cases, which must be wrong (overloads must all have the same
  1083.         // return type) but also it's very hard to find any modern (since 2017) references to this
  1084.         // class anywhere including WorkLight docs, so it may no longer be relevant anyway.
  1085.     ],
  1086.  
  1087.     'com.worklight.androidgap.plugin.WLCertificatePinningPlugin': [
  1088.         {
  1089.             methodName: 'execute',
  1090.             overload: '*',
  1091.             replacement: () => RETURN_TRUE
  1092.         }
  1093.     ],
  1094.  
  1095.     // --- CWAC-Netsecurity (unofficial back-port pinner for Android<4.2) CertPinManager
  1096.  
  1097.     'com.commonsware.cwac.netsecurity.conscrypt.CertPinManager': [
  1098.         {
  1099.             methodName: 'isChainValid',
  1100.             overload: '*',
  1101.             replacement: () => RETURN_TRUE
  1102.         }
  1103.     ],
  1104.  
  1105.     // --- Netty
  1106.  
  1107.     'io.netty.handler.ssl.util.FingerprintTrustManagerFactory': [
  1108.         {
  1109.             methodName: 'checkTrusted',
  1110.             replacement: () => NO_OP
  1111.         }
  1112.     ],
  1113.  
  1114.     // --- Cordova / PhoneGap Advanced HTTP Plugin (https://github.com/silkimen/cordova-plugin-advanced-http)
  1115.  
  1116.     // Modern version:
  1117.     'com.silkimen.cordovahttp.CordovaServerTrust': [
  1118.         {
  1119.             methodName: '$init',
  1120.             replacement: (targetMethod) => function () {
  1121.                 // Ignore any attempts to set trust to 'pinned'. Default settings will trust
  1122.                 // our cert because of the separate system-certificate injection step.
  1123.                 if (arguments[0] === 'pinned') {
  1124.                     arguments[0] = 'default';
  1125.                 }
  1126.  
  1127.                 return targetMethod.call(this, ...arguments);
  1128.             }
  1129.         }
  1130.     ],
  1131.  
  1132.     // --- Appmattus Cert Transparency (https://github.com/appmattus/certificatetransparency/)
  1133.  
  1134.     'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyHostnameVerifier': [
  1135.         {
  1136.             methodName: 'verify',
  1137.             replacement: () => RETURN_TRUE
  1138.             // This is not called unless the cert passes basic trust checks, so it's safe to blindly accept.
  1139.         }
  1140.     ],
  1141.  
  1142.     'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyInterceptor': [
  1143.         {
  1144.             methodName: 'intercept',
  1145.             replacement: () => (a) => a.proceed(a.request())
  1146.             // This is not called unless the cert passes basic trust checks, so it's safe to blindly accept.
  1147.         }
  1148.     ],
  1149.  
  1150.     'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager': [
  1151.         {
  1152.             methodName: 'checkServerTrusted',
  1153.             overload: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
  1154.             replacement: CHECK_OUR_TRUST_MANAGER_ONLY,
  1155.             methodName: 'checkServerTrusted',
  1156.             overload: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
  1157.             replacement: () => {
  1158.                 const trustManager = getCustomX509TrustManager();
  1159.                 return (certs, authType, _hostname) => {
  1160.                     // We ignore the hostname - if the certs are good (i.e they're ours), then the
  1161.                     // whole chain is good to go.
  1162.                     trustManager.checkServerTrusted(certs, authType);
  1163.                     return Java.use('java.util.Arrays').asList(certs);
  1164.                 };
  1165.             }
  1166.         }
  1167.     ]
  1168.  
  1169. };
  1170.  
  1171. const getJavaClassIfExists = (clsName) => {
  1172.     try {
  1173.         return Java.use(clsName);
  1174.     } catch {
  1175.         return undefined;
  1176.     }
  1177. }
  1178.  
  1179. Java.perform(function () {
  1180.     if (DEBUG_MODE) console.log('\n    === Disabling all recognized unpinning libraries ===');
  1181.  
  1182.     const classesToPatch = Object.keys(PINNING_FIXES);
  1183.  
  1184.     classesToPatch.forEach((targetClassName) => {
  1185.         const TargetClass = getJavaClassIfExists(targetClassName);
  1186.         if (!TargetClass) {
  1187.             // We skip patches for any classes that don't seem to be present. This is common
  1188.             // as not all libraries we handle are necessarily used.
  1189.             if (DEBUG_MODE) console.log(`[ ] ${targetClassName} *`);
  1190.             return;
  1191.         }
  1192.  
  1193.         const patches = PINNING_FIXES[targetClassName];
  1194.  
  1195.         let patchApplied = false;
  1196.  
  1197.         patches.forEach(({ methodName, getMethod, overload, replacement }) => {
  1198.             const namedTargetMethod = getMethod
  1199.                 ? getMethod(TargetClass)
  1200.                 : TargetClass[methodName];
  1201.  
  1202.             const methodDescription = `${methodName}${
  1203.                 overload === '*'
  1204.                     ? '(*)'
  1205.                 : overload
  1206.                     ? '(' + overload.map((argType) => {
  1207.                         // Simplify arg names to just the class name for simpler logs:
  1208.                         const argClassName = argType.split('.').slice(-1)[0];
  1209.                         if (argType.startsWith('[L')) return `${argClassName}[]`;
  1210.                         else return argClassName;
  1211.                     }).join(', ') + ')'
  1212.                 // No overload:
  1213.                     : ''
  1214.             }`
  1215.  
  1216.             let targetMethodImplementations = [];
  1217.             try {
  1218.                 if (namedTargetMethod) {
  1219.                     if (!overload) {
  1220.                             // No overload specified
  1221.                         targetMethodImplementations = [namedTargetMethod];
  1222.                     } else if (overload === '*') {
  1223.                         // Targetting _all_ overloads
  1224.                         targetMethodImplementations = namedTargetMethod.overloads;
  1225.                     } else {
  1226.                         // Or targetting a specific overload:
  1227.                         targetMethodImplementations = [namedTargetMethod.overload(...overload)];
  1228.                     }
  1229.                 }
  1230.             } catch (e) {
  1231.                 // Overload not present
  1232.             }
  1233.  
  1234.  
  1235.             // We skip patches for any methods that don't seem to be present. This is rarer, but does
  1236.             // happen due to methods that only appear in certain library versions or whose signatures
  1237.             // have changed over time.
  1238.             if (targetMethodImplementations.length === 0) {
  1239.                 if (DEBUG_MODE) console.log(`[ ] ${targetClassName} ${methodDescription}`);
  1240.                 return;
  1241.             }
  1242.  
  1243.             targetMethodImplementations.forEach((targetMethod, i) => {
  1244.                 const patchName = `${targetClassName} ${methodDescription}${
  1245.                     targetMethodImplementations.length > 1 ? ` (${i})` : ''
  1246.                 }`;
  1247.  
  1248.                 try {
  1249.                     const newImplementation = replacement(targetMethod);
  1250.                     if (DEBUG_MODE) {
  1251.                         // Log each hooked method as it's called:
  1252.                         targetMethod.implementation = function () {
  1253.                             console.log(` => ${patchName}`);
  1254.                             return newImplementation.apply(this, arguments);
  1255.                         }
  1256.                     } else {
  1257.                         targetMethod.implementation = newImplementation;
  1258.                     }
  1259.  
  1260.                     if (DEBUG_MODE) console.log(`[+] ${patchName}`);
  1261.                     patchApplied = true;
  1262.                 } catch (e) {
  1263.                     // In theory, errors like this should never happen - it means the patch is broken
  1264.                     // (e.g. some dynamic patch building fails completely)
  1265.                     console.error(`[!] ERROR: ${patchName} failed: ${e}`);
  1266.                 }
  1267.             })
  1268.         });
  1269.  
  1270.         if (!patchApplied) {
  1271.             console.warn(`[!] Matched class ${targetClassName} but could not patch any methods`);
  1272.         }
  1273.     });
  1274.  
  1275.     console.log('== Certificate unpinning completed ==');
  1276. });
  1277. //#endregion CERITIFATE_UNPINNING
  1278.  
  1279.  
  1280. //#region CERTIFICATE_UNPINNING_FALLBACK
  1281. /**************************************************************************************************
  1282.  *
  1283.  * Once we've set up the configuration and certificate, and then disabled all the pinning
  1284.  * techniques we're aware of, we add one last touch: a fallback hook, designed to spot and handle
  1285.  * unknown unknowns.
  1286.  *
  1287.  * This can also be useful for heavily obfuscated apps, where 3rd party libraries are obfuscated
  1288.  * sufficiently that our hooks no longer recognize the methods we care about.
  1289.  *
  1290.  * To handle this, we watch for methods that throw known built-in TLS errors (these are *very*
  1291.  * widely used, and always recognizable as they're defined natively), and then subsequently patch
  1292.  * them for all future calls. Whenever a method throws this, we attempt to recognize it from
  1293.  * signatures alone, and automatically hook it.
  1294.  *
  1295.  * These are very much a fallback! They might not work! They almost certainly won't work on the
  1296.  * first request, so applications will see at least one failure. Even when they fail though, they
  1297.  * will at least log the method that's failing, so this works well as a starting point for manual
  1298.  * reverse engineering. If this does fail and cause problems, you may want to skip this script
  1299.  * and use only the known-good patches provided elsewhere.
  1300.  *
  1301.  * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
  1302.  * SPDX-License-Identifier: AGPL-3.0-or-later
  1303.  * SPDX-FileCopyrightText: Tim Perry <[email protected]>
  1304.  *
  1305.  *************************************************************************************************/
  1306.  
  1307. // Capture the full fields or methods from a Frida class reference via JVM reflection:
  1308. const getFields = (cls) => getFridaValues(cls, cls.class.getDeclaredFields());
  1309. const getMethods = (cls) => getFridaValues(cls, cls.class.getDeclaredMethods());
  1310.  
  1311. // Take a Frida class + JVM reflection result, and turn it into a clear list
  1312. // of names -> Frida values (field or method references)
  1313. const getFridaValues = (cls, values) => values.map((value) =>
  1314.     [value.getName(), cls[value.getName()]]
  1315. );
  1316.  
  1317. Java.perform(function () {
  1318.     try {
  1319.         const X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
  1320.         const defaultTrustManager = getCustomX509TrustManager(); // Defined in the unpinning script
  1321.  
  1322.         const isX509TrustManager = (cls, methodName) =>
  1323.             methodName === 'checkServerTrusted' &&
  1324.             X509TrustManager.class.isAssignableFrom(cls.class);
  1325.  
  1326.         // There are two standard methods that X509TM implementations might override. We confirm we're
  1327.         // matching the methods we expect by double-checking against the argument types:
  1328.         const BASE_METHOD_ARGUMENTS = [
  1329.             '[Ljava.security.cert.X509Certificate;',
  1330.             'java.lang.String'
  1331.         ];
  1332.         const EXTENDED_METHOD_ARGUMENTS = [
  1333.             '[Ljava.security.cert.X509Certificate;',
  1334.             'java.lang.String',
  1335.             'java.lang.String'
  1336.         ];
  1337.  
  1338.         const isOkHttpCheckMethod = (errorMessage, method) =>
  1339.             errorMessage.startsWith("Certificate pinning failure!" + "\n  Peer certificate chain:") &&
  1340.             method.argumentTypes.length === 2 &&
  1341.             method.argumentTypes[0].className === 'java.lang.String';
  1342.  
  1343.         const isAppmattusOkHttpInterceptMethod = (errorMessage, method) => {
  1344.             if (errorMessage !== 'Certificate transparency failed') return;
  1345.  
  1346.             // Takes a single OkHttp chain argument:
  1347.             if (method.argumentTypes.length !== 1) return;
  1348.  
  1349.             // The method must take an Interceptor.Chain, for which we need to
  1350.             // call chain.proceed(chain.request()) to return a Response type.
  1351.             // To do that, we effectively pattern match our way through all the
  1352.             // related types to work out what's what:
  1353.  
  1354.             const chainType = Java.use(method.argumentTypes[0].className);
  1355.             const responseTypeName = method.returnType.className;
  1356.  
  1357.             const matchedChain = matchOkHttpChain(chainType, responseTypeName);
  1358.             return !!matchedChain;
  1359.         };
  1360.  
  1361.         const matchOkHttpChain = (cls, expectedReturnTypeName) => {
  1362.             // Find the chain.proceed() method:
  1363.             const methods = getMethods(cls);
  1364.             const matchingMethods = methods.filter(([_, method]) =>
  1365.                 method.returnType.className === expectedReturnTypeName
  1366.             );
  1367.             if (matchingMethods.length !== 1) return;
  1368.  
  1369.             const [proceedMethodName, proceedMethod] = matchingMethods[0];
  1370.             if (proceedMethod.argumentTypes.length !== 1) return;
  1371.  
  1372.             const argumentTypeName = proceedMethod.argumentTypes[0].className;
  1373.  
  1374.             // Find the chain.request private field (.request() getter can be
  1375.             // optimized out, so we read the field directly):
  1376.             const fields = getFields(cls);
  1377.             const matchingFields = fields.filter(([_, field]) =>
  1378.                 field.fieldReturnType?.className === argumentTypeName
  1379.             );
  1380.             if (matchingFields.length !== 1) return;
  1381.  
  1382.             const [requestFieldName] = matchingFields[0];
  1383.  
  1384.             return {
  1385.                 proceedMethodName,
  1386.                 requestFieldName
  1387.             };
  1388.         };
  1389.  
  1390.         const buildUnhandledErrorPatcher = (errorClassName, originalConstructor) => {
  1391.             return function (errorArg) {
  1392.                 try {
  1393.                     console.log('\n !!! --- Unexpected TLS failure --- !!!');
  1394.  
  1395.                     // This may be a message, or an cause, or plausibly maybe other types? But
  1396.                     // stringifying gives something consistently message-shaped, so that'll do.
  1397.                     const errorMessage = errorArg?.toString() ?? '';
  1398.  
  1399.                     // Parse the stack trace to work out who threw this error:
  1400.                     const stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace();
  1401.                     const exceptionStackIndex = stackTrace.findIndex(stack =>
  1402.                         stack.getClassName() === errorClassName
  1403.                     );
  1404.                     const callingFunctionStack = stackTrace[exceptionStackIndex + 1];
  1405.  
  1406.                     const className = callingFunctionStack.getClassName();
  1407.                     const methodName = callingFunctionStack.getMethodName();
  1408.  
  1409.                     const errorTypeName = errorClassName.split('.').slice(-1)[0];
  1410.                     console.log(`      ${errorTypeName}: ${errorMessage}`);
  1411.                     console.log(`      Thrown by ${className}->${methodName}`);
  1412.  
  1413.                     const callingClass = Java.use(className);
  1414.                     const callingMethod = callingClass[methodName];
  1415.  
  1416.                     callingMethod.overloads.forEach((failingMethod) => {
  1417.                         if (failingMethod.implementation) {
  1418.                             console.warn('      Already patched - but still failing!')
  1419.                             return; // Already patched by Frida - skip it
  1420.                         }
  1421.  
  1422.                         // Try to spot known methods (despite obfuscation) and disable them:
  1423.                         if (isOkHttpCheckMethod(errorMessage, failingMethod)) {
  1424.                             // See okhttp3.CertificatePinner patches in unpinning script:
  1425.                             failingMethod.implementation = () => {
  1426.                                 if (DEBUG_MODE) console.log(` => Fallback OkHttp patch`);
  1427.                             };
  1428.                             console.log(`      [+] ${className}->${methodName} (fallback OkHttp patch)`);
  1429.                         } else if (isAppmattusOkHttpInterceptMethod(errorMessage, failingMethod)) {
  1430.                             // See Appmattus CertificateTransparencyInterceptor patch in unpinning script:
  1431.                             const chainType = Java.use(failingMethod.argumentTypes[0].className);
  1432.                             const responseTypeName = failingMethod.returnType.className;
  1433.                             const okHttpChain = matchOkHttpChain(chainType, responseTypeName);
  1434.                             failingMethod.implementation = (chain) => {
  1435.                                 if (DEBUG_MODE) console.log(` => Fallback Appmattus+OkHttp patch`);
  1436.                                 const proceed = chain[okHttpChain.proceedMethodName].bind(chain);
  1437.                                 const request = chain[okHttpChain.requestFieldName].value;
  1438.                                 return proceed(request);
  1439.                             };
  1440.                             console.log(`      [+] ${className}->${methodName} (fallback Appmattus+OkHttp patch)`);
  1441.                         } else if (isX509TrustManager(callingClass, methodName)) {
  1442.                             const argumentTypes = failingMethod.argumentTypes.map(t => t.className);
  1443.                             const returnType = failingMethod.returnType.className;
  1444.  
  1445.                             if (
  1446.                                 argumentTypes.length === 2 &&
  1447.                                 argumentTypes.every((t, i) => t === BASE_METHOD_ARGUMENTS[i]) &&
  1448.                                 returnType === 'void'
  1449.                             ) {
  1450.                                 // For the base method, just check against the default:
  1451.                                 failingMethod.implementation = (certs, authType) => {
  1452.                                     if (DEBUG_MODE) console.log(` => Fallback X509TrustManager patch of ${
  1453.                                         className
  1454.                                     } base method`);
  1455.  
  1456.                                     const defaultTrustManager = getCustomX509TrustManager(); // Defined in the unpinning script
  1457.                                     defaultTrustManager.checkServerTrusted(certs, authType);
  1458.                                 };
  1459.                                 console.log(`      [+] ${className}->${methodName} (fallback X509TrustManager base patch)`);
  1460.                             } else if (
  1461.                                 argumentTypes.length === 3 &&
  1462.                                 argumentTypes.every((t, i) => t === EXTENDED_METHOD_ARGUMENTS[i]) &&
  1463.                                 returnType === 'java.util.List'
  1464.                             ) {
  1465.                                 // For the extended method, we just ignore the hostname, and if the certs are good
  1466.                                 // (i.e they're ours), then we say the whole chain is good to go:
  1467.                                 failingMethod.implementation = function (certs, authType, _hostname) {
  1468.                                     if (DEBUG_MODE) console.log(` => Fallback X509TrustManager patch of ${
  1469.                                         className
  1470.                                     } extended method`);
  1471.  
  1472.                                     try {
  1473.                                         defaultTrustManager.checkServerTrusted(certs, authType);
  1474.                                     } catch (e) {
  1475.                                         console.error('Default TM threw:', e);
  1476.                                     }
  1477.                                     return Java.use('java.util.Arrays').asList(certs);
  1478.                                 };
  1479.                                 console.log(`      [+] ${className}->${methodName} (fallback X509TrustManager ext patch)`);
  1480.                             } else {
  1481.                                 console.warn(`      [ ] Skipping unrecognized checkServerTrusted signature in class ${
  1482.                                     callingClass.class.getName()
  1483.                                 }`);
  1484.                             }
  1485.                         } else {
  1486.                             console.error('      [ ] Unrecognized TLS error - this must be patched manually');
  1487.                             return;
  1488.                             // Later we could try to cover other cases here - automatically recognizing other
  1489.                             // OkHttp interceptors for example, or potentially other approaches, but we need
  1490.                             // to do so carefully to avoid disabling TLS checks entirely.
  1491.                         }
  1492.                     });
  1493.                 } catch (e) {
  1494.                     console.log('      [ ] Failed to automatically patch failure');
  1495.                     console.warn(e);
  1496.                 }
  1497.  
  1498.                 return originalConstructor.call(this, ...arguments);
  1499.             }
  1500.         };
  1501.  
  1502.         // These are the exceptions we watch for and attempt to auto-patch out after they're thrown:
  1503.         [
  1504.             'javax.net.ssl.SSLPeerUnverifiedException',
  1505.             'java.security.cert.CertificateException'
  1506.         ].forEach((errorClassName) => {
  1507.             const ErrorClass = Java.use(errorClassName);
  1508.             ErrorClass.$init.overloads.forEach((overload) => {
  1509.                 overload.implementation = buildUnhandledErrorPatcher(
  1510.                     errorClassName,
  1511.                     overload
  1512.                 );
  1513.             });
  1514.         })
  1515.  
  1516.         console.log('== Unpinning fallback auto-patcher installed ==');
  1517.     } catch (err) {
  1518.         console.error(err);
  1519.         console.error(' !!! --- Unpinning fallback auto-patcher installation failed --- !!!');
  1520.     }
  1521.  
  1522. });
  1523. //#endregion
Add Comment
Please, Sign In to add comment