By Ryan Whittier – koalatea
I recently started playing with my home network as a side project. We had been having friends over who could automatically connect to our shared wireless due to re use of an old SSID and password, inherited by one of my roommates. Our house is collectively starting to run more servers off of dedicated machines or off of our own computers for learning purposes and I am not a fan of leaving these open to attack or even just discovery from all of these people flowing through our house. This lead to an upgrade of our wireless router to dd-wrt, the inclusion of a RADIUS server for 802.1x, subnetting with different SSIDs at the router, and firewalls also running at the router. For information on setting all this up here are some helpful links and guides. It is worth mentioning that you should ensure that your home router can support dd-wrt as changing the firmware can brick a device making it useless.
Where did my chromecast go?
After running different subnets I wanted to cast my music and start working. Instead I was greeted with
Figure 1 – No Cast
My Chromecast was still there, showing me all sorts of pictures of delicious looking food, but would not respond to my browser. I noticed that I could connect while on the guest network, but not while on the private one. I threw a quick search into google about Chromecast across subnets and got these links.
Both were not that promising. So I started digging into the way casting worked.
How does casting work?
Chromecast used to make use of a protocol called DIAL, but moved towards mDNS over time. Multicast Domain Name System or mDNS is a zeroconf protocol to resolve hostnames to ip addresses when there is no DNS server to do the job. The same format for queries and replies are used between DNS and mDNS.
When attempting to connect to a Chromecast, the application in use sends out a multicast packet asking for all the Chromecasts available. This packet will be received by all machines on the network and will always be querying for _googlecast._tcp.local. If a Chromecast lies on that network, it will respond with information about the address and settings of the Chromecast in the form of a TXT record, a SRV record, and an A record. After getting this information the application will reflect the available Chromecasts to the user based on the information stored in the TXT record. Once a Cast location is chosen the app will start connecting to the Chromecast using the information in the A record and the SRV record for the address and port.
Figure 2 – Query example
The query happens to include a TTL, time to live, of 1 and a destination address of 220.127.116.11 making it impossible to jump into another subnet. A TTL tells a router whether or not it should discard a packet. It decides this based on whether or not the TTL hits zero. Every time a router forwards a packet it removes one from the TTL. The destination address of 18.104.22.168 is used as a local only broadcast domain. Attempting to Cast from any device by default uses both of these which means by default there is no way to get a chromecast to work across subnets.
Fixing the problem
We do not need to worry about after a Chromecast is discovered and the application has all the information it needs, because then the application will start communicating directly with the Chromecast regardless of subnets. Thus the only problem we need to solve is the discovery and the limited scope of the broadcast. There are three ways that I have come up with or found to fix discovery.
- Implement a relay at the router, or on the network
- Unicast the mDNS traffic so that it does not get stopped by the limit’s of the broadcast destination address
- Fake the response of the mDNS with all the required information so that the application can take over
I will be focusing on the second and third solutions which castaway is attempting to solve. The uniqueness of these solutions is that to make use of them all you need is a python interpreter and an Administrator level account on the machine you are using.
Solution attempt 1 – Unicast mDNS
All code can be found at https://github.com/KoalaTea/castaway
The easiest way to solve this would to make the mDNS traffic unicast with a higher TTL. This way it could cross the router and the Chromecast would reply with all the information directly to the client. After digging through the mDNS RFC I found that there was a unicast-response bit that would make the response unicast. I decided to use scapy, a python library to form and edit network traffic. Scapy would capture any outgoing mDNS requests looking for _googlecast._tcp.local and edit it to set the unicast-response bit, and to change the destination address to the unicast address of a known Chromecast.
After testing it was found that the unicast bit was the highest bit of the class field. For this use case that meant that the last two bytes of a query would be 80 01. This ended up working on a local subnet or if the source ip was on the same subnet as the Chromecast, but would not work across subnets. The Chromecast would not respond in any way if the source address on a query was on a different subnet.
Castawaysinglepacket.py holds the code that was used to attempt this solution. The query packet remains hard coded because the Chromecast would not respond.
Solution 2 – Spoof response
Without adding more infrastructure or having control of the network, the only other way to get Casting to work was by spoofing the response. This meant learning all of the required information for the mDNS response and building the response when a request was run. The best way to go about this was to continue using Scapy. This stage involved reading RFCs and reading through wireshark captures. The first thing required was a response.
Figure 3 – Response data
The response consists of an A record which gives the address of the Chromecast, a SRV record which gives the port that the casting server is running on, and a TXT record which holds all the information about the Chromecast itself. All of these records need to be present for an application to Cast successfully. The responses are also all formatted similar to <uuid>.local. For instance the TXT record is formatted as Chromecast-uuid._googlecast._tcp.local. To spoof the response, the uuid and the TXT information need to be gathered, but what is the information in the TXT record?
The link speaks of the v2 protocol so some portions are out of date. Specifically the part about the format of the responses being <friendly-name>.local instead of <uuid>.local. The documentation does a good job of outlining the required fields, and it turns out the ones listed are the only fields needed. The rest of the fields that are in an actual request appear to be unneeded. What is required is the Friendly Name which is the user defined name that identifies the Chromecast from others that may be on the same network, the id which is the uuid, md of Chromecast, ic of /setup/icon.png, and a ve of whatever the version number is. I used ve=05 since that is what I found in my captures, but ve=02 would probably also work. The rest of the fields I recorded in case they meant something, but I found no information or clues to what they were meant for so I just dropped them from my spoofed packet.
The Chromecast helpfully serves the uuid and friendly name in the form of xml at http://<IP>/ssdp/device-desc.xml. We with this information we can build a mDNS response to spoof the Chromecast. We will make use of the Requests and ElementTree libraries in python to gather the required information.
Once we have all the information the hard part is building the query. I do the entire thing in hex to make use of the RAW field of Scapy. To build the query we need to understand the structure of a DNS response, we can hard code some things and take advantage of some captured packets and wireshark to help us understand what hex bytes refer to what fields. The structure of a DNS response is laid out as follows.
Figure 4 – DNS Response
The beginning section all the way from identification to questions can remain the same and be hardcoded. The transaction ID will always be 0x0000, the flags will always be 0x8400, and the numbers will always be 1 answer and 3 additional. Since questions is 0 that section is empty. Our first dynamic portion is the answers section. We need to dynamically generate the DNS name of the Chromecast by querying the xml and parsing for the uuid. We then remove the dashes from the uuid for the DNS name and then append it to Chromecast. We then append a pointer to a location earlier in the packet that stores the data “.local”. This is where knowing DNS is helpful.
DNS responses use offsets to compress a DNS response packet. Offsets always start with the two binary 1s which is then followed by an offset to the DNS entry that is being referenced. This offset is calculated from the start of the DNS data. In the earlier example the “.local” is added to the DNS name by using an offset to point to the start of “.local”.
We need to then continue through the TXT section and dynamically set the total length of the record and the lengths of each field. Then we need to add the friendly name, uuid, version number, icon location, and Chromecast as hex. This closes the TXT record and moves us to dynamically setting the offset of the TXT records name for the name of the SRV record. The rest of the SRV record we can just steal from captured packets. The a record only requires us to prepend an offset to a portion of the SRV record and to append the Chromecasts IP as hex.
For the code for this implementation look in castaway.py.
This project ended up being a lot more looking into DNS then I expected. The offsets threw me off for the longest time. I had no idea why there was a c at the beginning and no hex representation of the name. I also learned to actually read the RFC instead of just skimming through it that would have sped up development time by keeping me from guessing. I originally brute forced the unicast-response bit instead of just reading that it was the first bit in the RFC.
The tool castaway can be used by an Administrator level account because it needs to use Raw Sockets and it needs to sniff packets. It is easy to run just type
Castaway.py <Chromecast IP> <interface>
Then try to cast. It should give you the option to Cast to the device which IP you entered.
This is the only way I know of to Cast to a Chromecast on another subnet without adding more infrastructure to the network. There are other options if you have a router that runs a unix OS, such as dd-wrt or Ubiquiti. These can run full relays for mDNS traffic allowing all devices on the network to talk to Chromecasts on different subnets. I have plans to include a full relay option to castaway in the future to potentially add more control on the way that the relay works, but I may just end up using the tools already available. Clone the project here
Links to information on router mDNS forwarders
Links from the blog post
Dd-wrt and 802.1x set up
Chromecast across subnets info
Python libraries used documentation