Hacking My Water Meter


Two years ago our fish pond overflowed, our ever expanding Centella Asiatica crept into the buoy valve container and the valve remained open. The pond overflowed to the drainage and it took us days (and a lot of spilled water) to realize this.

We added a water meter to the pond, but in all honesty - who has the energy to go check on it manually?

Our Kibbutz has a wireless water meter system. I know this because I always look for antennas when I walk around (one of my many fetishes) and while walking around one day, I saw new boxes with antennas on the top of some street light poles.

I asked around and was told that these are the water meters’ relays.

The first thing that jumped to my mind was “if it’s wireless - I can intercept it!”, I checked the specs page of my water meter (MV-MA (NT)) and it said that the unit uses WiFi.

Knowing the power requirements of a bi-directional WiFi connection, I realized that unless they use some sort of dynamo, charging the system from the water flow, there is no way to set up a 2 way communication running on battery alone. I therefore concluded that the unit must simply be transmitting blindly over a WiFi Frame.

I ditched the idea of working on it due to lack of time, but then Coronavirus came in. Being trapped in my own house for about a month has had its effect and I cleared a weekend for this project.

Ethical Hacking

It is imperative to mention that my intention was, and still is, to only remotely read my own water meter. I do not condone any kind of manipulation of this data; this is not only illegal and immoral, it is also very foolish as these systems implement methods to detect such tampering and most places would also implement manual, physical reading every once in a while.

Source code

All the source code and configuration mentioned here are available at my github repo

Step One: Get the suitable hardware for the task

The first step was to get a unit suitable for sniffing, I had a few old Ubiquiti Bullet and Nanostation M units laying around, they were all bricked so I had to revive them using a ttl cable.

These units are really great to work with:

  1. They can run OpenWRT which means that I can customize everything.

  2. They have enough RAM (32MB) and Flash (8MB) to have a decent firmware loaded.

  3. They run off POE, so I could easily power them up and not worry about having an available power outlet next to my water cabinet.

  4. They have an excellent radio that can be configured to work in monitor mode.

  5. They are bulletproof and are suitable for being installed outside.

(Pictured is actually an M5 unit, but I used it to test my custom firmware)

Step Two: Run the tests


After hooking the ethernet port in my water meter cabinet into my switch and separating it into a different VLAN (I am not going to put my home LAN exposed like this…) I installed stock OpenWRT + tcpdump, socat and netcat on the unit.

I used OpenWRT10 because it was supposedly the latest version to support XM board revisions of the Nanostation Loco M2 (I knew this must be an error but didn’t care at the time, I just wanted to get things in motion and latest version (19.07) did not work well on my bullet test boards.)

Setting up for the initial sniffing!

On the Nanostation, after putting my wifi device into monitor mode, I simply ran tcpdump into netcat:
stdbuf -i0 -e -o0 tcpdump -ns0 -i wlan0 -w - | nc -l -p 7777
I also implemented a simple channel hopper because I had no idea which channel the meter would transmit on.

I then sniffed everything with wireshark running on my laptop connected to the remote socket. To do that I ran:
stdbuf -i0 -e0 -o0 nc 7777 | wireshark -k -i -

stdbuf is critical here as unix pipes are buffered (anything between 4 to 65K, osx has it at 16K). Without it, I would have had to wait a long time for the pipe buffer to fill until I actually saw anything, In addition, once I began to filter the frames, the problem would have become more severe.

OK, so I could sniff and all this is great, but I still had no idea which of the transmissions around me was MY water meter (There is a lot of noise in the 2.4GHz spectrum…).

Finding the needle (my water meter) in the WiFi haystack

The next step was to understand which transmission was originating from my own water meter.
I had my Nanostation M2 Loco placed a few cm from the water meter’s antenna.

This way I could differentiate it from other signals by RSSI (Received Signal Strength Indicator).

At 5cm it was going to blast my receiver.

Having my water-meter cabinet covered by a metal door and sides helped a lot to attenuate any other signal from my neighbors acting as a Faraday cage.

I added an RSSI column to the wireshark display, The Radiotap data contains this information so it's pretty straightforward.

I immediately saw the transmissions from my water meter. Sorting the signal by RSSI showed that it was much stronger than anything else I was receiving.

You might be thinking "but my home access point blasts and this meter probably sends a much weaker transmission".
Well, you are right! but the nearest access point was roughly 30m away, so even without factoring in the metal cabinet it was already roughly 70dB weaker by sheer distance alone.

This allowed me to filter out everything else and focus my effort on the actual transmissions originating from my own meter.

Finding the payload

The next step was to understand what was being sent and how to read it.

Looking at the data itself, I noticed that all probe requests had an undecoded sublayer, this means that wireshark cannot interpret the data (without using additional dissectors), this is good! If Wireshark can’t decode it - then it is most likely the water meter’s payload! I also saw the very same pattern being overloaded into the beacon frame sent by the same unit as “Vendor specific data”.

I saw that bytes 19&20 (49&50 in the entire frame) changed from 0 to something else when I turned on the tap, my gut feeling told me that this was most likely indeed the right section containing my water meter’s payload.

Analyzing the payload

The next step was to analyze the payload. In order to do that, I wrote a small python script to display the data and highlight any changed bytes between each frame:

You’ll notice that bytes 19 & 20 change from 0 to other values,
while byte 13 (and 14, not shown here) slowly grow.

I was still unsure as to the endianness and the type to read (int/float/etc…)
I then thought that the payload would most likely contain the serial number printed on the sticker (partially removed for obvious reasons… Starts with R1000 just above the LCD)

So let’s assume that the sticker said 2864434397. This translates to 0xaabbccdd, I couldn’t find it in the payload, but guess what? As you can see, 0xddccbbaa is there, Eureka: I found both the serial number and the endianness of the data! (little endian)

This also gave me an anchor as I knew that this was a whole int.

I guesstimated that the next 4 bytes would be the overall water consumption as it was slowly incrementing when water was flowing. I decided to naively look at the bytes as an integer with the same little endianness, guess what? 0x2de5 is 11749, my water meter (as pictured above) shows 117m²! Voila! Here is the volume measurement, in DAL (10 liters) increments.

I took the same approach for the next suspicious bytes (19-20) that I already guessed would be the flow since they zeroed when nothing was flowing.
For example, from line #3 in the previous illustration: These bytes are 0x8203. This would be 898. This is the flow in cL/min, so 898cL/m = 8.98 l/m. This was in line with the display on the meter as well!

Job done! Bytes 09-12 are our Device ID, 13-16 are our volume measurements in DAL, bytes 19-22 are our flow measured in cL/min.

But, is it “done-done”?

Step Three: Build custom firmware

Ok, so I had a stock firmware running with tons of stuff that I didn’t need (http server, firewall, a bunch of loaded yet unused kernel modules, etc...).

The next step for me was to build a customized OpenWRT firmware, compiled with only the bare necessities to be as conservative as possible, in both flash and RAM.

It’s really great to build everything you need straight into the firmware because it’s being compressed with SquashFS for a minimal flash footprint.

After Some glitches I managed to get the latest trunk up and running.

The idea was simple:
I used tcpdump served by socat and decided to remote process the data on my home server that would simply connect to the socket.

A better approach would have been to just run everything off the unit itself, but without mounting a remote filesystem it’s impossible to run Python (and Scapy) on this unit, so I chose the simpler path.

The script on the server would read the payload from the remote unit and push it into my mariadb server so I could analyze it later.

I had some old recordings that I ran in the background as I was still investigating the structure so I was able to backfill a few days worth of data.

My first Win

I graphed everything using highcharts and something immediate popped up

Looking at the graph I saw that every 2 days around 5am there was a very high consumption of about 2.2m² (2,200 liters/581 gallons!). I knew that this was in-line with my drip irrigation schedule but 2.2m² of water seemed very excessive to me. I turned it on manually and apparently one of the pipes’ terminations had burst and water was flowing out like endless rain onto the curb.
I had it fixed and consecutive figures went down to ~1.3m²(!)

More than the excessive costs (roughly 180 NIS a month or $50 US), the wasting of nearly 14 tons of water every month broke my heart.

I also started paying more attention to the time I spent in the shower, it’s really easy to identify water consumption, especially showers. My average shower consumed nearly 170 liters (I like standing under the hot water…), I started to reduce both the length of my showers and the water flow, my goal is to get under 100 liters per shower and then continue reducing from there.

What’s next?

Adding alerts

I am going to add some alerts and perhaps get into the world of ML to send alerts on water consumption anomalies. Our pattern is pretty stable; any deviation should send me an email but for a quick win, I’ll just monitor specific things right now.

Every byte counts

If I have the time, I’d like to understand what the rest of the data means.

Bytes 1 and 2 represent an increment that counts the number of transmissions.
Byte #4 counts down from 0x14 and resets at 0x0.

Other bytes are completely static and others have weird patterns right shifting 8 bits every few transmissions. They must mean something.

The last 2 bytes change more when there is a flow but I haven’t managed to get the sense of what they mean yet. It doesn’t look like checksum as it is sometimes static when the payload changes and sometimes changes when the payload is static.

Add a shower consumption “clock”.

I am going to put a large 7 segments display visible from the shower and it will display current water consumption so that I‘ll be able to know how much water I consume while still being in the shower


I was really lucky to get this done so easily. The data was encoded in a very straightforward way and most importantly, it was not encrypted.

Having visibility into this data means that I can detect leakage, excessive usage as well as improve my water conservation methods.

On the other hand, the fact that our data is so easy to collect remotely raises a few questions as to whether or not privacy was even considered when this implementation was devised. I can think of a few very malicious ways to exploit the system and the fact that the transmissions are not encrypted or signed in any way makes it, again, pretty straightforward.