How I hacked the vote at Chrome Dev Summit
I wouldn’t say I have a problem. I have an inclination. I like to take things apart and change how they work. As a kid I’d stay on the computer all night poking at bits in memory trying to change a program’s behavior. Most programs would just break. Sometimes you’d hit the right bit and be rewarded with infinite cash in a computer game.
Fast forward a few decades and this inclination led to a career in software development and web security. Websites are perfect for the curious. You can view source code, inspect every request, and tweak an app live via the dev tools. As an application developer, it’s a nightmare. But that’s a different post.
Last November, Google hosted its annual Chrome Dev Summit, a conference by web developers for web developers. It’s a beautiful event put on by brilliant people. Google’s top developer advocates emceed the event and kept the audience engaged between talks.
The conference celebrated web features released over the last year and emerging tech that everyone could look forward to. The emcees embraced that theme with an interactive game, the Big Web Quiz, that pitted each feature up against another in a battle of Ultimate Web Tech. Between every talk, the audience got to vote on their favorite features by using a simple web app.
Can you see where this is headed?
Online voting is easy to manipulate and this was an opportunity to see the effect live. Is it wrong? Well, it’s not “right.” But I was at a conference full of developers and unwanted, automated manipulation is a problem they aren’t paying enough attention to. That was enough for me. Let’s get at it then.
Here’s the kickoff for the quiz to get a feel for how it worked.
The speakers took turns presenting the candidates for each matchup before turning votes over to the audience. Here’s one of the actual matchups, take note of how long the voting period lasts.
The audience can interact with the app for a brief period between the time voting starts and closes. That was less than 20 seconds for this matchup. Others were even shorter. There were fourteen matchups per day over two days. That left twenty-eight windows of less than half a minute each, at forty-five minute intervals, to complete the task. That’s best case. I didn’t start thinking about it until the end of day one.
There was a second problem. I could only see the results projected on stage. There was no feedback in the app. The only way I could confirm a script worked was by skewing enough votes to make the impact obvious, live, during the voting window. I had to have a script working in advance of the final matchups so I could fix bugs before it was over.
The app communicated via websockets so I turned to Firefox’s websocket debugger to inspect the traffic. Firefox makes it easy to see incoming and outgoing messages. Intercepting and simulating network traffic is the fastest way to control an app without diving into its core logic.
window.someIdentifier = targetVariable, is all it takes to expose deeply scoped variables to the global namespace. Tossing variables onto the global makes them accessible on every refresh without dealing with breakpoints. I used this technique with the websocket instance variable to test requests and responses and see how the app responded.
Keep in mind I had to inspect, debug, and iterate in twenty second chunks. Each time the poll ended I closed my laptop and moved on for forty-five minutes.
I did find out how to trigger the voting-state by either simulating a websocket response or setting the app state directly. This extended the hacking window by letting me inspect app behavior outside of voting.
You can still visit bigwebquiz.com today and play around. You can break on line 128 of the prettified source…
…and set your own state like below…
You can then submit your response and inspect the websocket messages using Firefox.
The first step is to replay messages and see if they have the intended effect. I wrote the first attempt in the devtools console using a loop that sent requests via the now-global websocket instance. Low-level requests are always preferred, they execute the fastest. I could pump out thousands of requests in seconds. Thousands of requests should have a clear effect the next time voting opened. But they didn’t. There was no effect. I expected that but tried anyway. I closed my laptop and waited another forty-five minutes.
Refreshing the page didn’t reset the voting state but jumping into incognito mode would. I could try using Puppeteer to script Chrome and open up incognito windows before submitting votes. Using Puppeteer is much slower than making low level websocket requests. I had to parallelize Chrome instances to maximize the number of requests I could make in a voting window. Even still, we’re only talking hundreds of requests over a twenty second period.
The first attempt failed hard. I screwed up how I handled promises in a loop and brought down my computer. Turns out MacBook Pros can’t handle starting hundreds of Chrome instances at once. Another forty-five minutes down the drain.
By the end of the conference I was running out of chances. Incognito mode alone may have worked but I wasn’t sure if IP addresses played a role in limiting vote abuse. With the number of matchups dwindling I updated the script to proxy through Luminati, a service that hides the source of your traffic via proxies. The script wasn’t long. Here’s the lot of it, minus any Luminati account details.
This should work. As if by divine intervention, a matchup with a clear winner was next. Even the hosts resigned themselves to the futility of the vote:
Audio device client was the clear underdog. Let’s see if our script was up to saving the poor feature.
Something happened! Maybe the audience really wanted Jake to win. Probably not. Who could vote against the combination of portals AND Paul Lewis. Only a soulless robot would do that.
What was I hoping to accomplish here, anyway? I forgot. Let’s do it again.
Looks like the bot was discovered. Props to Jake, Mariko, and Paul for handling this live on stage without missing a beat. I did feel guilty and confessed on Twitter right away. I’m not cut out for a life of crime.
Here’s the response after my confession.
Paul looks more miffed than Jake and for that I felt bad. I’ve been on stage plenty and I should know better than to throw a wrench into someone else’s live show. Also, portals really should have won. Not cool on two counts.
Wait, I remember why I did this! It was to expose how bad actors can manipulate our apps in unwanted ways. Wait, no, it was fun. I did it because I have a problem. An inclination. The bad actors bit still fits though.
Much to my surprise (and eventual relief), Jake Archibald reached out via Twitter to see if I was available to receive a “prize” during the next intermission.
Right. “Don’t worry it isn’t a trick” is exactly what I would expect to be told right before being tricked. I played along regardless. I told you I wasn’t cut out for this.
And that’s how I became a failure at life.
What’s the moral of the story? Break stuff. You’ll either get a job or a T-Shirt out of it.