The air pollution in Sofia is really high during the winter, so I decided to buy an air purifier for my home. After some short research, I bought Philips AC2729:
it can purify and humidify the air at the same time, show particulate matter (PM 2.5) / humidity, and features quiet sleep mode. It also has a Wi-Fi interface which allows remote control with a mobile app. Being able to control the device with my phone sounds pretty cool but after reading so many stories about the so called Internet of Shit, I thought it’d be a good idea to inspect what kind of data this thing sends and ultimately create my own tool to control it. It’d be a fun side project as well :)
Network analysis
The first step of course is capturing the network traffic that comes in and goes out from the device. There are several ways to do this with a PC and good Wi-Fi adapter but I took advantage of the built-in packet sniffer that my router has and recorded the traffic during the initial setup of the purifier and some interactions with the mobile app. Then I opened the recorded .pcap file in Wireshark and quickly realized couple of things:
- the device is sending data to a Philips server (www.ecdinterface.philips.com), no surprise here …
- the device runs an HTTP server and is controlled via some REST API from the mobile app
- it uses plain HTTP with some custom encryption for the request/response body
I can easily disable the phone-home functionality by putting a firewall rule in my router and limit all traffic to my local network. But I also want to understand how the device is controlled and for this I need to decrypt the network traffic between the purifier and the mobile app.
Let’s take a closer look to the initial interaction between those two parties:
PUT /di/v1/products/0/security HTTP/1.1
content-type: application/json
connection: close
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Pixel XL Build/PQ1A.181205.002.A1)
Host: 192.168.0.17
Accept-Encoding: gzip
Content-Length: 269
{"diffie":"4FB7FC5543219711B7144FDA72E4A2..."}
HTTP/1.1 200
Content-Length: 343
Content-Type: application/json
Connection: Close
{"hellman":"4a5008cbc6e4523f27e...",
"key":"9d90df8e281855117467e09fa75de7aa60097c16657a5cc04b094e7dbb4b0518"}
GET /di/v1/products/1/device HTTP/1.1
content-type: application/json
connection: close
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Pixel XL Build/PQ1A.181205.002.A1)
Host: 192.168.0.17
Accept-Encoding: gzip
HTTP/1.1 200
Content-Length: 128
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Connection: Close
SwZyJzgE66DpOjL6nshHS/xjF9VHxL7Fg0XjHDmDtNhBBBMC4FeuJzBQiQx8QFcIlyoOTaEPUq
QqDmN1f2we0MRjSjyt4aEenNlvtvj/vCa+e15btQcZYWcqTHvpr1Pb
POST /di/v1/products/1/air HTTP/1.1
content-type: application/json
connection: close
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Pixel XL Build/PQ1A.181205.002.A1)
Host: 192.168.0.17
Accept-Encoding: gzip
Content-Length: 65
Mt32GhsP00A6cDdHf+8vBMMKsJU/rZsD7onyiTjNDxPlRhepKspv/lw9GKhhYc6O
HTTP/1.1 200
Content-Length: 256
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Connection: Close
NbV7wqswB4p3/yV6KSBN92c9sLy3tOjbUaCWqfddss1oIUNGl3EPacivoqROasGPS9x8VRwRIV734jQ0Q
Amm/0OehccirajReq0/yEcwV7jo+gbkEIaIUNdVE/XccXptR3VTsO3W/7ge5U9wM7NM9jAz7BgvkKoEtjg
7+3Rs7M9lgiTpcDr85b5NMo/tIpzeKM7+CMtmI9toOaDmnjxqoJO1JhpAk7VP5lCO5m/iFfFWPgeWYSlvw
YK6SGVqzqlP
The first request to /di/v1/products/0/security
is plain text and it seems to be a
Diffie-Hellman key
exchange (truncated by me for readability). The following requests are base64 encoded and
decoding them doesn’t produce anything meaningful. So it looks like a secret key is
exchanged at the beginning and then used to encrypt further communication.
There is only one way to verify this theory – reverse engineering the mobile app!
Reversing the original app
The mobile app is Air Matters and I downloaded its Android version which is an apk file. Then I used the jadx tool to decompile it. With a few greps over the decompiled sources, I found the relevant encryption/decryption classes:
com/philips/cdp2/commlib/lan/communication/ExchangeKeyRequest.java
com/philips/cdp/dicommclient/security/EncryptionUtil.java
com/philips/cdp/dicommclient/security/ByteUtil.java
With this I have been able to recover the following steps:
- The app and the device establish a shared secret using the Diffie-Hellman algorithm
- The device generates a session key and encrypts it with AES using the shared secret
from step 1 as a key. The encrypted session key is set in the
key
property of the response message fromPUT /di/v1/products/0/security
- The session key from step 2 is used to encrypt/decrypt with AES all further requests and responses
I also found the G
and P
values used for Diffie-Hellman in ByteUtil.java
:
static final String GVALUE = "A4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507FD6406CFF14266D31266FEA1E5C41564B777E690F5504F213160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28AD662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24855E6EEB22B3B2E5";
static final String PVALUE = "B10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C69A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C013ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD7098488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708DF1FB2BC2E4A4371";
However, this is not enough to find the shared secret which is being established.
In fact, G
and P
are public values. Cracking Diffie-Hellman boils down to solving the
discrete logarithm problem which is to find a value "a"
such that G^a mod P=A
.
We already have G and P from the decompiled source and we also have A
which corresponds
to the "diffie"
property in the network request. The shared secret is equal to
B^a mod P
where B
corresponds to the "hellman"
property in the network response.
The value "a"
is by definition a big random number which makes solving G^a mod P=A
very
hard. Let’s see how this random exponent is generated in ByteUtil.java
:
public static String generateRandomNum() {
return String.valueOf((new SecureRandom().nextInt(2147483546) + 1) + 101);
}
It seems this random number is only 31 bits. It also seems that the person who wrote this
didn’t know what he is doing :) Solving the discrete logarithm problem when a
is only
31 bits turns out to be not hard at all. In fact, it can be solved for less than a second
using the Baby-step Giant-step
algorithm. Check out my implementation at https://github.com/rgerganov/dlp.
Open source client
After finding the session key and decrypting the entire traffic, I have been able to implement my own fully-featured command line client. Check this out:
$ ./airctrl.py 192.168.0.17
[pwr] Power: ON
[pm25] PM25: 4
[rh] Humidity: 32
[rhset] Target humidity: 60
[iaql] Allergen index: 1
[temp] Temperature: 22
[func] Function: Purification & Humidification
[mode] Mode: M
[om] Fan speed: 2
[aqil] Light brightness: 100
[wl] Water level: 100
[cl] Child lock: False
As an added bonus, it also shows the current temperature which is missing in the original mobile app :)
Future work
As I said the purifier has a phone-home functionality which is using another form of custom encryption but this time implemented in a native library. Reversing this part will take more time but something tells me it may allow doing some crazy stuff ;)
Stay tuned!
UPDATE (Dec 2019): I also reverse engineered how purifiers are controlled over the internet.