Microcorruption 0x20 - Lagos

Prev: 0x0F Bangalore

According to the manual for this level, input is limited to alphanumeric characters. If we check the “login” function to confirm this, we can see that the process for verifying this is actually quite secure:

  • First, the r12 and r13 are set as 0x9 and 0x19 respectively, and the hex value of the input character is placed within r15
  • Then, 0x30 bytes are subtracted from this value in r11 and compared against r9. This checks to see if the character has a value within 0x30-0x39, or in other words is an ASCII character from 0-9.
  • Then, 0x11 bytes are subtracted from r11 and it is compared against r13. This checks to see if the character is between 0x41-0x5a, or the alpha capital letters A-Z.
  • Finally, 0x20 bytes are subtracted r11 and it is compared against r13. This checks to see if the character is between 0x61-0x7a, or the alpha lowercase letters a-z.
  • If all three of these checks fail, the string ceases copying to the stack and the program runs with the input provided thus far.

All in all, this password verification piece does its job in an airtight manner, and thus will not be what we need to exploit. Instead, we’ll need to work within the allowed hex values in order to generate arbitrary code execution.

Our allowed hex input values are 0x30-0x39, 0x41-0x5a, and 0x61-0x7a.

Before we concern ourselves with shellcode, let’s first begin by understanding how we can overwrite our instruction pointer, pc. Since the “getsn” call and subsequent instructions which copy our input to the stack are all located in the “login” function, the return address used by its “ret” instruction at 45f8 will be our target. Set a breakpoint for this instruction and run the program.


When we hit “ret” and examine the stack, we’re at address 43fe which is 17 bytes after the start of our password buffer. Checking the input length argument passed into “getsn” using r14, we see that we can write a staggering 0x200 bytes to the stack! However, there are two significant things to note here:

  1. We cannot simply return anywhere in our string – remember our alphanumeric restrictions from earlier. The closest address we can return to is 4430.
  2. Before we return from “login,” we call “conditional_unlock_door” which is located at 4446. If we start at 4430, we only have 0x16 (or 22) bytes of input before we begin overwriting instructions.

If we want to assemble some input to get us to arbitrary code execution, it looks like this:

  • 17 bytes of “A” to fill the buffer
  • 2 bytes of “3044” to return to address 4430
  • 48 bytes of “A” to fill the remaining bytes before 4430

This will overflow all the way to 4430, and set pc to 4430 via a "ret" instruction for us to start executing. All we need now is to write some shellcode and append it!

Looking for instructions that fit within our hex criteria proves to be very challenging – ultimately, a combination of referencing instructions in this level and prior levels along with some trial-and-error in the site’s Disassembler will help us to find some usable (and hopefully useful) instructions: add, sub, mov, pop, and ret. However, not all variations of these instructions work: one may only go register-to-register, while another may just be able to use bytes instead of the full word. Finally, the most useful instruction here, “push,” is unfortunately not available to us.

Let’s now look for our win condition. Given that we don’t have “unlock_door” available to us, we’ll need to examine “INT” to think of ways to execute it:


It appears that we can either somehow call "INT" directly with either 0x7f OR 0xff at 2 bytes offset from sp (as both become 0xff after bitwise OR with 0x80), or we can try to replicate the way this function works and place 0xff into the sr register before calling 0x10.

If we consider our first option – try to get 0x2(sp) to be either 0x7f or 0xff and then call INT – we can see that some words up ahead have “0xff." However, they don't have them as their most-significant byte, meaning that will not be loaded into r14 correctly. We can test this by setting sr to "fcff" and setting pc to the start of "INT" at "45fc" - we get a crash.

So, now we know we’ll need to go the latter route: place 0xff00 into sr and call 0x10 ourselves. We can add constants directly to a register, so if we find the right combination of hex values we'll be able to get 0xff00 into sr:

  • add #0x4748, sr
  • add #0x7072, sr
  • add #0x4746, sr

The resulting opcodes are within our restrictions: "325048477325070703250484"

(Note that, if you want to try getting sr to 0xff00 yourself, you may need to play around with the values you add so as to avoid causing an unintended interrupt and turning off the CPU.)

Now, we need to set a register equal to 0x10, and then load it into pc. Unfortunately, we cannot use "mov" between registers, but we can use "mov.b" to move the bytes located at a register address into another register, and if we look at the "INT" function we see that the "0x10" in the "call 0x10" instruction is located at 460e:

  • add #0x787a, r8
  • subc #0x326c, r8
  • mov.b @r8, pc

This puts the value 460e in r8, and then loads the value at that address ("0x10") into pc. Here are the resulting opcodes: "38507a7838706c323048"

In theory, we should be able to append this to our prior output and unlock the door. However, if we attempt this in the order listed above, we notice sr gets immediately overwritten with flags that result from the arithmetic on r8. Let's set r8 first, then sr, then pc. Here are our final instructions, in order (note that the hex values in the "add" instructions for sr changed due to sr now starting at 0x1):

  • add #0x787a, r8
  • sub #0x326c, r8
  • add #0x4748, sr
  • add #0x4746, sr
  • add #0x7071, sr
  • mov.b @r8, pc

Success! Our total final input is "414141414141414141414141414141414130444f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f38507a7838706c323250484732504647325071703048"

Prev: 0x0F Bangalore