How to make an Amazon button do your bidding

April 22, 2018

The other day a collogue dropped a small box on my desk with the cryptic message "See what you can do". 

Turns out he acquired a few Amazon Dash buttons and wanted to use them for purposes other than ordering diapers and peppermints.

I had remarked a few weeks earlier how it's almost impossible to stop people from using Amazon Dash buttons outside their normal scope - securing hardware is tricky.  When you own the network on which devices reside it's no problem to fool them it into accepting a reality you designed.

But talk is cheap... now with a button in front of me it was time to turn theory into practice.


Let's talk about what I knew about these buttons at this point: They are sold by Amazon for a low price of 5 US dollars, it talks to the servers of Amazon using your own WiFi network and a single button can be used for a few different products of a specific brand.

The low cost implies that they are using off-the-shelf components and are limited in the trickery they can perform, the fact that it uses your own WiFi means it should at least somewhat behave like a regular device. It should also have some kind of configuration step, otherwise you wouldn't be able to change the specific product that gets ordered. As long I never completed this setup I should never accidentally end up with a lifetime supply of diapers.
amazon-dash-button
With these assumptions in mind I started with some monitoring: Since the button at some point wants to connect over WiFi to the internet it needs to request access to our own network. To track this I ran `tail -f /var/log/syslog | grep dnsmasq-dhcp` on the server that grants devices internet access: When the button wants to go online I should be able to see it.                                         

Now I was ready to turn on the button to see how it works. I followed the short manual that came with the button: I downloaded the Amazon app for my Android phone, logged in on my account and pushed the Dash button. On my phone a popup showed up where I had to enter the WiFi password. I pressed submit and bingo: Our DHCP server saw a request coming by for a new device!

Feb 29 10:08:00 gatekeeper dnsmasq-dhcp[20311]: DHCPDISCOVER(eth1) a0:02:dc:a3:be:30
Feb 29 10:08:00 gatekeeper dnsmasq-dhcp[20311]: DHCPOFFER(eth1) 10.50.254.254 a0:02:dc:a3:be:30

Just for good measure I null-routed the MAC address of the button so it wouldn't be able to go outside our own network no matter what - no internet for you.

no-internet-for-you
The button was active and it was time to see what it was doing.  With a packet logger I dumped all the traffic coming from a0:02:dc:a3:be:30 and soon discovered it first resolves the host parker-gateway-na.amazon.com and then tries to connect to it on port 443, the default port for encrypted HTTP traffic. The use of encryption means I wouldn't be able to see any more details... unless I could fool the button into thinking it's talking to Amazon's server.

While encryption done properly is nearly impossible to break, it's not uncommon to have a badly set up system could still be susceptible to attacks. For example, the Python 2 binary used to skip the verification of the CN/SAN and also skip the certificate signature (PEP 476). Default connections over HTTPs used to be encrypted, but *not* secured!

A nice way to test this is the CLI tools provided by the OpenSSL project. I generated a self-signed key for parker-gateway-na.amazon.com by running

openssl req -x509 -new -keyout key.pem -out cert.pem -days 365 -nodes

and then I ran a simple TLS server with

openssl s_server -accept 443 -cert cert.pem -key key.pem

I tested it out with Firefox: When connecting I got a big warning in Firefox about the connection being unsecured due to a self signed certificate. When I told Firefox to go ahead anyway I saw Firefox's HTTP headers showing in my console.

your-connection-is-not-secure

To get the button to talk to my fake server I modified the DNS server to resolve parker-gateway-na.amazon.com to my own server's IP address. I hit the button and... not much happened. The button would connect, but instead of the button sending the HTTP headers it disconnected. This suggests that either the button can't handle the type of certificate / TLS setup or that it does proper certificate verification.


At this point I could do two things: Try to dig deeper into the SSL business in order to get the button to spill its guts or use the existing observable behaviour to detect when the button was pressed.

A quick Google revealed some people were able to intercept the HTTP traffic, which would be nice because then you could potentially also control the LED color. A downside of this approach would be that any changes introduced by Amazon could easily break the detection, requiring me to fix it again.

Alternatively, I could use the ARP traffic that is used to try to go online to detect button presses. It's a much simpler approach, it's unlikely to break because of Amazon and it already worked reliably.

In the end it came down to my preference for a simple reliable system over a fancy, fragile piece of work: ARP detection it is.​


The last part of this blog post would describe the elaborate process of creating an ARP detection system. Unfortunately, with Python and Scapy it's all quite straight forward and there's not much to discuss.

The interpretation of the script is left as an exercise to the reader.

#!/usr/bin/env python3
from datetime import datetime
from scapy.all import sniff, ARP

BUTTONS = {
    'a0:02:dc:a3:be:30': 'Ice Breakers',
}

def arp_display(pkt):
    # op=1 -> who-has (request)
    # psrc='0.0.0.0' -> ARP probe
    if pkt[ARP].op == 1 and pkt[ARP].psrc == '0.0.0.0':
        mac = pkt[ARP].hwsrc
        if mac in BUTTONS:
            print("{} ARP Probe from Dash button {} ({})".format(datetime.now().isoformat(), BUTTONS[mac], mac))
            # insert mundane API calls here

def run_arp_monitor():
    print('{} Monitoring ARP probes'.format(datetime.now()))
    sniff(prn=arp_display, filter='arp', store=0)

if __name__ == '__main__':
    run_arp_monitor()