Reverse-engineering

Reverse-engineering Google Cast

Google Cast is a proprietary protocol by Google which enables controlling playback of Internet-streamed audiovisual content on the Chromecast, Android TV and other compatible devices.


From the consumer perspective, Google Cast connects two devices: a sender (i.e a smartphone) and a receiver (such as a Chromecast). Using the Google Cast framework, an app on the sender device (i,e, YouTube or Netflix) communicates with a corresponding browser application on the receiver to play and control media.Google Cast

Ostensibly, Google Cast is an open, albeit largely closed-source, framework. On the sender application side, content providers can use the Google Cast SDK, Android, iOS and Google Chrome applications can communicate with receiver devices.

Similarly, on the receiver application side, using the Google Cast Application Framework (CAF) Receiver SDK, browser applications can be hosted on a Chromecast or a similar device, and communicate with sender applications over Google Cast.

The trick, as it happens, lies with the receiver device.

The Google Cast protocol includes device authentication mechanisms to guarantee that sender applications can only communicate with approved receiver devices. The authentication is optional at the protocol level, but it is enforced by the Google Cast SDK, and this behavior cant be configured. The end result is that Google holds a real monopoly on the Google Cast protocol – only devices manufactured/approved by Google can run Google Cast receiver software.

Previous work and protocol overview:

The Google Cast protocol itself has been well-studied. While the Android and iOS SDKs are closed-source, the sender functionality is implemented within Chromium and therefore open-source. Most of this discussion is drawn from Thibaut Séguy’s work on node-castv2 (1, 2), Huaiyuan Gu’s chromecast-receiver-emulator as well as Romain Picard’s detailed write-ups on his blog (1, 2, 3).

Modern Google Cast receiver devices announce themselves to sender devices using mDNS. The devices then communicate over a very simple TCP protocol, using Protobuf to exchange messages. When it comes time to launch a receiver application, the receiver device spawns a browser to host the receiver application, which communicates with the receiver device using WebSockets. Messages are then forwarded over the TCP link between the sender and receiver applications as necessary.

 

Device authentication protocol:

The TCP connection between the sender and receiver devices is secured with TLS. The certificate used for the connection (the peer certificate) is self-signed by the receiver device, and valid for 24 hrs. The receiver device also possesses a platform certificate, which is then signed by a trusted Google CA.

When the device authentication mechanism is engaged, the receiver device signs the peer cert using the platform certificate. The sender device can then verify that the signature is valid, and the platform certificate has been duly signed by the trusted Google CA.

The protocol is secure and prevents a stock sender device from communicating with an unauthorized receiver device. As before, this behavior cannot be disabled within the official Google Cast SDK.

Finding the device authentication implementation:

We know that the device authentication takes place over the urn:x-cast:com.google.cast.tp.deviceauth Protobuf namespace. On an Android devices, we hypothesize that there are a few places this device authentication code could be located:

We begin our investigation by downloading as well as unpacking each of these, and performing a simple grep for the string deviceauth, which leads us to the Google Play Services app. We can then use jadx to decompile the Google Play Services APK file, and look for the specific class where this is indeed implemented.

The code has been obfuscated, but we do indeed find a single occurrence of this string:

public final class ptd extends qbq {
    private static final String e = qbz.c("com.google.cast.tp.deviceauth");

The next line has some promising strings, too:

    private static final String[] f = {"success", "error received", "client auth cert malformed", "client auth cert not X509", "client auth cert not trusted", "SSL cert not trusted", "response malformed", "device capability not supported", "CRL is invalid", "CRL revocation check failed"};

And scrolling down slightly further, we find a very revealing declaration:

    static {
        try {
            j = new HashSet();
            k = CertificateFactory.getInstance("X.509");
            j.add(new TrustAnchor(a("MIIDwzCC[...]3ov1Mw=="), (byte[]) null));
            j.add(new TrustAnchor(a("MIIDxTCC[...]Rhx1LB9N"), (byte[]) null));
        } catch (CertificateException e2) {
            Log.wtf("DeviceAuthChannel", "Error parsing built-in cert.", e2);
        }
    }

TrustAnchor is a Java class used as a ‘trust anchor for validating X.509 certification paths’, so it seems reasonable to suspect that these are the trusted Google CAs used to validate the platform certs.

Digging Deeper:

Looking now to references for this j HashSet (containing the trusted Google CAs), we come across a lengthy method called a. jadx has failed to decompile this method, so it is a little harder to make sense of, but the broad strokes are clear:

    /* Code decompiled incorrectly, please refer to instructions dump. */
    public final void a(byte[] r15) {
        /*
            r14 = this;
            [...]
            java.util.HashSet r9 = j    // Note the reference to the HashSet "j" from earlier
            r8.<init>(r9)
            r8.setRevocationEnabled(r0)
            java.lang.String r9 = "PKIX"
            java.security.cert.CertPathValidator r9 = java.security.cert.CertPathValidator.getInstance(r9)
            java.security.cert.CertPathValidatorResult r8 = r9.validate(r6, r8)
            java.security.cert.PKIXCertPathValidatorResult r8 = (java.security.cert.PKIXCertPathValidatorResult) r8

Clearly, this method is responsible for performing the platform certificate validation, among other things. Our initial hypothesis was that, since the return type is void (does not return any value), this method would throw an Exception if the certificate validation failed.

Watching the code live:

Having found a method of interest we can now turn to the Xposed Framework, a framework for rooted Android devices to hook & modify arbitrary Java code. With the code below, we hook the certificate validation ptd.a method to log the method call and result:

public class GCastNoAuth implements IXposedHookLoadPackage {
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.google.android.gms")) {
            return;
        }

        // Hook certificate check function
        findAndHookMethod("ptd", lpparam.classLoader, "a", "[B", new XC_MethodHook() {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("[GCastNoAuth] beforeHookedMethod");
            }

            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("[GCastNoAuth] afterHookedMethod");
                if (param.hasThrowable()) {
                    XposedBridge.log("[GCastNoAuth] An exception was raised");
                    XposedBridge.log(param.getThrowable());
                } else {
                    XposedBridge.log("[GCastNoAuth] No exception");
                }
            }
        });
    }
}

Connecting to a receiver device with a valid certificate, then an invalid certificate, we can then observe the following log output:

12-18 18:53:37.574  6208  6347 I EdXposed-Bridge: [GCastNoAuth] beforeHookedMethod
12-18 18:53:37.600  6208  6347 I EdXposed-Bridge: [GCastNoAuth] afterHookedMethod
12-18 18:53:37.600  6208  6347 I EdXposed-Bridge: [GCastNoAuth] No exception
[...]
12-18 18:56:40.453  6208  8297 I EdXposed-Bridge: [GCastNoAuth] beforeHookedMethod
12-18 18:56:40.461  6208  8297 I CastService: [instance-4] onConnectionFailed: package: com.google.android.apps.chromecast.app status=AUTHENTICATION_FAILED
12-18 18:56:40.463  6208  8297 I EdXposed-Bridge: [GCastNoAuth] afterHookedMethod
12-18 18:56:40.463  6208  8297 I EdXposed-Bridge: [GCastNoAuth] No exception

We have successfully hooked the certificate validation function, but the result returned from the a method was the same in both cases. We must look further…

Locating & enabling debug output:

Returning to the ptd.a method, we find some handlers for some error cases:

        L_0x021d:
            qco r15 = r14.s
            java.lang.Object[] r0 = new java.lang.Object[r0]
            java.lang.String r1 = "Received DeviceAuthMessage with no response (ignored)."
            r15.a(r1, r0)
            return

Note from the earlier code block that r15 contains a reference to this, so r15.a(r1, r0) in this code snippet is calling the method this.s.a, passing a message as well as an empty array.

A quick bit of digging reveals that this.s is a reference to an object of class qco:

import android.util.Log;
/* [...] */
public class qco {
    /* [...] */
    public final void a(String str, Object... objArr) {
        if (a() || a) {
            e(str, objArr);
        }
    }

    public final void b(Throwable th, String str, Object... objArr) {
        Log.w(this.b, e(str, objArr), th);
    }

    public final void c(Throwable th, String str, Object... objArr) {
        Log.e(this.b, e(str, objArr), th);
    }

    public final void a(Throwable th, String str, Object... objArr) {
        Log.i(this.b, e(str, objArr), th);
    }
}

Obviously, this is a class that has something to do with logging. The final three methods call the warning, error and information logging methods of the standard Android Log class. If one looks closely, however, the top a function  does not. It calls the e function (which clearly calculates the log message), but doesn’t do anything with it. We deduce that a, therefore, is probably some debug logging function. The actual logging itself has been disabled or optimized away, but the method call itself remains.

Returning to Xposed, with the code below, we can now hook into the debug logging qco.a method, and print the  debug messages:

        // Hook debug logging function to show logs
        findAndHookMethod("qco", lpparam.classLoader, "a", String.class, "[Ljava.lang.Object;", new XC_MethodHook() {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                String logMessage = (String) callMethod(param.thisObject, "e", new Class<?>[] {String.class, Class.forName("[Ljava.lang.Object;")}, param.args);
                XposedBridge.log("[GCastNoAuth] [Debug] " + logMessage);
            }
        });

With this code, we now see a lot more debug output: The debug output when connecting to a receiver with an invalid certificate now shows:

12-18 19:12:52.872  3234  4283 I EdXposed-Bridge: [GCastNoAuth] beforeHookedMethod
[...]
12-18 19:12:52.873  3234  4283 I EdXposed-Bridge: [GCastNoAuth] [Debug] [controller-0003-com.google.android.apps.chromecast.app API] Received a protobuf: # bmbs@1f762c8
12-18 19:12:52.873  3234  4283 I EdXposed-Bridge: [GCastNoAuth] [Debug] [controller-0003-com.google.android.apps.chromecast.app API] Device authentication failed: error received - 0
[...]
12-18 19:12:52.880  3234  4283 I CastService: [instance-3] onConnectionFailed: package: com.google.android.apps.chromecast.app status=AUTHENTICATION_FAILED
12-18 19:12:52.881  3234  4283 I EdXposed-Bridge: [GCastNoAuth] afterHookedMethod
12-18 19:12:52.881  3234  4283 I EdXposed-Bridge: [GCastNoAuth] No exception

Honing in on the target:

The string Device authentication failed appears in another function in the ptd class, also (unfortunately) called a (but with a different method signature):

    private final void a(int i2, Exception exc) {
        String str;
        if (i2 == 0) {
            this.s.a("Device authentication succeeded.", new Object[0]);
            /* [...] */
        } else {
            /* [...] */
            qco.a(String.format(locale, "Device authentication failed: %s - %s", objArr), new Object[0]);
        }
        if (i2 == 0) {
            this.s.a("authentication succeeded", new Object[0]);
            /* Lots more interesting communication-related code! */
            return;
        }
        this.n.a(2000, false, pqf.b(i2));
    }

(For your interest, the constant 2000 appears in another class in connection with the string AUTHENTICATION_FAILED from the log!)

It appears that this is a callback method, which is passed the result of the certificate validation in the argument i2. If i2 is 0, then authentication succeeds, and further interesting communication-related things are proceeded with. If i2 is nonzero, then device authentication fails, and some other method is called with an error code.

The final implementation:

Using Xposed, we hook the ptd.a method (the second one), and modify the argument i2 so that it is always 0, and so device authentication always succeeds:

public class GCastNoAuth implements IXposedHookLoadPackage {
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.google.android.gms")) {
            return;
        }

        // Hook certificate post-check function
        findAndHookMethod("ptd", lpparam.classLoader, "a", int.class, Exception.class, new XC_MethodHook() {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // Force success
                param.args[0] = 0;
            }
        });
    }
}

With this code in place, our Android sender device now duly connects to as well as communicates with a custom receiver.

Future steps and limitations:

Given that open-source receiver software implementations already exist (e.g. node-castv2), this Xposed module removes the final barrier to creating a 3rd-party receiver compatible with the Android Google Cast sender framework.

This could be used, for instance, to sniff traffic between the Android Google Cast sender and an official receiver device. It could also be used to supplant an official receiver device entirely.

However, this result does not uncover the Google Cast ecosystem entirely open. While device authentication does provide the ‘holy grail’ at a Google Cast protocol level, actual Google Cast receiver applications by content providers (e.g. YouTube) make use of further security measures, such as EME, Widevine Verified Media Path and HDCP, which make use of hardware security functionality on the Google Cast device itself and are significantly more difficult to overcome.

In view of this our thoughts on where this could go are that it could be used to receive Google Cast transmissions, without emulating the entirety of Google Cast receiver stack. For instance, an Android phone could be used to communicate with our custom receiver software running on a Raspberry Pi, which would translate the commands received into the relevant Kodi video add-on. This would certainly obviate the need ourselves to emulate or defeat the DRM functionality used by Google Cast-specific receiver applications.

CBNN

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.