Present Location: News >> Blog >> Withings Scale Hacking

Blog

> Withings Scale Hacking
Posted by prox, from Charlotte, on September 18, 2010 at 21:25 local (server) time

I came across the Withings Wi-Fi Scale on ThinkGeek awhile back, but only recently decided to purchase it.  Over the past few months, I've (on & off, I might add) been keeping a record of my mass each morning, using a piece of paper and pen, then transferring it to digital form when it seemed necessary.  I figured a change was needed.

The Withings scale was easy to setup, but only afterwards did I realize I'd be forced to use a crappy Flash-based web portal (or iOS app.) to view reports and graphs on my mass, BMI, and body fat.  However, I was able to hack the communication between the scale and the web portal, and actually keep track of the data myself.

I'll try to describe the process I took, without getting into too much detail.

The Setup

Let's start from the beginning.  I received the Withings scale in a rather huge cardboard box.  I'm not sure what ThinkGeek was smoking, but the size of the scale (in its box) was fairly tiny compared to the person-sized cardboard shipping container it arrived in.  I opened the box to find no manuals, just a USB cable, batteries, the scale, and a single sheet of paper that directed me to the Withings' web site to sign up and setup my scale.

Withings Scale

This is where I started worrying that I might have to use a web portal to view things, but I kept going anyway.  The site instructed me to plug in my scale via USB, and then download an installer executable.  Strangely enough there was one for Linux, so I grabbed that one, and used it to setup the Wi-Fi connectivity.  It also did a firmware upgrade, supposedly.

I logged into the web portal, and.. yep, Iceweasel crashed instantly.  Honestly, that's rare for me, so I had to use Windows 7 in the meantime (turns out it was time for me to ditch the amd64 build of the Adobe Flash 10.0 plugin, and upgrade to 10.1 w/nspluginwrapper).  After filling out my name, nickname, current mass, height, age, and sex, I tried out the scale.  The LCD display lit up as I stood on it.  After a few seconds of repositioning myself with the aid of the arrows that lit up on the screen, it displayed my mass and then BMI.

Mass

The web portal accurately reflected these values, with the exception of body fat, which I figured would show up after a few more data points.

Web Portal

The web portal was clunky, and generally difficult to use.  To make matters worse, it was horribly slow.  Sometimes it would take 10-20 seconds for the points to display on the graph.

The Discovery

Irritated by the sluggishness of the web portal, I did a trace to my.withings.net, and a whois on the associated IPs (88.191.98.77 and 88.191.97.104):

[...]
 4  paix-ny.proxad.net (198.32.118.197)  0.576 ms
 5  londres-6k-1-po103.intf.routers.proxad.net (212.27.58.205)  81.819 ms
 6  bzn-crs16-1-be1102.intf.routers.proxad.net (212.27.51.185)  82.700 ms
 7  dedibox-2-p.intf.routers.proxad.net (212.27.50.162)  82.367 ms
 8  88.191.2.57 (88.191.2.57)  82.428 ms
 9  s1.withings.net (88.191.98.77)  82.162 ms

inetnum:       88.191.3.0 - 88.191.248.255
netname:       FR-DEDIBOX
descr:         Dedibox SAS
descr:         Customers
descr:         Paris, France
descr:         NCC#2007023902
remarks:       trouble: Information: http://www.dedibox.fr/
remarks:       trouble: Spam/Abuse requests: http://www.dedibox.fr/abuse/
remarks:       trouble: Spam/Abuse requests: mailto:abuse@support.dedibox.fr
country:       FR
admin-c:       ACP23-RIPE
tech-c:        TCP8-RIPE
status:        ASSIGNED PA
mnt-by:        PROXAD-MNT
source:        RIPE # Filtered

Well, that's why it's slow.  It's in Paris, in addition to the Flash content.  It's also hosted at a dedicated server provider, Dedibox (now online.net?), which is well-known for cheap hosting solutions (that usually means it's crawling with botnet controllers and warez).

I then took a tcpdump of the traffic from the scale, and found that it was talking to another destination in that same network range, scalews.withings.net. [88.191.224.77].  In fact, the communication wasn't encrypted and fairly basic.  After following the TCP stream with Wireshark, I figured out the basic order of operations:

  1. User steps on scale, then steps off.
  2. After 15-30 seconds, scale connects to Wi-Fi network, and obtains an IP by DHCP (no hostname given in the DHCPREQUEST packet).
  3. Scale does DNS lookup for scalews.withings.net.
  4. Scale connects to TCP/80 on IP returned for scalews.withings.net. and issues 4x POSTs, while getting JSON-encoded data in return
  5. Scale disconnects from Wi-Fi without sending DHCPRELEASE (meanie!)
  6. (data appears on my.withings.com portal page shortly after)

For your viewing pleasure, the entire conversation can be found here.  As with the rest of the stuff I'm posting here, don't think I've kept all the values intact.. I'm not an idiot!

The first thing that jumped out at me was that the server runs Ubuntu, and spills all of its version information.  Apache 2.2.8, PHP 5.2.4-2ubuntu5.10.  Jeez, 5.10 uh.. "Breezy Badger" is back from 2005!  I'd say they need to upgrade.

The 4x HTTP POST requests are pretty simple:

POST /cgi-bin/once: Sending a simple "action=get" apparently just asks the server for a random ID or cookie value.  It returns back this value along with a status (0, which apparently is equivalent to "no error").  I don't think the scale cares if it gets the same one every time.

POST /cgi-bin/session: The scale then sends an "action=new" along with the MAC address of the scale, an MD5 hash, firmware version, battery level, and two other values that I haven't been able to figure out (duration & zreboot).  The server then responds with a larger JSON-encoded string:

{"status":0,"body":{"sessionid":"546-4c818c8e-3b918091","sp":{"users":[{"id":101010,"sn":"PRX","wt":66.3,"re":565,"ri":2337,"ht":1.7,"agt":30.2,"sx":0,"fm":1,"cr":1283558096,"att":0}]},"ind":{"lg":"en_GB","imt":1,"stp":0,"f":0,"g":97973},"syp":{"utc":1283558546},"ctp":{"goff":-14400,"dst":1289109600,"ngoff":-18000}}}

Let's go what I've been able to figure out of the values, here:

POST /cgi-bin/measure: This is the good stuff.  The scale then POSTs a whole bunch of data that we care about:

action=store&sessionid=546-4c818c8e-3b918091&macaddress=00:24:e4:ff:fc:01&userid=101010&meastime=1283558512&devtype=1&attribstatus=0&measures=%7B%22measures%22%3A%5B%7B%22value%22%3A66850%2C%22type%22%3A1%2C%22unit%22%3A%2D3%7D%5D%7D

The real JSON of the values looks like this:

{"measures":[{"value":68950,"type":1,"unit":-3},{"value":583,"type":2,"unit":0},{"value":2292,"type":3,"unit":0}]}

Apparently, though, not all measurements may be given.  I think sometimes the scale can't determine bioelectrical impedance (the re & ri values), if the user is wearing shoes (duh).

POST /cgi-bin/session: The scale just deletes the session it had, here.

So, certainly hackable!

The Hack

So, my first thought was getting the scale to talk to one of my webservers instead of scalews.withings.net.  Easy enough!  A few lines in the named.conf of my local DNS cache and a small zone file did the trick:

// Withings
zone "scalews.withings.net" {
     type master;
     file "/etc/bind/prolixium/scalews.withings.net";
};

Zone file:

$ORIGIN .           
$TTL 3600 ; 1 hour
scalews.withings.net IN SOA atlantis.prolixium.com. hostmaster.prolixium.com. (
      2010091101 ; serial
      7200       ; refresh (2 hours)
      1800       ; retry (30 minutes)
      1209600    ; expire (2 weeks)
      3600       ; minimum (1 hour)
      )
      NS   atlantis.prolixium.com.
      A    10.3.4.6

atlantis is my local DNS server and 10.3.4.6 is the webserver (dax) I want scalews.withings.net to resolve to.

destiny% host scalews.withings.net.
scalews.withings.net has address 10.3.4.6

Good enough!  I then wrote a Python script to respond to each of the queries, store the bioelectrical impedance, mass, timestamps, MAC address, and battery levels in a MySQL database.  It took a little bit to get everything working right (mostly fighting with Python modules), but I eventually got it returning responses that made the scale happy.

Rather than creating a bunch of separate Python scripts, I just made symlinks to the various URLs that receive POST requests:

lrwxr-xr-x 1 root wheel     11 2010-09-11 17:19 measure -> withings.py
lrwxr-xr-x 1 root wheel     11 2010-09-11 17:19 once -> withings.py
lrwxr-xr-x 1 root wheel     11 2010-09-11 17:19 session -> withings.py
-rwxr-xr-x 1 root wheel   3983 2010-09-11 22:02 withings.py

The MySQL schema looks like this:

CREATE TABLE `measure` (
  `sourceport` int(11) DEFAULT NULL,
  `sourceip` varchar(15) DEFAULT NULL,
  `mac` varchar(20) DEFAULT NULL,
  `battery` int(11) DEFAULT NULL,
  `firmware` int(11) DEFAULT NULL,
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `recvtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

CREATE TABLE `value` (
  `value` int(11) DEFAULT NULL,
  `unit` int(11) DEFAULT NULL,
  `type` int(11) DEFAULT NULL,
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `measid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `measid` (`measid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1

I tested it for a bit, and realized that sometimes the scale won't send the measurements to the server after every step on it.  I'm not sure why, maybe it's a bug - it'll queue it up and then send two POSTs to /cgi-bin/measure on the next connect.  Also, it seems very inconsistent on whether it'll actually report the bioelectrical impedance, or not.  Even barefooted, it seems hit or miss.

Also, I think that my script isn't perfect, or it's sending some wrong values to the scale, because sometimes my BMI is 99.9, which means I'm as big as a house, or something:

Bad BMI

Also, because I have this love affair with MRTG, I wrote another Python script to provide MRTG with a snapshot of the last reported values (mass, battery, re, ei) to store in a couple RRDs.  Here's the graph for mass (via drraw):

Withings MRTG

You can grab all of my scripts from here.  To use them, make sure you change the variables (MySQL credentials, user profile, etc.) and the MD5 hash value and cookie returned from the "once" script.  They may be related, and the values in the scripts are just examples, they will not work most likely.  This means you'll have to do a packet capture yourself.  Sorry.

Other Thoughts

The scale itself takes 4x AAA batteries, and eats them like crazy!  It came with 4x off-brand batteries that were depleted after the initial setup and about 10-12 measures.  I replaced them with some Energizer "advanced" lithium batteries, and it seems to now only decrease by 2-4% for every measure.  Still quite a hog, considering it's only on for a few seconds during and after the measurements.

I wish the top of the scale wasn't a mirror.  It feels like glass, and is very reflective, so any footprints mess with my OCD.

I'm a little worried that this thing is going to do an automatic firmware upgrade on its own, and break the scripts.  Watch out for it!  Maybe applying a firewall rule to the scale would be a good idea to prevent such things from happening.

I created a live dashboard of the data I've been graphing.  Yeah, my mass seems to fluctuate quite a bit…

Comment by @russenreaktor on October 12, 2010 at 19:01 local (server) time

Very great post. I will definitely buy this product ;)

Comment by Stefan Fouant [Website] on November 07, 2010 at 01:59 local (server) time

This is pure genius!  Really, lots of good ideas in here!

Comment by Rapsoelfabrik on January 01, 2011 at 19:34 local (server) time

Thankx. Usefull information. I have redirected scalews.withings.net and s5.withings.net to a local server, which receives the initial POST. But, after sending the "once" the scale doesn't answer with the further POST's.

Any idea?

greez

Comment by Mark Kamichoff [Website] on January 01, 2011 at 23:08 local (server) time

Hi Rapsoelfabrik.  I had to sniff the first couple connections to the real scalews.withings.net before being able to correctly respond to the POST to /cgi-bin/once.  I then replayed the ID or cookie value that's sent from the real server.  I don't think just any value will work.  The values shown in my blog entry are only samples, they are slightly modified from what I saw on the wire, and probably won't work if copied verbatim.

Comment by Rapsoelfabrik on January 02, 2011 at 16:19 local (server) time

The problem were the correct HTTP-Headers. As I use IIS, that sends its own Microsoft IIS 7 specific header values, the scale refuses any following POSTs. I had explicitly to set the "Ubuntu"  Values in the HTTP-Headers. The Scale obviously checks the Headers. Cheap trick :)

Comment by Mark Kamichoff [Website] on January 02, 2011 at 16:53 local (server) time

Rapsoelfabrik, wow.. that is indeed a cheap way of doing validation.  I guess I didn't run into it because I replayed the "server" header along with the other values, without even thinking!  I guess I got lucky, thanks for the info.!

Comment by Rapsoelfabrik on January 02, 2011 at 19:33 local (server) time

Oh dear. I was completely wrong. The Server name in the header doesn't matter at all. "transfer-encoding" needs to be "chunked", so the byte counts is important. With IIS just always put a Response.Flush() at the end of your "once" and "session" and IIS will automatically add the correct transfer-encoding and length info.

Comment by Newman on March 21, 2011 at 00:40 local (server) time

Hello, I'm doing this experiment, but I'm new to python script. Could anyone tell me or share the python scripts which respond to each of the queries. Thanks a lot.

Comment by Newman on March 21, 2011 at 01:14 local (server) time

Oh.. how blind I am, I found the author's python script at the end of the post. Anyway, I tried PHP to get the POST and write it to a file using "print_r($_POST)", but it got nothing. Does anyone have a clue?

Comment by Newman on March 23, 2011 at 09:44 local (server) time

We got it in PHP. Here is my steps. Assuming your DNS (Bind9) and webserver (Apache2) is working.
1. Create "cgi-bin" folder in /var/www/
2. Comment out original "cgi-bin" configuration in "/etc/apache2/sites-enabled/000-default", like #ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
3. In "cgi-bin" folder, crate .htaccess, withings.php and 3 softlinks as follow:
-------.htaccess--------
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule [b]^/([/b]([^/]+/)*[^.]+)$ /$1.php [L]
-------- withings.php -----
<?php
print '{"status":0,"body":{"once":"21112cb3-1b433eef"}}';
$myFile = "output.txt";
$fh = fopen($myFile, 'a') or die("can't open file");
foreach ( $_POST as $key => $value ) {
fwrite($fh, $key . " " . "=" . " " . $value . "\n");
}
fclose($fh);
-------- 3 links -------
measure.php -> withings.php
once.php -> withings.php
session.php -> withings.php

If succeed, you'll get it in output.txt

Comment by Newman on March 23, 2011 at 09:45 local (server) time

We got it in PHP. Here is my steps. Assuming your DNS (Bind9) and webserver (Apache2) is working.
1. Create "cgi-bin" folder in /var/www/
2. Comment out original "cgi-bin" configuration in "/etc/apache2/sites-enabled/000-default", like #ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
3. In "cgi-bin" folder, crate .htaccess, withings.php and 3 softlinks as follow:
-------.htaccess--------
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule [b]^/([/b]([^/]+/)*[^.]+)$ /$1.php [L]
-------- withings.php -----
<?php
print '{"status":0,"body":{"once":"21112cb3-1b433eef"}}';
$myFile = "output.txt";
$fh = fopen($myFile, 'a') or die("can't open file");
foreach ( $_POST as $key => $value ) {
fwrite($fh, $key . " " . "=" . " " . $value . "\n");
}
fclose($fh);
-------- 3 links -------
measure.php -> withings.php
once.php -> withings.php
session.php -> withings.php

If succeed, you'll get it in output.txt


> Add Comment

New comments are currently disabled for this entry.