Microcorruption writeup

Microcorruption is my ongoing “distraction” – it’s an online CTF. I’m way late to the party and have been doing it on and off since… 2013. How does it work exactly? To use their own description: “tl;dr: Given a debugger and a device, find an input that unlocks it. Solve the level with that input. You’ve been given access to a device that controls a lock. Your job: defeat the lock by exploiting bugs in the device’s code.” Device’s code is written in a simple assembler and as far as I can tell (remember, I still have not finished it :), typically the solution is some kind of stack overflow/“return oriented programming”. Give it a try if you’re interested (and stop reading).

First tasks are fairly straightforward, you write too many characters to the input buffer and overflow the stack with a return address that leads to an unlock door function. It gets more interesting around Whitehorse (each task is a different world location). Today I’d like to write a bit about Whitehorse and Montevideo as I found these 2 quite interesting. First, however, let’s take a quick look at Cusco just so that you can get a better “feel” of a task (it’s one of the “straightforward” ones):

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

getsn is where the app reads our input, it will then check if it’s valid, unlock the door if so (unlock_door), output some messages and return. There are 2 things that interest us here:

  • the address of the unlock_door function (0x4446)
  • the fact that sp points to the beginning of our input and happens to be 16 bytes before the return address

If you’ve ever done any kind of ROP, it should be fairly obvious what to do next – enter 16 random bytes followed by 4644 (address of unlock_door) and the function will jump there when trying to return.

Whitehorse

OK, let’s try something more tricky – Whitehorse. When it loads, we’re greeted with the following note: “This is Software Revision 01. The firmware has been updated to connect with the new hardware security module. We have removed the function to unlock the door from the LockIT Pro firmware.” Sadly, that is exactly what they did, no more easy jumps to unlock_door. Instead, it’s replaced with conditional_unlock_door, which will verify the password and only then unlock the door. Theoretically, we could try to jump to the middle of that function (past the password verification), but it depends on proper values of registers, too (not saying it’s impossible, but didn’t take that route). If you read the lock manual, you’ll notice that what we really want to do is trigger an interrupt: “Lockitall has extended the MSP430 to support software interrupts, implemented with a callgate at address 0x0010 on the MCU. […] The interrupt kind is passed in R2, the status register, on the high byte. Arguments are passed on the stack.”

Two interrupt kinds that are most interesting for us are:

  • INT 0x7E - Interface with the HSM-2. Trigger the deadbolt unlock if the password is correct. Takes one argument: the password to test.”
  • INT 0x7F - Interface with deadbolt to trigger an unlock if the password is correct. Takes no arguments”

Now.. I have to admit I wasted quite a bit of time here as I assumed I only have to set SR (status register) to one of these values and jump to 0x0010. However, it turned out I should pay closer attention to what the code was doing, it’s a bit more involved:

4446 <conditional_unlock_door>
[...]
4458:  0e12           push	r14
445a:  0f12           push	r15
445c:  3012 7e00      push	#0x7e  ; ---> INT type
4460:  b012 3245      call	#0x4532 <INT>

[...]
4532 <INT>
453e:  32d0 0080      bis	#0x8000, sr  ; Set the highest bit
4542:  b012 1000      call	#0x10

As you can see, procedure will also set the highest bit, so when entering #0x10, SR contains 0xFF00 (for 07xF) or 0xFE00 (for 0x7E). (The fact I missed it, will actually come handy in Montevideo).

Now that we know more or less what code we need to run (push #07xF/call 0x4532), the question is how do we run it? Well, how about we put the code on the stack itself and jump to stack memory? Documentation mentions memory protection, but I imagine it comes up later. Let’s just copy the opcodes we need: 3012 7f00/b012 3245. We also note the stack address just when we are ready to exit the last function (after add #0x10, sp). In this case it’s 3ad0, so we want to jump to the next word (3ad2) and write our opcodes immediately after. Putting it all, together, our hex input is something like: 12345678123456781234567812345678d23a30127f00b0123245. First 16 characters (32 bytes) does not matter, but I use 1-8 to make counting easier. It’s followed by the jump address (stack - 0x3ad2) and opcodes for the interrupt.

Montevideo

Montevideo is somewhat similar to Whitehorse in that there’s no simple unlock door function, in fact, it uses the same conditional_unlock_door. However, our input is now copied to another buffer before verifying. It assumes a zero-terminated string, which sadly means we can’t use same trick here as 0x7f00 will cause the strcpy to terminate (and return before copying the remaining opcodes :/). Remember how I said I tried jumping to 0x10 directly first? This will come handy now! What we can do is skip the part, set SR to 0xFF00 and jump to 0x10 ourselves. Sadly, at first glances it does not seem like it helps us, 0xFF00 contains 0 bytes as well. Luckily, we can work around that by first setting it to 0xFF01 and then decreasing it by one. Microcorruption site has a handy assembler/disassembler page, so we can use it to get our opcodes. What we want is:

3240 01ff      mov	#0xff01, sr
1283           dec	sr
b012 1000      call	#0x10

so, 324001ff1283b0121000 in hex. However, when I tried, it turned out there is another problem… The stack address I tried to jump to (where I put my code) was 4400… I could not write that as it has a zero-byte. Solution was to add some bogus data and jump to 4402 instead. A careful reader will notice that our opcode has zeroes at the end as well, but in this case we were lucky. The area it was being copied to was already filled with zeros, so it didn’t matter they were not copied, in fact we didn’t need them at all. Final input is: 123456781234567812345678123456780244aaaa324001ff1283b01210 (we do need that 0 at the end, just because we need an even number of bytes.. 0xAAAA is “padding” I mentioned before).

More Reading
Older// API granularity