MicroCorruption

About

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.

microcorruption website image

Tools

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.

Review

Hints
Click here to open hints for each level
Tutorial

If you don't know how C strings are stored in memory, now would be a good time to learn.

New Orleans

Take a look at the create_password() function.

Sydney

Numbers are stored in little-endian format on the MSP430.

Hanoi

Don't follow instructions. check_password_valid() is a red herring.

Cusco

Have you heard of buffer overflows?

Solutions
Tutorial

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.

New Orleans

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.

Sydney

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.

Hanoi

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.

Cusco

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.