Our TCL Roku TV primarily exists to play Youtube videos. We recently noticed that it plays ads significantly more often than it used to, and much more frequently than our other Youtube viewing devices.
The first step I took to block the ads was installing Pi-hole in a Docker container on one of the Linux boxes on our network. Pi-hole's main purpose is to block ads by blackholing the DNS records of known advertising and tracking hosts. You can subscribe to enormous blocklists using Pi-hole's web interface to effectively prevent any devices on your network from communicating with any of these hosts.
You can use Pi-hole to block Youtube ads, but I found that Youtube videos are served from the same hosts that serve the ads, so using a Pi-hole to block the ads also prevents you from viewing any of the videos you want to see.
If Youtube ads are served from the same hosts as the videos, I figured there must be some way to differentiate between the two. To try to determine what these differences are, I checked the HTTP requests my browser makes when trying to load a Youtube video and compared those to the requests made when loading ads.
These two types of requests appear very similar at first glance, but there are some differences.
After comparing the requests from a bunch of different videos and ads, I found that requests for ads consistently contained the URL parameter "ctier=L", and the initial requests for any video contained the parameter "aitags". The value of "aitags" varied, but it was always present.
This ended up being the only thing I needed to programmatically determine whether or not a request was for an ad or a normal video. To verify, I loaded up mitmproxy to create a man-in-the-middle proxy. mitmproxy sits in between your client (e.g. your TV) and the server (Youtube) and gives you full visibility and control over all of the data sent between them. I can see and modify anything that the client sends to Youtube, and vice versa.
I initially tested the mitmproxy ad blocker using my laptop, just by setting Firefox to send all requests through the proxy.
Then I could see requests from my browser appearing in mitmproxy.
I utilized mitmproxy's scripting engine to perform the actual content matching and request blocking. mitmproxy allows you to write scripts that can interact with every request that goes through the proxy. You can view every detail of the request and alter the response in any way you please.
My script verifies the following things about a request before deciding to block it:
- .googlevideo.com is in the URL
- /videoplayback is in the URL
- aitags appears in the URL parameters
- ctier=L appears in the URL parameters
from mitmproxy import http, ctx
def request(flow: http.HTTPFlow) -> None:
if ".googlevideo.com" in flow.request.pretty_url and \
"/videoplayback" in flow.request.path and \
"aitags" in flow.request.query and \
flow.request.query.get("ctier", None) == "L":
# we found an ad, return dummy response
ctx.log.info("***INTERCEPTING***")
flow.response = http.HTTPResponse.make(
200, # status code
b"BLOCKED", # content
{"Content-Type": "text/html"} # headers
)
It works! When I visited a Youtube video without an adblocker enabled in my browser, the video appeared to buffer for a few extra seconds while Youtube tried and failed to load in ads, and ultimately just loaded the video without playing any ads.
The log message from the script also appeared in the mitmproxy window, indicating that it decided to block an ad request.
The next step was to get my TV sending requests through the proxy server. Unfortunately, Roku devices give users very limited access to network settings, so you can't even set a static IP address, let alone force the use of an HTTP proxy.
Luckily, mitmproxy also features a transparent proxy mode. This mode allows mitmproxy to receive normal HTTP/HTTPS requests as though it was an ordinary web server. The mitmproxy documentation contains instructions for setting up a Linux system to act as a transparent proxy: https://docs.mitmproxy.org/stable/howto-transparent/
As mentioned in those instructions, in order to get requests flowing through my transparent mitmproxy, I had to make the Linux host the target device's default gateway. On many other systems, this would've been a trivial settings change, but due to the restricted nature of the Roku TV, I had to make this change elsewhere.
My Roku TV receives its IP address and default gateway from my network's DHCP server, which is running on an Ubiquiti EdgeRouter Lite. To change my Roku TV's default gateway, I gave the TV a static DHCP mapping from the router's web interface. Then, from the router's CLI, I can assign that mapping a different default gateway than the rest of the devices on the network:
configure
edit service dhcp-server LAN1 DHCP subnet 192.168.0.0/24
set static-mapping ROKU-TV static-mapping-parameters "option router 192.168.0.105;"
commit; save
Now, after power cycling the TV, I can see in the settings menu that it receives a default gateway of 192.168.0.105, instead of my router's IP. This means that all traffic from my TV goes directly to my Linux machine instead of the Ubiquiti router. Now, HTTP and HTTPS requests made by the TV should appear in my mitmproxy window.
Interestingly, when powering on the TV and moving through the menus, I can see it make many unencrypted HTTP requests to various servers to download background images for the main screen, fonts, and layout information in XML format.
Unfortunately, although the Youtube app on my Roku TV does use HTTPS, they seem to have properly implemented certificate validation checks. This means that while my proxy setup functioned properly, the Youtube app on the Roku TV noticed that mitmproxy identified itself with an invalid certificate, so it aborts the connection.
There are a few other attack vectors I'm interested in exploring.
- Hardware-based attacks, such as physically opening the TV and looking for flash chips to dump. This is our only TV though, and I don't want to break it. It looks like people are selling replacement boards from inside the TV for ~$20 on eBay, so maybe I'll pick one of those up. Looking at the board images from the FCC filing, it looks like they used an MStar ARM SoC and a Realtek RTL8812BU based wifi module. There are a few headers on the board that look interesting, but nothing immediately jumps out at me as being a clear way in.
- The unencrypted HTTP requests the Roku makes in the main menu. Messing with the layout XML or the images could potentially lead to some kind of vulnerability.
- Uploading a channel to the Roku TV using developer mode
Until then, I guess we will just deal with the ads.