├── android ├── .js ├── android-system-certificate-injection.js ├── android-proxy-override.js ├── android-antiroot.js ├── android-disable-flutter-certificate-pinning.js ├── android-certificate-unpinning-fallback.js ├── fridantiroot.js ├── android-disable-root-detection.js └── android-certificate-unpinning.js ├── .github ├── frida4burp.jpg └── frida4burp-small.jpg ├── .gitignore ├── .gitmodules ├── run-without-magiskhide.sh ├── getburpcert.sh ├── ios ├── ios-disable-detection.js └── ios-connect-hook.js ├── corellium-setup.sh ├── update-external.sh ├── README.md ├── config.js.sample ├── native-tls-hook.js ├── native-connect-hook.js └── LICENSE /android/.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/frida4burp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y0k4i-1337/frida4burp/HEAD/.github/frida4burp.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | certs 2 | build 3 | .DS_Store 4 | config.js 5 | .tool-versions 6 | .vscode 7 | .idea 8 | -------------------------------------------------------------------------------- /.github/frida4burp-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y0k4i-1337/frida4burp/HEAD/.github/frida4burp-small.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "misc/frida-flutterproxy"] 2 | path = misc/frida-flutterproxy 3 | url = https://github.com/hackcatml/frida-flutterproxy 4 | -------------------------------------------------------------------------------- /run-without-magiskhide.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run_proc=$(adb shell monkey -p $1 1) 4 | get_pid=$(adb shell ps | grep -i $1 | awk '{printf $2}') 5 | 6 | # Shift to the next argument 7 | shift 8 | 9 | if [[ -z "$get_pid" ]]; 10 | then 11 | echo "Didn't find PID :(" 12 | else 13 | echo "Attaching to process.." 14 | # Call frida passing any remaining arguments 15 | frida -U -p $get_pid $@ 16 | fi 17 | -------------------------------------------------------------------------------- /getburpcert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # https://securitychops.com/2019/08/31/dev/random/one-liner-to-install-burp-cacert-into-android.html 3 | # 4 | BURP_PROXY="$1" 5 | if [ -z "$BURP_PROXY" ]; then 6 | BURP_PROXY="http://127.0.0.1:8080" 7 | fi 8 | echo "Using Burp proxy: $BURP_PROXY" 9 | 10 | DST_DIR="./certs" 11 | 12 | # Verify if directory exists, if not create it 13 | if [ ! -d "$DST_DIR" ]; then 14 | echo "Creating directory $DST_DIR for storing certificates" 15 | mkdir -p "$DST_DIR" 16 | else 17 | echo "Directory $DST_DIR already exists. Cleaning up existing certificates" 18 | rm -f "$DST_DIR"/* 19 | fi 20 | 21 | echo "Downloading certificate from Burp proxy" 22 | curl -s --proxy "$BURP_PROXY" -o "$DST_DIR/cacert.der" http://burp/cert && 23 | echo "Certificate downloaded to $DST_DIR/cacert.der" 24 | 25 | echo "Converting certificate to PEM format" 26 | openssl x509 -inform DER -in "$DST_DIR/cacert.der" -out "$DST_DIR/cacert.pem" && 27 | echo "Certificate converted to $DST_DIR/cacert.pem" 28 | 29 | echo "Calculating hash from PEM certificate" 30 | HASH=$(openssl x509 -inform PEM -subject_hash_old -in "$DST_DIR/cacert.pem" | head -1) && 31 | echo "Certificate hash: $HASH" 32 | 33 | echo "Copying PEM certificate to $DST_DIR/$HASH.0" 34 | cp "$DST_DIR/cacert.pem" "$DST_DIR/$HASH.0" && 35 | echo "Certificate copied to $DST_DIR/$HASH.0" 36 | -------------------------------------------------------------------------------- /ios/ios-disable-detection.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * Some iOS apps attempt to detect jailbroken devices and similar. This script disables these 4 | * detections to ensure you can freely manage your device and modify your apps. 5 | * 6 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | * SPDX-FileCopyrightText: Tim Perry 9 | * 10 | *************************************************************************************************/ 11 | 12 | if (ObjC.available) { 13 | try { 14 | const JailMonkey = ObjC.classes.JailMonkey; 15 | if (JailMonkey) { 16 | const isJailBroken = JailMonkey["- isJailBroken"]; 17 | Interceptor.attach(isJailBroken.implementation, { 18 | onLeave: function(retval) { 19 | if (DEBUG_MODE) console.log("JailMonkey isJailBroken check hooked & skipped"); 20 | retval.replace(ptr("0x0")); 21 | } 22 | }); 23 | 24 | console.log('== Hooked JailMonkey detection =='); 25 | } else { 26 | if (DEBUG_MODE) console.log('Skipping JailMonkey hook - not present'); 27 | } 28 | } catch (err) { 29 | console.error(`[!] ERROR: JailMonkey isJailBroken hook failed: ${err}`); 30 | } 31 | } -------------------------------------------------------------------------------- /corellium-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # This script sets up Corellium device for use with Burp Suite 3 | # The script expects VPN connection to be active and 4 | # device IP to be passed as the first argument. 5 | # The certificate should already be downloaded using getburpcert.sh 6 | # Optional second argument is the Burp proxy (e.g., 10.11.3.2:8080) 7 | 8 | # Usage: ./corellium-setup.sh [burp_proxy] 9 | DEVICE_IP="$1" 10 | if [ -z "$DEVICE_IP" ]; then 11 | echo "Usage: $0 [burp_proxy]" 12 | exit 1 13 | fi 14 | 15 | # Run script to get Burp certificate 16 | ./getburpcert.sh 17 | 18 | CERTS_DIR="./certs" 19 | if [ ! -d "$CERTS_DIR" ]; then 20 | echo "[!] Certificates directory $CERTS_DIR does not exist. Please run getburpcert.sh first." 21 | exit 1 22 | fi 23 | 24 | CERT_PATH="$CERTS_DIR/$(ls $CERTS_DIR | grep '\.0$' | head -1)" 25 | if [ ! -f "$CERT_PATH" ]; then 26 | echo "[!] Certificate file not found in $CERTS_DIR. Please ensure getburpcert.sh has been run successfully." 27 | exit 1 28 | fi 29 | 30 | echo "[*] Setting up Corellium device at $DEVICE_IP for Burp Suite" 31 | 32 | # Connect to the device and install the certificate 33 | adb connect "$DEVICE_IP:5001" 34 | adb root 35 | adb push "$CERT_PATH" /system/etc/security/cacerts/ 36 | 37 | # Verify installation 38 | echo "[*] Verifying certificate installation on device" 39 | # Get certificate name 40 | filename=$(basename "$CERT_PATH") 41 | echo "[*] Expected certificate filename on device: $filename" 42 | adb shell "ls /system/etc/security/cacerts/$filename" 43 | 44 | # Setup proxy settings if $2 is provided 45 | BURP_PROXY="$2" 46 | if [ -z "$BURP_PROXY" ]; then 47 | echo "[-] No Burp proxy provided, skipping proxy setup." 48 | exit 0 49 | fi 50 | 51 | echo "[*] Setting up proxy settings on device" 52 | adb shell settings put global http_proxy $BURP_PROXY 53 | 54 | echo "[*] Verifying proxy settings on device" 55 | adb shell settings get global http_proxy 56 | 57 | echo "[*] Corellium device setup complete.\n" 58 | echo "[*] To remove the proxy settings, run:" 59 | echo "adb shell settings put global http_proxy :0" 60 | -------------------------------------------------------------------------------- /ios/ios-connect-hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In some cases, proxy configuration by itself won't work. This notably includes Flutter apps (which ignore 3 | * system/JVM configuration entirely) and plausibly other apps intentionally ignoring proxies. To handle that 4 | * we hook low-level connection attempts within Network Framework directly, to redirect traffic on all ports 5 | * to the target. 6 | * 7 | * This handles all attempts to connect an outgoing socket, and for all TCP connections opened it will 8 | * manually replace the nw_connection_create() endpoint parameter so that the socket connects to the proxy 9 | * instead of the 'real' destination. 10 | * 11 | * This doesn't help with certificate trust (you still need some kind of certificate setup) but it does ensure 12 | * the proxy receives all connections (and so will see if connections don't trust its CA). It's still useful 13 | * to do proxy config alongside this, as applications may behave a little more 'correctly' if they're aware 14 | * they're using a proxy rather than doing so unknowingly. 15 | * 16 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 17 | * SPDX-License-Identifier: AGPL-3.0-or-later 18 | * SPDX-FileCopyrightText: Tim Perry 19 | */ 20 | 21 | // This is the method we're going to patch: 22 | // https://developer.apple.com/documentation/network/2976677-nw_connection_create (iOS 12+) 23 | const libnetwork = Process.getModuleByName('libnetwork.dylib'); 24 | const nw_connection_create = libnetwork.getExportByName('nw_connection_create'); 25 | 26 | // This is the method to make a new endpoint to connect to: 27 | // https://developer.apple.com/documentation/network/2976720-nw_endpoint_create_host (iOS 12+) 28 | const nw_endpoint_create_host = new NativeFunction( 29 | libnetwork.findExportByName('nw_endpoint_create_host'), 30 | 'pointer', ['pointer', 'pointer'] 31 | ); 32 | 33 | const newHostStr = Memory.allocUtf8String(PROXY_HOST); 34 | const newPortStr = Memory.allocUtf8String(PROXY_PORT.toString()); 35 | 36 | Interceptor.attach(nw_connection_create, { 37 | onEnter: function (args) { 38 | // Replace the endpoint argument entirely with our own: 39 | args[0] = nw_endpoint_create_host(newHostStr, newPortStr); 40 | } 41 | }); -------------------------------------------------------------------------------- /update-external.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check if third party scripts are up to date based on their hash 4 | # If not, update them 5 | # Parameters: 6 | # $1: remote_base_url: The base URL to check for updates 7 | # $2: local_base_url: The base URL to check for updates 8 | # $3: remote_file: The file to check for updates 9 | # $4: local_file: The file to check for updates 10 | update_external() { 11 | local remote_base_url="${1}" 12 | local local_base_url="${2}" 13 | local remote_file_name="${3}" 14 | local local_file_name="${4}" 15 | local remote_file_url="${remote_base_url}/${remote_file_name}" 16 | local local_file_url="${local_base_url}/${local_file_name}" 17 | 18 | # test if local file exists 19 | if [ ! -f "${local_file_url}" ]; then 20 | echo "Downloading ${remote_file_name} for the first time" 21 | curl -m 10 -s "${remote_file_url}" >"${local_file_url}" 22 | return 23 | fi 24 | local remote_hash=$(curl -s "${remote_file_url}" | shasum -a 256 | cut -d ' ' -f 1) 25 | local local_hash=$(cat "${local_file_url}" | shasum -a 256 | cut -d ' ' -f 1) 26 | 27 | if [ "${remote_hash}" != "${local_hash}" ]; then 28 | echo "Updating ${local_file_name}" 29 | curl -m 10 -s "${remote_file_url}" >"${local_file_url}" 30 | else 31 | echo "${local_file_name} is up to date" 32 | fi 33 | } 34 | 35 | # Function to parse JSON from projects on frida codeshare 36 | # and create a file with the content of the "source" property 37 | # Parameters: 38 | # $1: project: The project to download (e.g. dzonerzy/fridantiroot) 39 | # $2: dst_dir: The destination directory to save the file 40 | parse_json_and_create_file() { 41 | local project="${1}" 42 | local dst_dir="${2}" 43 | 44 | local base_url="https://codeshare.frida.re/api/project" 45 | 46 | # Download JSON from the URL 47 | local json=$(curl -s "${base_url}/${project}") 48 | 49 | # Extract the value of the "source" property 50 | local source=$(echo "${json}" | jq -r '.source') 51 | 52 | # Extract the value of the "project_name" property 53 | local project_name=$(echo "${json}" | jq -r '.project_name') 54 | 55 | # Create a file with the content of the "source" property 56 | echo "${source}" >"${dst_dir}/${project_name}.js" 57 | } 58 | 59 | DEPENDENCIES=( 60 | "jq" 61 | "curl" 62 | "shasum" 63 | ) 64 | 65 | # Check if dependencies are installed 66 | for dependency in "${DEPENDENCIES[@]}"; do 67 | if ! command -v "${dependency}" &>/dev/null; then 68 | echo "${dependency} could not be found" 69 | exit 1 70 | fi 71 | done 72 | 73 | REMOTE_BASE_URL="https://raw.githubusercontent.com/httptoolkit/frida-interception-and-unpinning/main" 74 | LOCAL_BASE_URL="." 75 | 76 | FILES=( 77 | "native-connect-hook.js" 78 | "native-tls-hook.js" 79 | "android/android-certificate-unpinning-fallback.js" 80 | "android/android-certificate-unpinning.js" 81 | "android/android-disable-flutter-certificate-pinning.js" 82 | "android/android-disable-root-detection.js" 83 | "android/android-proxy-override.js" 84 | "android/android-system-certificate-injection.js" 85 | "ios/ios-connect-hook.js" 86 | "ios/ios-disable-detection.js" 87 | ) 88 | 89 | # Check config.js and update if necessary 90 | update_external "${REMOTE_BASE_URL}" "${LOCAL_BASE_URL}" "config.js" "config.js.sample" 91 | 92 | # Check other files and update if necessary 93 | for file in "${FILES[@]}"; do 94 | update_external "${REMOTE_BASE_URL}" "${LOCAL_BASE_URL}" "${file}" "${file}" 95 | done 96 | 97 | # Download scripts from frida codeshare (android) 98 | CODESHARE_SCRIPTS=( 99 | "dzonerzy/fridantiroot" 100 | "akabe1/frida-multiple-unpinning" 101 | "fdciabdul/frida-multiple-bypass" 102 | ) 103 | 104 | for script in "${CODESHARE_SCRIPTS[@]}"; do 105 | echo "Downloading project @${script} from frida codeshare to ${LOCAL_BASE_URL}/android" 106 | parse_json_and_create_file "${script}" "${LOCAL_BASE_URL}/android" 107 | done 108 | 109 | # Download flutter proxy script 110 | update_external "https://raw.githubusercontent.com/hackcatml/frida-flutterproxy/refs/heads/main" "${LOCAL_BASE_URL}" "script.js" "flutterproxy.js" 111 | -------------------------------------------------------------------------------- /android/android-system-certificate-injection.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * Once we have captured traffic (once it's being sent to our proxy port) the next step is 4 | * to ensure any clients using TLS (HTTPS) trust our CA certificate, to allow us to intercept 5 | * encrypted connections successfully. 6 | * 7 | * This script does so by attaching to the internals of Conscrypt (the Android SDK's standard 8 | * TLS implementation) and pre-adding our certificate to the 'already trusted' cache, so that 9 | * future connections trust it implicitly. This ensures that all normal uses of Android APIs 10 | * for HTTPS & TLS will allow interception. 11 | * 12 | * This does not handle all standalone certificate pinning techniques - where the application 13 | * actively rejects certificates that are trusted by default on the system. That's dealt with 14 | * in the separate certificate unpinning script. 15 | * 16 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 17 | * SPDX-License-Identifier: AGPL-3.0-or-later 18 | * SPDX-FileCopyrightText: Tim Perry 19 | * 20 | *************************************************************************************************/ 21 | 22 | Java.perform(() => { 23 | // First, we build a JVM representation of our certificate: 24 | const String = Java.use("java.lang.String"); 25 | const ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream'); 26 | const CertFactory = Java.use('java.security.cert.CertificateFactory'); 27 | 28 | let cert; 29 | try { 30 | const certFactory = CertFactory.getInstance("X.509"); 31 | const certBytes = String.$new(CERT_PEM).getBytes(); 32 | cert = certFactory.generateCertificate(ByteArrayInputStream.$new(certBytes)); 33 | } catch (e) { 34 | console.error('Could not parse provided certificate PEM!'); 35 | console.error(e); 36 | Java.use('java.lang.System').exit(1); 37 | } 38 | 39 | // Then we hook TrustedCertificateIndex. This is used for caching known trusted certs within Conscrypt - 40 | // by prepopulating all instances, we ensure that all TrustManagerImpls (and potentially other 41 | // things) automatically trust our certificate specifically (without disabling validation entirely). 42 | // This should apply to Android v7+ - previous versions used SSLContext & X509TrustManager. 43 | [ 44 | 'com.android.org.conscrypt.TrustedCertificateIndex', 45 | 'org.conscrypt.TrustedCertificateIndex', // Might be used (com.android is synthetic) - unclear 46 | 'org.apache.harmony.xnet.provider.jsse.TrustedCertificateIndex' // Used in Apache Harmony version of Conscrypt 47 | ].forEach((TrustedCertificateIndexClassname, i) => { 48 | let TrustedCertificateIndex; 49 | try { 50 | TrustedCertificateIndex = Java.use(TrustedCertificateIndexClassname); 51 | } catch (e) { 52 | if (i === 0) { 53 | throw new Error(`${TrustedCertificateIndexClassname} not found - could not inject system certificate`); 54 | } else { 55 | // Other classnames are optional fallbacks 56 | if (DEBUG_MODE) { 57 | console.log(`[ ] Skipped cert injection for ${TrustedCertificateIndexClassname} (not present)`); 58 | } 59 | return; 60 | } 61 | } 62 | 63 | TrustedCertificateIndex.$init.overloads.forEach((overload) => { 64 | overload.implementation = function () { 65 | this.$init(...arguments); 66 | // Index our cert as already trusted, right from the start: 67 | this.index(cert); 68 | } 69 | }); 70 | 71 | TrustedCertificateIndex.reset.overloads.forEach((overload) => { 72 | overload.implementation = function () { 73 | const result = this.reset(...arguments); 74 | // Index our cert in here again, since the reset removes it: 75 | this.index(cert); 76 | return result; 77 | }; 78 | }); 79 | 80 | if (DEBUG_MODE) console.log(`[+] Injected cert into ${TrustedCertificateIndexClassname}`); 81 | }); 82 | 83 | // This effectively adds us to the system certs, and also defeats quite a bit of basic certificate 84 | // pinning too! It auto-trusts us in any implementation that uses TrustManagerImpl (Conscrypt) as 85 | // the underlying cert checking component. 86 | 87 | console.log('== System certificate trust injected =='); 88 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Frida4burp 3 | 4 |   5 | 6 | 7 |
8 | 9 |

Frida4burp

10 | 11 |

12 | Github top language 13 | 14 | Github language count 15 | 16 | Repository size 17 | 18 | License 19 | 20 | 21 | 22 | 23 | 24 | 25 |

26 | 27 | 28 | 29 |
30 | 31 |

32 | About   |   33 | Features   |   34 | Technologies   |   35 | Requirements   |   36 | Starting   |   37 | Disclaimer   |   38 | License   |   39 | Author 40 |

41 | 42 |
43 | 44 | ## :dart: About ## 45 | 46 | A set of scripts to facilitate HTTP interception on mobile apps using Frida and Burp. 47 | 48 | ## :sparkles: Features ## 49 | 50 | :heavy_check_mark: Update third-party scripts;\ 51 | :heavy_check_mark: Generate Burp CA certificate in proper format to be used by scripts;\ 52 | :heavy_check_mark: Bypass SSL unpinning;\ 53 | :heavy_check_mark: Bypass anti-root;\ 54 | :heavy_check_mark: Any other script compatible with Frida. 55 | 56 | ## :rocket: Technologies ## 57 | 58 | The following tools were used in this project: 59 | 60 | - [Frida Mobile Interception Scripts](https://github.com/httptoolkit/frida-interception-and-unpinning) 61 | - [Frida CodeShare](https://codeshare.frida.re/) 62 | - [Frida](https://frida.re/) 63 | - [Burp Suite](https://portswigger.net/burp) 64 | 65 | ## :white_check_mark: Requirements ## 66 | 67 | Before starting :checkered_flag:, you need to have 68 | [Burp](https://portswigger.net/burp) and [Frida](https://frida.re/) installed 69 | and properly configured. 70 | 71 | ## :checkered_flag: Starting ## 72 | 73 | ```bash 74 | # Clone this project 75 | $ git clone https://github.com/y0k4i-1337/frida4burp 76 | 77 | # Clone with submodules 78 | $ git clone --recurse-submodules https://github.com/y0k4i-1337/frida4burp 79 | 80 | # Access 81 | $ cd frida4burp 82 | 83 | # Open Burp and run the script to get your certificate in PEM format 84 | $ ./getburpcert.sh 85 | 86 | # Create a copy of config.js.sample named config.js 87 | $ cp config.js.sample config.js 88 | 89 | # Copy the content of ./certs/cacert.pem into the marked location at `config.js` 90 | 91 | # Update `config.js` according to your needs 92 | 93 | # Use Frida to launch the app you're interested in with the scripts injected (starting with config.js). Which scripts to use is up to you, but for Android a good command to start with is: 94 | $ frida -U \ 95 | -l ./config.js \ 96 | -l ./android/android-antiroot.js \ 97 | -l ./android/fridantiroot.js \ 98 | -l ./native-connect-hook.js \ 99 | -l ./native-tls-hook.js \ 100 | -l ./android/android-proxy-override.js \ 101 | -l ./android/android-system-certificate-injection.js \ 102 | -l ./android/frida-multiple-unpinning.js \ 103 | -l ./android/android-certificate-unpinning.js \ 104 | -l ./android/android-certificate-unpinning-fallback.js \ 105 | -f $PACKAGE_ID 106 | 107 | # You can, optionally, build all the scripts into a single one for convenience 108 | $ ./build.sh 109 | 110 | # In this case, you just need to run: 111 | $ frida -U -l ./build/android-frida-single-script.js -f $PACKAGE_ID 112 | ``` 113 | 114 | ## :snowman: Disclaimer ## 115 | 116 | This repository is basically a collection of third-party scripts that I found 117 | useful for instrumenting mobile applications, specially for intercepting 118 | HTTP requests in Burp. Almost all the scripts stored here were simply copied from 119 | [@httptoolkit/frida-interception-and-unpinning](https://github.com/httptoolkit/frida-interception-and-unpinning). 120 | 121 | For now, I have just added some custom scripts to make this process easier. 122 | 123 | ## :memo: License ## 124 | 125 | This project is under license from GNU Affero. For more details, see the [LICENSE](LICENSE) file. 126 | 127 | Made with :heart: by y0k4i 128 | 129 |   130 | 131 | Back to top 132 | -------------------------------------------------------------------------------- /android/android-proxy-override.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * The first step in intercepting HTTP & HTTPS traffic is to set the default proxy settings, 4 | * telling the app that all requests should be sent via our HTTP proxy. 5 | * 6 | * In this script, we set that up via a few different mechanisms, which cumulatively should 7 | * ensure that all connections are sent via the proxy, even if they attempt to use their 8 | * own custom proxy configurations to avoid this. 9 | * 10 | * Despite that, this still only covers well behaved apps - it's still possible for apps 11 | * to send network traffic directly if they're determined to do so, or if they're built 12 | * with a framework that does not do this by default (Flutter is notably in this category). 13 | * To handle those less tidy cases, we manually capture traffic to recognized target ports 14 | * in the native connect() hook script. 15 | * 16 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 17 | * SPDX-License-Identifier: AGPL-3.0-or-later 18 | * SPDX-FileCopyrightText: Tim Perry 19 | * 20 | *************************************************************************************************/ 21 | 22 | Java.perform(() => { 23 | // Set default JVM system properties for the proxy address. Notably these are used 24 | // to initialize WebView configuration. 25 | Java.use('java.lang.System').setProperty('http.proxyHost', PROXY_HOST); 26 | Java.use('java.lang.System').setProperty('http.proxyPort', PROXY_PORT.toString()); 27 | Java.use('java.lang.System').setProperty('https.proxyHost', PROXY_HOST); 28 | Java.use('java.lang.System').setProperty('https.proxyPort', PROXY_PORT.toString()); 29 | 30 | Java.use('java.lang.System').clearProperty('http.nonProxyHosts'); 31 | Java.use('java.lang.System').clearProperty('https.nonProxyHosts'); 32 | 33 | // Some Android internals attempt to reset these settings to match the device configuration. 34 | // We block that directly here: 35 | const controlledSystemProperties = [ 36 | 'http.proxyHost', 37 | 'http.proxyPort', 38 | 'https.proxyHost', 39 | 'https.proxyPort', 40 | 'http.nonProxyHosts', 41 | 'https.nonProxyHosts' 42 | ]; 43 | Java.use('java.lang.System').clearProperty.implementation = function (property) { 44 | if (controlledSystemProperties.includes(property)) { 45 | if (DEBUG_MODE) console.log(`Ignoring attempt to clear ${property} system property`); 46 | return this.getProperty(property); 47 | } 48 | return this.clearProperty(...arguments); 49 | } 50 | Java.use('java.lang.System').setProperty.implementation = function (property) { 51 | if (controlledSystemProperties.includes(property)) { 52 | if (DEBUG_MODE) console.log(`Ignoring attempt to override ${property} system property`); 53 | return this.getProperty(property); 54 | } 55 | return this.setProperty(...arguments); 56 | } 57 | 58 | // Configure the app's proxy directly, via the app connectivity manager service: 59 | const ConnectivityManager = Java.use('android.net.ConnectivityManager'); 60 | const ProxyInfo = Java.use('android.net.ProxyInfo'); 61 | ConnectivityManager.getDefaultProxy.implementation = () => ProxyInfo.$new(PROXY_HOST, PROXY_PORT, ''); 62 | // (Not clear if this works 100% - implying there are ConnectivityManager subclasses handling this) 63 | 64 | console.log(`== Proxy system configuration overridden to ${PROXY_HOST}:${PROXY_PORT} ==`); 65 | 66 | // Configure the proxy indirectly, by overriding the return value for all ProxySelectors everywhere: 67 | const Collections = Java.use('java.util.Collections'); 68 | const ProxyType = Java.use('java.net.Proxy$Type'); 69 | const InetSocketAddress = Java.use('java.net.InetSocketAddress'); 70 | const ProxyCls = Java.use('java.net.Proxy'); // 'Proxy' is reserved in JS 71 | 72 | const targetProxy = ProxyCls.$new( 73 | ProxyType.HTTP.value, 74 | InetSocketAddress.$new(PROXY_HOST, PROXY_PORT) 75 | ); 76 | const getTargetProxyList = () => Collections.singletonList(targetProxy); 77 | 78 | const ProxySelector = Java.use('java.net.ProxySelector'); 79 | 80 | // Find every implementation of ProxySelector by quickly scanning method signatures, and 81 | // then checking whether each match actually implements java.net.ProxySelector: 82 | const proxySelectorClasses = Java.enumerateMethods('*!select(java.net.URI): java.util.List/s') 83 | .flatMap((matchingLoader) => matchingLoader.classes 84 | .map((classData) => Java.use(classData.name)) 85 | .filter((Cls) => ProxySelector.class.isAssignableFrom(Cls.class)) 86 | ); 87 | 88 | // Replace the 'select' of every implementation, so they all send traffic to us: 89 | proxySelectorClasses.forEach(ProxySelectorCls => { 90 | if (DEBUG_MODE) { 91 | console.log('Rewriting', ProxySelectorCls.toString()); 92 | } 93 | ProxySelectorCls.select.implementation = () => getTargetProxyList() 94 | }); 95 | 96 | console.log(`== Proxy configuration overridden to ${PROXY_HOST}:${PROXY_PORT} ==`); 97 | }); 98 | 99 | -------------------------------------------------------------------------------- /android/android-antiroot.js: -------------------------------------------------------------------------------- 1 | // Downloaded from https://raw.githubusercontent.com/AshenOneYe/FridaAntiRootDetection/main/antiroot.js 2 | const commonPaths = [ 3 | "/data/local/bin/su", 4 | "/data/local/su", 5 | "/data/local/xbin/su", 6 | "/dev/com.koushikdutta.superuser.daemon/", 7 | "/sbin/su", 8 | "/system/app/Superuser.apk", 9 | "/system/bin/failsafe/su", 10 | "/system/bin/su", 11 | "/su/bin/su", 12 | "/system/etc/init.d/99SuperSUDaemon", 13 | "/system/sd/xbin/su", 14 | "/system/xbin/busybox", 15 | "/system/xbin/daemonsu", 16 | "/system/xbin/su", 17 | "/system/sbin/su", 18 | "/vendor/bin/su", 19 | "/cache/su", 20 | "/data/su", 21 | "/dev/su", 22 | "/system/bin/.ext/su", 23 | "/system/usr/we-need-root/su", 24 | "/system/app/Kinguser.apk", 25 | "/data/adb/magisk", 26 | "/sbin/.magisk", 27 | "/cache/.disable_magisk", 28 | "/dev/.magisk.unblock", 29 | "/cache/magisk.log", 30 | "/data/adb/magisk.img", 31 | "/data/adb/magisk.db", 32 | "/data/adb/magisk_simple", 33 | "/init.magisk.rc", 34 | "/system/xbin/ku.sud", 35 | "/data/adb/ksu", 36 | "/data/adb/ksud" 37 | ]; 38 | 39 | const ROOTmanagementApp = [ 40 | "com.noshufou.android.su", 41 | "com.noshufou.android.su.elite", 42 | "eu.chainfire.supersu", 43 | "com.koushikdutta.superuser", 44 | "com.thirdparty.superuser", 45 | "com.yellowes.su", 46 | "com.koushikdutta.rommanager", 47 | "com.koushikdutta.rommanager.license", 48 | "com.dimonvideo.luckypatcher", 49 | "com.chelpus.lackypatch", 50 | "com.ramdroid.appquarantine", 51 | "com.ramdroid.appquarantinepro", 52 | "com.topjohnwu.magisk", 53 | "me.weishu.kernelsu" 54 | ]; 55 | 56 | 57 | 58 | function stackTraceHere(isLog){ 59 | var Exception = Java.use('java.lang.Exception'); 60 | var Log = Java.use('android.util.Log'); 61 | var stackinfo = Log.getStackTraceString(Exception.$new()) 62 | if(isLog){ 63 | console.log(stackinfo) 64 | }else{ 65 | return stackinfo 66 | } 67 | } 68 | 69 | function stackTraceNativeHere(isLog){ 70 | var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE) 71 | .map(DebugSymbol.fromAddress) 72 | .join("\n\t"); 73 | console.log(backtrace) 74 | } 75 | 76 | 77 | function bypassJavaFileCheck(){ 78 | var UnixFileSystem = Java.use("java.io.UnixFileSystem") 79 | UnixFileSystem.checkAccess.implementation = function(file,access){ 80 | 81 | var stack = stackTraceHere(false) 82 | 83 | const filename = file.getAbsolutePath(); 84 | 85 | if (filename.indexOf("magisk") >= 0) { 86 | console.log("Anti Root Detect - check file: " + filename) 87 | return false; 88 | } 89 | 90 | if (commonPaths.indexOf(filename) >= 0) { 91 | console.log("Anti Root Detect - check file: " + filename) 92 | return false; 93 | } 94 | 95 | return this.checkAccess(file,access) 96 | } 97 | } 98 | 99 | function bypassNativeFileCheck(){ 100 | var fopen = Module.findExportByName("libc.so","fopen") 101 | Interceptor.attach(fopen,{ 102 | onEnter:function(args){ 103 | this.inputPath = args[0].readUtf8String() 104 | }, 105 | onLeave:function(retval){ 106 | if(retval.toInt32() != 0){ 107 | if (commonPaths.indexOf(this.inputPath) >= 0) { 108 | console.log("Anti Root Detect - fopen : " + this.inputPath) 109 | retval.replace(ptr(0x0)) 110 | } 111 | } 112 | } 113 | }) 114 | 115 | var access = Module.findExportByName("libc.so","access") 116 | Interceptor.attach(access,{ 117 | onEnter:function(args){ 118 | this.inputPath = args[0].readUtf8String() 119 | }, 120 | onLeave:function(retval){ 121 | if(retval.toInt32()==0){ 122 | if(commonPaths.indexOf(this.inputPath) >= 0){ 123 | console.log("Anti Root Detect - access : " + this.inputPath) 124 | retval.replace(ptr(-1)) 125 | } 126 | } 127 | } 128 | }) 129 | } 130 | 131 | function setProp(){ 132 | var Build = Java.use("android.os.Build") 133 | var TAGS = Build.class.getDeclaredField("TAGS") 134 | TAGS.setAccessible(true) 135 | TAGS.set(null,"release-keys") 136 | 137 | var FINGERPRINT = Build.class.getDeclaredField("FINGERPRINT") 138 | FINGERPRINT.setAccessible(true) 139 | FINGERPRINT.set(null,"google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys") 140 | 141 | // Build.deriveFingerprint.inplementation = function(){ 142 | // var ret = this.deriveFingerprint() //该函数无法通过反射调用 143 | // console.log(ret) 144 | // return ret 145 | // } 146 | 147 | var system_property_get = Module.findExportByName("libc.so", "__system_property_get") 148 | Interceptor.attach(system_property_get,{ 149 | onEnter(args){ 150 | this.key = args[0].readCString() 151 | this.ret = args[1] 152 | }, 153 | onLeave(ret){ 154 | if(this.key == "ro.build.fingerprint"){ 155 | var tmp = "google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys" 156 | var p = Memory.allocUtf8String(tmp) 157 | Memory.copy(this.ret,p,tmp.length+1) 158 | } 159 | } 160 | }) 161 | 162 | } 163 | 164 | //android.app.PackageManager 165 | function bypassRootAppCheck(){ 166 | var ApplicationPackageManager = Java.use("android.app.ApplicationPackageManager") 167 | ApplicationPackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(str,i){ 168 | // console.log(str) 169 | if (ROOTmanagementApp.indexOf(str) >= 0) { 170 | console.log("Anti Root Detect - check package : " + str) 171 | str = "ashen.one.ye.not.found" 172 | } 173 | return this.getPackageInfo(str,i) 174 | } 175 | 176 | //shell pm check 177 | } 178 | 179 | function bypassShellCheck(){ 180 | var String = Java.use('java.lang.String') 181 | 182 | var ProcessImpl = Java.use("java.lang.ProcessImpl") 183 | ProcessImpl.start.implementation = function(cmdarray,env,dir,redirects,redirectErrorStream){ 184 | 185 | if(cmdarray[0] == "mount"){ 186 | console.log("Anti Root Detect - Shell : " + cmdarray.toString()) 187 | arguments[0] = Java.array('java.lang.String',[String.$new("")]) 188 | return ProcessImpl.start.apply(this,arguments) 189 | } 190 | 191 | if(cmdarray[0] == "getprop"){ 192 | console.log("Anti Root Detect - Shell : " + cmdarray.toString()) 193 | const prop = [ 194 | "ro.secure", 195 | "ro.debuggable" 196 | ]; 197 | if(prop.indexOf(cmdarray[1]) >= 0){ 198 | arguments[0] = Java.array('java.lang.String',[String.$new("")]) 199 | return ProcessImpl.start.apply(this,arguments) 200 | } 201 | } 202 | 203 | if(cmdarray[0].indexOf("which") >= 0){ 204 | const prop = [ 205 | "su" 206 | ]; 207 | if(prop.indexOf(cmdarray[1]) >= 0){ 208 | console.log("Anti Root Detect - Shell : " + cmdarray.toString()) 209 | arguments[0] = Java.array('java.lang.String',[String.$new("")]) 210 | return ProcessImpl.start.apply(this,arguments) 211 | } 212 | } 213 | 214 | return ProcessImpl.start.apply(this,arguments) 215 | } 216 | } 217 | 218 | 219 | console.log("Attach") 220 | bypassNativeFileCheck() 221 | bypassJavaFileCheck() 222 | setProp() 223 | bypassRootAppCheck() 224 | bypassShellCheck() 225 | -------------------------------------------------------------------------------- /config.js.sample: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * This file defines various config parameters, used later within the other scripts. 4 | * 5 | * In all cases, you'll want to set CERT_PEM and likely PROXY_HOST and PROXY_PORT. 6 | * 7 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 8 | * SPDX-License-Identifier: AGPL-3.0-or-later 9 | * SPDX-FileCopyrightText: Tim Perry 10 | * 11 | *************************************************************************************************/ 12 | 13 | // Put your CA certificate data here in PEM format: 14 | const CERT_PEM = `-----BEGIN CERTIFICATE----- 15 | 16 | [!! Put your CA certificate data here, in PEM format !!] 17 | 18 | -----END CERTIFICATE-----`; 19 | 20 | // Put your intercepting proxy's address here: 21 | const PROXY_HOST = '127.0.0.1'; 22 | const PROXY_PORT = 8000; 23 | 24 | // If you like, set to to true to enable extra logging: 25 | const DEBUG_MODE = false; 26 | 27 | // If you find issues with non-HTTP traffic being captured (due to the 28 | // native connect hook script) you can add ports here to exempt traffic 29 | // on that port from being redirected. Note that this will only affect 30 | // traffic captured by the raw connection hook - for apps using the 31 | // system HTTP proxy settings, traffic on these ports will still be 32 | // sent via the proxy and intercepted despite this setting. 33 | const IGNORED_NON_HTTP_PORTS = []; 34 | 35 | // As HTTP/3 is often not well supported by MitM proxies, by default it 36 | // is blocked entirely, so all outgoing UDP connections to port 443 37 | // will fail. If this is set to false, they will instead be left unintercepted. 38 | const BLOCK_HTTP3 = true; 39 | 40 | // Set this to true if your proxy supports SOCKS5 connections. 41 | // This makes it possible for native-connect-hook to redirect 42 | // non-HTTP traffic through your proxy (to view it raw, and 43 | // avoid breaking non-HTTP traffic en route). 44 | const PROXY_SUPPORTS_SOCKS5 = false; 45 | 46 | 47 | // ---------------------------------------------------------------------------- 48 | // You don't need to modify any of the below, it just checks and applies some 49 | // of the configuration that you've entered above. 50 | // ---------------------------------------------------------------------------- 51 | 52 | 53 | if (DEBUG_MODE) { 54 | // Add logging just for clean output & to separate reloads: 55 | console.log('\n*** Starting scripts ***'); 56 | if (globalThis.Java?.available) { 57 | Java.perform(() => { 58 | setTimeout(() => console.log('*** Scripts completed ***\n'), 5); 59 | // (We assume that nothing else will take more than 5ms, but app startup 60 | // probably will, so this should separate script & runtime logs) 61 | }); 62 | } else { 63 | setTimeout(() => console.log('*** Scripts completed ***\n'), 5); 64 | // (We assume that nothing else will take more than 5ms, but app startup 65 | // probably will, so this should separate script & runtime logs) 66 | } 67 | } else { 68 | console.log(''); // Add just a single newline, for minimal clarity 69 | } 70 | 71 | // Check the certificate (without literally including the instruction phrasing 72 | // here, as that can be confusing for some users): 73 | if (CERT_PEM.match(/\[!!.* CA certificate data .* !!\]/)) { 74 | throw new Error('No certificate was provided' + 75 | '\n\n' + 76 | 'You need to set CERT_PEM in the Frida config script ' + 77 | 'to the contents of your CA certificate.' 78 | ); 79 | } 80 | 81 | 82 | 83 | // ---------------------------------------------------------------------------- 84 | // Don't modify any of the below unless you know what you're doing! 85 | // This section defines various utilities & calculates some constants which may 86 | // be used by later scripts elsewhere in this project. 87 | // ---------------------------------------------------------------------------- 88 | 89 | 90 | 91 | // As web atob & Node.js Buffer aren't available, we need to reimplement base64 decoding 92 | // in pure JS. This is a quick rough implementation without much error handling etc! 93 | 94 | // Base64 character set (plus padding character =) and lookup: 95 | const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 96 | const BASE64_LOOKUP = new Uint8Array(123); 97 | for (let i = 0; i < BASE64_CHARS.length; i++) { 98 | BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i; 99 | } 100 | 101 | 102 | /** 103 | * Take a base64 string, and return the raw bytes 104 | * @param {string} input 105 | * @returns Uint8Array 106 | */ 107 | function decodeBase64(input) { 108 | // Calculate the length of the output buffer based on padding: 109 | let outputLength = Math.floor((input.length * 3) / 4); 110 | if (input[input.length - 1] === '=') outputLength--; 111 | if (input[input.length - 2] === '=') outputLength--; 112 | 113 | const output = new Uint8Array(outputLength); 114 | let outputPos = 0; 115 | 116 | // Process each 4-character block: 117 | for (let i = 0; i < input.length; i += 4) { 118 | const a = BASE64_LOOKUP[input.charCodeAt(i)]; 119 | const b = BASE64_LOOKUP[input.charCodeAt(i + 1)]; 120 | const c = BASE64_LOOKUP[input.charCodeAt(i + 2)]; 121 | const d = BASE64_LOOKUP[input.charCodeAt(i + 3)]; 122 | 123 | // Assemble into 3 bytes: 124 | const chunk = (a << 18) | (b << 12) | (c << 6) | d; 125 | 126 | // Add each byte to the output buffer, unless it's padding: 127 | output[outputPos++] = (chunk >> 16) & 0xff; 128 | if (input.charCodeAt(i + 2) !== 61) output[outputPos++] = (chunk >> 8) & 0xff; 129 | if (input.charCodeAt(i + 3) !== 61) output[outputPos++] = chunk & 0xff; 130 | } 131 | 132 | return output; 133 | } 134 | 135 | /** 136 | * Take a single-certificate PEM string, and return the raw DER bytes 137 | * @param {string} input 138 | * @returns Uint8Array 139 | */ 140 | function pemToDer(input) { 141 | const pemLines = input.split('\n'); 142 | if ( 143 | pemLines[0] !== '-----BEGIN CERTIFICATE-----' || 144 | pemLines[pemLines.length- 1] !== '-----END CERTIFICATE-----' 145 | ) { 146 | throw new Error( 147 | 'Your certificate should be in PEM format, starting & ending ' + 148 | 'with a BEGIN CERTIFICATE & END CERTIFICATE header/footer' 149 | ); 150 | } 151 | 152 | const base64Data = pemLines.slice(1, -1).map(l => l.trim()).join(''); 153 | if ([...base64Data].some(c => !BASE64_CHARS.includes(c))) { 154 | throw new Error( 155 | 'Your certificate should be in PEM format, containing only ' + 156 | 'base64 data between a BEGIN & END CERTIFICATE header/footer' 157 | ); 158 | } 159 | 160 | return decodeBase64(base64Data); 161 | } 162 | 163 | const CERT_DER = pemToDer(CERT_PEM); 164 | 165 | // Right now this API is a bit funky - the callback will be called with a Frida Module instance 166 | // if the module is properly detected, but may be called with just { name, path, base, size } 167 | // in some cases (e.g. shared libraries loaded from inside an APK on Android). Works OK right now, 168 | // as it's not widely used but needs improvement in future if we extend this. 169 | function waitForModule(moduleName, callback) { 170 | if (Array.isArray(moduleName)) { 171 | moduleName.forEach(module => waitForModule(module, callback)); 172 | } 173 | 174 | try { 175 | const module = Process.getModuleByName(moduleName) 176 | module.ensureInitialized(); 177 | callback(module); 178 | return; 179 | } catch (e) { 180 | try { 181 | const module = Module.load(moduleName); 182 | callback(module); 183 | return; 184 | } catch (e) {} 185 | } 186 | 187 | MODULE_LOAD_CALLBACKS[moduleName] = callback; 188 | } 189 | 190 | const getModuleName = (nameOrPath) => { 191 | const endOfPath = nameOrPath.lastIndexOf('/'); 192 | return nameOrPath.slice(endOfPath + 1); 193 | }; 194 | 195 | const MODULE_LOAD_CALLBACKS = {}; 196 | new ApiResolver('module').enumerateMatches('exports:linker*!*dlopen*').forEach((dlopen) => { 197 | Interceptor.attach(dlopen.address, { 198 | onEnter(args) { 199 | const moduleArg = args[0].readCString(); 200 | if (moduleArg) { 201 | this.path = moduleArg; 202 | this.moduleName = getModuleName(moduleArg); 203 | } 204 | }, 205 | onLeave(retval) { 206 | if (!this.path || !retval || retval.isNull()) return; 207 | if (!MODULE_LOAD_CALLBACKS[this.moduleName]) return; 208 | 209 | let module = Process.findModuleByName(this.moduleName) 210 | ?? Process.findModuleByAddress(retval); 211 | if (!module) { 212 | // Some modules are loaded in ways that mean Frida can't detect them, and 213 | // can't look them up by name (notably when loading libraries from inside an 214 | // APK on Android). To handle this, we can use dlsym to look up an example 215 | // symbol and find the underlying module details directly, where possible. 216 | module = getAnonymousModule(this.moduleName, this.path, retval); 217 | if (!module) return; 218 | } 219 | 220 | Object.keys(MODULE_LOAD_CALLBACKS).forEach((key) => { 221 | if (this.moduleName === key) { 222 | if (module) { 223 | MODULE_LOAD_CALLBACKS[key](module); 224 | delete MODULE_LOAD_CALLBACKS[key]; 225 | } 226 | } 227 | }); 228 | } 229 | }); 230 | }); 231 | 232 | const getAnonymousModule = (name, path, handle) => { 233 | const dlsymAddr = Module.findGlobalExportByName('dlsym'); 234 | if (!dlsymAddr) { 235 | console.error(`[!] Cannot find dlsym, cannot get anonymous module info for ${name}`); 236 | return; 237 | } 238 | 239 | const dlsym = new NativeFunction(dlsymAddr, 'pointer', ['pointer', 'pointer']); 240 | 241 | // Handle here is the return value from dlopen - but in this scenario, it's just an 242 | // opaque handle into to 'soinfo' data that other methods can use to get the 243 | // real pointer to parts of the module, like so: 244 | const onLoadPointer = dlsym(handle, Memory.allocUtf8String('JNI_OnLoad')); 245 | 246 | // Once we have an actual pointer, we can get the range that holds it: 247 | const range = Process.getRangeByAddress(onLoadPointer); 248 | 249 | return { 250 | base: range.base, 251 | size: range.size, 252 | name, 253 | path, 254 | } 255 | }; -------------------------------------------------------------------------------- /native-tls-hook.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * Once we have captured traffic (once it's being sent to our proxy port) the next step is 4 | * to ensure any clients using TLS (HTTPS) trust our CA certificate, to allow us to intercept 5 | * encrypted connections successfully. 6 | * 7 | * This script does this, by defining overrides to hook BoringSSL (used by iOS 11+) and Cronet 8 | * (the Chromium network stack, used by some Android apps including TikTok). This is the primary 9 | * certificate trust mechanism for iOS, and only a niche addition for Android edge cases. 10 | * 11 | * The hooks defined here ensure that normal certificate validation is skipped, and instead any 12 | * TLS connection using our trusted CA is always trusted. In general use this disables both 13 | * normal & certificate-pinned TLS/HTTPS validation, so that all connections which use your CA 14 | * should always succeed. 15 | * 16 | * This does not completely disable TLS validation, but it does significantly relax it - it's 17 | * intended for use with the other scripts in this repo that ensure all traffic is routed directly 18 | * to your MitM proxy (generally on your local network). You probably don't want to use this for 19 | * any sensitive traffic sent over public/untrusted networks - it is difficult to intercept, and 20 | * any attacker would need a copy of the CA certificate you're using, but by its nature as a messy 21 | * hook around TLS internals it's probably not 100% secure. 22 | * 23 | * Since iOS 11 (2017) Apple has used BoringSSL internally to handle all TLS. This code 24 | * hooks low-level BoringSSL calls, to override all custom certificate validation completely. 25 | * https://nabla-c0d3.github.io/blog/2019/05/18/ssl-kill-switch-for-ios12/ to the general concept, 26 | * but note that this script goes further - reimplementing basic TLS cert validation, rather than 27 | * just returning OK blindly for all connections. 28 | * 29 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 30 | * SPDX-License-Identifier: AGPL-3.0-or-later 31 | * SPDX-FileCopyrightText: Tim Perry 32 | * 33 | *************************************************************************************************/ 34 | 35 | const TARGET_LIBS = [ 36 | { name: 'libboringssl.dylib', hooked: false }, // iOS primary TLS implementation 37 | { name: 'libsscronet.so', hooked: false }, // Cronet on Android 38 | { name: 'boringssl', hooked: false }, // Bundled by some apps e.g. TikTok on iOS 39 | { name: 'libssl.so', hooked: false }, // Native OpenSSL in Android 40 | ]; 41 | 42 | TARGET_LIBS.forEach((targetLib) => { 43 | waitForModule(targetLib.name, (targetModule) => { 44 | patchTargetLib(targetModule, targetLib.name); 45 | targetLib.hooked = true; 46 | }); 47 | 48 | if ( 49 | targetLib.name === 'libboringssl.dylib' && 50 | Process.platform === 'darwin' && 51 | !targetLib.hooked 52 | ) { 53 | // On iOS, we expect this to always work immediately, so print a warning if we 54 | // ever have to skip this TLS patching process. 55 | console.log(`\n !!! --- Could not load ${targetLib.name} to hook TLS --- !!!`); 56 | } 57 | }); 58 | 59 | function patchTargetLib(targetModule, targetName) { 60 | // Get the peer certificates from an SSL pointer. Returns a pointer to a STACK_OF(CRYPTO_BUFFER) 61 | // which requires use of the next few methods below to actually access. 62 | // https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get0_peer_certificates 63 | const SSL_get0_peer_certificates = new NativeFunction( 64 | targetModule.getExportByName('SSL_get0_peer_certificates'), 65 | 'pointer', ['pointer'] 66 | ); 67 | 68 | // Stack methods: 69 | // https://commondatastorage.googleapis.com/chromium-boringssl-docs/stack.h.html 70 | const sk_num = new NativeFunction( 71 | targetModule.getExportByName('sk_num'), 72 | 'size_t', ['pointer'] 73 | ); 74 | 75 | const sk_value = new NativeFunction( 76 | targetModule.getExportByName('sk_value'), 77 | 'pointer', ['pointer', 'int'] 78 | ); 79 | 80 | // Crypto buffer methods: 81 | // https://commondatastorage.googleapis.com/chromium-boringssl-docs/pool.h.html 82 | const crypto_buffer_len = new NativeFunction( 83 | targetModule.getExportByName('CRYPTO_BUFFER_len'), 84 | 'size_t', ['pointer'] 85 | ); 86 | 87 | const crypto_buffer_data = new NativeFunction( 88 | targetModule.getExportByName('CRYPTO_BUFFER_data'), 89 | 'pointer', ['pointer'] 90 | ); 91 | 92 | const SSL_VERIFY_OK = 0x0; 93 | const SSL_VERIFY_INVALID = 0x1; 94 | 95 | // We cache the verification callbacks we create. In general (in testing, 100% of the time) the 96 | // 'real' callback is always the exact same address, so this is much more efficient than creating 97 | // a new callback every time. 98 | const verificationCallbackCache = {}; 99 | 100 | const buildVerificationCallback = (realCallbackAddr) => { 101 | if (!verificationCallbackCache[realCallbackAddr]) { 102 | const realCallback = (realCallbackAddr && !realCallbackAddr.isNull()) 103 | ? new NativeFunction(realCallbackAddr, 'int', ['pointer', 'pointer']) 104 | : () => SSL_VERIFY_INVALID; 105 | 106 | let pendingCheckThreads = new Set(); 107 | 108 | const hookedCallback = new NativeCallback(function (ssl, out_alert) { 109 | let realResult = false; // False = not yet called, 0/1 = call result 110 | 111 | const threadId = Process.getCurrentThreadId(); 112 | const alreadyHaveLock = pendingCheckThreads.has(threadId); 113 | 114 | // We try to have only one thread running these checks at a time, as parallel calls 115 | // here on the same underlying callback seem to crash in some specific scenarios 116 | while (pendingCheckThreads.size > 0 && !alreadyHaveLock) { 117 | Thread.sleep(0.01); 118 | } 119 | pendingCheckThreads.add(threadId); 120 | 121 | if (targetName !== 'libboringssl.dylib') { 122 | // Cronet assumes its callback is always called, and crashes if not. iOS's BoringSSL 123 | // meanwhile seems to use some negative checks in its callback, and rejects the 124 | // connection independently of the return value here if it's called with a bad cert. 125 | // End result: we *only sometimes* proactively call the callback. 126 | realResult = realCallback(ssl, out_alert); 127 | } 128 | 129 | // Extremely dumb certificate validation: we accept any chain where the *exact* CA cert 130 | // we were given is present. No flexibility for non-trivial cert chains, and no 131 | // validation beyond presence of the expected CA certificate. BoringSSL does do a 132 | // fair amount of essential validation independent of the certificate comparison 133 | // though, so some basics may be covered regardless (see tls13_process_certificate_verify). 134 | 135 | // This *intentionally* does not reject certs with the wrong hostname, expired CA 136 | // or leaf certs, and lots of other issues. This is significantly better than nothing, 137 | // but it is not production-ready TLS verification for general use in untrusted envs! 138 | 139 | const peerCerts = SSL_get0_peer_certificates(ssl); 140 | 141 | // Loop through every cert in the chain: 142 | for (let i = 0; i < sk_num(peerCerts); i++) { 143 | // For each cert, check if it *exactly* matches our configured CA cert: 144 | const cert = sk_value(peerCerts, i); 145 | const certDataLength = crypto_buffer_len(cert).toNumber(); 146 | 147 | if (certDataLength !== CERT_DER.byteLength) continue; 148 | 149 | const certPointer = crypto_buffer_data(cert); 150 | const certData = new Uint8Array(certPointer.readByteArray(certDataLength)); 151 | 152 | if (certData.every((byte, j) => CERT_DER[j] === byte)) { 153 | if (!alreadyHaveLock) pendingCheckThreads.delete(threadId); 154 | return SSL_VERIFY_OK; 155 | } 156 | } 157 | 158 | // No matched peer - fallback to the provided callback instead: 159 | if (realResult === false) { // Haven't called it yet 160 | realResult = realCallback(ssl, out_alert); 161 | } 162 | 163 | if (!alreadyHaveLock) pendingCheckThreads.delete(threadId); 164 | return realResult; 165 | }, 'int', ['pointer','pointer']); 166 | 167 | verificationCallbackCache[realCallbackAddr] = hookedCallback; 168 | } 169 | 170 | return verificationCallbackCache[realCallbackAddr]; 171 | }; 172 | 173 | const customVerifyAddrs = [ 174 | targetModule.findExportByName("SSL_set_custom_verify"), 175 | targetModule.findExportByName("SSL_CTX_set_custom_verify") 176 | ].filter(Boolean); 177 | 178 | customVerifyAddrs.forEach((set_custom_verify_addr) => { 179 | const set_custom_verify_fn = new NativeFunction( 180 | set_custom_verify_addr, 181 | 'void', ['pointer', 'int', 'pointer'] 182 | ); 183 | 184 | // When this function is called, ignore the provided callback, and 185 | // configure our callback instead: 186 | Interceptor.replace(set_custom_verify_fn, new NativeCallback(function(ssl, mode, providedCallbackAddr) { 187 | set_custom_verify_fn(ssl, mode, buildVerificationCallback(providedCallbackAddr)); 188 | }, 'void', ['pointer', 'int', 'pointer'])); 189 | }); 190 | 191 | if (customVerifyAddrs.length) { 192 | if (DEBUG_MODE) { 193 | console.log(`[+] Patched ${customVerifyAddrs.length} ${targetName} verification methods`); 194 | } 195 | console.log(`== Hooked native TLS lib ${targetName} ==`); 196 | } else { 197 | console.log(`\n !!! Hooking native TLS lib ${targetName} failed - no verification methods found`); 198 | } 199 | 200 | const get_psk_identity_addr = targetModule.findExportByName("SSL_get_psk_identity"); 201 | if (get_psk_identity_addr) { 202 | // Hooking this is apparently required for some verification paths which check the 203 | // result is not 0x0. Any return value should work fine though. 204 | Interceptor.replace(get_psk_identity_addr, new NativeCallback(function(ssl) { 205 | return "PSK_IDENTITY_PLACEHOLDER"; 206 | }, 'pointer', ['pointer'])); 207 | } else if (customVerifyAddrs.length) { 208 | console.log(`Patched ${customVerifyAddrs.length} custom_verify methods, but couldn't find get_psk_identity`); 209 | } 210 | } 211 | 212 | -------------------------------------------------------------------------------- /native-connect-hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In some cases, proxy configuration by itself won't work. This notably includes Flutter apps (which ignore 3 | * system/JVM configuration entirely) and plausibly other apps intentionally ignoring proxies. To handle that 4 | * we hook native connect() calls directly, to redirect traffic on all ports to the target. 5 | * 6 | * This handles all attempts to connect an outgoing socket, and for all TCP connections opened it will 7 | * manually replace the connect() parameters so that the socket connects to the proxy instead of the 8 | * 'real' destination. 9 | * 10 | * This doesn't help with certificate trust (you still need some kind of certificate setup) but it does ensure 11 | * the proxy receives all connections (and so will see if connections don't trust its CA). It's still useful 12 | * to do proxy config alongside this, as applications may behave a little more 'correctly' if they're aware 13 | * they're using a proxy rather than doing so unknowingly. 14 | * 15 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 16 | * SPDX-License-Identifier: AGPL-3.0-or-later 17 | * SPDX-FileCopyrightText: Tim Perry 18 | */ 19 | 20 | (() => { 21 | const PROXY_HOST_IPv4_BYTES = PROXY_HOST.split('.').map(part => parseInt(part, 10)); 22 | const IPv6_MAPPING_PREFIX_BYTES = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff]; 23 | const PROXY_HOST_IPv6_BYTES = IPv6_MAPPING_PREFIX_BYTES.concat(PROXY_HOST_IPv4_BYTES); 24 | 25 | // Flags for fcntl(): 26 | const F_GETFL = 3; 27 | const F_SETFL = 4; 28 | const O_NONBLOCK = (Process.platform === 'darwin') 29 | ? 4 30 | : 2048; // Linux/Android 31 | 32 | let fcntl, send, recv, conn; 33 | try { 34 | const systemModule = Process.findModuleByName('libc.so') ?? // Android 35 | Process.findModuleByName('libc.so.6') ?? // Linux 36 | Process.findModuleByName('libsystem_c.dylib'); // iOS 37 | 38 | if (!systemModule) throw new Error("Could not find libc or libsystem_c"); 39 | 40 | fcntl = new NativeFunction(systemModule.getExportByName('fcntl'), 'int', ['int', 'int', 'int']); 41 | send = new NativeFunction(systemModule.getExportByName('send'), 'ssize_t', ['int', 'pointer', 'size_t', 'int']); 42 | recv = new NativeFunction(systemModule.getExportByName('recv'), 'ssize_t', ['int', 'pointer', 'size_t', 'int']); 43 | 44 | conn = systemModule.getExportByName('connect') 45 | } catch (e) { 46 | console.error("Failed to set up native hooks:", e.message); 47 | console.warn('Could not initialize system functions to to hook raw traffic'); 48 | return; 49 | } 50 | 51 | Interceptor.attach(conn, { 52 | onEnter(args) { 53 | const fd = this.sockFd = args[0].toInt32(); 54 | const sockType = Socket.type(fd); 55 | 56 | const addrPtr = ptr(args[1]); 57 | const addrLen = args[2].toInt32(); 58 | const addrData = addrPtr.readByteArray(addrLen); 59 | 60 | const isTCP = sockType === 'tcp' || sockType === 'tcp6'; 61 | const isUDP = sockType === 'udp' || sockType === 'udp6'; 62 | const isIPv6 = sockType === 'tcp6' || sockType === 'udp6'; 63 | 64 | if (isTCP || isUDP) { 65 | const portAddrBytes = new DataView(addrData.slice(2, 4)); 66 | const port = portAddrBytes.getUint16(0, false); // Big endian! 67 | 68 | const shouldBeIgnored = IGNORED_NON_HTTP_PORTS.includes(port); 69 | const shouldBeBlocked = BLOCK_HTTP3 && !shouldBeIgnored && isUDP && port === 443; 70 | 71 | // N.b for now we only support TCP interception - UDP direct should be doable, 72 | // but SOCKS5 UDP would require a whole different flow. Rarely relevant, especially 73 | // if you're blocking HTTP/3. 74 | const shouldBeIntercepted = isTCP && !shouldBeIgnored && !shouldBeBlocked; 75 | 76 | const hostBytes = isIPv6 77 | // 16 bytes offset by 8 (2 for family, 2 for port, 4 for flowinfo): 78 | ? new Uint8Array(addrData.slice(8, 8 + 16)) 79 | // 4 bytes, offset by 4 (2 for family, 2 for port) 80 | : new Uint8Array(addrData.slice(4, 4 + 4)); 81 | 82 | const isIntercepted = port === PROXY_PORT && areArraysEqual(hostBytes, 83 | isIPv6 84 | ? PROXY_HOST_IPv6_BYTES 85 | : PROXY_HOST_IPv4_BYTES 86 | ); 87 | 88 | if (isIntercepted) return; 89 | 90 | if (shouldBeBlocked) { 91 | if (isIPv6) { 92 | // Skip 8 bytes: 2 family, 2 port, 4 flowinfo, then write :: (all 0s) 93 | for (let i = 0; i < 16; i++) { 94 | addrPtr.add(8 + i).writeU8(0); 95 | } 96 | } else { 97 | // Skip 4 bytes: 2 family, 2 port, then write 0.0.0.0 98 | addrPtr.add(4).writeU32(0); 99 | } 100 | 101 | console.debug(`Blocking QUIC connection to ${getReadableAddress(hostBytes, isIPv6)}:${port}`); 102 | this.state = 'Blocked'; 103 | } else if (shouldBeIntercepted) { 104 | // Otherwise, it's an unintercepted connection that should be captured: 105 | this.state = 'intercepting'; 106 | 107 | // For SOCKS, we preserve the original destionation to use in the SOCKS handshake later 108 | // and we temporarily set the socket to blocking mode to do the handshake itself. 109 | if (PROXY_SUPPORTS_SOCKS5) { 110 | this.originalDestination = { host: hostBytes, port, isIPv6 }; 111 | this.originalFlags = fcntl(this.sockFd, F_GETFL, 0); 112 | this.isNonBlocking = (this.originalFlags & O_NONBLOCK) !== 0; 113 | if (this.isNonBlocking) { 114 | fcntl(this.sockFd, F_SETFL, this.originalFlags & ~O_NONBLOCK); 115 | } 116 | } 117 | 118 | console.log(`Manually intercepting ${sockType} connection to ${getReadableAddress(hostBytes, isIPv6)}:${port}`); 119 | 120 | // Overwrite the port with the proxy port: 121 | portAddrBytes.setUint16(0, PROXY_PORT, false); // Big endian 122 | addrPtr.add(2).writeByteArray(portAddrBytes.buffer); 123 | 124 | // Overwrite the address with the proxy address: 125 | if (isIPv6) { 126 | // Skip 8 bytes: 2 family, 2 port, 4 flowinfo 127 | addrPtr.add(8).writeByteArray(PROXY_HOST_IPv6_BYTES); 128 | } else { 129 | // Skip 4 bytes: 2 family, 2 port 130 | addrPtr.add(4).writeByteArray(PROXY_HOST_IPv4_BYTES); 131 | } 132 | } else { 133 | // Explicitly being left alone 134 | if (DEBUG_MODE) { 135 | console.debug(`Allowing unintercepted ${sockType} connection to port ${port}`); 136 | } 137 | this.state = 'ignored'; 138 | } 139 | } else { 140 | // Should just be unix domain sockets - UDP & TCP are covered above 141 | if (DEBUG_MODE) console.log(`Ignoring ${sockType} connection`); 142 | this.state = 'ignored'; 143 | } 144 | }, 145 | onLeave: function (retval) { 146 | if (this.state === 'ignored') return; 147 | 148 | if (this.state === 'intercepting' && PROXY_SUPPORTS_SOCKS5) { 149 | const connectSuccess = retval.toInt32() === 0; 150 | 151 | let handshakeSuccess = false; 152 | 153 | const { host, port, isIPv6 } = this.originalDestination; 154 | if (connectSuccess) { 155 | handshakeSuccess = performSocksHandshake(this.sockFd, host, port, isIPv6); 156 | } else { 157 | console.error(`SOCKS: Failed to connect to proxy at ${PROXY_HOST}:${PROXY_PORT}`); 158 | } 159 | 160 | if (this.isNonBlocking) { 161 | fcntl(this.sockFd, F_SETFL, this.originalFlags); 162 | } 163 | 164 | if (handshakeSuccess) { 165 | const readableHost = getReadableAddress(host, isIPv6); 166 | if (DEBUG_MODE) console.debug(`SOCKS redirect successful for fd ${this.sockFd} to ${readableHost}:${port}`); 167 | retval.replace(0); 168 | } else { 169 | if (DEBUG_MODE) console.error(`SOCKS redirect FAILED for fd ${this.sockFd}`); 170 | retval.replace(-1); 171 | } 172 | } else if (DEBUG_MODE) { 173 | const fd = this.sockFd; 174 | const sockType = Socket.type(fd); 175 | const address = Socket.peerAddress(fd); 176 | console.debug( 177 | `${this.state} ${sockType} fd ${fd} to ${JSON.stringify(address)} (${retval.toInt32()})` 178 | ); 179 | } 180 | } 181 | }); 182 | 183 | console.log(`== Redirecting ${ 184 | IGNORED_NON_HTTP_PORTS.length === 0 185 | ? 'all' 186 | : 'all unrecognized' 187 | } TCP connections to ${PROXY_HOST}:${PROXY_PORT} ==`); 188 | 189 | const getReadableAddress = ( 190 | /** @type {Uint8Array} */ hostBytes, 191 | /** @type {boolean} */ isIPv6 192 | ) => { 193 | if (!isIPv6) { 194 | // Return simple a.b.c.d IPv4 format: 195 | return [...hostBytes].map(x => x.toString()).join('.'); 196 | } 197 | 198 | if ( 199 | hostBytes.slice(0, 10).every(b => b === 0) && 200 | hostBytes.slice(10, 12).every(b => b === 255) 201 | ) { 202 | // IPv4-mapped IPv6 address - print as IPv4 for readability 203 | return '::ffff:'+[...hostBytes.slice(12)].map(x => x.toString()).join('.'); 204 | } 205 | 206 | else { 207 | // Real IPv6: 208 | return `[${[...hostBytes].map(x => x.toString(16)).join(':')}]`; 209 | } 210 | }; 211 | 212 | const areArraysEqual = (arrayA, arrayB) => { 213 | if (arrayA.length !== arrayB.length) return false; 214 | return arrayA.every((x, i) => arrayB[i] === x); 215 | }; 216 | 217 | function performSocksHandshake(sockfd, targetHostBytes, targetPort, isIPv6) { 218 | const hello = Memory.alloc(3).writeByteArray([0x05, 0x01, 0x00]); 219 | if (send(sockfd, hello, 3, 0) < 0) { 220 | console.error("SOCKS: Failed to send hello"); 221 | return false; 222 | } 223 | 224 | const response = Memory.alloc(2); 225 | if (recv(sockfd, response, 2, 0) < 0) { 226 | console.error("SOCKS: Failed to receive server choice"); 227 | return false; 228 | } 229 | 230 | if (response.readU8() !== 0x05 || response.add(1).readU8() !== 0x00) { 231 | console.error("SOCKS: Server rejected auth method"); 232 | return false; 233 | } 234 | 235 | let req = [0x05, 0x01, 0x00]; // VER, CMD(CONNECT), RSV 236 | 237 | if (isIPv6) { 238 | req.push(0x04); // ATYP: IPv6 239 | } else { // IPv4 240 | req.push(0x01); // ATYP: IPv4 241 | } 242 | 243 | req.push(...targetHostBytes, (targetPort >> 8) & 0xff, targetPort & 0xff); 244 | const reqBuf = Memory.alloc(req.length).writeByteArray(req); 245 | 246 | if (send(sockfd, reqBuf, req.length, 0) < 0) { 247 | console.error("SOCKS: Failed to send connection request"); 248 | return false; 249 | } 250 | 251 | const replyHeader = Memory.alloc(4); 252 | if (recv(sockfd, replyHeader, 4, 0) < 0) { 253 | console.error("SOCKS: Failed to receive reply header"); 254 | return false; 255 | } 256 | 257 | const replyCode = replyHeader.add(1).readU8(); 258 | if (replyCode !== 0x00) { 259 | console.error(`SOCKS: Server returned error code ${replyCode}`); 260 | return false; 261 | } 262 | 263 | const atyp = replyHeader.add(3).readU8(); 264 | let remainingBytes = 0; 265 | if (atyp === 0x01) remainingBytes = 4 + 2; // IPv4 + port 266 | else if (atyp === 0x04) remainingBytes = 16 + 2; // IPv6 + port 267 | if (remainingBytes > 0) recv(sockfd, Memory.alloc(remainingBytes), remainingBytes, 0); 268 | 269 | return true; 270 | } 271 | })(); -------------------------------------------------------------------------------- /android/android-disable-flutter-certificate-pinning.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * This script hooks Flutter internal certificate handling, to trust our certificate (and ignore 4 | * any custom certificate validation - e.g. pinning libraries) for all TLS connections. 5 | * 6 | * Unfortunately Flutter is shipped as native code with no exported symbols, so we have to do this 7 | * by matching individual function signatures by known patterns of assembly instructions. In 8 | * some cases, this goes further and uses larger functions as anchors - allowing us to find the 9 | * very short functions correctly, where the patterns would otherwise have false positives. 10 | * 11 | * The patterns here have been generated from every non-patch release of Flutter from v2.0.0 12 | * to v3.32.0 (the latest at the time of writing). They may need updates for new versions 13 | * in future. 14 | * 15 | * Currently this is limited to just Android, but in theory this can be expanded to iOS and 16 | * desktop platforms in future. 17 | * 18 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 19 | * SPDX-License-Identifier: AGPL-3.0-or-later 20 | * SPDX-FileCopyrightText: Tim Perry 21 | * 22 | *************************************************************************************************/ 23 | 24 | (() => { 25 | const PATTERNS = { 26 | "android/x64": { 27 | "dart::bin::SSLCertContext::CertificateCallback": { 28 | "signatures": [ 29 | "41 57 41 56 53 48 83 ec 10 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3", 30 | "41 57 41 56 41 54 53 48 83 ec 18 b8 01 00 00 00 83 ff 01 0f 84 ?? ?? ?? ?? 48 89 f3" 31 | ] 32 | }, 33 | "X509_STORE_CTX_get_current_cert": { 34 | "signatures": [ 35 | "48 8b 47 50 c3", 36 | "48 8b 47 60 c3", 37 | "48 8b 87 a8 00 00 00 c3", 38 | "48 8b 87 b8 00 00 00 c3" 39 | ], 40 | "anchor": "dart::bin::SSLCertContext::CertificateCallback" 41 | }, 42 | "bssl::x509_to_buffer": { 43 | "signatures": [ 44 | "41 56 53 50 48 89 f0 48 89 fb 48 89 e6 48 83 26 00 48 89 c7 e8 ?? ?? ?? ?? 85 c0 7e 1b", 45 | "53 48 83 ec 10 48 89 f0 48 89 fb 48 8d 74 24 08 48 83 26 00 48 89 c7 e8 ?? ?? ?? ?? 85 c0", 46 | "41 56 53 48 83 ec 18 48 89 f0 48 89 fb 48 8d 74 24 08 48 83 26 00 48 89 c7 e8", 47 | "41 56 53 48 83 ec 18 48 89 f0 49 89 fe 48 8d 74 24 08 48 83 26 00 48 89 c7 e8", 48 | "41 57 41 56 53 48 83 ec 10 48 89 f0 49 89 fe 48 89 e6 48 83 26 00 48 89 c7 e8" 49 | ] 50 | }, 51 | "i2d_X509": { 52 | "signatures": [ 53 | "55 41 56 53 48 83 ec 70 48 85 ff 0f 84 ?? ?? ?? ?? 48 89 f3 49 89 fe 48 8d 7c 24 40 6a 40", 54 | "48 8d 15 ?? ?? ?? ?? e9" 55 | ], 56 | "anchor": "bssl::x509_to_buffer" 57 | } 58 | }, 59 | "android/x86": { 60 | "dart::bin::SSLCertContext::CertificateCallback": { 61 | "signatures": [ 62 | "55 89 e5 53 57 56 83 e4 f0 83 ec 30 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? bf 01 00 00 00 83 7d 08 01 0f 84" 63 | ] 64 | }, 65 | "X509_STORE_CTX_get_current_cert": { 66 | "signatures": [ 67 | "55 89 e5 83 e4 fc 8b 45 08 8b 40 2c 89 ec 5d c3", 68 | "55 89 e5 83 e4 fc 8b 45 08 8b 40 34 89 ec 5d c3", 69 | "55 89 e5 83 e4 fc 8b 45 08 8b 40 5c 89 ec 5d c3", 70 | "55 89 e5 83 e4 fc 8b 45 08 8b 40 64 89 ec 5d c3" 71 | ], 72 | "anchor": "dart::bin::SSLCertContext::CertificateCallback" 73 | }, 74 | "bssl::x509_to_buffer": { 75 | "signatures": [ 76 | "55 89 e5 53 57 56 83 e4 f0 83 ec 10 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 08 83 20 00 83 ec 08 50 52", 77 | "55 89 e5 53 56 83 e4 f0 83 ec 10 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 0c 83 20 00 83 ec 08 50 52", 78 | "55 89 e5 53 57 56 83 e4 f0 83 ec 20 89 ce e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8d 44 24 14 83 20 00 89 44 24 04 89 14 24" 79 | ] 80 | }, 81 | "i2d_X509": { 82 | "signatures": [ 83 | "55 89 e5 53 57 56 83 e4 f0 83 ec 40 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 8b 7d 08 85 ff 0f 84 ?? ?? ?? ?? 83 ec 08", 84 | "55 89 e5 53 83 e4 f0 83 ec 10 e8 ?? ?? ?? ?? 5b 81 c3 ?? ?? ?? ?? 83 ec 04 8d 83 ?? ?? ?? ?? 50 ff 75 0c ff 75 08" 85 | ], 86 | "anchor": "bssl::x509_to_buffer" 87 | } 88 | }, 89 | 90 | "android/arm64": { 91 | "dart::bin::SSLCertContext::CertificateCallback": { 92 | "signatures": [ 93 | "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 c0 07 00 54 f3 03 01 aa ?? ?? ?? 94", 94 | "ff c3 00 d1 fe 57 01 a9 f4 4f 02 a9 1f 04 00 71 c0 02 00 54 f3 03 01 aa ?? ?? ?? 94" 95 | ] 96 | }, 97 | "X509_STORE_CTX_get_current_cert": { 98 | "signatures": [ 99 | "00 ?? ?? f9 c0 03 5f d6" 100 | ], 101 | "anchor": "dart::bin::SSLCertContext::CertificateCallback" 102 | }, 103 | "bssl::x509_to_buffer": { 104 | "signatures": [ 105 | "fe 0f 1e f8 f4 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 04 00 71", 106 | "fe 0f 1e f8 f4 4f 01 a9 e8 03 01 aa f3 03 00 aa e1 ?? ?? 91 e0 03 08 aa ff 07 00 f9", 107 | "ff 83 00 d1 fe 4f 01 a9 e1 ?? ?? 91 f3 03 08 aa ff 07 00 f9 ?? ?? ?? 97 1f 00 00 71", 108 | "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 00 00 71", 109 | "ff c3 00 d1 fe 7f 01 a9 f4 4f 02 a9 e1 ?? ?? 91 f3 03 08 aa ?? ?? ?? 97 1f 04 00 71" 110 | ] 111 | }, 112 | "i2d_X509": { 113 | "signatures": [ 114 | "ff 43 02 d1 fe 57 07 a9 f4 4f 08 a9 a0 06 00 b4 f4 03 00 aa f3 03 01 aa e0 ?? ?? 91", 115 | "?2 ?? ?? ?? 42 ?? ?? 91 ?? ?? ?? 17" 116 | ], 117 | "anchor": "bssl::x509_to_buffer" 118 | } 119 | }, 120 | "android/arm": { 121 | "dart::bin::SSLCertContext::CertificateCallback": { 122 | "signatures": [ 123 | "70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 4d d0 20 46 ?? f? ?? f? 05 46 ?? f? ?? f", 124 | "70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 52 d0 20 46 ?? f? ?? f? 06 46 ?? f? ?? f", 125 | "70 b5 84 b0 01 28 02 d1 01 20 04 b0 70 bd 0c 46 ?? f? ?? f? 00 28 50 d0 20 46 ?? f? ?? f? 06 46 ?? f? ?? f" 126 | ] 127 | }, 128 | "X509_STORE_CTX_get_current_cert": { 129 | "signatures": [ 130 | "c0 6a 70 47", 131 | "40 6b 70 47", 132 | "c0 6d 70 47", 133 | "40 6e 70 47" 134 | ], 135 | "anchor": "dart::bin::SSLCertContext::CertificateCallback" 136 | }, 137 | "bssl::x509_to_buffer": { 138 | "signatures": [ 139 | "bc b5 00 25 0a 46 01 95 01 a9 04 46 10 46 ?? f? ?? f? 01 28 08 db 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98", 140 | "bc b5 00 25 0a 46 01 95 01 a9 04 46 10 46 ?? f? ?? f? 00 28 09 dd 01 46 01 98 00 22 ?? f? ?? f? 20 60 01 98", 141 | "7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 00 28 0e dd 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98", 142 | "7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 01 28 0d db 01 46 01 98 00 22 ?? f? ?? f? 05 46 01 98", 143 | "7c b5 00 26 0a 46 01 96 01 a9 04 46 10 46 ?? f? ?? f? 01 28 0e db 01 46 01 98 00 22 ?? f? ?? f? 05 46 00 90" 144 | ] 145 | }, 146 | "i2d_X509": { 147 | "signatures": [ 148 | "70 b5 8e b0 00 28 4f d0 05 46 08 a8 0c 46 40 21 ?? f? ?? f? 00 28 43 d0 2a 4a 08 a8 02 a9 ?? f? ?? f? e8 b3", 149 | "01 4a 7a 44 ?? f? ?? b" 150 | ], 151 | "anchor": "bssl::x509_to_buffer" 152 | } 153 | } 154 | } 155 | 156 | 157 | const MAX_ANCHOR_INSTRUCTIONS_TO_SCAN = 100; 158 | 159 | const CALL_MNEMONICS = ['call', 'bl', 'blx']; 160 | 161 | function scanForSignature(base, size, patterns) { 162 | const results = []; 163 | for (const pattern of patterns) { 164 | const result = Memory.scanSync(base, size, pattern); 165 | results.push(...result); 166 | } 167 | return results; 168 | } 169 | 170 | function scanForFunction(moduleRXRanges, platformPatterns, functionName, anchorFn) { 171 | const patternInfo = platformPatterns[functionName]; 172 | const signatures = patternInfo.signatures; 173 | 174 | if (patternInfo.anchor) { 175 | const maxPatternByteLength = Math.max(...signatures.map(p => (p.length + 1) / 3)); 176 | 177 | let addr = ptr(anchorFn); 178 | 179 | for (let i = 0; i < MAX_ANCHOR_INSTRUCTIONS_TO_SCAN; i++) { 180 | const instr = Instruction.parse(addr); 181 | addr = instr.next; 182 | if (CALL_MNEMONICS.includes(instr.mnemonic)) { 183 | const callTargetAddr = ptr(instr.operands[0].value); 184 | const results = scanForSignature(callTargetAddr, maxPatternByteLength, signatures); 185 | if (results.length === 1) { 186 | return results[0].address; 187 | } else if (results.length > 1) { 188 | console.log(`Found multiple matches for ${functionName} anchored by ${anchorFunction}:`, results); 189 | throw new Error(`Found multiple matches for ${functionName}`); 190 | } 191 | } 192 | } 193 | 194 | throw new Error(`Failed to find any match for ${functionName} anchored by ${anchorFn}`); 195 | } else { 196 | const results = moduleRXRanges.flatMap((range) => scanForSignature(range.base, range.size, signatures)); 197 | if (results.length !== 1 && signatures.length > 1) { 198 | console.log(results); 199 | throw new Error(`Found multiple matches for ${functionName}`); 200 | } 201 | 202 | return results[0].address; 203 | } 204 | } 205 | 206 | function hookFlutter(moduleBase, moduleSize) { 207 | if (DEBUG_MODE) console.log('\n=== Disabling Flutter certificate pinning ==='); 208 | 209 | const relevantRanges = Process.enumerateRanges('r-x').filter(range => { 210 | return range.base >= moduleBase && range.base < moduleBase.add(moduleSize); 211 | }); 212 | 213 | try { 214 | const arch = Process.arch; 215 | const patterns = PATTERNS[`android/${arch}`]; 216 | 217 | // This callback is called for all TLS connections. It immediately returns 1 (success) if BoringSSL 218 | // trusts the cert, or it calls the configured BadCertificateCallback if it doesn't. Note that this 219 | // is called for every cert in the chain individually - not the whole chain at once. 220 | const dartCertificateCallback = new NativeFunction( 221 | scanForFunction(relevantRanges, patterns, 'dart::bin::SSLCertContext::CertificateCallback'), 222 | 'int', 223 | ['int', 'pointer'] 224 | ); 225 | 226 | // We inject code to check the certificate ourselves - getting the cert, converting to DER, and 227 | // ignoring all validation results if the certificate matches our trusted cert. 228 | const x509GetCurrentCert = new NativeFunction( 229 | scanForFunction(relevantRanges, patterns, 'X509_STORE_CTX_get_current_cert', dartCertificateCallback), 230 | 'pointer', 231 | ['pointer'] 232 | ); 233 | 234 | // Just used as an anchor for searching: 235 | const x509ToBufferAddr = scanForFunction(relevantRanges, patterns, 'bssl::x509_to_buffer'); 236 | const i2d_X509 = new NativeFunction( 237 | scanForFunction(relevantRanges, patterns, 'i2d_X509', x509ToBufferAddr), 238 | 'int', 239 | ['pointer', 'pointer'] 240 | ); 241 | 242 | Interceptor.attach(dartCertificateCallback, { 243 | onEnter: function (args) { 244 | this.x509Store = args[1]; 245 | }, 246 | onLeave: function (retval) { 247 | if (retval.toInt32() === 1) return; // Ignore successful validations 248 | 249 | // This certificate isn't trusted by BoringSSL or the app's certificate callback. Check it ourselves 250 | // and override the result if it exactly matches our cert. 251 | try { 252 | const x509Cert = x509GetCurrentCert(this.x509Store); 253 | 254 | const derLength = i2d_X509(x509Cert, NULL); 255 | if (derLength <= 0) { 256 | throw new Error('Failed to get DER length for X509 cert'); 257 | } 258 | 259 | // We create our own target buffer (rather than letting BoringSSL do so, which would 260 | // require more hooks to handle cleanup). 261 | const derBuffer = Memory.alloc(derLength) 262 | const outPtr = Memory.alloc(Process.pointerSize); 263 | outPtr.writePointer(derBuffer); 264 | 265 | const certDataLength = i2d_X509(x509Cert, outPtr) 266 | const certData = new Uint8Array(derBuffer.readByteArray(certDataLength)); 267 | 268 | if (certData.every((byte, j) => CERT_DER[j] === byte)) { 269 | retval.replace(1); // We trust this certificate, return success 270 | } 271 | } catch (error) { 272 | console.error('[!] Internal error in Flutter certificate unpinning:', error); 273 | } 274 | } 275 | }); 276 | 277 | console.log('=== Flutter certificate pinning disabled ==='); 278 | } catch (error) { 279 | console.error('[!] Error preparing Flutter certificate pinning hooks:', error); 280 | throw error; 281 | } 282 | } 283 | 284 | let flutter = Process.findModuleByName('libflutter.so'); 285 | if (flutter) { 286 | hookFlutter(flutter.base, flutter.size); 287 | } else { 288 | waitForModule('libflutter.so', function (module) { 289 | hookFlutter(module.base, module.size); 290 | }); 291 | } 292 | })(); -------------------------------------------------------------------------------- /android/android-certificate-unpinning-fallback.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * Once we've set up the configuration and certificate, and then disabled all the pinning 4 | * techniques we're aware of, we add one last touch: a fallback hook, designed to spot and handle 5 | * unknown unknowns. 6 | * 7 | * This can also be useful for heavily obfuscated apps, where 3rd party libraries are obfuscated 8 | * sufficiently that our hooks no longer recognize the methods we care about. 9 | * 10 | * To handle this, we watch for methods that throw known built-in TLS errors (these are *very* 11 | * widely used, and always recognizable as they're defined natively), and then subsequently patch 12 | * them for all future calls. Whenever a method throws this, we attempt to recognize it from 13 | * signatures alone, and automatically hook it. 14 | * 15 | * These are very much a fallback! They might not work! They almost certainly won't work on the 16 | * first request, so applications will see at least one failure. Even when they fail though, they 17 | * will at least log the method that's failing, so this works well as a starting point for manual 18 | * reverse engineering. If this does fail and cause problems, you may want to skip this script 19 | * and use only the known-good patches provided elsewhere. 20 | * 21 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 22 | * SPDX-License-Identifier: AGPL-3.0-or-later 23 | * SPDX-FileCopyrightText: Tim Perry 24 | * 25 | *************************************************************************************************/ 26 | 27 | // Capture the full fields or methods from a Frida class reference via JVM reflection: 28 | const getFields = (cls) => getFridaValues(cls, cls.class.getDeclaredFields()); 29 | const getMethods = (cls) => getFridaValues(cls, cls.class.getDeclaredMethods()); 30 | 31 | // Take a Frida class + JVM reflection result, and turn it into a clear list 32 | // of names -> Frida values (field or method references) 33 | const getFridaValues = (cls, values) => values.map((value) => 34 | [value.getName(), cls[value.getName()]] 35 | ); 36 | 37 | Java.perform(function () { 38 | try { 39 | const X509TrustManager = Java.use("javax.net.ssl.X509TrustManager"); 40 | const defaultTrustManager = getCustomX509TrustManager(); // Defined in the unpinning script 41 | const certBytes = Java.use("java.lang.String").$new(CERT_PEM).getBytes(); 42 | const trustedCACert = buildX509CertificateFromBytes(certBytes); // Ditto 43 | 44 | const isX509TrustManager = (cls, methodName) => 45 | methodName === 'checkServerTrusted' && 46 | X509TrustManager.class.isAssignableFrom(cls.class); 47 | 48 | // There are two standard methods that X509TM implementations might override. We confirm we're 49 | // matching the methods we expect by double-checking against the argument types: 50 | const BASE_METHOD_ARGUMENTS = [ 51 | '[Ljava.security.cert.X509Certificate;', 52 | 'java.lang.String' 53 | ]; 54 | const EXTENDED_METHOD_ARGUMENTS = [ 55 | '[Ljava.security.cert.X509Certificate;', 56 | 'java.lang.String', 57 | 'java.lang.String' 58 | ]; 59 | 60 | const isOkHttpCheckMethod = (errorMessage, method) => 61 | errorMessage.startsWith("Certificate pinning failure!" + "\n Peer certificate chain:") && 62 | method.argumentTypes.length === 2 && 63 | method.argumentTypes[0].className === 'java.lang.String'; 64 | 65 | const isAppmattusOkHttpInterceptMethod = (errorMessage, method) => { 66 | if (errorMessage !== 'Certificate transparency failed') return; 67 | 68 | // Takes a single OkHttp chain argument: 69 | if (method.argumentTypes.length !== 1) return; 70 | 71 | // The method must take an Interceptor.Chain, for which we need to 72 | // call chain.proceed(chain.request()) to return a Response type. 73 | // To do that, we effectively pattern match our way through all the 74 | // related types to work out what's what: 75 | 76 | const chainType = Java.use(method.argumentTypes[0].className); 77 | const responseTypeName = method.returnType.className; 78 | 79 | const matchedChain = matchOkHttpChain(chainType, responseTypeName); 80 | return !!matchedChain; 81 | }; 82 | 83 | const isMetaPinningMethod = (errorMessage, method) => 84 | method.argumentTypes.length === 1 && 85 | method.argumentTypes[0].className === 'java.util.List' && 86 | method.returnType.className === 'void' && 87 | errorMessage.includes('pinning error'); 88 | 89 | const matchOkHttpChain = (cls, expectedReturnTypeName) => { 90 | // Find the chain.proceed() method: 91 | const methods = getMethods(cls); 92 | const matchingMethods = methods.filter(([_, method]) => 93 | method.returnType.className === expectedReturnTypeName 94 | ); 95 | if (matchingMethods.length !== 1) return; 96 | 97 | const [proceedMethodName, proceedMethod] = matchingMethods[0]; 98 | if (proceedMethod.argumentTypes.length !== 1) return; 99 | 100 | const argumentTypeName = proceedMethod.argumentTypes[0].className; 101 | 102 | // Find the chain.request private field (.request() getter can be 103 | // optimized out, so we read the field directly): 104 | const fields = getFields(cls); 105 | const matchingFields = fields.filter(([_, field]) => 106 | field.fieldReturnType?.className === argumentTypeName 107 | ); 108 | if (matchingFields.length !== 1) return; 109 | 110 | const [requestFieldName] = matchingFields[0]; 111 | 112 | return { 113 | proceedMethodName, 114 | requestFieldName 115 | }; 116 | }; 117 | 118 | const buildUnhandledErrorPatcher = (errorClassName, originalConstructor) => { 119 | return function (errorArg) { 120 | try { 121 | console.log('\n !!! --- Unexpected TLS failure --- !!!'); 122 | 123 | // This may be a message, or an cause, or plausibly maybe other types? But 124 | // stringifying gives something consistently message-shaped, so that'll do. 125 | const errorMessage = errorArg?.toString() ?? ''; 126 | 127 | // Parse the stack trace to work out who threw this error: 128 | const stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace(); 129 | const exceptionStackIndex = stackTrace.findIndex(stack => 130 | stack.getClassName() === errorClassName 131 | ); 132 | const callingFunctionStack = stackTrace[exceptionStackIndex + 1]; 133 | 134 | const className = callingFunctionStack.getClassName(); 135 | const methodName = callingFunctionStack.getMethodName(); 136 | 137 | const errorTypeName = errorClassName.split('.').slice(-1)[0]; 138 | console.log(` ${errorTypeName}: ${errorMessage}`); 139 | console.log(` Thrown by ${className}->${methodName}`); 140 | 141 | const callingClass = Java.use(className); 142 | const callingMethod = callingClass[methodName]; 143 | 144 | callingMethod.overloads.forEach((failingMethod) => { 145 | if (failingMethod.implementation) { 146 | console.warn(' Already patched - but still failing!') 147 | return; // Already patched by Frida - skip it 148 | } 149 | 150 | // Try to spot known methods (despite obfuscation) and disable them: 151 | if (isOkHttpCheckMethod(errorMessage, failingMethod)) { 152 | // See okhttp3.CertificatePinner patches in unpinning script: 153 | failingMethod.implementation = () => { 154 | if (DEBUG_MODE) console.log(` => Fallback OkHttp patch`); 155 | }; 156 | console.log(` [+] ${className}->${methodName} (fallback OkHttp patch)`); 157 | } else if (isAppmattusOkHttpInterceptMethod(errorMessage, failingMethod)) { 158 | // See Appmattus CertificateTransparencyInterceptor patch in unpinning script: 159 | const chainType = Java.use(failingMethod.argumentTypes[0].className); 160 | const responseTypeName = failingMethod.returnType.className; 161 | const okHttpChain = matchOkHttpChain(chainType, responseTypeName); 162 | failingMethod.implementation = (chain) => { 163 | if (DEBUG_MODE) console.log(` => Fallback Appmattus+OkHttp patch`); 164 | const proceed = chain[okHttpChain.proceedMethodName].bind(chain); 165 | const request = chain[okHttpChain.requestFieldName].value; 166 | return proceed(request); 167 | }; 168 | console.log(` [+] ${className}->${methodName} (fallback Appmattus+OkHttp patch)`); 169 | } else if (isX509TrustManager(callingClass, methodName)) { 170 | const argumentTypes = failingMethod.argumentTypes.map(t => t.className); 171 | const returnType = failingMethod.returnType.className; 172 | 173 | if ( 174 | argumentTypes.length === 2 && 175 | argumentTypes.every((t, i) => t === BASE_METHOD_ARGUMENTS[i]) && 176 | returnType === 'void' 177 | ) { 178 | // For the base method, just check against the default: 179 | failingMethod.implementation = (certs, authType) => { 180 | if (DEBUG_MODE) console.log(` => Fallback X509TrustManager patch of ${ 181 | className 182 | } base method`); 183 | 184 | const defaultTrustManager = getCustomX509TrustManager(); // Defined in the unpinning script 185 | defaultTrustManager.checkServerTrusted(certs, authType); 186 | }; 187 | console.log(` [+] ${className}->${methodName} (fallback X509TrustManager base patch)`); 188 | } else if ( 189 | argumentTypes.length === 3 && 190 | argumentTypes.every((t, i) => t === EXTENDED_METHOD_ARGUMENTS[i]) && 191 | returnType === 'java.util.List' 192 | ) { 193 | // For the extended method, we just ignore the hostname, and if the certs are good 194 | // (i.e they're ours), then we say the whole chain is good to go: 195 | failingMethod.implementation = function (certs, authType, _hostname) { 196 | if (DEBUG_MODE) console.log(` => Fallback X509TrustManager patch of ${ 197 | className 198 | } extended method`); 199 | 200 | try { 201 | defaultTrustManager.checkServerTrusted(certs, authType); 202 | } catch (e) { 203 | console.error('Default TM threw:', e); 204 | } 205 | return Java.use('java.util.Arrays').asList(certs); 206 | }; 207 | console.log(` [+] ${className}->${methodName} (fallback X509TrustManager ext patch)`); 208 | } else { 209 | console.warn(` [ ] Skipping unrecognized checkServerTrusted signature in class ${ 210 | callingClass.class.getName() 211 | }`); 212 | } 213 | } else if (isMetaPinningMethod(errorMessage, failingMethod)) { 214 | failingMethod.implementation = function (certs) { 215 | if (DEBUG_MODE) console.log(` => Fallback patch for meta proxygen pinning`); 216 | for (const cert of certs.toArray()) { 217 | if (cert.equals(trustedCACert)) { 218 | return; // Our own cert - all good 219 | } 220 | } 221 | 222 | if (DEBUG_MODE) { 223 | console.warn(' Meta unpinning fallback found only untrusted certificates'); 224 | } 225 | // Fall back to normal logic, in case of passthrough or similar 226 | return failingMethod.call(this, certs); 227 | } 228 | 229 | console.log(` [+] ${className}->${methodName} (Meta proxygen pinning fallback patch)`); 230 | } else { 231 | console.error(' [ ] Unrecognized TLS error - this must be patched manually'); 232 | return; 233 | // Later we could try to cover other cases here - automatically recognizing other 234 | // OkHttp interceptors for example, or potentially other approaches, but we need 235 | // to do so carefully to avoid disabling TLS checks entirely. 236 | } 237 | }); 238 | } catch (e) { 239 | console.log(' [ ] Failed to automatically patch failure'); 240 | console.warn(e); 241 | } 242 | 243 | return originalConstructor.call(this, ...arguments); 244 | } 245 | }; 246 | 247 | // These are the exceptions we watch for and attempt to auto-patch out after they're thrown: 248 | [ 249 | 'javax.net.ssl.SSLPeerUnverifiedException', 250 | 'java.security.cert.CertificateException' 251 | ].forEach((errorClassName) => { 252 | const ErrorClass = Java.use(errorClassName); 253 | ErrorClass.$init.overloads.forEach((overload) => { 254 | overload.implementation = buildUnhandledErrorPatcher( 255 | errorClassName, 256 | overload 257 | ); 258 | }); 259 | }) 260 | 261 | console.log('== Unpinning fallback auto-patcher installed =='); 262 | } catch (err) { 263 | console.error(err); 264 | console.error(' !!! --- Unpinning fallback auto-patcher installation failed --- !!!'); 265 | } 266 | 267 | }); -------------------------------------------------------------------------------- /android/fridantiroot.js: -------------------------------------------------------------------------------- 1 | /* 2 | Original author: Daniele Linguaglossa 3 | 28/07/2021 - Edited by Simone Quatrini 4 | Code amended to correctly run on the latest frida version 5 | Added controls to exclude Magisk Manager 6 | */ 7 | 8 | Java.perform(function() { 9 | var RootPackages = ["com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu", 10 | "com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.koushikdutta.rommanager", 11 | "com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch", 12 | "com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.devadvance.rootcloak", "com.devadvance.rootcloakplus", 13 | "de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot", 14 | "com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "me.phh.superuser", 15 | "eu.chainfire.supersu.pro", "com.kingouser.com", "com.topjohnwu.magisk" 16 | ]; 17 | 18 | var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk", "magisk"]; 19 | 20 | var RootProperties = { 21 | "ro.build.selinux": "1", 22 | "ro.debuggable": "0", 23 | "service.adb.root": "0", 24 | "ro.secure": "1" 25 | }; 26 | 27 | var RootPropertiesKeys = []; 28 | 29 | for (var k in RootProperties) RootPropertiesKeys.push(k); 30 | 31 | var PackageManager = Java.use("android.app.ApplicationPackageManager"); 32 | 33 | var Runtime = Java.use('java.lang.Runtime'); 34 | 35 | var NativeFile = Java.use('java.io.File'); 36 | 37 | var String = Java.use('java.lang.String'); 38 | 39 | var SystemProperties = Java.use('android.os.SystemProperties'); 40 | 41 | var BufferedReader = Java.use('java.io.BufferedReader'); 42 | 43 | var ProcessBuilder = Java.use('java.lang.ProcessBuilder'); 44 | 45 | var StringBuffer = Java.use('java.lang.StringBuffer'); 46 | 47 | var loaded_classes = Java.enumerateLoadedClassesSync(); 48 | 49 | send("Loaded " + loaded_classes.length + " classes!"); 50 | 51 | var useKeyInfo = false; 52 | 53 | var useProcessManager = false; 54 | 55 | send("loaded: " + loaded_classes.indexOf('java.lang.ProcessManager')); 56 | 57 | if (loaded_classes.indexOf('java.lang.ProcessManager') != -1) { 58 | try { 59 | //useProcessManager = true; 60 | //var ProcessManager = Java.use('java.lang.ProcessManager'); 61 | } catch (err) { 62 | send("ProcessManager Hook failed: " + err); 63 | } 64 | } else { 65 | send("ProcessManager hook not loaded"); 66 | } 67 | 68 | var KeyInfo = null; 69 | 70 | if (loaded_classes.indexOf('android.security.keystore.KeyInfo') != -1) { 71 | try { 72 | //useKeyInfo = true; 73 | //var KeyInfo = Java.use('android.security.keystore.KeyInfo'); 74 | } catch (err) { 75 | send("KeyInfo Hook failed: " + err); 76 | } 77 | } else { 78 | send("KeyInfo hook not loaded"); 79 | } 80 | 81 | PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pname, flags) { 82 | var shouldFakePackage = (RootPackages.indexOf(pname) > -1); 83 | if (shouldFakePackage) { 84 | send("Bypass root check for package: " + pname); 85 | pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it"; 86 | } 87 | return this.getPackageInfo.overload('java.lang.String', 'int').call(this, pname, flags); 88 | }; 89 | 90 | NativeFile.exists.implementation = function() { 91 | var name = NativeFile.getName.call(this); 92 | var shouldFakeReturn = (RootBinaries.indexOf(name) > -1); 93 | if (shouldFakeReturn) { 94 | send("Bypass return value for binary: " + name); 95 | return false; 96 | } else { 97 | return this.exists.call(this); 98 | } 99 | }; 100 | 101 | var exec = Runtime.exec.overload('[Ljava.lang.String;'); 102 | var exec1 = Runtime.exec.overload('java.lang.String'); 103 | var exec2 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;'); 104 | var exec3 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;'); 105 | var exec4 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File'); 106 | var exec5 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;', 'java.io.File'); 107 | 108 | exec5.implementation = function(cmd, env, dir) { 109 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 110 | var fakeCmd = "grep"; 111 | send("Bypass " + cmd + " command"); 112 | return exec1.call(this, fakeCmd); 113 | } 114 | if (cmd == "su") { 115 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 116 | send("Bypass " + cmd + " command"); 117 | return exec1.call(this, fakeCmd); 118 | } 119 | return exec5.call(this, cmd, env, dir); 120 | }; 121 | 122 | exec4.implementation = function(cmdarr, env, file) { 123 | for (var i = 0; i < cmdarr.length; i = i + 1) { 124 | var tmp_cmd = cmdarr[i]; 125 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 126 | var fakeCmd = "grep"; 127 | send("Bypass " + cmdarr + " command"); 128 | return exec1.call(this, fakeCmd); 129 | } 130 | 131 | if (tmp_cmd == "su") { 132 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 133 | send("Bypass " + cmdarr + " command"); 134 | return exec1.call(this, fakeCmd); 135 | } 136 | } 137 | return exec4.call(this, cmdarr, env, file); 138 | }; 139 | 140 | exec3.implementation = function(cmdarr, envp) { 141 | for (var i = 0; i < cmdarr.length; i = i + 1) { 142 | var tmp_cmd = cmdarr[i]; 143 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 144 | var fakeCmd = "grep"; 145 | send("Bypass " + cmdarr + " command"); 146 | return exec1.call(this, fakeCmd); 147 | } 148 | 149 | if (tmp_cmd == "su") { 150 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 151 | send("Bypass " + cmdarr + " command"); 152 | return exec1.call(this, fakeCmd); 153 | } 154 | } 155 | return exec3.call(this, cmdarr, envp); 156 | }; 157 | 158 | exec2.implementation = function(cmd, env) { 159 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 160 | var fakeCmd = "grep"; 161 | send("Bypass " + cmd + " command"); 162 | return exec1.call(this, fakeCmd); 163 | } 164 | if (cmd == "su") { 165 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 166 | send("Bypass " + cmd + " command"); 167 | return exec1.call(this, fakeCmd); 168 | } 169 | return exec2.call(this, cmd, env); 170 | }; 171 | 172 | exec.implementation = function(cmd) { 173 | for (var i = 0; i < cmd.length; i = i + 1) { 174 | var tmp_cmd = cmd[i]; 175 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") { 176 | var fakeCmd = "grep"; 177 | send("Bypass " + cmd + " command"); 178 | return exec1.call(this, fakeCmd); 179 | } 180 | 181 | if (tmp_cmd == "su") { 182 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 183 | send("Bypass " + cmd + " command"); 184 | return exec1.call(this, fakeCmd); 185 | } 186 | } 187 | 188 | return exec.call(this, cmd); 189 | }; 190 | 191 | exec1.implementation = function(cmd) { 192 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") { 193 | var fakeCmd = "grep"; 194 | send("Bypass " + cmd + " command"); 195 | return exec1.call(this, fakeCmd); 196 | } 197 | if (cmd == "su") { 198 | var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; 199 | send("Bypass " + cmd + " command"); 200 | return exec1.call(this, fakeCmd); 201 | } 202 | return exec1.call(this, cmd); 203 | }; 204 | 205 | String.contains.implementation = function(name) { 206 | if (name == "test-keys") { 207 | send("Bypass test-keys check"); 208 | return false; 209 | } 210 | return this.contains.call(this, name); 211 | }; 212 | 213 | var get = SystemProperties.get.overload('java.lang.String'); 214 | 215 | get.implementation = function(name) { 216 | if (RootPropertiesKeys.indexOf(name) != -1) { 217 | send("Bypass " + name); 218 | return RootProperties[name]; 219 | } 220 | return this.get.call(this, name); 221 | }; 222 | 223 | Interceptor.attach(Module.findExportByName("libc.so", "fopen"), { 224 | onEnter: function(args) { 225 | var path = Memory.readCString(args[0]); 226 | path = path.split("/"); 227 | var executable = path[path.length - 1]; 228 | var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1) 229 | if (shouldFakeReturn) { 230 | Memory.writeUtf8String(args[0], "/notexists"); 231 | send("Bypass native fopen"); 232 | } 233 | }, 234 | onLeave: function(retval) { 235 | 236 | } 237 | }); 238 | 239 | Interceptor.attach(Module.findExportByName("libc.so", "system"), { 240 | onEnter: function(args) { 241 | var cmd = Memory.readCString(args[0]); 242 | send("SYSTEM CMD: " + cmd); 243 | if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") { 244 | send("Bypass native system: " + cmd); 245 | Memory.writeUtf8String(args[0], "grep"); 246 | } 247 | if (cmd == "su") { 248 | send("Bypass native system: " + cmd); 249 | Memory.writeUtf8String(args[0], "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"); 250 | } 251 | }, 252 | onLeave: function(retval) { 253 | 254 | } 255 | }); 256 | 257 | /* 258 | 259 | TO IMPLEMENT: 260 | 261 | Exec Family 262 | 263 | int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0); 264 | int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); 265 | int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0); 266 | int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); 267 | int execv(const char *path, char *const argv[]); 268 | int execve(const char *path, char *const argv[], char *const envp[]); 269 | int execvp(const char *file, char *const argv[]); 270 | int execvpe(const char *file, char *const argv[], char *const envp[]); 271 | 272 | */ 273 | 274 | 275 | BufferedReader.readLine.overload('boolean').implementation = function() { 276 | var text = this.readLine.overload('boolean').call(this); 277 | if (text === null) { 278 | // just pass , i know it's ugly as hell but test != null won't work :( 279 | } else { 280 | var shouldFakeRead = (text.indexOf("ro.build.tags=test-keys") > -1); 281 | if (shouldFakeRead) { 282 | send("Bypass build.prop file read"); 283 | text = text.replace("ro.build.tags=test-keys", "ro.build.tags=release-keys"); 284 | } 285 | } 286 | return text; 287 | }; 288 | 289 | var executeCommand = ProcessBuilder.command.overload('java.util.List'); 290 | 291 | ProcessBuilder.start.implementation = function() { 292 | var cmd = this.command.call(this); 293 | var shouldModifyCommand = false; 294 | for (var i = 0; i < cmd.size(); i = i + 1) { 295 | var tmp_cmd = cmd.get(i).toString(); 296 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd.indexOf("mount") != -1 || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd.indexOf("id") != -1) { 297 | shouldModifyCommand = true; 298 | } 299 | } 300 | if (shouldModifyCommand) { 301 | send("Bypass ProcessBuilder " + cmd); 302 | this.command.call(this, ["grep"]); 303 | return this.start.call(this); 304 | } 305 | if (cmd.indexOf("su") != -1) { 306 | send("Bypass ProcessBuilder " + cmd); 307 | this.command.call(this, ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]); 308 | return this.start.call(this); 309 | } 310 | 311 | return this.start.call(this); 312 | }; 313 | 314 | if (useProcessManager) { 315 | var ProcManExec = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File', 'boolean'); 316 | var ProcManExecVariant = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.lang.String', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'boolean'); 317 | 318 | ProcManExec.implementation = function(cmd, env, workdir, redirectstderr) { 319 | var fake_cmd = cmd; 320 | for (var i = 0; i < cmd.length; i = i + 1) { 321 | var tmp_cmd = cmd[i]; 322 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") { 323 | var fake_cmd = ["grep"]; 324 | send("Bypass " + cmdarr + " command"); 325 | } 326 | 327 | if (tmp_cmd == "su") { 328 | var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]; 329 | send("Bypass " + cmdarr + " command"); 330 | } 331 | } 332 | return ProcManExec.call(this, fake_cmd, env, workdir, redirectstderr); 333 | }; 334 | 335 | ProcManExecVariant.implementation = function(cmd, env, directory, stdin, stdout, stderr, redirect) { 336 | var fake_cmd = cmd; 337 | for (var i = 0; i < cmd.length; i = i + 1) { 338 | var tmp_cmd = cmd[i]; 339 | if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") { 340 | var fake_cmd = ["grep"]; 341 | send("Bypass " + cmdarr + " command"); 342 | } 343 | 344 | if (tmp_cmd == "su") { 345 | var fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]; 346 | send("Bypass " + cmdarr + " command"); 347 | } 348 | } 349 | return ProcManExecVariant.call(this, fake_cmd, env, directory, stdin, stdout, stderr, redirect); 350 | }; 351 | } 352 | 353 | if (useKeyInfo) { 354 | KeyInfo.isInsideSecureHardware.implementation = function() { 355 | send("Bypass isInsideSecureHardware"); 356 | return true; 357 | } 358 | } 359 | 360 | }); 361 | -------------------------------------------------------------------------------- /android/android-disable-root-detection.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * This script defines a large set of root detection bypasses for Android. Hooks included here 4 | * block detection of many known root indicators, including file paths, package names, commands, 5 | * notably binaries, and system properties. 6 | * 7 | * Enable DEBUG_MODE to see debug output for each bypassed check. 8 | * 9 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 10 | * SPDX-License-Identifier: AGPL-3.0-or-later 11 | * SPDX-FileCopyrightText: Tim Perry 12 | * SPDX-FileCopyrightText: Riyad Mondal 13 | * 14 | *************************************************************************************************/ 15 | 16 | (() => { 17 | let loggedRootDetectionWarning = false; 18 | function logFirstRootDetection() { 19 | if (!loggedRootDetectionWarning) { 20 | console.log(" => Blocked possible root detection checks. Enable DEBUG_MODE for more details."); 21 | loggedRootDetectionWarning = true; 22 | } 23 | } 24 | 25 | const LIB_C = Process.findModuleByName("libc.so"); 26 | 27 | const BUILD_FINGERPRINT_REGEX = /^([\w.-]+\/[\w.-]+\/[\w.-]+):([\w.]+\/[\w.-]+\/[\w.-]+):(\w+\/[\w,.-]+)$/; 28 | 29 | const CONFIG = { 30 | secureProps: { 31 | "ro.secure": "1", 32 | "ro.debuggable": "0", 33 | "ro.build.type": "user", 34 | "ro.build.tags": "release-keys" 35 | } 36 | }; 37 | 38 | const ROOT_INDICATORS = { 39 | paths: new Set([ 40 | "/data/local/bin/su", 41 | "/data/local/su", 42 | "/data/local/xbin/su", 43 | "/dev/com.koushikdutta.superuser.daemon/", 44 | "/sbin/su", 45 | "/su/bin/su", 46 | "/system/bin/su", 47 | "/system/xbin/su", 48 | "/system/sbin/su", 49 | "/vendor/bin/su", 50 | "/data/adb/su/bin/su", 51 | "/system/bin/failsafe/su", 52 | "/system/bin/.ext/.su", 53 | "/system/bin/.ext/su", 54 | "/system/bin/failsafe/su", 55 | "/system/sd/xbin/su", 56 | "/system/usr/we-need-root/su", 57 | "/cache/su", 58 | "/data/su", 59 | "/dev/su", 60 | "/data/adb/magisk", 61 | "/sbin/.magisk", 62 | "/cache/.disable_magisk", 63 | "/dev/.magisk.unblock", 64 | "/cache/magisk.log", 65 | "/data/adb/magisk.img", 66 | "/data/adb/magisk.db", 67 | "/data/adb/magisk_simple", 68 | "/init.magisk.rc", 69 | "/system/app/Superuser.apk", 70 | "/system/etc/init.d/99SuperSUDaemon", 71 | "/system/xbin/daemonsu", 72 | "/system/xbin/ku.sud", 73 | "/data/adb/ksu", 74 | "/data/adb/ksud", 75 | "/system/xbin/busybox", 76 | "/system/app/Kinguser.apk" 77 | ]), 78 | 79 | packages: new Set([ 80 | "com.noshufou.android.su", 81 | "com.noshufou.android.su.elite", 82 | "eu.chainfire.supersu", 83 | "com.koushikdutta.superuser", 84 | "com.thirdparty.superuser", 85 | "com.yellowes.su", 86 | "com.koushikdutta.rommanager", 87 | "com.koushikdutta.rommanager.license", 88 | "com.dimonvideo.luckypatcher", 89 | "com.chelpus.lackypatch", 90 | "com.ramdroid.appquarantine", 91 | "com.ramdroid.appquarantinepro", 92 | "com.topjohnwu.magisk", 93 | "me.weishu.kernelsu" 94 | ]), 95 | 96 | commands: new Set([ 97 | "su", 98 | "which su", 99 | "whereis su", 100 | "locate su", 101 | "find / -name su", 102 | "mount", 103 | "magisk", 104 | "/system/bin/su", 105 | "/system/xbin/su", 106 | "/sbin/su", 107 | "/su/bin/su" 108 | ]), 109 | 110 | binaries: new Set([ 111 | "su", 112 | "busybox", 113 | "magisk", 114 | "supersu", 115 | "ksud", 116 | "daemonsu" 117 | ]) 118 | }; 119 | 120 | function bypassNativeFileCheck() { 121 | const fopen = LIB_C.findExportByName("fopen"); 122 | if (fopen) { 123 | Interceptor.attach(fopen, { 124 | onEnter(args) { 125 | this.path = args[0].readUtf8String(); 126 | }, 127 | onLeave(retval) { 128 | if (retval.toInt32() !== 0) { 129 | const path = this.path.toLowerCase(); 130 | if (ROOT_INDICATORS.paths.has(this.path) || path.includes("magisk") || path.includes("/su") || path.endsWith("/su")) { 131 | if (DEBUG_MODE) { 132 | console.log(`Blocked possible root-detection: fopen ${this.path}`); 133 | } else logFirstRootDetection(); 134 | retval.replace(ptr(0x0)); 135 | } 136 | } 137 | } 138 | }); 139 | } 140 | 141 | const access = LIB_C.findExportByName("access"); 142 | if (access) { 143 | Interceptor.attach(access, { 144 | onEnter(args) { 145 | this.path = args[0].readUtf8String(); 146 | }, 147 | onLeave(retval) { 148 | if (retval.toInt32() === 0) { 149 | const path = this.path.toLowerCase(); 150 | if (ROOT_INDICATORS.paths.has(this.path) || path.includes("magisk") || path.includes("/su") || path.endsWith("/su")) { 151 | if (DEBUG_MODE) { 152 | console.debug(`Blocked possible root detection: access ${this.path}`); 153 | } else logFirstRootDetection(); 154 | retval.replace(ptr(-1)); 155 | } 156 | } 157 | } 158 | }); 159 | } 160 | 161 | const stat = LIB_C.findExportByName("stat"); 162 | if (stat) { 163 | Interceptor.attach(stat, { 164 | onEnter(args) { 165 | this.path = args[0].readUtf8String(); 166 | }, 167 | onLeave(retval) { 168 | const path = this.path.toLowerCase(); 169 | if (ROOT_INDICATORS.paths.has(this.path) || path.includes("magisk") || path.includes("/su") || path.endsWith("/su")) { 170 | if (DEBUG_MODE) { 171 | console.debug(`Blocked possible root detection: stat ${this.path}`); 172 | } else logFirstRootDetection(); 173 | retval.replace(ptr(-1)); 174 | } 175 | } 176 | }); 177 | } 178 | 179 | const lstat = LIB_C.findExportByName("lstat"); 180 | if (lstat) { 181 | Interceptor.attach(lstat, { 182 | onEnter(args) { 183 | this.path = args[0].readUtf8String(); 184 | }, 185 | onLeave(retval) { 186 | const path = this.path.toLowerCase(); 187 | if (ROOT_INDICATORS.paths.has(this.path) || path.includes("magisk") || path.includes("/su") || path.endsWith("/su")) { 188 | if (DEBUG_MODE) { 189 | console.debug(`Blocked possible root detection: lstat ${this.path}`); 190 | } else logFirstRootDetection(); 191 | retval.replace(ptr(-1)); 192 | } 193 | } 194 | }); 195 | } 196 | } 197 | 198 | function bypassJavaFileCheck() { 199 | function isRootIndicatorFile(file) { 200 | const path = file.getAbsolutePath(); 201 | const filename = file.getName(); 202 | return ROOT_INDICATORS.paths.has(path) || 203 | path.includes("magisk") || 204 | filename === "su"; 205 | } 206 | 207 | const UnixFileSystem = Java.use("java.io.UnixFileSystem"); 208 | UnixFileSystem.checkAccess.implementation = function(file, access) { 209 | if (isRootIndicatorFile(file)) { 210 | if (DEBUG_MODE) { 211 | console.debug(`Blocked possible root detection: filesystem access check for ${file.getAbsolutePath()}`); 212 | } else logFirstRootDetection(); 213 | return false; 214 | } 215 | return this.checkAccess(file, access); 216 | }; 217 | 218 | const File = Java.use("java.io.File"); 219 | File.exists.implementation = function() { 220 | if (isRootIndicatorFile(this)) { 221 | if (DEBUG_MODE) { 222 | console.debug(`Blocked possible root detection: file exists check for ${this.getAbsolutePath()}`); 223 | } else logFirstRootDetection(); 224 | return false; 225 | } 226 | return this.exists(); 227 | }; 228 | 229 | File.length.implementation = function() { 230 | if (isRootIndicatorFile(this)) { 231 | if (DEBUG_MODE) { 232 | console.debug(`Blocked possible root detection: file length check for ${this.getAbsolutePath()}`); 233 | } else logFirstRootDetection(); 234 | return 0; 235 | } 236 | return this.length(); 237 | }; 238 | 239 | const FileInputStream = Java.use("java.io.FileInputStream"); 240 | FileInputStream.$init.overload('java.io.File').implementation = function(file) { 241 | if (isRootIndicatorFile(file)) { 242 | if (DEBUG_MODE) { 243 | console.debug(`Blocked possible root detection: file stream for ${file.getAbsolutePath()}`); 244 | } else logFirstRootDetection(); 245 | throw Java.use("java.io.FileNotFoundException").$new(file.getAbsolutePath()); 246 | } 247 | return this.$init(file); 248 | }; 249 | } 250 | 251 | function setProp() { 252 | const Build = Java.use("android.os.Build"); 253 | 254 | // We do a little work to make the minimum changes required to hide in the BUILD fingerprint, 255 | // but otherwise keep matching the real device wherever possible. 256 | const realFingerprint = Build.FINGERPRINT.value; 257 | 258 | const fingerprintMatch = BUILD_FINGERPRINT_REGEX.exec(realFingerprint); 259 | let fixedFingerprint; 260 | if (fingerprintMatch) { 261 | let [, device, versions, tags] = BUILD_FINGERPRINT_REGEX.exec(realFingerprint); 262 | tags = 'user/release-keys'; // Should always be the case in production builds 263 | if (device.includes('generic') || device.includes('sdk') || device.includes('lineage')) { 264 | device = 'google/raven/raven'; 265 | } 266 | 267 | fixedFingerprint = `${device}:${versions}:${tags}`; 268 | } else { 269 | console.warn(`Unexpected BUILD fingerprint format: ${realFingerprint}`); 270 | // This should never happen in theory (the format is standard), but just in case, 271 | // we use this fallback fingerprint: 272 | fixedFingerprint = "google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys"; 273 | } 274 | 275 | const fields = { 276 | "TAGS": "release-keys", 277 | "TYPE": "user", 278 | "FINGERPRINT": fixedFingerprint 279 | }; 280 | 281 | Object.entries(fields).forEach(([field, value]) => { 282 | const fieldObj = Build.class.getDeclaredField(field); 283 | fieldObj.setAccessible(true); 284 | fieldObj.set(null, value); 285 | }); 286 | 287 | const system_property_get = LIB_C.findExportByName("__system_property_get"); 288 | if (system_property_get) { 289 | Interceptor.attach(system_property_get, { 290 | onEnter(args) { 291 | this.key = args[0].readCString(); 292 | this.ret = args[1]; 293 | }, 294 | onLeave(retval) { 295 | const secureValue = CONFIG.secureProps[this.key]; 296 | if (secureValue !== undefined) { 297 | if (DEBUG_MODE) { 298 | console.debug(`Blocked possible root detection: system_property_get ${this.key}`); 299 | } else logFirstRootDetection(); 300 | const valuePtr = Memory.allocUtf8String(secureValue); 301 | Memory.copy(this.ret, valuePtr, secureValue.length + 1); 302 | } 303 | } 304 | }); 305 | } 306 | 307 | const Runtime = Java.use('java.lang.Runtime'); 308 | Runtime.exec.overload('java.lang.String').implementation = function(cmd) { 309 | if (cmd.startsWith("getprop ")) { 310 | const prop = cmd.split(" ")[1]; 311 | if (CONFIG.secureProps[prop]) { 312 | if (DEBUG_MODE) { 313 | console.debug(`Blocked possible root detection: getprop ${prop}`); 314 | } else logFirstRootDetection(); 315 | return null; 316 | } 317 | } 318 | return this.exec(cmd); 319 | }; 320 | } 321 | 322 | function bypassRootPackageCheck() { 323 | const ApplicationPackageManager = Java.use("android.app.ApplicationPackageManager"); 324 | 325 | ApplicationPackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(str, i) { 326 | if (ROOT_INDICATORS.packages.has(str)) { 327 | if (DEBUG_MODE) { 328 | console.debug(`Blocked possible root detection: package info for ${str}`); 329 | } else logFirstRootDetection(); 330 | str = "invalid.example.nonexistent.package"; 331 | } 332 | return this.getPackageInfo(str, i); 333 | }; 334 | 335 | ApplicationPackageManager.getInstalledPackages.overload('int').implementation = function(flags) { 336 | const packages = this.getInstalledPackages(flags); 337 | const packageList = packages.toArray(); 338 | const filteredPackages = packageList.filter(pkg => !ROOT_INDICATORS.packages.has(pkg.packageName?.value)); 339 | return Java.use("java.util.ArrayList").$new(Java.use("java.util.Arrays").asList(filteredPackages)); 340 | }; 341 | } 342 | 343 | function bypassShellCommands() { 344 | const ProcessBuilder = Java.use('java.lang.ProcessBuilder'); 345 | ProcessBuilder.command.overload('java.util.List').implementation = function(commands) { 346 | const cmdArray = commands.toArray(); 347 | if (cmdArray.length > 0) { 348 | const cmd = cmdArray[0].toString(); 349 | if (ROOT_INDICATORS.commands.has(cmd) || (cmdArray.length > 1 && ROOT_INDICATORS.binaries.has(cmdArray[1].toString()))) { 350 | if (DEBUG_MODE) { 351 | console.debug(`Blocked possible root detection: ProcessBuilder with ${cmdArray.join(' ')}`); 352 | } else logFirstRootDetection(); 353 | return this.command(Java.use("java.util.Arrays").asList([""])); 354 | } 355 | } 356 | return this.command(commands); 357 | }; 358 | 359 | const Runtime = Java.use('java.lang.Runtime'); 360 | Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdArray) { 361 | if (cmdArray.length > 0) { 362 | const cmd = cmdArray[0]; 363 | if (ROOT_INDICATORS.commands.has(cmd) || (cmdArray.length > 1 && ROOT_INDICATORS.binaries.has(cmdArray[1]))) { 364 | if (DEBUG_MODE) { 365 | console.debug(`Blocked possible root detection: Runtime.exec for ${cmdArray.join(' ')}`); 366 | } else logFirstRootDetection(); 367 | return this.exec([""]); 368 | } 369 | } 370 | return this.exec(cmdArray); 371 | }; 372 | 373 | const ProcessImpl = Java.use("java.lang.ProcessImpl"); 374 | ProcessImpl.start.implementation = function(cmdArray, env, dir, redirects, redirectErrorStream) { 375 | if (cmdArray.length > 0) { 376 | const cmd = cmdArray[0].toString(); 377 | const arg = cmdArray.length > 1 ? cmdArray[1].toString() : ""; 378 | 379 | if (ROOT_INDICATORS.commands.has(cmd) || ROOT_INDICATORS.binaries.has(arg)) { 380 | if (DEBUG_MODE) { 381 | console.debug(`Blocked possible root detection: ProcessImpl.start for ${cmdArray.join(' ')}`); 382 | } else logFirstRootDetection(); 383 | return ProcessImpl.start.call(this, [Java.use("java.lang.String").$new("")], env, dir, redirects, redirectErrorStream); 384 | } 385 | } 386 | return ProcessImpl.start.call(this, cmdArray, env, dir, redirects, redirectErrorStream); 387 | }; 388 | } 389 | 390 | try { 391 | bypassNativeFileCheck(); 392 | bypassJavaFileCheck(); 393 | setProp(); 394 | bypassRootPackageCheck(); 395 | bypassShellCommands(); 396 | console.log("== Disabled Android root detection =="); 397 | } catch (error) { 398 | console.error("\n !!! Error setting up root detection bypass !!!", error); 399 | } 400 | })(); 401 | -------------------------------------------------------------------------------- /android/android-certificate-unpinning.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************************** 2 | * 3 | * This script defines a large set of targeted certificate unpinning hooks: matching specific 4 | * methods in certain classes, and transforming their behaviour to ensure that restrictions to 5 | * TLS trust are disabled. 6 | * 7 | * This does not disable TLS protections completely - each hook is designed to disable only 8 | * *additional* restrictions, and to explicitly trust the certificate provided as CERT_PEM in the 9 | * config.js configuration file, preserving normal TLS protections wherever possible, even while 10 | * allowing for controlled MitM of local traffic. 11 | * 12 | * The file consists of a few general-purpose methods, then a data structure declaratively 13 | * defining the classes & methods to match, and how to transform them, and then logic at the end 14 | * which uses this data structure, applying the transformation for each found match to the 15 | * target process. 16 | * 17 | * For more details on what was matched, and log output when each hooked method is actually used, 18 | * enable DEBUG_MODE in config.js, and watch the Frida output after running this script. 19 | * 20 | * Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/ 21 | * SPDX-License-Identifier: AGPL-3.0-or-later 22 | * SPDX-FileCopyrightText: Tim Perry 23 | * 24 | *************************************************************************************************/ 25 | 26 | function buildX509CertificateFromBytes(certBytes) { 27 | const ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream'); 28 | const CertFactory = Java.use('java.security.cert.CertificateFactory'); 29 | const certFactory = CertFactory.getInstance("X.509"); 30 | return certFactory.generateCertificate(ByteArrayInputStream.$new(certBytes)); 31 | } 32 | 33 | function getCustomTrustManagerFactory() { 34 | // This is the one X509Certificate that we want to trust. No need to trust others (we should capture 35 | // _all_ TLS traffic) and risky to trust _everything_ (risks interception between device & proxy, or 36 | // worse: some traffic being unintercepted & sent as HTTPS with TLS effectively disabled over the 37 | // real web - potentially exposing auth keys, private data and all sorts). 38 | const certBytes = Java.use("java.lang.String").$new(CERT_PEM).getBytes(); 39 | const trustedCACert = buildX509CertificateFromBytes(certBytes); 40 | 41 | // Build a custom TrustManagerFactory with a KeyStore that trusts only this certificate: 42 | 43 | const KeyStore = Java.use("java.security.KeyStore"); 44 | const keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 45 | keyStore.load(null); 46 | keyStore.setCertificateEntry("ca", trustedCACert); 47 | 48 | const TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory"); 49 | const customTrustManagerFactory = TrustManagerFactory.getInstance( 50 | TrustManagerFactory.getDefaultAlgorithm() 51 | ); 52 | customTrustManagerFactory.init(keyStore); 53 | 54 | return customTrustManagerFactory; 55 | } 56 | 57 | function getCustomX509TrustManager() { 58 | const customTrustManagerFactory = getCustomTrustManagerFactory(); 59 | const trustManagers = customTrustManagerFactory.getTrustManagers(); 60 | 61 | const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager'); 62 | 63 | const x509TrustManager = trustManagers.find((trustManager) => { 64 | return trustManager.class.isAssignableFrom(X509TrustManager.class); 65 | }); 66 | 67 | // We have to cast it explicitly before Frida will allow us to use the X509 methods: 68 | return Java.cast(x509TrustManager, X509TrustManager); 69 | } 70 | 71 | // Some standard hook replacements for various cases: 72 | const NO_OP = () => {}; 73 | const RETURN_TRUE = () => true; 74 | const CHECK_OUR_TRUST_MANAGER_ONLY = () => { 75 | const trustManager = getCustomX509TrustManager(); 76 | return (certs, authType) => { 77 | trustManager.checkServerTrusted(certs, authType); 78 | }; 79 | }; 80 | 81 | const PINNING_FIXES = { 82 | // --- Native HttpsURLConnection 83 | 84 | 'javax.net.ssl.HttpsURLConnection': [ 85 | { 86 | methodName: 'setDefaultHostnameVerifier', 87 | replacement: () => NO_OP 88 | }, 89 | { 90 | methodName: 'setSSLSocketFactory', 91 | replacement: () => NO_OP 92 | }, 93 | { 94 | methodName: 'setHostnameVerifier', 95 | replacement: () => NO_OP 96 | }, 97 | ], 98 | 99 | // --- Native SSLContext 100 | 101 | 'javax.net.ssl.SSLContext': [ 102 | { 103 | methodName: 'init', 104 | overload: ['[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'], 105 | replacement: (targetMethod) => { 106 | const customTrustManagerFactory = getCustomTrustManagerFactory(); 107 | 108 | // When constructor is called, replace the trust managers argument: 109 | return function (keyManager, _providedTrustManagers, secureRandom) { 110 | return targetMethod.call(this, 111 | keyManager, 112 | customTrustManagerFactory.getTrustManagers(), // Override their trust managers 113 | secureRandom 114 | ); 115 | } 116 | } 117 | } 118 | ], 119 | 120 | // --- Native Conscrypt CertPinManager 121 | 122 | 'com.android.org.conscrypt.CertPinManager': [ 123 | { 124 | methodName: 'isChainValid', 125 | replacement: () => RETURN_TRUE 126 | }, 127 | { 128 | methodName: 'checkChainPinning', 129 | replacement: () => NO_OP 130 | } 131 | ], 132 | 133 | // --- Native pinning configuration loading (used for configuration by many libraries) 134 | 135 | 'android.security.net.config.NetworkSecurityConfig': [ 136 | { 137 | methodName: '$init', 138 | overload: '*', 139 | replacement: (targetMethod) => { 140 | const PinSet = Java.use('android.security.net.config.PinSet'); 141 | const EMPTY_PINSET = PinSet.EMPTY_PINSET.value; 142 | return function () { 143 | // Always ignore the 2nd 'pins' PinSet argument entirely: 144 | arguments[2] = EMPTY_PINSET; 145 | targetMethod.call(this, ...arguments); 146 | } 147 | } 148 | } 149 | ], 150 | 151 | // --- Native HostnameVerification override (n.b. Android contains its own vendored OkHttp v2!) 152 | 153 | 'com.android.okhttp.internal.tls.OkHostnameVerifier': [ 154 | { 155 | methodName: 'verify', 156 | overload: [ 157 | 'java.lang.String', 158 | 'javax.net.ssl.SSLSession' 159 | ], 160 | replacement: (targetMethod) => { 161 | // Our trust manager - this trusts *only* our extra CA 162 | const trustManager = getCustomX509TrustManager(); 163 | 164 | return function (hostname, sslSession) { 165 | try { 166 | const certs = sslSession.getPeerCertificates(); 167 | 168 | // https://stackoverflow.com/a/70469741/68051 169 | const authType = "RSA"; 170 | 171 | // This throws if the certificate isn't trusted (i.e. if it's 172 | // not signed by our extra CA specifically): 173 | trustManager.checkServerTrusted(certs, authType); 174 | 175 | // If the cert is from our CA, great! Skip hostname checks entirely. 176 | return true; 177 | } catch (e) {} // Ignore errors and fallback to default behaviour 178 | 179 | // We fallback to ensure that connections with other CAs (e.g. direct 180 | // connections allowed past the proxy) validate as normal. 181 | return targetMethod.call(this, ...arguments); 182 | } 183 | } 184 | } 185 | ], 186 | 187 | 'com.android.okhttp.Address': [ 188 | { 189 | methodName: '$init', 190 | overload: [ 191 | 'java.lang.String', 192 | 'int', 193 | 'com.android.okhttp.Dns', 194 | 'javax.net.SocketFactory', 195 | 'javax.net.ssl.SSLSocketFactory', 196 | 'javax.net.ssl.HostnameVerifier', 197 | 'com.android.okhttp.CertificatePinner', 198 | 'com.android.okhttp.Authenticator', 199 | 'java.net.Proxy', 200 | 'java.util.List', 201 | 'java.util.List', 202 | 'java.net.ProxySelector' 203 | ], 204 | replacement: (targetMethod) => { 205 | const defaultHostnameVerifier = Java.use("com.android.okhttp.internal.tls.OkHostnameVerifier") 206 | .INSTANCE.value; 207 | const defaultCertPinner = Java.use("com.android.okhttp.CertificatePinner") 208 | .DEFAULT.value; 209 | 210 | return function () { 211 | // Override arguments, to swap any custom check params (widely used 212 | // to add stricter rules to TLS verification) with the defaults instead: 213 | arguments[5] = defaultHostnameVerifier; 214 | arguments[6] = defaultCertPinner; 215 | 216 | targetMethod.call(this, ...arguments); 217 | } 218 | } 219 | }, 220 | // Almost identical patch, but for Nougat and older. In these versions, the DNS argument 221 | // isn't passed here, so the arguments to patch changes slightly: 222 | { 223 | methodName: '$init', 224 | overload: [ 225 | 'java.lang.String', 226 | 'int', 227 | // No DNS param 228 | 'javax.net.SocketFactory', 229 | 'javax.net.ssl.SSLSocketFactory', 230 | 'javax.net.ssl.HostnameVerifier', 231 | 'com.android.okhttp.CertificatePinner', 232 | 'com.android.okhttp.Authenticator', 233 | 'java.net.Proxy', 234 | 'java.util.List', 235 | 'java.util.List', 236 | 'java.net.ProxySelector' 237 | ], 238 | replacement: (targetMethod) => { 239 | const defaultHostnameVerifier = Java.use("com.android.okhttp.internal.tls.OkHostnameVerifier") 240 | .INSTANCE.value; 241 | const defaultCertPinner = Java.use("com.android.okhttp.CertificatePinner") 242 | .DEFAULT.value; 243 | 244 | return function () { 245 | // Override arguments, to swap any custom check params (widely used 246 | // to add stricter rules to TLS verification) with the defaults instead: 247 | arguments[4] = defaultHostnameVerifier; 248 | arguments[5] = defaultCertPinner; 249 | 250 | targetMethod.call(this, ...arguments); 251 | } 252 | } 253 | } 254 | ], 255 | 256 | // --- OkHttp v3 257 | 258 | 'okhttp3.CertificatePinner': [ 259 | { 260 | methodName: 'check', 261 | overload: ['java.lang.String', 'java.util.List'], 262 | replacement: () => NO_OP 263 | }, 264 | { 265 | methodName: 'check', 266 | overload: ['java.lang.String', 'java.security.cert.Certificate'], 267 | replacement: () => NO_OP 268 | }, 269 | { 270 | methodName: 'check', 271 | overload: ['java.lang.String', '[Ljava.security.cert.Certificate;'], 272 | replacement: () => NO_OP 273 | }, 274 | { 275 | methodName: 'check$okhttp', 276 | replacement: () => NO_OP 277 | }, 278 | ], 279 | 280 | // --- SquareUp OkHttp (< v3) 281 | 282 | 'com.squareup.okhttp.CertificatePinner': [ 283 | { 284 | methodName: 'check', 285 | overload: ['java.lang.String', 'java.security.cert.Certificate'], 286 | replacement: () => NO_OP 287 | }, 288 | { 289 | methodName: 'check', 290 | overload: ['java.lang.String', 'java.util.List'], 291 | replacement: () => NO_OP 292 | } 293 | ], 294 | 295 | // --- Trustkit (https://github.com/datatheorem/TrustKit-Android/) 296 | 297 | 'com.datatheorem.android.trustkit.pinning.PinningTrustManager': [ 298 | { 299 | methodName: 'checkServerTrusted', 300 | replacement: CHECK_OUR_TRUST_MANAGER_ONLY 301 | } 302 | ], 303 | 304 | // --- Appcelerator (https://github.com/tidev/appcelerator.https) 305 | 306 | 'appcelerator.https.PinningTrustManager': [ 307 | { 308 | methodName: 'checkServerTrusted', 309 | replacement: CHECK_OUR_TRUST_MANAGER_ONLY 310 | } 311 | ], 312 | 313 | // --- PhoneGap sslCertificateChecker (https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin) 314 | 315 | 'nl.xservices.plugins.sslCertificateChecker': [ 316 | { 317 | methodName: 'execute', 318 | overload: ['java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext'], 319 | replacement: () => (_action, _args, context) => { 320 | context.success("CONNECTION_SECURE"); 321 | return true; 322 | } 323 | // This trusts _all_ certs, but that's fine - this is used for checks of independent test 324 | // connections, rather than being a primary mechanism to secure the app's TLS connections. 325 | } 326 | ], 327 | 328 | // --- IBM WorkLight 329 | 330 | 'com.worklight.wlclient.api.WLClient': [ 331 | { 332 | methodName: 'pinTrustedCertificatePublicKey', 333 | getMethod: (WLClientCls) => WLClientCls.getInstance().pinTrustedCertificatePublicKey, 334 | overload: '*' 335 | } 336 | ], 337 | 338 | 'com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning': [ 339 | { 340 | methodName: 'verify', 341 | overload: '*', 342 | replacement: () => NO_OP 343 | } 344 | // This covers at least 4 commonly used WorkLight patches. Oddly, most sets of hooks seem 345 | // to return true for 1/4 cases, which must be wrong (overloads must all have the same 346 | // return type) but also it's very hard to find any modern (since 2017) references to this 347 | // class anywhere including WorkLight docs, so it may no longer be relevant anyway. 348 | ], 349 | 350 | 'com.worklight.androidgap.plugin.WLCertificatePinningPlugin': [ 351 | { 352 | methodName: 'execute', 353 | overload: '*', 354 | replacement: () => RETURN_TRUE 355 | } 356 | ], 357 | 358 | // --- CWAC-Netsecurity (unofficial back-port pinner for Android<4.2) CertPinManager 359 | 360 | 'com.commonsware.cwac.netsecurity.conscrypt.CertPinManager': [ 361 | { 362 | methodName: 'isChainValid', 363 | overload: '*', 364 | replacement: () => RETURN_TRUE 365 | } 366 | ], 367 | 368 | // --- Netty 369 | 370 | 'io.netty.handler.ssl.util.FingerprintTrustManagerFactory': [ 371 | { 372 | methodName: 'checkTrusted', 373 | replacement: () => NO_OP 374 | } 375 | ], 376 | 377 | // --- Cordova / PhoneGap Advanced HTTP Plugin (https://github.com/silkimen/cordova-plugin-advanced-http) 378 | 379 | // Modern version: 380 | 'com.silkimen.cordovahttp.CordovaServerTrust': [ 381 | { 382 | methodName: '$init', 383 | replacement: (targetMethod) => function () { 384 | // Ignore any attempts to set trust to 'pinned'. Default settings will trust 385 | // our cert because of the separate system-certificate injection step. 386 | if (arguments[0] === 'pinned') { 387 | arguments[0] = 'default'; 388 | } 389 | 390 | return targetMethod.call(this, ...arguments); 391 | } 392 | } 393 | ], 394 | 395 | // --- Appmattus Cert Transparency (https://github.com/appmattus/certificatetransparency/) 396 | 397 | 'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyHostnameVerifier': [ 398 | { 399 | methodName: 'verify', 400 | replacement: () => RETURN_TRUE 401 | // This is not called unless the cert passes basic trust checks, so it's safe to blindly accept. 402 | } 403 | ], 404 | 405 | 'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyInterceptor': [ 406 | { 407 | methodName: 'intercept', 408 | replacement: () => (a) => a.proceed(a.request()) 409 | // This is not called unless the cert passes basic trust checks, so it's safe to blindly accept. 410 | } 411 | ], 412 | 413 | 'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager': [ 414 | { 415 | methodName: 'checkServerTrusted', 416 | overload: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'], 417 | replacement: CHECK_OUR_TRUST_MANAGER_ONLY, 418 | methodName: 'checkServerTrusted', 419 | overload: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'], 420 | replacement: () => { 421 | const trustManager = getCustomX509TrustManager(); 422 | return (certs, authType, _hostname) => { 423 | // We ignore the hostname - if the certs are good (i.e they're ours), then the 424 | // whole chain is good to go. 425 | trustManager.checkServerTrusted(certs, authType); 426 | return Java.use('java.util.Arrays').asList(certs); 427 | }; 428 | } 429 | } 430 | ] 431 | 432 | }; 433 | 434 | const getJavaClassIfExists = (clsName) => { 435 | try { 436 | return Java.use(clsName); 437 | } catch { 438 | return undefined; 439 | } 440 | } 441 | 442 | Java.perform(function () { 443 | if (DEBUG_MODE) console.log('\n === Disabling all recognized unpinning libraries ==='); 444 | 445 | const classesToPatch = Object.keys(PINNING_FIXES); 446 | 447 | classesToPatch.forEach((targetClassName) => { 448 | const TargetClass = getJavaClassIfExists(targetClassName); 449 | if (!TargetClass) { 450 | // We skip patches for any classes that don't seem to be present. This is common 451 | // as not all libraries we handle are necessarily used. 452 | if (DEBUG_MODE) console.log(`[ ] ${targetClassName} *`); 453 | return; 454 | } 455 | 456 | const patches = PINNING_FIXES[targetClassName]; 457 | 458 | let patchApplied = false; 459 | 460 | patches.forEach(({ methodName, getMethod, overload, replacement }) => { 461 | const namedTargetMethod = getMethod 462 | ? getMethod(TargetClass) 463 | : TargetClass[methodName]; 464 | 465 | const methodDescription = `${methodName}${ 466 | overload === '*' 467 | ? '(*)' 468 | : overload 469 | ? '(' + overload.map((argType) => { 470 | // Simplify arg names to just the class name for simpler logs: 471 | const argClassName = argType.split('.').slice(-1)[0]; 472 | if (argType.startsWith('[L')) return `${argClassName}[]`; 473 | else return argClassName; 474 | }).join(', ') + ')' 475 | // No overload: 476 | : '' 477 | }` 478 | 479 | let targetMethodImplementations = []; 480 | try { 481 | if (namedTargetMethod) { 482 | if (!overload) { 483 | // No overload specified 484 | targetMethodImplementations = [namedTargetMethod]; 485 | } else if (overload === '*') { 486 | // Targetting _all_ overloads 487 | targetMethodImplementations = namedTargetMethod.overloads; 488 | } else { 489 | // Or targetting a specific overload: 490 | targetMethodImplementations = [namedTargetMethod.overload(...overload)]; 491 | } 492 | } 493 | } catch (e) { 494 | // Overload not present 495 | } 496 | 497 | 498 | // We skip patches for any methods that don't seem to be present. This is rarer, but does 499 | // happen due to methods that only appear in certain library versions or whose signatures 500 | // have changed over time. 501 | if (targetMethodImplementations.length === 0) { 502 | if (DEBUG_MODE) console.log(`[ ] ${targetClassName} ${methodDescription}`); 503 | return; 504 | } 505 | 506 | targetMethodImplementations.forEach((targetMethod, i) => { 507 | const patchName = `${targetClassName} ${methodDescription}${ 508 | targetMethodImplementations.length > 1 ? ` (${i})` : '' 509 | }`; 510 | 511 | try { 512 | const newImplementation = replacement(targetMethod); 513 | if (DEBUG_MODE) { 514 | // Log each hooked method as it's called: 515 | targetMethod.implementation = function () { 516 | console.log(` => ${patchName}`); 517 | return newImplementation.apply(this, arguments); 518 | } 519 | } else { 520 | targetMethod.implementation = newImplementation; 521 | } 522 | 523 | if (DEBUG_MODE) console.log(`[+] ${patchName}`); 524 | patchApplied = true; 525 | } catch (e) { 526 | // In theory, errors like this should never happen - it means the patch is broken 527 | // (e.g. some dynamic patch building fails completely) 528 | console.error(`[!] ERROR: ${patchName} failed: ${e}`); 529 | } 530 | }) 531 | }); 532 | 533 | if (!patchApplied) { 534 | console.warn(`[!] Matched class ${targetClassName} but could not patch any methods`); 535 | } 536 | }); 537 | 538 | console.log('== Certificate unpinning completed =='); 539 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------