This is a write-up of my solution to the Microcorruption CTF challenge “Santa Cruz” (LOCKIT PRO r b.05).
This challenge required a few tricks in order to unlock the door, so let’s get down to it.
The meat of the logic lives inside login(), and there’s substantially more going on here than compared with the previous challenges. Let’s dissect it and learn what it’s doing…
The first part of login() outputs a message to the user indicating that a username and password are both required.
The second part of login() prompts a message asking for a username. After submitting the username, the code prints it back to the user, and then it is copied onto the stack.
The third part of login(), similar to the last one, prompts a message asking for a password. After submitting the password, the code will print it back to the user, and will go on to copy the password onto the stack.
Let’s go into the debugger and pause execution at this point and observe what the memory looks like (I’ll use 10 A for the username and 10 B for the password)…
We can see that after the second call to strcpy(), our username was copied onto the stack starting at 0x43a2, and our password was copied onto the stack starting at 0x43b5. Notice that the program left 16 bytes on the stack for the username, in addition to a null byte. Then we see two bytes, 0x08 and 0x10, followed by our password. Also note that about 6 bytes after the end of the password buffer, we see the bytes 0x4044, which looks like the return address from the login() call inside main(). Let’s keep this in mind and continue dissecting login().
After the second call to strcpy(), this part of the code will loop through the bytes of the password while at the same time incrementing a byte offset pointer which is located in r14. If the value at the address location of r14 is 0x0, the jnz will not be executed.
At this point, the length of the password will be calculated by subtracting the byte offset pointer located in r14 with the pointer to the beginning of password located in r15. This length will be stored in r11. This is where things get a little interesting. The instruction mov.b -0x18(r4), r15 will move the byte 0x10 from the memory located between the username and the password on the stack into r15. Then sxt r15 will perform a sign-extend on r15, and after this, cmp r15, r11 will compare the two registers (in our example, r11 == 0x000a and r15 == 0x0010). The following jnc instruction will jump execution to 0x45fa if r11 is greater than r15, else, an “Invalid Password Length” error will be printed and execution will be halted. It’s clear that 0x10 is a length value that we can potentially control with an overflow… Let’s continue.
This next chunk of code will move the byte located at r4 - 0x19 (the 0x08 before the 0x10) into r15, will do a sign-extend on r15, and then will cmp r15, r11 (r5 == 0x08, r11 == 0x000a). If r11 is less than r15, the program will print an error message indicating the password length was too short, and will exit.
We now know that the two bytes between the username and password buffers are length fields that we can potentially control.
Let’s figure out the last part of the login() function…
The instructions between 0x4610 and 0x462a are manipulating registers in various ways and are pushing various addresses (related to the username and password buffers) onto the stack. After the call to the interrupt, things become interesting. Let’s set a breakpoint on the tst.b instruction at 0x4634 and examine the stack…
Notice how the program is going to test the memory location at r4 - 0x2c (0x43a0 a.k.a. sp) for 0x0, and if it is, will continue to jump to 0x4644 which will eventually exit the code. This is strange because it appears that this memory is never going to contain anything besides0x0, due to the fact that earlier in the code, the instruction mov.b #0x0, -0x2c(r4) was executed.
After printing out the message “That password is not correct”, we encounter another tst.b instruction, however this time, we are checking the address r4 - 0x6. If these two tst instructions pass, we will eventually return out of the function. So what can we do with this?
Remember the return address pushed onto the stack from the call to login() from main()? Check out the memory dump and the location in relation to the password buffer. Surely we can reach it by overflowing one of the buffers! However, we must account for a number of checks that are in place. Let’s recap:
The byte value 0x10 (address 0x43b4) will be used to check the max length of the password.
The byte value 0x08 (address 0x43b3) will be used to check the min length of the password.
The memory address 0x43a0 (tst.b r4 - 0x2c) must contain 0x0.
The memory address 0x43c6 (tst.b r4 - 0x6) must contain 0x0.
If we can satisfy these conditions, then we can cleanly reach the location of the return address on the stack, while at the same time avoiding any premature calls to __stop_progExec__(). Sweet!
We should be able to safely ignore the check happening at #3; this area is towards the top of the stack, and the parts that we are going to modify are below this. Our payload will be split across two strings.
The username will overwrite both the min-length byte and the max-length byte, and we need to make sure that the max-length byte is >= the length of whatever password we decide. The min-length byte needs to be <= the password we choose.
We need to overwrite the return address located at r4, however #4 requires that a byte between the end of the original password buffer and the return address must be 0x0. Because strcpy() terminates the copy on null bytes, we cannot have any null bytes in our payload, therefore, we cannot overflow the password buffer to overwrite the return address. However, we can overflow the username buffer and not only overwrite the length values, but write all the way until we overflow the return address. So how will we write the null byte to satisfy requirement #4? Well we can just have strcpy() null terminate the string for us, right where we need the null byte! Let’s craft our payloads and cross our fingers…