Series
Microcorruption Embedded CTF — View all write-ups

Summary

This is a write-up of my solution to the Microcorruption CTF challenge "Jakarta" (LOCKIT PRO r b.06).

From the challenge overview, they indicate that input length checking has been beefed up some more:

OVERVIEW
    - A firmware update further rejects passwords which are too long.
    - This lock is attached the the LockIT Pro HSM-1.

Let's dissect the code and see what they're up to.

In the main() function we see a similar call to login(). However in this program, we're presented with a message indicating that we need to provide a username and password. Additionally, they indicate that a length restriction is in place:

Authentication requires a username and password.
Your username and password together may be no more than 32 characters.

Further down, we see multiple calls to puts(), two calls to strcpy() and then some code that surrounds a call to unlock_door().

Let's run the program and see how the stack looks after the first strcpy()...

We'll start off by entering AAAAAAAAAA as the username and setting breakpoints before and after the first strcpy(). We see that the username is copied to the stack starting at address 3ff2. Also note, there appears to be a return address not too far away from our input on the stack, located at address 4016 and containing 4440. This is the return address from the call to login() from main(). Also pay attention to the instructions starting at address 45c8 — we'll discuss this in a bit.

Before we move on to the second strcpy(), let's see what happens when we try to overflow the return address with a long username...

The offset to the return address is 36 bytes. We input AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB and watch as the return address is replaced with 4242. However, the program errors out because we failed the cmp.b #0x21, r11 check.

Notice from the first video what was happening at 45c8, where we passed the first length check and stepped through the following code:

45c8:  3e40 1f00      mov   #0x1f, r14
45cc:  0e8b           sub   r11, r14
45ce:  3ef0 ff01      and   #0x1ff, r14
45d2:  3f40 0224      mov   #0x2402, r15
45d6:  b012 b846      call  #0x46b8 <getsn>

The program loads the value 0x1f into r14 then subtracts r11 (the length of the username) from it, which effectively gives us the allowed length of the password. Once getsn() is called, r14 and r15 are pushed on to the stack, and then we're prompted to enter in a password. The r14 register appears to contain an upper limit of the number of bytes that can be read in from getsn() into the input buffer (which is located at 2402 and pointed to by r15).

This means that we'll be unable to read in more than 0x1f - len(username) number of bytes... or does it? Let's see what happens when we input 32 bytes:

Because of an integer underflow vulnerability, we're able to increase the password buffer read-limit from 0x1f - len(username) to 511 bytes (0x01ff), all while staying within the length check (cmp.b #0x21, r11).

Now that we're able to get more than 32 bytes onto the stack, let's try to overflow the return address (located at 4016) with a 6-byte password...

Although we're able to successfully overwrite the return address, the program calls __stop_progExec__ before returning. Stepping through the code, we can see that there's an additional length check occurring at 45ee:

45ee:  3f40 0124      mov   #0x2401, r15
45f2:  1f53           inc   r15
45f4:  cf93 0000      tst.b 0x0(r15)
45f8:  fc23           jnz   #0x45f2 <login+0x92>
45fa:  3f80 0224      sub   #0x2402, r15
45fe:  0f5b           add   r11, r15
4600:  7f90 2100      cmp.b #0x21, r15

After our password is copied onto the stack, the code iterates over the password in the input buffer (which is not on the stack), and calculates the combined length of the username and password which will be stored in r15. Then, if the calculated length is greater than 0x21, the program exits without returning. However, pay close attention to the comparison instruction being used: cmp.b.

The .b extension means that the comparison is performed on the lower byte of r15. What if we were able to roll the length past 0xff? Remember that we were able to find a bug that increased the password read length to 511. Let's see...

We're able to control PC. By using a 224-byte password (with the 5th and 6th byte overwriting the return address), we'll roll the final value in r15 when it gets added to r11 (i.e. 0x20 + 224 == 256 == 0x100), and when the cmp.b instruction is performed, we'll effectively bypass the length comparison because only the lower byte 0x00 is checked. Then instead of exiting out of the program, the code finishes until it returns. Let's try jumping execution to unlock_door()...

Jakarta Solve

Username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (32 × A)

Password: BBBBLDBB... (224 bytes total — bytes 5–6 are LD which is 4c44, little-endian 444c = address of unlock_door())

← Montevideo 12 / 12 Last →