What’s on the telly ?

A partially successful attempt to MITM the TV

Right well I finally got around to getting a “smart” TV, i.e. a TV that is capable of detecting what I’m watching and then selling that information to data brokers who then sell it to advertisers .

The TV I ended up getting was a LG 55QNED81SRA, and as an incorrugible couch potato, I thought it’d be grouse to get some stats on how much of my life I spend splaid out on the settee.

What I had intended to do was to get a rasberry pi with a couple of ethernet ports, stick that in the cable leading out of the TV, and inspect the traffic getting sent down the wire. Of course everything’s going to be encrypted, so I was planning on MITM-ing the SSL connections, which would probably involve adding a new CA certificate to the telly to get it to trust the raspi-in-the-middle, which would probably involve rooting the TV.

So once I’d done that, it turns out that the apps that use the builtin browser app ( i.e. the apps for ABC, 7, 9, 10, SBS, Binge ) can be intercepted, but the other apps ( i.e. Netflix, Stan, Apple TV etc ) can’t as they use certificate pinning, or at least that’s my theory at the moment.

tl;dr It sort of works

The current plan is to write it up so that (a) I can remember what the hell I did to my TV a year from now, and (b) save anyone else thinking of doing this the hassle, with the possibility that someone out there actually gets this to work, notices my current dilemma, and then decides to tell me how to do it properly in the comments below.

This is how I choose to spend my christmas holidays, incidentally.

So I got a few pointers from the discord channel and got a bit further, I’ve updated this post but there’s still a couple of things to try which I’ll have burl at later on.

Bits and bobs

OK so what I’ve got is:

The plan

The way this is going to be set up is that I’ll be SSHing into the raspberry pi over it’s wi-fi interface (wlan0), with the big cable from the router to the raspi plugged into eth0, the little cable from the raspi to the TV will plugged into eth1. You could probably SSH in via eth0 as well, but I really don’t trust my ability to keep that connection functioning whilst faffing about with iptables rules.

All the devices are wi-fi enabled, so I could have probably avoided the cables altogether, but I have a natural aversion to broadcasting my network traffic over the airwaves, unlike the rest of the world, apparently.

So the TV is going to live in a new subnet separate from the rest of the home network. My home router is on 192.168.0.1 with hosts on the 192.168.0.x subnet, so I went with 192.168.4.x for the TV-facing side of the network. The TV itself going to be on 192.168.4.100, and the raspi is going to straddle those networks on 192.168.4.1 and 192.168.0.138.

So something like this:

The raspi is going to run a squid transparent proxy which intercepts HTTP and HTTPS traffic, and also a dnsmasq server to dole out the one solitary IP address to the TV on the 192.168.4.x network.

On the TV:

So there’s a few ways of rooting the TV, other than the way you’re probably thinking, if you’re Australian. The one I chose to use was the crashd exploit here, which uses the webOS developer toolkit + a poorly configured crashd daemon, which is the daemon responsible for crash diagnostics, apparently. So if you’re thinking of doing any of this, maybe read through that page a few times, as there’s a risk you’ll brick your TV if you stuff it up.

WebOS is the linux-based operating system the TV uses, and the version of WebOS this TV has is 8.1.0-4805 (number1-nairobi), which is based on linux 5.4. It’s running firmware version 03.11.55. Anything from 03.30.60 onwards won’t work, so don’t update your firmware.

So anyway, from the ‘Apps’ tile on the TV’s home screen, I installed the “Developer Mode” app, signed up for a developer account, entered those account details in the developer kit thing which then put the TV into developer mode.

Then installed the WebOS SDK on my laptop, and the homebrew installer ( dev manager ), and then installed the webOS ‘homebrew’ app which gives you access to a small set of horrifically low-quality apps like the ones you used to be able to get on a first generation ipod touch if the word “cydia” means anything to you at all.

What homebrew also does is give you telnet access to the TV, from which you can run a few commands and scripts, which will cause the homebrew app to tell you that the TV is rooted after you reboot it two or three times.

What you then do is uninstall the developer kit thing, and enable SSH in homebrew so that you’re not telnetting around, and switch off automatic OS updates.

On the raspberry pi:

Come up with a hilarious name for the device. I’m going with sorbet, as ‘beret’ was already taken by the other raspberry pi I set up ages ago and definitely didn’t leave in a drawer over there gathering dust.

To get that running, I installed the raspi imager, used that to install whatever the latest 64-bit linux distro is (as of writing, Debian Bookworm), and once that’s installed, reboot it, ssh into it using the IP address discovered off the router, then grabbed the MAC addresses and assigned those some IP addresses in my DHCP server, and then set a static IP on the tv-facing side of the pi.

Static IP

Spent a fair amount of time trying to google how to set a static IP on this thing, which is made arbitrarily more difficult by the purveyors of pi reinventing their network stack again . The approved way in bookworm appears to be using “nmcli“, which uses connection names rather than device names. So to configure those, do this:

knoxg@sorbet:~ $ sudo nmcli c show
NAME                  UUID                                  TYPE      DEVICE
Wired connection 2    b9e86596-f07f-3af4-9bc6-d06e3ebef75b  ethernet  eth1
Wired connection 1    c38126f3-4849-3c24-9aba-c42cd14e3892  ethernet  eth0
Grasshopper-NBN-5GHz  c68e7dcc-256d-444a-b619-9ab3d63af5f9  wifi      wlan0
lo                    53226046-3865-4402-a382-aa90b69e811a  loopback  lo

knoxg@sorbet:~ $ sudo nmcli c mod 'Wired connection 2' ipv4.addresses 192.168.4.1/24 ipv4.method manual
knoxg@sorbet:~ $ sudo nmcli c down 'Wired connection 2' && sudo nmcli c up 'Wired connection 2'
knoxg@sorbet:~ $ ifconfig

And then installed a bunch of things which I definitely wrote down earlier.

  • dnsmasq
  • iptables
  • squid

Installing dnsmasq

knoxg@sorbet:~ $  sudo apt update -y && sudo apt upgrade -y
knoxg@sorbet:~ $  sudo apt-get install dnsmasq
knoxg@sorbet:~ $  sudo apt-get install dnsutils telnet
knoxg@sorbet:~ $  sudo shutdown -hP now

## after a reboot

knoxg@sorbet:~ $  sudo bash -c 'cat >/etc/dnsmasq.d/sorbet.conf' <<'EOF'
# settings for sorbet

# log settings
log-facility=/var/log/dnsmasq.log

# dns settings
log-queries
domain-needed
bogus-priv
interface=eth1
# bind-interfaces

# prevent webOS upgrades
address=/aic.rdl.lgtvcommon.com/127.0.0.1
address=/lgtvonline.lge.com/127.0.0.1
address=/us.lgtvsdp.com/127.0.0.1
address=/aic.recommend.lgtvcommon.com/127.0.0.1
address=/snu.lge.com/127.0.0.1
address=/ueiwsp.com/127.0.0.1

# dhcp settings
log-dhcp
domain=mul.randomnoun
dhcp-range=192.168.4.200,192.168.4.250,12h

# TV was 192.168.0.x, now its going to be 192.168.4.100
dhcp-host=64:e4:a5:55:6d:67,tv,192.168.4.100,12h

# Override the default route supplied by dnsmasq, which assumes the
# router is the same machine as the one running dnsmasq.
dhcp-option=option:router,192.168.4.1
EOF

knoxg@sorbet:/etc/dnsmasq.d $ sudo service dnsmasq restart
knoxg@sorbet:/etc/dnsmasq.d $ sudo less /var/log/dnsmasq.log

The 64:e4:a5:55:6d:67 bit is the MAC address of my TV, which is under Settings -> General -> Network -> Wired Connection.

Enabling routing and iptables

Then enabled IP routing,

knoxg@sorbet:/etc/dnsmasq.d $ sudo vi /etc/sysctl.conf   # uncommented 'net.ipv4.ip_forward=1'
knoxg@sorbet:/etc/dnsmasq.d $ sudo sysctl net.ipv4.ip_forward=1

And added some iptables rules to send packets received on eth1 (from the tv) through to eth0 (the router). The iptables-persistent package will reapply the rules if the raspberry pi restarts.

knoxg@sorbet:~ $ sudo apt-get install iptables
knoxg@sorbet:~ $ sudo apt-get install iptables-persistent
knoxg@sorbet:~ $ sudo systemctl is-enabled netfilter-persistent.service # should be 'enabled'
knoxg@sorbet:~ $ sudo systemctl enable netfilter-persistent.service     # if it isn't

knoxg@sorbet:~ $ sudo bash -c '/sbin/iptables-save > /etc/iptables/rules.v4'
knoxg@sorbet:~ $ sudo cp /etc/iptables/rules.v4 /etc/iptables/rules.v4.orig

knoxg@sorbet:~ $ sudo iptables --list-rules
knoxg@sorbet:~ $ sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
knoxg@sorbet:~ $ sudo iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
knoxg@sorbet:~ $ sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
knoxg@sorbet:~ $ sudo iptables --list-rules
knoxg@sorbet:~ $ sudo bash -c '/sbin/iptables-save > /etc/iptables/rules.v4'

Checking that all works

From here you should be able to telnet / ssh to the TV, and from there test that the network is still available.

knoxg@sorbet:~ $ ssh root@192.168.4.100
root@192.168.4.100's password:
NEVER EVER OVERWRITE SYSTEM PARTITIONS LIKE KERNEL, ROOTFS, TVSERVICE.
Your TV will be bricked, guaranteed! See https://rootmy.tv/warning for more info.

 /!\ Your system is using a default password.
 /!\ Insert SSH public key into /home/root/.ssh/authorized_keys and perform a reboot to remove this warning.

root@tv:~# curl -vvv http://www.google.com   # test HTTP access
root@tv:~# curl -vvv https://www.google.com  # test HTTPS access
root@tv:~# exit
Connection to 192.168.4.100 closed.

Take note of that warning about not changing the fileystems. I can’t seem to to dig up much detail about it, but apparently webOS verifies the signatures of its filesystems. It’s probably a UEFI thing, although I don’t really know how that works. So you can only change things in certain places, and then use overlay mounts to make those changes appear in the places you want them to appear.

Installing squid

Back on the raspberry pi, you’ll need the squid package that has the openssl libraries included, not the other one.

knoxg@sorbet:~ $ sudo apt-get install squid squid-openssl
knoxg@sorbet:~ $ cd /etc/squid/

knoxg@sorbet:/etc/squid $ sudo bash -c 'cat >/etc/squid/conf.d/sorbet.conf' <<'EOF'  
# SSL-related config adapted from https://www.reddit.com/r/PFSENSE/comments/ijp8nz/is_it_even_possible_to_run_an_ssl_mitm_squid/

http_access allow all
http_upgrade_request_protocols websocket allow all

acl step1 at_step SslBump1
acl step2 at_step SslBump2
acl step3 at_step SslBump3

# splice domains that don't work
# file should be plain text, 1 domain per line in the format
# .example.tld
acl broken_sites ssl::server_name "/etc/squid/ssl_broken_sites.txt"

# was using this to skip some IPs when this failed to work
# acl broken_ip dst 40.112.195.32
# acl broken_ip dst 104.110.68.93
# ssl_bump none broken_ip

# SSL Bump Commands
ssl_bump peek step1 all
ssl_bump splice broken_sites
ssl_bump stare step2 all
ssl_bump bump step3 all

visible_hostname sorbet.mul.randomnoun

# port 3128 already opened in the main /etc/squid/squid.conf file
# http_port sorbet.mul.randomnoun:3128
http_port sorbet.mul.randomnoun:3129 intercept
https_port sorbet.mul.randomnoun:3130 intercept ssl-bump generate-host-certificates=on \
  dynamic_cert_mem_cache_size=4MB \
  cert=/etc/squid/certs/cacert.pem \
  key=/etc/squid/certs/cacert-key.pem

sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/squid/ssl_db -M 4Mb
EOF

I’m using ‘.mul.randomnoun‘ as the domain for the multimedia subnet.

You’ll need to create a CA for squid, which if you’re anything like me, you’ll already have, due to that k8s time sink you’ve got running that doesn’t use letsencrypt.

knoxg@sorbet:/etc/squid $ sudo mkdir certs
knoxg@sorbet:/etc/squid $ cd certs
knoxg@sorbet:/etc/squid/certs $ sudo vi cacert.pem      # and paste that in
knoxg@sorbet:/etc/squid/certs $ sudo vi cacert-key.pem  # and paste that in
knoxg@sorbet:/etc/squid/certs $ ls -l
total 8
-rw-r--r-- 1 root root 3268 Dec 20 16:42 cacert-key.pem
-rw-r--r-- 1 root root 2114 Dec 20 16:42 cacert.pem
knoxg@sorbet:/etc/squid/certs $ sudo chmod 600 cacert-key.pem

And create the ssl_db thing, otherwise squid won’t start:

knoxg@sorbet:/etc/squid $ sudo /usr/lib/squid/security_file_certgen -c -s /var/lib/squid/ssl_db -M 4MB
knoxg@sorbet:/etc/squid $ sudo chown -R proxy /var/lib/squid

And restart the server:

knoxg@sorbet:/etc/squid $ sudo service squid restart
knoxg@sorbet:/etc/squid $ sudo service squid status
knoxg@sorbet:/etc/squid $ service --status-all

knoxg@sorbet:/etc/squid $ sudo less /var/log/squid/cache.log   # check for startup errors
knoxg@sorbet:/etc/squid $ sudo less /var/log/squid/access.log  # check for network traffic

Updating iptables

OK so this is what you need to do to get HTTP/HTTPS connections from the TV getting funnelled through squid:

knoxg@sorbet:/etc/squid $ sudo iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 -j REDIRECT --to-port 3129
knoxg@sorbet:/etc/squid $ sudo iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 443 -j REDIRECT --to-port 3130
knoxg@sorbet:/etc/squid $ sudo bash -c '/sbin/iptables-save > /etc/iptables/rules.v4'

and then you can watch the squid logs to see what happens:

knoxg@sorbet:/etc/squid $ sudo less /var/log/squid/access.log

Installing the CA certificate on the tv:

Install the CA on the TV using this script. Create the script as /tmp/update-ca-certs.sh, make it executable, and run it to create a boot script which will install the certificate.

It’ll reboot when it completes but I imagine you’ll comment that bit out to check it worked properly, in which case you’ll need to reboot the TV for the boot scripts to take effect.

#!/bin/bash

# Adapted from https://github.com/tf318/lg/blob/main/update-ca-certs.sh

# Adds the randomnoun cacert to the trust store
# Create a post-boot script to overlay this cert data over the readonly filesystem and reload CA cert config on boot

export PURPOSE="Add randomnoun CA"

export STARTUP_SCRIPTS_DIR=/var/lib/webosbrew/init.d

export CERT_FIX_SCRIPT=${STARTUP_SCRIPTS_DIR}/randomnoun-ca-cert-overlay
export CERT_FIX_DIR=/home/randomnoun-ca-cert-overlay

if [ ! -d "${STARTUP_SCRIPTS_DIR}" ] ; then
cat << EOM
----------
FIX FAILED
----------

Error: Homebrew Channel init.d directory does not exist

       ${STARTUP_SCRIPTS_DIR}

Before running this script, ensure you have rooted your TV.

EOM
exit 1
fi

if [ ! -f "${CERT_FIX_SCRIPT}" ]; then

        mkdir -p ${CERT_FIX_DIR}/etc_ssl/upper/
        mkdir -p ${CERT_FIX_DIR}/etc_ssl/work/
        mkdir -p ${CERT_FIX_DIR}/etc_pki/upper/
        mkdir -p ${CERT_FIX_DIR}/etc_pki/work/
        mkdir -p ${CERT_FIX_DIR}/usr_share_ca-certificates/upper/
        mkdir -p ${CERT_FIX_DIR}/usr_share_ca-certificates/work/

        # if you run this script multiple times, this ends up adding the certificate multiple times
        # (seems OK)
        echo "Adding randomoun CA certificate..."
        cp /etc/ca-certificates.conf ${CERT_FIX_DIR}/fixed-ca-certificates.conf

cat <<EOF >"${CERT_FIX_DIR}/usr_share_ca-certificates/upper/randomnoun.crt"
-----BEGIN CERTIFICATE-----
MIIF7TCCA9WgAwIBAgIUFMPkXke2j7uuzldHYxOwR23JqMgwDQYJKoZIhvcNAQEL
BQAwgYQxCzAJBgNVBAYTAkFVMQwwCgYDVQQIDANRTEQxETAPBgNVBAcMCEJyaXNi
... the CA certificate goes here ...
kpc4j3Q+JasQ6Z/mElHq65auaXYTtKwC6NVib6nQnB+PFe6z9exfXIOMT5CrEBlH
RnwUf47R+lsNy6JOoKcVWPndeih4ylu15CU9AKaM0YftdCwnmy9cnx+lKLSar0/O
5432RmvmFekLSwu0tp8CiuaMo7ZL4CgtJ6fvSxzFor2B
-----END CERTIFICATE-----

EOF

        echo "randomnoun.crt" >> ${CERT_FIX_DIR}/fixed-ca-certificates.conf

        echo
        echo "Creating startup certificate overlay script..."

        echo "#!/bin/bash" > ${CERT_FIX_SCRIPT}
        echo "# ${PURPOSE}" >> ${CERT_FIX_SCRIPT}
        echo "set -e" >> ${CERT_FIX_SCRIPT}
	echo "mount --bind ${CERT_FIX_DIR}/fixed-ca-certificates.conf /etc/ca-certificates.conf" >> ${CERT_FIX_SCRIPT}
        echo "mount -t overlay overlay -o lowerdir=/etc/ssl,upperdir=${CERT_FIX_DIR}/etc_ssl/upper,workdir=${CERT_FIX_DIR}/etc_ssl/work /etc/ssl" >> ${CERT_FIX_SCRIPT}
        echo "mount -t overlay overlay -o lowerdir=/etc/pki,upperdir=${CERT_FIX_DIR}/etc_pki/upper,workdir=${CERT_FIX_DIR}/etc_pki/work /etc/pki" >> ${CERT_FIX_SCRIPT}
        echo "mount -t overlay overlay -o lowerdir=/usr/share/ca-certificates,upperdir=${CERT_FIX_DIR}/usr_share_ca-certificates/upper,workdir=${CERT_FIX_DIR}/usr_share_ca-certificates/work /usr/share/ca-certificates" >> ${CERT_FIX_SCRIPT}
        
        # changes in /etc/ssl and /usr/share from here change the overlay filesystem, not the original filesystem
        echo "update-ca-certificates" >> ${CERT_FIX_SCRIPT}
        echo "c_rehash" >> ${CERT_FIX_SCRIPT}
        echo "cat ${CERT_FIX_DIR}/usr_share_ca-certificates/upper/randomnoun.crt >> /usr/share/ca-certificates/sdp/sdp-ca.pem" >> ${CERT_FIX_SCRIPT}
        echo "cat ${CERT_FIX_DIR}/usr_share_ca-certificates/upper/randomnoun.crt >> /etc/ssl/certs/trusted_cas.crt" >> ${CERT_FIX_SCRIPT}
        echo "certutil -d /etc/pki/nssdb/ -A -t 'C,,' -n randomnoun -i /usr/share/ca-certificates/randomnoun.crt" >> ${CERT_FIX_SCRIPT}
        echo "cp -Tr /etc/pki/ /var/palm/jail/com.webos.app.browser/etc/pki/" >> ${CERT_FIX_SCRIPT}
        echo "" >> ${CERT_FIX_SCRIPT}

        chmod a+x ${CERT_FIX_SCRIPT}

        echo
        echo "randomnoun CA configuration complete. Rebooting to apply fix..."

        reboot
else
cat << EOM2
-------------------
FIX ALREADY APPLIED
-------------------

It looks like the fix was already in place.

If you are sure it was not working, please delete the following file:

        ${CERT_FIX_SCRIPT}

Then re-run this script ($0).

EOM2
fi

If it works, it will install the certificates to these locations on startup:

  • /usr/share/ca-certificates/randomnoun.crt
  • /etc/ca-certificates.conf
  • /usr/share/ca-certificates/sdp/sdp-ca.pem
  • /etc/ssl/certs/ca-certificates.crt
  • /etc/ssl/certs/trusted_cas.crt
  • /etc/pki/nssdb
  • /var/palm/jail/com.webos.app.browser/etc/pki/

Discovering it almost works

Once you’ve run that and rebooted, you can try those curl commands to see if those requests are getting MITMed by squid:

root@tv:~# curl -vvv http://www.google.com   # test HTTP access
root@tv:~# curl -vvv https://www.google.com  # test HTTPS access

And all things going well, you should be able to intercept traffic from the handful of apps that use the com.webos.app.browser app.

Other apps will fail with these kinds of messages:

The error message for Netflix The error message from Prime
The error message from Apple TV The empty void from Youtube and Binge

The reason those apps don’t work is they appear to use certificate pinning to prevent this kind of MITM shenanigans.

It might be possible to remove the pinning though. The apps are installed as IPKs, and there’s some other apps that exist which will remove certificate pinning from IPKs, but I don’t see anyone successfully using that on apps running on a TV. Edit: that won’t work .

So after chatting in the discord server, there might be a couple of other things to try, and if/when I do that I’ll update this post.

Failing that, I’ll probably just set up some BGP filters to capture how much network traffic gets sent to different content providers, but I’m assuming they all go through the same CDNs so that’s probably not going to be all that infotaining.

The End.

Oh, to remove those iptables rules to get those apps running again, you’ll need to do this from the raspi:

knoxg@sorbet:~ $ sudo iptables -t nat -v -L PREROUTING -n --line-number   # view rules
knoxg@sorbet:~ $ sudo iptables -t nat -D PREROUTING 2                     # delete rules
knoxg@sorbet:~ $ sudo iptables -t nat -D PREROUTING 1
knoxg@sorbet:~ $ sudo bash -c '/sbin/iptables-save > /etc/iptables/rules.v4'

Update 2023-12-30: Forgot to put the nat and forwarding iptables rules above, so added those in, and reordered some of the steps.

Update 2024-01-28: Added the /var/palm/jail/com.webos.app.browser/etc/pki/ bit, which allows MITMing some of the apps at least


    unless you opt out of that, which you can easily do by:

    • notifying LG via their privacy page, which has links to various third parties, and from where you can “explore your options for opting out of the sale of your personal data”
    • notifying the data brokers that LG uses via the Digital Advertising Alliance webapp, which it turns out doesn’t actually work
    • not agreeing to the ts & cs in your TV, which unfortunately means you can’t use the microphone in your remote control, but hey, like anyone uses that after the novelty wears off
    • clicking on half a dozen separate menu items in the TV settings menu:
      • General -> AI Service -> disable everything
      • General -> AI Service -> AI Recommendation -> disable everything
      • General -> Programmes -> HbbTV -> disable ‘Provide HbbTV Distinctive ID’
      • General -> Programmes -> HbbTV -> enable ‘Do Not Track’
      • General -> System -> Home Settings -> disable ‘Home Promotion’
      • General -> System -> Home Settings -> disable ‘Content Recommendation’
      • General -> System -> Additional Settings -> disable ‘Live Plus’
      • Support -> Privacy & Terms -> User Agreements
        • deselect ‘Viewing Information Agreement’
        • deselect ‘Voice Information’
        • deselect ‘Interest-Based Advertisement Agreement’
        • deselect ‘LG Channels Service’

    obviously

    which appears to be the one thing everyone agrees needs to change in every release of every operating system

    these are IPKs, not APKs. Whole different kettle of fish



Add a Comment

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