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's for the username and 10 B's 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 the password located in r15. This length will be stored in r11. 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. 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.
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 compare it against r11. 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. After printing out the message "That password is not correct", we encounter another tst.b instruction — this time checking the address r4 - 0x6. If these two tst instructions pass, we will eventually return out of the function.
Remember the return address pushed onto the stack from the call to login() from main()? 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:
0x10 (address 0x43b4) will be used to check the max length of the password.0x08 (address 0x43b3) will be used to check the min length of the password.0x43a0 (tst.b r4 - 0x2c) must contain 0x0.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__().
We should be able to safely ignore the check happening at requirement #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 greater than or equal to the length of whatever password we decide. The min-length byte needs to be less than or equal to the password we choose.
We need to overwrite the return address, however requirement #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? 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...
Username: 4141414141414141414141414141414141022042424242424242424242424242424242424242424242423a46
Password: 4242424242424242424242424242424242