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:
- 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 call 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 like 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. Bummer, I guess we have to keep trying .
Notice from the first video what was happening at 45c8, where we passed the first length check and stepped through the following code:
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 is pushed on to the stack, and then we’re prompted to enter in a password. It’s not clear from the code, but r14 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) - sweet!
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:
After our password is copied onto the stack, the code iterates over the password that’s 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
All single-operand and dual-operand instructions can be byte or word
instructions by using .B or .W extensions. Byte instructions are used to access
byte data or byte peripherals. Word instructions are used to access word data
or word peripherals. If no extension is used, the instruction is a word
The .b 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. I think we’re on to something …
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. Then instead of exiting out of the program, the code finishes until it returns. Let’s try jumping execution to unlock_door() instead of 4141…