In this brand new level we will take advantage of buffer overflow vulnerability and create our first shellcode in order to solve the level. Keep reading to learn more! :)
When you first enter the level, as always, there is the user manual. If you were to read it, you’d notice the following useful info:
We have updated the lock firmware to connect with this hardware security module.
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.
Sounds like the door unlocking is now being carried out by some external piece of device. As always, we
main function is pretty simple.
4438 <main> 4438: b012 f444 call #0x44f4 <login>
Let’s see what
login is up to.
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 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
It’s easy for us to identify what code we want to execute for the purpose of solving the level. We can see there is a call to
conditional_unlock_door at address
0x4514. Right at the next instruction, there is a
tst based on
r15’s value. Is it is zero, it jumps elsewhere, otherwise it grants access. Let us not explore
conditional_unlock_door to see how
r15 is assigned.
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
We can see apparently there is nothing of interest. It calls
0x7e argument at instructions
0x4460. At page 9 of LockitAll User Manual we can read the following:
INT 0x7E. Interface with the HSM-2. Trigger the deadbolt unlock if the password is correct.
It unlocks the door if HSM-2 device evaluates the password as correct. We have no access to such device.
Apparently there is no unlock logic in the code we’ve been given that we could actually trick. Then,
Moving back to
login function, we can see there is a
getsn call right at instruction
0x450e. It will read up to 48 (0x30) bytes of input and will place them starting off at wherever
sp is at that particular moment, as we can see at instruction
0x450c. If we look closely, right before
getsn, there is a
puts call at instruction
0x4504 that prints out “… passwords are between 8 and 16 characters.” Once again
Let us now actually execute the program and scrutinize its behavior inspecting where data is placed into memory.
I suggest placing two breakpoints after
getsn call. One at address
0x4512 and another one right at the end of
login function, when its epilogue is about to get executed at address
0x452e. You might think about placing a breakpoint at
0x4518, right after
conditional_unlock_door. You can do it and inspect what
r15 value is at that particular moment but, since we have no access to HSM-2 device, I won’t even bother about it.
We can now execute the program. When we’re asked to input our data, it’ll be easier for us if we input some easily recognizable characters. I inserted a bunch of ASCII printable bytes.
0x4512, if we inspect Live Memory Dump window we can note our input is being saved starting off at address
0x3802. We’ve also successfully inserted into memory all our bytes, despite the program telling us 16 bytes in the maximum length.
Now that we know where our input is being saved, we can further execute the program until we hit our next breakpoint at
It looks like right when
ret instruction is about to get executed,
0x3812 and that address is fulfilled with our input. This is, we can overwrite the return address of
login function and hijack the execution flow of the whole program. This is called Buffer Overflow and it was already explained and used in past levels like Cusco. Please make sure you truly understand how it works and what’s happening inside the memory.
We must note that
sp points at address
ip must be assigned the value at that particular address thanks to
This means that our buffer starts at
0x3802, as we’ve previously seen, and the return address will be read from
0x3812. That’s a 16 (0x10) bytes long padding input. After the padding we can write where we want the execution flow to jump.
But now the question is:
We’ve seen there is no procedure/routine we can jump to in order to unlock the door but, by the other hand, we have at our disposition a 48 bytes long input, with the first 16 being free, 17th and 18th are for return address and the rest of them are also free.
We will now build our first
Now, how can we create our shellcode? We’ve already used Microcorruption Assembler in previous levels, such as Reykjavik. We do also know from previous levels that calling
0x7f as parameter unlocks the door.
In case you don’t remember what the syntax of these two instructions is, you have them at address
conditional_unlock_door function. So, we want to assemble (get the respective bytes representing the machine code) the following instructions:
push #0x7f call #0x4532
The resulting bytes are:
If you count them, you’ll notice they’re exactly 8 bytes (Remember each byte ranges from 00 to ff, and we are providing the input hex encoded). This means we can insert the payload, our shellcode, in our padding zone of our input.
Our input will be of the following form:
The address where we want to jump is the address of our shellcode because we want
ip to execute it. That is, the very beginning of our input. As we’ve previously seen, our input is being stored at
So, the solving input (hex encoded) will be:
Please notice how I added 8 bytes (8 * 41) to fulfill the padding zone.
An alternative solution would be, for example, to keep the padding bytes and insert our shellcode after the return address. (There are other combinations, you can try all of them and get used to shellcoding and overflowing a buffer to execute it)
We will change the return address, so the
ip jumps right into the shellcode. That is,
0x12(18 in hexadecimal) =
We’ve seen how we can take advantage of a Buffer Overflow vulnerability and change the program execution flow in order to execute our on shellcode, which is part of the payload we provide as input. We’ve overwritten the return address so the
ip jumps inside the buffer and executes our shellcode.