This is a write-up of my solution to the Microcorruption CTF challenge “Johannesburg” (LOCKIT PRO r b.04).
This challenge does away with printf() altogether. Probably for the best. Let’s check out what they have up their sleeve for this one.
Let’s try running the program with AAAAAAAAAAAAAAAAAAAA and see what happens…
We see the message Invalid Password Length: password too long. It appears as if they may have finally implemented proper bounds checking. I’m still a little suspicious. Let’s try a lot more A's (like 100 or so)…
In addition to the password length message, we notice a few more things:
More than 16 bytes were allowed to be input
We actually overwrote part of the data segment in memory (e.g. see [overwritten])
The user input is actually being copied to a higher location in memory
Since main() only calls login(), let’s dissect it and see how it works:
After prompting the user for input, strcpy() is eventually called and our user input is copied onto the stack. This is where things get a little interesting. After returning from the call to test_password_valid(), we eventually execute the jz #0x4570 instruction which will jump us to 0x4570, which will execute a puts() call on the string That password is not correct. Immediately after, we execute the instruction cmp.b #0xec, 0x11(sp), and then jeq #0x458c <login+0x60>. That’s a really strange cmp instruction - the code is checking to see if the memory location after the buffer on the stack is 0xec, and if it isn’t, proceeds to call puts() on Invalid Password Length: password too long. Let’s set a break point on that cmp instruction and check out what the debugger looks like when it pauses. We’ll try using 10 A's this time, so as to not corrupt any memory…
It looks like the code is using a hard-coded stack canary (which lives at 0x43fd)! If the canary is over-written, the program uses this logic to assume that the length of the user input was too long. However, we can control what’s overwritten memory location of the stack canary - Great!
Right after the location of the stack canary in memory, we see another interesting pair of bytes: 0x3c44 (or when reversed, 0x44c3). Recall the memory values after the call to main():
4438: b012 2c45 call #0x452c <login>
443c: 32d0 f000 bis #0xf0, sr
As it turns out, 0x443c is the return address pushed onto the stack from the call to login() from main(), which is conveniently placed right after the value of the stack canary in memory! Sounds like we’re ready to craft our exploit. We’ll load up the buffer with A's, place the stack canary value 0x3c at the 18th byte offset, and we’ll overwrite the return address to 0x4566 (the call to unlock_door() from login())…