Closed baltpeter closed 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')]))"
}
}
]
Here's what I did so far:
eu.usercentrics
.kl.e.c()
that returns either https://config.eu.usercentrics.eu
or https://api.usercentrics.eu
depending on some condition.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:
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
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:
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();
};
Oops, the other overload is also there in jadx, just at the end of the file. :D
From this:
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.
PR opened upstream: https://github.com/httptoolkit/frida-android-unpinning/pull/34
Really cool!
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.
This is now merged upstream.
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/.