Apparatus

Reverse-engineering an IoT cloud service

One of my hobbies is home automation, and as part of that hobby, I love spending way too much time with Home Assistant. Home Assistant is an open-source home automation software suite. It comes with a fairly good UI, powerful and easily configurable scripts and automations, and most importantly: a large ecosystem of third-party integrations for various smart-home devices, cloud services, and anything that can be accessed via the Internet or hooked into a computer.

In late 2023, I bought new smart radiators. These radiators came with a cloud-backed app for controlling the radiators remotely. Usually, before I purchase new smart devices, I ensure that I can integrate them with Home Assistant. In this case, I deliberately went the other way and decided to give something back to the community and develop an integration myself. And that meant first implementing a library that can talk to the cloud service.

Before starting, I contacted the customer support of the vendor to describe my idea of an open-source integration for their product and ask for documentation. While they didn’t provide any documentation, they were neutral or even cautiously positive to my idea, and nothing in their terms of service disallowed reverse-engineering the public cloud service either. Hence, I don’t believe I’m doing anything legally or morally wrong. To be cautious, I won’t mention the brand directly in this article. It is discoverable from my GitHub page if you’re interested.

First idea: Decompiling the official application

The intended way to access the cloud service is via the official app freely available in the App Store for iOS and Google Play. My first assumption was that the easiest starting point would be to decompile the official app and then analyze the source code. Maybe it wouldn’t give me the complete picture, but enough pointers that I could start making requests to the cloud service.

It didn’t turn out to be the case. The source code for the app was obfuscated and without clear entry points to the actual code communicating with the server. It might be that with more effort and more powerful analyzing tools, I could have made more progress. But then again, I was looking for an easy starting point and decided that analyzing the network traffic would be the way forward.

I won’t write more in this article about how to decompile an Android or iOS app. If you want to try it, there are multiple software packages and guides online. Based on my brief experience, I cannot recommend any particular one.

Capturing network traffic 

The first thing to do was to download an Android emulator and the official app. I chose Android-x86 running in VirtualBox. There are many great resources online describing how to install and set it up.

After installing the app on the virtual machine, I now had an application talking to the cloud service, with my laptop acting as the host machine. As the first naive attempt, I decided to try to capture the raw TCP traffic. For that, I used tcpdump on the network interface of the host machine. To no one’s surprise, the traffic was encrypted. In fact, had the traffic been unencrypted, I would have considered my new radiators compromised, and returned them to the vendor. The only thing I learned was that the cloud service was hosted in AWS. In the contemporary IoT landscape that scarcely counts as information at all.

Capturing traffic with HTTP proxy

To analyze encrypted network traffic, I needed a more sophisticated tool than tcpdump. After some research, I decided to try mitmproxy. As its name suggests, it is a proxy that performs a (voluntary) MITM (man-in-the-middle) attack.

The easiest way to use mitmproxy would be to configure it as a regular HTTP proxy, and then configure the app to (voluntarily) use it as an HTTP proxy. Sadly, most apps don’t honor the proxy setting in Android network configuration, and connect to their upstream server directly. The app I was studying was no exception. While this didn’t come as a complete surprise, it meant that to capture the traffic, I would need to run mitmproxy in the transparent mode which was more complicated to set up.

Capturing traffic transparently

HTTPS, the dominant protocol for IoT cloud services, is an end-to-end encrypted protocol. I’m not going into details about how end-to-end encryption works since there are great resources online explaining it in varying technical depth. In a nutshell, the guarantee it gives is this: Even if the traffic is relayed by a proxy server, the packets between the client and the server can only be read by the endpoints. Any attempt of the middlemen to pose as the server (say by performing the TLS handshake with the client and the server, and then decrypting and re-encrypting the traffic both ways) will be detected because the public key used to secure the traffic won't match the one in the TLS certificate. That’s why mitmproxy employs a clever trick to intercept HTTPS connections: It acts as a certificate authority and issues a new certificate for the server on the fly.

To make my Android VM trust the certificates mitmproxy issues, I installed its root certificate to the certificate store on the virtual machine. In practice, this meant copying a file to a directory on my Android VM.

If you decide to try out the techniques described in this post, it goes without saying that you should never install certificates that you don’t trust. When using mitmproxy to analyze the traffic of an application, you’re voluntarily performing an MITM attack on yourself. You should only install the certificate that the mitmproxy instance you control generates, and never share the secret key with anyone. Otherwise, you’re inviting someone else to perform an MITM attack on you.

Planning and executing the MITM attack

The next step was to get my Android VM to send the traffic to mitmproxy for intercepting. The plan was to run the official app within an Android emulator whose gateway to the Internet is the host machine. I could then use network analyzing tools on the host machine to peek into the network traffic.

To do that, I needed the following:

The mitmproxy documentation suggests an overall plan for proxying the network traffic from the virtual machine via the host machine and then intercepting that traffic. The details in the article are somewhat outdated and didn’t work out of the box on a more recent Ubuntu distro (24.04 as of writing this post). Still, I could follow the overall plan:

  1. Configure the VM to route network traffic to a network interface inside the host machine.
  2. Configure DHCP and DNS servers in the host machine. These are used to inject the desired network configuration into the virtual machine.
  3. Configure the host network to forward packages from the network interface used by the virtual machine to mitmproxy.

The network interface that the virtual machine connects to can be entirely virtual. The simplest way I found was setting up a tap interface:

$ sudo ip tuntap add dev tap0 mode tap
$ sudo ip link set dev tap0 address 00:11:22:33:44:55 promisc on
$ sudo ip addr add 192.168.100.199/24 dev tap0
$ sudo ip link set dev tap0 up

Then configure the virtual machine to use it in bridged mode:

 

VirtualBox network settings with tap0 adapter configured in bridged mode.

I also installed dnsmasq and configured it to listen to the tap0 interface I had just configured to assign IP addresses to clients:

$ sudo cat > /etc/default/dnsmasq <<EOF
# Listen for DNS requests on the internal network
interface=tap0
listen-address=192.168.100.199
bind-interfaces
# Act as a DHCP server, assign IP addresses to clients
dhcp-range=192.168.100.10,192.168.100.100,96h
# Broadcast gateway and DNS server information
dhcp-option=option:router,192.168.100.199
dhcp-option=option:dns-server,192.168.100.199
EOF

If you try this setup and have IGNORE_RESOLVCONF=yes or DNSMASQ_EXCEPT="lo" commented in /etc/default/dnsmasq, they have to be uncommented.

The remaining step was to enable IP forwarding from the tap0 interface to the port mitmproxy is listening to. This serves two purposes: To connect the virtual machine to the Internet and also to intercept the traffic.

$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo sysctl -w net.ipv6.conf.all.forwarding=1
$ sudo sysctl -w net.ipv4.conf.all.send_redirects=0
$ sudo iptables -t nat -A PREROUTING -i tap0 -p tcp --dport 80 -j REDIRECT --to-port 8080
$ sudo iptables -t nat -A PREROUTING -i tap0 -p tcp --dport 443 -j REDIRECT --to-port 8080
$ sudo ip6tables -t nat -A PREROUTING -i tap0 -p tcp --dport 80 -j REDIRECT --to-port 8080
$ sudo ip6tables -t nat -A PREROUTING -i tap0 -p tcp --dport 443 -j REDIRECT --to-port 8080

After this, the only thing remaining was restarting the network services, and firing mitmproxy:

$ sudo systemctl enable systemd-networkd.service
$ sudo systemctl restart dnsmasq
$ mitmproxy --mode transparent

Conclusion

After having set up all this, I fired up my Android VM. To my delight, it got an IP from the dnsmasq running in my host machine. Not only that, it could connect to the Internet with mitmproxy faithfully recording every exchange.

The experience wasn’t only delightful, but also a bit frightening. I spent a few hours researching and downloading software from publicly available sources. After that, I could read every supposedly end-to-end encrypted network package my poor virtual machine would send. Granted, I had voluntarily brought this attack onto myself, but still: The core of the attack was one CA certificate that the virtual machine trusted even if it shouldn’t. A typical OS distribution ships with hundreds of pre-installed CA certificates. A significant fraction comes from governments and other organizations interested in your private network usage.

This post is the first part of a two-part series about reverse-engineering an IoT cloud service. It has concentrated on intercepting the network traffic between the client app and the cloud service. In the second part which will be published later, I’ll describe how intercepting the network traffic can be used to write a client of your own.