Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions overrides/frida/android/android-certificate-unpinning-fallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,30 @@ Java.perform(function () {
callingClass.class.getName()
}`);
}
} else if (
className === 'com.android.org.conscrypt.TrustManagerImpl' &&
(methodName === 'verifyChain' || methodName === 'checkTrustedRecursive')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is for handling cases where we can't see the class or method names at all due to obfuscation. When we do have them like this, we should just put the hook into the normal hooks list in android-certificate-unpinning.js instead.

Do you know why the existing TrustedCertificateIndex patch isn't handling this case? It would be very useful to look at the Conscrypt code and see what's changed there, as it might be possible to just fix that to handle this instead.

) {
const returnType = failingMethod.returnType.className;
const argTypes = failingMethod.argumentTypes.map(t => t.className);
const x509Idx = argTypes.findIndex(t => t === '[Ljava.security.cert.X509Certificate;');

if (x509Idx >= 0 && returnType === 'java.util.List') {
failingMethod.implementation = function () {
try {
const x509Chain = arguments[x509Idx];
const first = x509Chain && x509Chain[0];
const alg = first?.getPublicKey?.()?.getAlgorithm?.() || 'RSA';
defaultTrustManager.checkServerTrusted(x509Chain, alg);
return Java.use('java.util.Arrays').asList(x509Chain);
} catch (e) {
return failingMethod.call(this, ...arguments);
}
};
console.log(` [+] ${className}->${methodName} (fallback Conscrypt trust patch)`);
} else {
console.error(` [ ] ${className}->${methodName} signature mismatch - cannot auto-patch`);
}
} else if (isMetaPinningMethod(errorMessage, failingMethod)) {
failingMethod.implementation = function (certs) {
if (DEBUG_MODE) console.log(` => Fallback patch for meta proxygen pinning`);
Expand Down
17 changes: 17 additions & 0 deletions overrides/frida/android/android-certificate-unpinning.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ const PINNING_FIXES = {
}
],

// --- Native Conscrypt Certificate Transparency (Android 16+ / API 36+)
// Android 16 enforces CT verification for all certificates. Proxy-generated
// MITM certs lack SCTs, causing NOT_ENOUGH_SCTS failures. Since all traffic
// is routed through the local proxy, CT checks are unnecessary.

'com.android.org.conscrypt.ct.CertificateTransparency': [
{
methodName: 'isCTVerificationRequired',
replacement: () => () => false
},
{
methodName: 'checkCT',
overload: '*',
replacement: () => NO_OP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually already implemented in httptoolkit/frida-interception-and-unpinning#166 (although that hasn't been pulled into this repo yet).

The isCTVerificationRequired hook wasn't added there though. I'm open to that if there's a good reason, can you explain why that's needed?

}
],

// --- Native pinning configuration loading (used for configuration by many libraries)

'android.security.net.config.NetworkSecurityConfig': [
Expand Down
84 changes: 72 additions & 12 deletions overrides/frida/frida-java-bridge.js

Large diffs are not rendered by default.

41 changes: 36 additions & 5 deletions overrides/frida/native-tls-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ function patchTargetLib(targetModule, targetName) {
let pendingCheckThreads = new Set();

const hookedCallback = new NativeCallback(function (ssl, out_alert) {
if (!ssl || ssl.isNull()) {
return SSL_VERIFY_INVALID;
}

let realResult = false; // False = not yet called, 0/1 = call result

const threadId = Process.getCurrentThreadId();
Expand All @@ -123,7 +127,11 @@ function patchTargetLib(targetModule, targetName) {
// meanwhile seems to use some negative checks in its callback, and rejects the
// connection independently of the return value here if it's called with a bad cert.
// End result: we *only sometimes* proactively call the callback.
realResult = realCallback(ssl, out_alert);
try {
realResult = realCallback(ssl, out_alert);
} catch (e) {
realResult = SSL_VERIFY_INVALID;
}
}

// Extremely dumb certificate validation: we accept any chain where the *exact* CA cert
Expand All @@ -136,17 +144,36 @@ function patchTargetLib(targetModule, targetName) {
// or leaf certs, and lots of other issues. This is significantly better than nothing,
// but it is not production-ready TLS verification for general use in untrusted envs!

const peerCerts = SSL_get0_peer_certificates(ssl);
let peerCerts;
try {
peerCerts = SSL_get0_peer_certificates(ssl);
} catch (e) {
if (!alreadyHaveLock) pendingCheckThreads.delete(threadId);
return (realResult !== false) ? realResult : SSL_VERIFY_INVALID;
}

if (!peerCerts || peerCerts.isNull()) {
if (!alreadyHaveLock) pendingCheckThreads.delete(threadId);
return (realResult !== false) ? realResult : SSL_VERIFY_INVALID;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like some of this could be better handled with a large try/finally around a larger block.

Are you adding these checks blindly to every possible call, or because you've found specific cases where these can fail? It would be useful to know what those cases are if so.

}

// Loop through every cert in the chain:
for (let i = 0; i < sk_num(peerCerts); i++) {
// For each cert, check if it *exactly* matches our configured CA cert:
const cert = sk_value(peerCerts, i);
const certDataLength = crypto_buffer_len(cert).toNumber();

let certDataLength;
try {
certDataLength = crypto_buffer_len(cert).toNumber();
} catch (e) { continue; }

if (certDataLength !== CERT_DER.byteLength) continue;

const certPointer = crypto_buffer_data(cert);
let certPointer;
try {
certPointer = crypto_buffer_data(cert);
} catch (e) { continue; }

const certData = new Uint8Array(certPointer.readByteArray(certDataLength));

if (certData.every((byte, j) => CERT_DER[j] === byte)) {
Expand All @@ -157,7 +184,11 @@ function patchTargetLib(targetModule, targetName) {

// No matched peer - fallback to the provided callback instead:
if (realResult === false) { // Haven't called it yet
realResult = realCallback(ssl, out_alert);
try {
realResult = realCallback(ssl, out_alert);
} catch (e) {
realResult = SSL_VERIFY_INVALID;
}
}

if (!alreadyHaveLock) pendingCheckThreads.delete(threadId);
Expand Down
10 changes: 5 additions & 5 deletions src/interceptors/frida/frida-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface FridaTarget {
name: string;
}

export const FRIDA_VERSION = '17.5.1';
export const FRIDA_VERSION = '17.7.3';

export const FRIDA_DEFAULT_PORT = 27042;
export const FRIDA_ALTERNATE_PORT = 24072; // Reversed to mildly inconvenience detection
Expand All @@ -41,10 +41,10 @@ export const FRIDA_BINARY_NAME = `adirf-server-${FRIDA_VERSION}`; // Reversed to
// To update this, run `await require('frida-js').calculateFridaSRI({ version, arch, platform })`
export const FRIDA_SRIS = {
'android': {
'arm': 'sha512-MEfmAabQzrZFRmgC+TcAht7vSa+43X5nWwOU0vZf8E1xFD1IO5mHHapCmxjeOTTa/M3EVmtuH+bfAFaR+ZAPBg==',
'arm64': 'sha512-x914zjMkQJMlfaQ5+DtO4cG8Q9+Jx/geudjAI/SmUYAP6nycNq0oY5fpOzIwrUkoy0WNy9nE42FvuXgcF61DUA==',
'x86': 'sha512-pdCEABZspB9pDejfU5YTHFL7V3BxDuIeyVq6eH1pZL0zb4mlNW3BwSgJ2Ts1zu+rR1KULIp10klHFlFYo3Z61g==',
'x86_64': 'sha512-ZTMGMZGdoR/NqPZJQlbBhUBaXetzdlxnFdeBDfLNAvek7WP6F6yVOQskD2r3SGSNlUfDA6RPmbj07IV2I6lBuA=='
'arm': 'sha512-9f5maGChsAeiFQHJLuBnACbZGiAcjNWZ+ioMU9oLAeVyNBv4e0s7t1T0CzNEU4R3ptZkY9Uqq2b5CQjdweJaDQ==',
'arm64': 'sha512-COC9NhKDOgCwY5qa6lToAHDPStKSZbIbP7FIZS9AoTmxlLqGaGTNgfuE40RgDIVqhPBaw0QIb4zDDLQYVLGtlw==',
'x86': 'sha512-2l/1ZZmmTbDMRKoGfh/3VwER6/jZkvttXr5YTfK85d4dLgxIBztB341bxqvhqpEkldAUuf/wNtZaHViWJ+EQjA==',
'x86_64': 'sha512-G+L/LiYl2B23TVVPjiLgj3BkkSq+VCGLxX07F4QYfGHYcD+bDlHJHgGi7GRFZ0JHd8uwd5rOKhaJHOBAo1X1YQ=='
}
} as const;

Expand Down