Last time I reverse engineered how AirMatters controls air purifiers which are in the same local network. This time we will look into how AirMatters controls devices using the Philips cloud and how secure it is.
Network analysis
The network communication is over SSL, so the first step is to bypass the SSL verification in AirMatters.
The application loads its CA certificates from /resources/assets
and tries to establish a trust chain to them.
My favourite tool for sniffing SSL traffic is mitmproxy
, so I simply took ~/.mitmproxy/mitmproxy-ca-cert.cer
from my machine and re-packaged AirMatters with it.
This is what I found after capturing the network traffic:
- Calling the cloud services requires logging with
client_id
andclient_key
first. - AirMatters is using hardcoded credentials to provision a new account and obtain
client_id
andclient_key
- Every request contains an
Authorization
HTTP header which is verified by the server - You need to pair the
client_id
with the purifier’sdevice_id
and then you can control it over the internet. The pairing is done by sending a special pairing request to the device and another pairing request to the corresponding cloud service.
The problem with this pairing mechanism is that it is completely silent and doesn’t require any physical interaction with the device. This means that an attacker needs one time network access to your device to pwn it forever. As far as I can tell, there is no way to “unpair” a device or to see how many accounts your device is paired with.
The only missing piece in the above is how the Authorization
HTTP header is calculated. This is how it looks like:
Authorization: CBAuth Type="SSO", Client="000000fff10af31f", RequestNr="2", Nonce="kj3PUx56Cpo3UdcIiICoPQ==", SSOToken="kcK0TgzC..", Authentication="XAR9WB443KTDT0A7pNUQZA=="
We get SSOToken
and Nonce
from the response of the login request. The Authentication
is 16 bytes base64 encoded value which is a calculated in libICPClient.so
.
Reversing libICPClient.so
I used Ghidra to reverse engineer this native library and find how the authentication bytes are computed. This is how the relevant function looks like after being decompiled:
It does SHA1 hash of client_id, host, sso_key, the request number, nonce and then it takes the first 16 bytes of the digest:
And this is how I implemented it in Python:
m = hashlib.sha1()
m.update(b'\x05')
m.update(client_id.encode('ascii'))
m.update(host.encode('ascii'))
m.update(base64.b64decode(sso_key))
m.update(struct.pack('<I', request_num))
m.update(base64.b64decode(nonce))
key = m.digest()[:16]
auth = base64.b64encode(key).decode('ascii')
Open source client
With this I put together cloudctrl
which can be used together with airctrl
to provision accounts, pair and control purifiers over the internet.
Check out the README of my project for more information.
Disclosure
After publishing my previous post, I have been contacted by Philips and they told me about their coordinated vulnerability disclosure process. So this time I coordinated my new findings with them before making it public. As a result they put me into their hall of honors :)