Reverse Engineering my Router (Part 1)

Jack Reeve
Version 1
Published in
7 min readNov 20, 2023
Router with 4 PIR motion sensors on top

My smart home uses WiFi PIR motion sensors to control the lights in each room. These connect to my network when motion is detected and just chill there for a bit before disconnecting (about 10–20 seconds). It’s up to the listener to watch for sensor’s presence. My NodeJS app does this by pinging each of their static IPs every 3 seconds, the device is online (and thus motion detected) if the ping gets a response. This works, but it seems inefficient and is unlikely to scale beyond the 4 PIR detectors currently in use. There must be a better way.

Surely the router itself must know what devices are connected? If we can obtain that information then we can ask the router instead and eliminate (most of) our scalability issues. Luckily, my router does display this information on its web admin panel (as I would expect most do).

My end goal here is to be able to fetch a list of the connected devices automatically and programmatically from the router (that is, without my intervention). As a general rule, if I can see the information on a page in front of me, it will be possible to grab programmatically. There are two main ways to go about this:

Automate the browser using tools like Puppeteer

This would be the easiest since we’re effectively just mimicking a human using the website. The code already exists for encrypting, decrypting and displaying that information, we just need to interpret it from HTML. The downside is that automation tools like Puppeteer running a headless chrome is very resource intensive (you’re running a browser basically) and much slower since you’re running all the extra code that comes from the vendor.

Reverse Engineer

A more efficient method would be to reverse engineer the code run in the browser into our own. Breakdown what the original code is doing and strip out all the unnecessary bits like rendering into HTML etc…

The first option is totally valid, and always good to have as a backup, but I would urge you to try reverse engineering first. It’s more flexible and might learn something in the process. Most web communication I have seen just relies on HTTPS for its encryption and thus is transparent to us. You can see all the required information from the network request itself, the headers, body and cookies. It’s easier to understand and replicate for your own purposes.

Things get more complicated when encryption or signing or hashing is involved. Complicated is not impossible though, and the more you do this sort of stuff the better you’ll get at it. If you find that something is too complex (like bypassing Cloudflare’s bot detection and having to solve a captcha) then the first option might be favourable.

We’re going to be attempting reverse engineering…

First step here is having the network tab open in devtools to capture any HTTP requests made when navigating between pages. I can see that my router makes use of XHR to fetch its data dynamically without having to reload the page. I find what I believe to be the request for getting connected devices judging by what I clicked on when it was made.

Instead of seeing a GET request with a response of plaintext device information, I get one long 26KB string of what looks to be base64. Plugging this into a base64 decoder reveals garbage symbols

Network tab showing request payload and response data
Failed attempt to decode the response data

Okay, so if we’re going to emulate the client here we’re going to need to learn how this communication works.

Reverse engineering can be a very time consuming process, it always worth having a quick search around the internet to see if anyone else has solved this exact problem before.

From my prior research, I found that other libraries for older models were using AES and RSA keys. This is a big clue, but doesn’t mean I’ll need it for my model. Using the Application tab in Devtools and inspecting the webpage’s localStorage I found the following entries: encryptorAES and encryptorRSA make it pretty obvious that I will also be needing to use AES and RSA keys.

Local storage view in dev tools for the admin panel interface

This answers our question as to why we got garbage after decoding that base64 string, We need to decrypt that information first.

This could be a big problem and its important not to get overwhelmed. This is a challenge and you must rise to the occasion, just think of the juicy dopamine hit you’ll get when you’re successful. Like any problem, this can be broken up into smaller more manageable problems (and you’ll get a mini dopamine hit for solving those too!). I’ll be splitting this problem into 4 parts, solving any of these will prove that we’re making progress. These are:

  1. Decrypt a response — Without worrying about the other parts of the puzzle like cookies, request headers or body. Once we get a response from the router, how do we get meaningful data out of the encrypted response? Validating that we know how to decrypt messages will be important moving forward. Cutting the other variables makes this easier.
  2. Encrypt request data — Now that we can decrypt data, its time to focus on sending. We can copy cookies and other request data for now. We just need to make sure that we can craft the body correctly to get a valid response from the router.
  3. Get / Generate keys — We know there are keys involved, and we’ve been using the ones given to us by the browser, but we need to get or generate this ourselves.
  4. Craft a request — Up until now we’ve been using headers and cookies copied from the browser. But ideally we need to work these out for ourselves. This will probably mean logging in

These are the breakdowns I came up with for this problem, it won’t be applicable in every case and there might be more steps discovered midway.

Decrypt a Response

It’s time to dive into the code and try to figure out how the encrypted data gets from the response into the actual page. Starting with the network tab of a request, the initiator tab shows us the stacktrace leading up to the request. It stands to reason that something requested data (ie, a page refresh) and triggered a flow that eventually leads to sending the encrypted request. We want to trace back up the stack as one of the methods will contain code for actually using the response data. Our stacktrace looks like this

Initiator tab on a network request showing its stack trace

Where the most recent code is at the top, this will always be something akin to send. One of the functions listed here will call another function to eventually decrypt the data, so lets go spelunking. It’s worth mentioning that most likely this code will be minified to reduce bandwidth requirements, so variables will not have nice names.

After digging around for a bit I discovered that e.request from our stacktrace above is an ugly function that seems to proxy every request through it. Stepping through this method revealed that we’re adding a callback on ajax to decrypt the response before calling off to whatever function originally requested this (likely getInternetStatus at the bottom of the stack)

Breakpoints are extremely useful in deciphering where callbacks are coming from, it saves a ton of time when hovering over a variable tells you the callback functions location instead of needing to actually read and interpret all of the code yourself.

Active breakpoint on a request function

There is a lot going on in this screenshot, but the debugger tells us all we need to know. These last two lines in the function actually contain call offs to both encrypt and decrypt. We’ll keep that knowledge in mind for encrypting later. For decrypting, a method is referenced in the success object for the ajax call. Navigating to this method reveals an interesting line

a = JSON.parse(v.su.encryptor.dataDecrypt(e.data))

Spelt out plainly even in minified code, JSON.parse the results of running e.datathrough dataDecrypt.Stepping over this line reveals that a does in fact store a JSON result of connected devices. Exactly what we’re looking for. Simply follow this method and it reveals that we’re decrypting the message using an AES key. We can see from the file structure that we’re using the CryptoJS library to do this. The consuming code is tpEncrypt (which I assume is my vendor’s custom library for handling encryption).

Recreating this in a NodeJS app, I can use the same CryptoJS library from npm and rewrite the consuming code as

function decryptResponse(aesKey, aesIV, encryptedResponse) {
const iv = CryptoJS.enc.Utf8.parse(aesIV)
const key = CryptoJS.enc.Utf8.parse(aesKey)
const opts = { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
const raw = CryptoJS.AES.decrypt(encryptedResponse, key, opts).toString(CryptoJS.enc.Utf8)
const json = JSON.parse(raw)
console.log(json)
}

// values from encryptorAES in localStorage
const encryptorAES = {k: '1051656383083341', i: '0715610566380555'}
const sample = 'LjJdSZzHsAjrVu9m28rulT9q/…'
decryptResponse(encryptorAES.k, encryptorAES.i, sample)

I’ve plugged in the encryptorAESvalues from localStorage and copied a sample response from the network tab. If running this results in an actual JSON object then we have successfully decrypted the response. At this point we now have one piece of the puzzle down.

Let’s take a break here, we’ve earned it. Read on here for part 2!

About the Author:
Jack Reeve is a Full Stack Software Developer at Version 1.

--

--