This is a write-up of my solution to the Microcorruption CTF challenge “Montevideo” (LOCKIT PRO r c.03).
In the manual, they claim to have increased their security practices:
- Lockitall developers have rewritten the code to conform to the
internal secure development process.
- This lock is attached the the LockIT Pro HSM-2.
We’ll see about that . Let’s take a look at the code.
Looking at the main() function, we can see that a single function named login() is called. Inside of login(), we see that after a password prompt, a call to strcpy() is made. The password prompt itself, “Remember: passwords are between 8 and 16 characters.”, implies that there’s some sort of length checking in place. Let’s play around in the debugger and see if this is actually the case.
After setting breakpoints before and after the call to strcpy(), we run the program and enter AAAAAAAAAA as the input. We keep an eye the address pointed to by the stackpointer (sp == 43ee) and watch the function place our input onto the stack.
Pay close attention to the bytes soon after our input ends: 3c44. If you stepped through the program from the beginning, you’ll notice that this value on the stack is actually the return address (443c) that was pushed as a result of the call to login() from main() originally! Let’s try overwriting these bytes (which are at offset 17 and 18), and see if we can control the program counter…
We try the input BBBBBBBBBBBBBBBBAA and watch the memory address containing the return address get overwritten. After continuing execution past the second breakpoint, we see the program crash and pc contain 4141!
Now that we’ve confirmed a stack buffer overflow vulnerability exists, we need to find a way exploit it. In classical buffer overflow exploit examples, the payload (usually shellcode) is stuffed at the beginning of the buffer that we’re overflowing. Then, the return address is overwritten so that it points back to the start of our buffer. When the executing function returns, the address of the buffer is popped into the program counter and our payload gets executed. If we were to try something similar here, that means we’d have to stuff our payload into 16 bytes (because the 17th and 18th byte map to the return address on the stack). That’s not that much room . Oh and don’t forget, if the payload includes any null bytes, strcpy() won’t copy the entire input to the stack .
Since this program contains the familiar conditional_unlock_door() function which pushes (a useless) 0x7e to the stack before calling the interrupt, I decided to write shellcode that will overwrite the 0x7e with 0x7f (the code to unlock the door).
After some careful trial and error with the assembler, I meticulously wrote the following code:
mov #0x7f01, r12 # load 0x7f01 into r12
add #-0x1, r12 # subtract 0x01 from r12 (resulting in 0x7f00)
swpb r12 # swap the two bytes (resulting in 0x007f)
mov r12, 0x445e(r6) # write 0x007f into 0x445e
call #0x4446 # call to conditional_unlock_door()
My shellcode will essentially put the value of 0x7f into register r12 (by performing some byte manipulations in order to avoid null-bytes in my payload), and will then replace the 0x7e in conditional_unlock_door() with 0x7f. After the overwrite happens, a call to conditional_unlock_door() is made…