Microcorruption writeup
7/Jun 2020
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,
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
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).