Microcorruption is an online embedded-security CTF. Given a debugger and a device, solving each level consists of finding an input which will unlock the device.
No tools are required to solve each challenge beyond the debugger provided on their website. Potentially useful reference material includes the MSP430 Assembly Overview and the LockItAll Pro User Guide.
Take a look at the create_password()
function.
Numbers are stored in little-endian format on the MSP430.
Don't follow instructions. check_password_valid()
is a red herring.
Have you heard of buffer overflows?
The online debugger shows six windows: "Disassembly", "Register State", "Live Memory Dump", "I/O Console", "Debugger Console", and "Current Instruction". The "Disassembly" and "Debugger Console" sections are most important, since they allow us to see the code being run and to step through the code, respectively. The remaining windows display the current hardware state. To start, let's look at the main()
function:
4438: 3150 9cff add #0xff9c, sp
443c: 3f40 a844 mov #0x44a8 "Enter the password to continue", r15
4440: b012 5845 call #0x4558 <puts>
4444: 0f41 mov sp, r15
4446: b012 7a44 call #0x447a <get_password>
444a: 0f41 mov sp, r15
444c: b012 8444 call #0x4484 <check_password>
4450: 0f93 tst r15
4452: 0520 jnz #0x445e <main+0x26>
4454: 3f40 c744 mov #0x44c7 "Invalid password: try again.", r15
4458: b012 5845 call #0x4558 <puts>
445c: 063c jmp #0x446a <main+0x32>
445e: 3f40 e444 mov #0x44e4 "Access Granted!", r15
4462: b012 5845 call #0x4558 <puts>
4466: b012 9c44 call #0x449c <unlock_door>
446a: 0f43 clr r15
446c: 3150 6400 add #0x64, sp
After prompting the user for the password, get_password()
and check_password()
are called. If check_password()
returns a non-zero value, the door is unlocked. It appears that register r15
is used for passing parameters to functions
Now, let's take a look at the check_password()
function and see how we can get it to return a non-zero value:
4484: 6e4f mov.b @r15, r14 ; count until NULL byte is encountered
4486: 1f53 inc r15
4488: 1c53 inc r12
448a: 0e93 tst r14
448c: fb23 jnz #0x4484 <check_password+0x0>
448e: 3c90 0900 cmp #0x9, r12 ; should occur at 9th character
4492: 0224 jeq #0x4498 <check_password+0x14>
4494: 0f43 clr r15 ; return 0
4496: 3041 ret
4498: 1f43 mov #0x1, r15 ; return 1
449a: 3041 ret
One byte at a time, our input string is loaded into r14
and checked to see if it's a non-zero value. If it isn't, then counter r12
is incremented. Since strings are terminated with null bytes '\x00'
, this is equivalent to counting the length of our string (plus one, since r12
is incremented one last time for the null byte). Thus, we must simply supply an 8-character input to pass this level. password
should do fine.
This time, looking at the code for main()
reveals that a password is created upon device initialization.
4438: 3150 9cff add #0xff9c, sp ; create password
443c: b012 7e44 call #0x447e <create_password>
4440: 3f40 e444 mov #0x44e4 "Enter the password to continue", r15
4444: b012 9445 call #0x4594 <puts> ; get password from user
4448: 0f41 mov sp, r15
444a: b012 b244 call #0x44b2 <get_password>
444e: 0f41 mov sp, r15 ; check password
4450: b012 bc44 call #0x44bc <check_password>
4454: 0f93 tst r15
4456: 0520 jnz #0x4462 <main+0x2a>
4458: 3f40 0345 mov #0x4503 "Invalid password: try again.", r15
445c: b012 9445 call #0x4594 <puts> ; fail
4460: 063c jmp #0x446e <main+0x36>
4462: 3f40 2045 mov #0x4520 "Access Granted!", r15 ; succeed
4466: b012 9445 call #0x4594 <puts>
446a: b012 d644 call #0x44d6 <unlock_door>
446e: 0f43 clr r15
4470: 3150 6400 add #0x64, sp
The create_password()
function will probably tell us a lot about what kind of password we need to supply:
447e: 3f40 0024 mov #0x2400, r15
4482: ff40 6e00 0000 mov.b #0x6e, 0x0(r15)
4488: ff40 5700 0100 mov.b #0x57, 0x1(r15)
448e: ff40 6a00 0200 mov.b #0x6a, 0x2(r15)
4494: ff40 4200 0300 mov.b #0x42, 0x3(r15)
449a: ff40 6100 0400 mov.b #0x61, 0x4(r15)
44a0: ff40 2900 0500 mov.b #0x29, 0x5(r15)
44a6: ff40 6200 0600 mov.b #0x62, 0x6(r15)
44ac: cf43 0700 mov.b #0x0, 0x7(r15)
44b0: 3041 ret
It looks like the password is stored at memory location 0x2400
, and is 0x6e576a4261296200
. Let's see what check_password()
does with this information:
44bc: 0e43 clr r14 ; password[r14] ?= input[r14]
44be: 0d4f mov r15, r13
44c0: 0d5e add r14, r13
44c2: ee9d 0024 cmp.b @r13, 0x2400(r14)
44c6: 0520 jne #0x44d2 <check_password+0x16> ; if not, fail
44c8: 1e53 inc r14 ; r14++
44ca: 3e92 cmp #0x8, r14 ; return 1 if we reach end of string
44cc: f823 jne #0x44be <check_password+0x2>
44ce: 1f43 mov #0x1, r15 ; return 1
44d0: 3041 ret
44d2: 0f43 clr r15 ; return 0
44d4: 3041 ret
Perhaps predictably, check_password()
checks that the user-supplied password is the same as the hard-coded password. As soon as a single character is different, it returns 0
. If we reach the end of the hard-coded password and all characters are the same, it returns 1
. Thus, we should supply 0x6e576a4261296200
as hexadecimal input.
This time, the create_password()
function has been removed:
4438: 3150 9cff add #0xff9c, sp
443c: 3f40 b444 mov #0x44b4 "Enter the password to continue.", r15
4440: b012 6645 call #0x4566 <puts>
4444: 0f41 mov sp, r15
4446: b012 8044 call #0x4480 <get_password>
444a: 0f41 mov sp, r15
444c: b012 8a44 call #0x448a <check_password>
4450: 0f93 tst r15
4452: 0520 jnz #0x445e <main+0x26>
4454: 3f40 d444 mov #0x44d4 "Invalid password: try again.", r15
4458: b012 6645 call #0x4566 <puts>
445c: 093c jmp #0x4470 <main+0x38>
445e: 3f40 f144 mov #0x44f1 "Access Granted!", r15
4462: b012 6645 call #0x4566 <puts>
4466: 3012 7f00 push #0x7f
446a: b012 0245 call #0x4502 <INT>
446e: 2153 incd sp
4470: 0f43 clr r15
4472: 3150 6400 add #0x64, sp
Everything else has stayed the same, so let's check how the check_password()
function now validates our input.
448a: bf90 6528 0000 cmp #0x2865, 0x0(r15) ; return 0 if input[0:1] != 0x2865
4490: 0d20 jnz $+0x1c
4492: bf90 2b77 0200 cmp #0x772b, 0x2(r15) ; return 0 if input[2:3] != 0x772b
4498: 0920 jnz $+0x14
449a: bf90 756b 0400 cmp #0x6b75, 0x4(r15) ; return 0 if input[4:5] != 0x6b75
44a0: 0520 jne #0x44ac <check_password+0x22>
44a2: 1e43 mov #0x1, r14 ; return 1 if input[6:7] == 0x664e
44a4: bf90 4e66 0600 cmp #0x664e, 0x6(r15)
44aa: 0124 jeq #0x44ae <check_password+0x24>
44ac: 0e43 clr r14 ; return 0
44ae: 0f4e mov r14, r15
44b0: 3041 ret
The new concept in this level is that instead of a single byte being compared at a time, the password is read as four successive 2-byte integers. Typically, integers are stored in little-endian format, so when 0x2865
is compared to the first two bytes of our input, our input should actually start with 0x6528
. Stringing together the four numerical comparisons our input must pass, we should enter0x65282b77756b4e66
as hexadecimal input.
In this level, the main()
function has been reduced to:
4438: b012 2045 call #0x4520 <login>
443c: 0f43 clr r15
The disassembled login()
function shows:
4520: c243 1024 mov.b #0x0, &0x2410 ; prompt user
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 ; get input
4538: 3f40 0024 mov #0x2400, r15
453c: b012 ce45 call #0x45ce <getsn>
4540: 3f40 0024 mov #0x2400, r15 ; test_pw_valid must return 0
4544: b012 5444 call #0x4454 <test_password_valid>
4548: 0f93 tst r15
454a: 0324 jz $+0x8
454c: f240 1400 1024 mov.b #0x14, &0x2410
4552: 3f40 d344 mov #0x44d3 "Testing if password is valid.", r15
4556: b012 de45 call #0x45de <puts>
455a: f290 d800 1024 cmp.b #0xd8, &0x2410 ; fail if input[16] != 0xd8
4560: 0720 jne #0x4570 <login+0x50>
4562: 3f40 f144 mov #0x44f1 "Access granted.", r15 ; success!
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> ; fail
4578: 3041 ret
Whether or not test_password_valid()
fails, the comparison on line 0x455a
between 0xd8
and &0x2410 = input[16]
must succeed or else the door will not unlock. In order to set input[16] = 0xd8
, we must ignore the suggestion of maximum password length, and ensure that test_password_valid()
returns 0
. If it does not, line 0x454c
ensures that input[16]
is overwritten and the later test will fail. Now let's look at test_password_valid()
:
4454: 0412 push r4
4456: 0441 mov sp, r4
4458: 2453 incd r4
445a: 2183 decd sp
445c: c443 fcff mov.b #0x0, -0x4(r4) ; *(r4-4) = 0
4460: 3e40 fcff mov #0xfffc, r14
4464: 0e54 add r4, r14
4466: 0e12 push r14 ; interrupt(0x7d, &input, &flag)
4468: 0f12 push r15
446a: 3012 7d00 push #0x7d
446e: b012 7a45 call #0x457a <INT>
4472: 5f44 fcff mov.b -0x4(r4), r15 ; r15 = *(r4-4) = 0
4476: 8f11 sxt r15
4478: 3152 add #0x8, sp
447a: 3441 pop r4
447c: 3041 ret
Lines 0x4466
through 0x446e
are our first encounter with interrupts. By consulting the LockItAll Pro User Guide, we can see:
__INT 0x7D__
Interface with the HSM-1. Set a flag in memory if the password passed in is
correct.
Takes two arguments. The first argument is the password to test, the
second is the location of a flag to overwrite if the password is correct.
Cool! But as it turns out, this has no effect on our solution, since we only need test_password_valid()
to return 0
, which it does every time! This means we only need to ensure that the 17th character of our input is 0xd8
. Any hex input such as 0x41414141414141414141414141414141d8
will work.
As in the previous level, main()
is simply used as a wrapper to call login()
:
4500: 3150 f0ff add #0xfff0, sp ; prompt user
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 ; get input
4518: 0f41 mov sp, r15
451a: b012 9645 call #0x4596 <getsn>
451e: 0f41 mov sp, r15 ; if 0, fail
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> ; success!
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> ; fail
453a: 3150 1000 add #0x10, sp
453e: 3041 ret
From this, we know that we need to find a way for test_password_valid()
to return a non-zero value.
4452: 0412 push r4
4454: 0441 mov sp, r4
4456: 2453 incd r4
4458: 2183 decd sp
445a: c443 fcff mov.b #0x0, -0x4(r4)
445e: 3e40 fcff mov #0xfffc, r14
4462: 0e54 add r4, r14
4464: 0e12 push r14 ; interrupt(0x7d, &input, &flag)
4466: 0f12 push r15
4468: 3012 7d00 push #0x7d
446c: b012 4245 call #0x4542 <INT>
4470: 5f44 fcff mov.b -0x4(r4), r15
4474: 8f11 sxt r15
4476: 3152 add #0x8, sp
4478: 3441 pop r4
447a: 3041 ret
It seems that nothing has changed from the previous level, which means we won't be able to return anything other than 0
from test_password_valid()
. In order to unlock the door, we have to avoid ordinary execution flow by performing a buffer overflow.
We can see that in the login()
function, the stack pointer is first decreased by 16 bytes to allocate room on the stack for our password, and then increased by 16 bytes to de-allocate this memory. If we write beyond these 16 bytes, we can overwrite other important pieces of memory and change where we jump to. According to the MSP430 Assembly Guide, at the end of a subroutine, the ret
instruction jumps to the pointer at the top of the stack. By overwriting this with 0x4528
we can immediately jump to unlock_door()
. Adding the appropriate offset, we should enter a string like 0x414141414141414141414141414141412845
.