tweaselORG / meta

(Currently) only used for the issue tracker.
2 stars 0 forks source link

Write a certificate pinning bypass for O2 app (Appmattus `CertificateTransparencyTrustManager.checkServerTrusted`) #31

Closed baltpeter closed 1 year ago

baltpeter commented 1 year ago

In https://github.com/tweaselORG/meta/issues/16, I noticed that none of our existing certificate pinning bypass scripts can deal with Usercentrics in canvasm.myo2. That's seems like a good place to try and write my first own bypass script.

I'll follow https://httptoolkit.com/blog/android-reverse-engineering/.

baltpeter commented 1 year ago

These are the errors we observed:

[
    {
        "status": "tlsFailed",
        "context": {
            "clientAddress": ["10.0.0.1", 44688],
            "serverAddress": ["116.203.1.20", 443],
            "error": "The client does not trust the proxy's certificate for config.eu.usercentrics.eu (OpenSSL Error([('SSL routines', '', 'sslv3 alert certificate unknown')]))"
        }
    },
    {
        "status": "tlsFailed",
        "context": {
            "clientAddress": ["10.0.0.1", 44704],
            "serverAddress": ["116.203.1.20", 443],
            "error": "The client does not trust the proxy's certificate for app.eu.usercentrics.eu (OpenSSL Error([('SSL routines', '', 'sslv3 alert certificate unknown')]))"
        }
    }
]
baltpeter commented 1 year ago

Here's what I did so far:

Then I had a look at logcat. This looks promising:

06-23 13:44:06.861 18501 18501 I System.out: [USERCENTRICS][ERROR] Usercentrics initialization failed:
06-23 13:44:06.861 18501 18501 I System.out:  - Something went wrong while fetching the available languages.
06-23 13:44:06.861 18501 18501 I System.out:  - Failed to read from cache, key: languages | cause: wk.e: Usercentrics initialization failed:
06-23 13:44:06.861 18501 18501 I System.out:  - Something went wrong while fetching the available languages.
06-23 13:44:06.861 18501 18501 I System.out:  - Failed to read from cache, key: languages
06-23 13:44:06.861 18501 18501 I System.out:    at pk.t$b.invoke(SourceFile:2)
06-23 13:44:06.862 18501 18501 I System.out:    at pk.t$b.invoke(SourceFile:1)
06-23 13:44:06.862 18501 18501 I System.out:    at fn.a$b.invokeSuspend(SourceFile:31)
06-23 13:44:06.862 18501 18501 I System.out:    at fn.a$b.invoke(Unknown Source:8)
06-23 13:44:06.862 18501 18501 I System.out:    at fn.a$b.invoke(Unknown Source:4)
06-23 13:44:06.862 18501 18501 I System.out:    at xm.c$b.invokeSuspend(Unknown Source:32)
06-23 13:44:06.862 18501 18501 I System.out:    at xo.a.resumeWith(SourceFile:11)
06-23 13:44:06.862 18501 18501 I System.out:    at np.u0.run(SourceFile:116)
06-23 13:44:06.862 18501 18501 I System.out:    at android.os.Handler.handleCallback(Handler.java:942)
06-23 13:44:06.862 18501 18501 I System.out:    at android.os.Handler.dispatchMessage(Handler.java:99)
06-23 13:44:06.862 18501 18501 I System.out:    at android.os.Looper.loopOnce(Looper.java:201)
06-23 13:44:06.862 18501 18501 I System.out:    at android.os.Looper.loop(Looper.java:288)
06-23 13:44:06.862 18501 18501 I System.out:    at android.app.ActivityThread.main(ActivityThread.java:7884)
06-23 13:44:06.862 18501 18501 I System.out:    at java.lang.reflect.Method.invoke(Native Method)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
06-23 13:44:06.862 18501 18501 I System.out: Caused by: wk.j: Something went wrong while fetching the available languages.
06-23 13:44:06.862 18501 18501 I System.out:    at fn.a$b.invokeSuspend(SourceFile:27)
06-23 13:44:06.862 18501 18501 I System.out:    ... 13 more
06-23 13:44:06.862 18501 18501 I System.out: Caused by: javax.net.ssl.SSLHandshakeException: Certificate transparency failed
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.SSLUtils.toSSLHandshakeException(SSLUtils.java:363)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.convertException(ConscryptEngine.java:1134)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.readPlaintextData(ConscryptEngine.java:1089)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.unwrap(ConscryptEngine.java:876)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.unwrap(ConscryptEngine.java:747)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.unwrap(ConscryptEngine.java:712)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.processDataFromSocket(ConscryptEngineSocket.java:858)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.-$$Nest$mprocessDataFromSocket(Unknown Source:0)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngineSocket.doHandshake(ConscryptEngineSocket.java:241)
06-23 13:44:06.862 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngineSocket.startHandshake(ConscryptEngineSocket.java:220)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:153)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.connect(DelegatingHttpsURLConnection.java:90)
06-23 13:44:06.863 18501 18501 I System.out:    at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:30)
06-23 13:44:06.863 18501 18572 D TrafficStats: tagSocket(102) with statsTag=0xffffffff, statsUid=-1
06-23 13:44:06.863 18501 18501 I System.out:    at ll.a.b(SourceFile:19)
06-23 13:44:06.863 18501 18501 I System.out:    at vk.c.c(SourceFile:14)
06-23 13:44:06.863 18501 18501 I System.out:    at en.b.a(SourceFile:21)
06-23 13:44:06.863 18501 18501 I System.out:    at gn.b$a.invoke(SourceFile:2)
06-23 13:44:06.863 18501 18501 I System.out:    at gn.b$a.invoke(SourceFile:1)
06-23 13:44:06.863 18501 18501 I System.out:    at mn.a.n(SourceFile:18)
06-23 13:44:06.863 18501 18501 I System.out:    at gn.b.a(SourceFile:15)
06-23 13:44:06.863 18501 18501 I System.out:    at hn.b.a(SourceFile:17)
06-23 13:44:06.863 18501 18501 I System.out:    at fn.a$a.invokeSuspend(SourceFile:22)
06-23 13:44:06.863 18501 18501 I System.out:    at fn.a$a.invoke(Unknown Source:8)
06-23 13:44:06.863 18501 18501 I System.out:    at fn.a$a.invoke(Unknown Source:4)
06-23 13:44:06.863 18501 18501 I System.out:    at xm.b$a.invokeSuspend(SourceFile:47)
06-23 13:44:06.864 18501 18501 I System.out:    at xo.a.resumeWith(SourceFile:11)
06-23 13:44:06.864 18501 18501 I System.out:    at np.u0.run(SourceFile:116)
06-23 13:44:06.864 18501 18501 I System.out:    at rp.a.J(SourceFile:0)
06-23 13:44:06.864 18501 18501 I System.out:    at rp.a$c.d(SourceFile:14)
06-23 13:44:06.864 18501 18501 I System.out:    at rp.a$c.n(SourceFile:28)
06-23 13:44:06.864 18501 18501 I System.out:    at rp.a$c.run(Unknown Source:0)
06-23 13:44:06.864 18501 18501 I System.out: Caused by: java.security.cert.CertificateException: Certificate transparency failed
06-23 13:44:06.864 18501 18501 I System.out:    at com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager.checkServerTrusted(SourceFile:12)
06-23 13:44:06.864 18501 18501 I System.out:    at java.lang.reflect.Method.invoke(Native Method)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.Platform.checkTrusted(Platform.java:203)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:257)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.verifyCertificateChain(ConscryptEngine.java:1638)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.NativeCrypto.ENGINE_SSL_read_direct(Native Method)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.NativeSsl.readDirectByteBuffer(NativeSsl.java:569)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.readPlaintextDataDirect(ConscryptEngine.java:1095)
06-23 13:44:06.864 18501 18501 I System.out:    at com.android.org.conscrypt.ConscryptEngine.readPlaintextData(ConscryptEngine.java:1079)
06-23 13:44:06.864 18501 18501 I System.out:    ... 37 more

Seems like Usercentrics isn't using certificate pinning but checking certificate transparency logs (which our certificate obviously doesn't appear in). Likely using https://github.com/appmattus/certificatetransparency. The HTTP Toolkit script does have a handler for that already:

https://github.com/httptoolkit/frida-android-unpinning/blob/f82daadf7d1cce1aeab4a38a591dc0a4fadbbf0d/frida-script.js#L506-L516

According to the Frida logs, that is getting installed but never called:

[…]
[+] Appmattus (Transparency)
Unpinning setup completed
---
  --> Bypassing Trustmanager (Android < 7) request
  --> Bypassing Trustmanager (Android < 7) request
  --> Bypassing TrustManagerImpl checkTrusted 
  --> Bypassing TrustManagerImpl checkTrusted
baltpeter commented 1 year ago

Looking at the stack trace, com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager.checkServerTrusted seemed like the most likely culprit.

Searching for that in jadx revealed this function:

image

Looks like that just needs to not throw an error to pass. Jadx has a very helpful "Copy as Frida snippet" feature. That produced:

let CertificateTransparencyTrustManager = Java.use(
    'com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager'
);
CertificateTransparencyTrustManager['checkServerTrusted'].overload(
    '[Ljava.security.cert.X509Certificate;',
    'java.lang.String'
).implementation = function (x509CertificateArr, str) {
    console.log(
        `CertificateTransparencyTrustManager.checkServerTrusted is called: x509CertificateArr=${x509CertificateArr}, str=${str}`
    );
    this['checkServerTrusted'](x509CertificateArr, str);
};

I replaced the last line of the function with a return to not call the original function. However, that didn't work. Running the app with the script, I didn't get any log output.

But I saw that checkServerTrusted seems to have multiple overloads (we are replacing the implementation on a .overload() call. Maybe the other overload/s is/are responsible.

I didn't easily find that in jadx but Frida did help:

[moto g 7  power::canvasm.myo2 ]-> CertificateTransparencyTrustManager = Java.use('com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager')
"<class: com.appmattus.certificatetransparency.internal.verifier.CertificateTransparencyTrustManager>"
[moto g 7  power::canvasm.myo2 ]-> CertificateTransparencyTrustManager.checkServerTrusted.overload()
Error: checkServerTrusted(): specified argument types do not match any of:
    .overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String')
    .overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String')

I did also find that other overload in the source code: https://github.com/appmattus/certificatetransparency/blob/91e6707dc8e193ef8d2b23cc54602ea27ab68b6c/certificatetransparency/src/main/kotlin/com/appmattus/certificatetransparency/internal/verifier/CertificateTransparencyTrustManager.kt#L98-L113

And indeed, that is getting called! More than that, while Frida did throw the following error when just returning:

Error: Implementation for checkServerTrusted expected return value compatible with java.util.List
    at ne (frida/node_modules/frida-java-bridge/lib/class-factory.js:674)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:651)

The requests now actually did go through! From reading the source, it looks like it expects the accepted certs as the return value. But we can't just return x509CertificateArr, as that is an array but the return type is a list (sigh). Seems like just returning an empty list is enough, though. This works:

CertificateTransparencyTrustManager['checkServerTrusted'].overload(
    '[Ljava.security.cert.X509Certificate;',
    'java.lang.String',
    'java.lang.String'
).implementation = function (x509CertificateArr, str, str2) {
    console.log(
        `CertificateTransparencyTrustManager.checkServerTrusted is called: x509CertificateArr=${x509CertificateArr}, str=${str}`
    );
    return Java.use('java.util.ArrayList').$new();
};
baltpeter commented 1 year ago

Oops, the other overload is also there in jadx, just at the end of the file. :D

baltpeter commented 1 year ago

From this:

https://github.com/appmattus/certificatetransparency/blob/91e6707dc8e193ef8d2b23cc54602ea27ab68b6c/certificatetransparency/src/main/kotlin/com/appmattus/certificatetransparency/internal/verifier/CertificateTransparencyTrustManager.kt#L58-L67

I would have expected that appmattus just calls through to javax.net.ssl.X509TrustManager.checkServerTrusted. But that doesn't seem to be the case. This doesn't log anything:

let X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
X509TrustManager['checkServerTrusted'].implementation = function (x509CertificateArr, str) {
    console.log(
        `X509TrustManager.checkServerTrusted is called: x509CertificateArr=${x509CertificateArr}, str=${str}`
    );
    return;
};

And it only has the two argument overload:

[moto g 7  power::canvasm.myo2 ]-> X509TrustManager.checkServerTrusted.overload()
Error: checkServerTrusted(): specified argument types do not match any of:
    .overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String')
    at X (frida/node_modules/frida-java-bridge/lib/class-factory.js:622)
    at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1066)
    at <eval> (<input>:1)

Oh well, specific handler for appmattus, it is.

baltpeter commented 1 year ago

PR opened upstream: https://github.com/httptoolkit/frida-android-unpinning/pull/34

zner0L commented 1 year ago

Really cool!

baltpeter commented 1 year ago

Looks like the pinning here was specific to the O2 app and not part of the Usercentrics SDK. I tested their sample app and didn't see any certificate pinning there. I didn't find appmatus in the sources, either.

baltpeter commented 1 year ago

This is now merged upstream.