queenp's blog

Posted Mon 06 June 2016

MicroCorruption CTF (part 1)

So ages back I meant to write up a bunch of notes on the Matasano series, but they're not ready yet because writing isn't as fun as hacking stuff and because I've mostly got enough crypto basics sorted in my head, it's not so useful for me personally.

On the other hand I've been trying to improve on my CTF game generally and saw someone doing the (published: 2013) Matasano Microcorruption CTF [1]

I'm not going to write up all of the challenges, particularly the early ones in the series the tutorial gives a pretty good introduction to, and I expect (while numerous people in the previous 3 years have done writeups of the whole lot) it's more benefit to people to actually just look at the ealier problems and work them out themselves (and didn't involve much on my part either).

Important tips:

If you want to have a go at it yourself, (and if you are interested in learning to read and exploit binaries you probably should as the graded exercises and simple environment make it much more forgiving than a full scale PC architecture program) bear in mind, there are SPOILERS from here on in.

Skipping quickly over the first few

  • New Orleans and Sydney: Strongly reminiscent of the tutorial
  • Hanoi: Backdoor/OF - read the code for CMPs. First HSM-1.
  • Cusco: My First Stack Smash and PC Hijack
  • Reykjavik: Military Grade Encryptionâ„¢ (main difficulty caused by minimalist debugger)
  • Whitehorse: First HSM-2.

Whitehorse and a quick aside on HSM-1 vs HSM2

HSM1 and HSM2 Serve similar purposes -- offloading security properties of the password verification somewhere software faults can't reach the secret data.

HSM1 provides a secure password store which the software can query to check if a given password is valid before unlocking the deadbolt.

HSM2 augments the secure password store with direct control of the deadbolt itself. The main insight needed here is what does that mean for the attack surface?

Without HSM-1 and HSM-2 there other examples primarily involve either hardcoded keys or some similar form of backdoor.

With HSM-1 as well as looking for back doors the fact that password checking and unlocking are dealt with separately from each other allows us opportunities to interfere with the return value of the validity check before the unlocking code uses it to decide whether the deadbolt should be released.

On the other hand for the HSM-2, there is no such intermediate value to tamper with. However the Unlocking Interrupt 7F is still available in user-space, so if we can gain control of PC, and can run our own code, we can call the interrupt ourselves.

This is exactly what we do for Whitehorse.

<login>
add       #0xfff0, sp
mov       #0x4470 "Enter the password to continue.", r15
call      #0x4596 <puts>
mov       #0x4490 "Remember: passwords are between 8 and 16 characters.", r15
call      #0x4596 <puts>
mov       #0x30, r14
mov   sp, r15
call       #0x4586 <getsn>

Routinely in the disassembly, subtractions happen by adding a Two's Complement format, signed negative, native word to a register. In this case, add #0xfff0 is really another way of saying sub #0x10. If you're familiar with assembly from other platforms this is pretty clearly setting up space on the stack to store local variables. Specifically 16 (0x10) bytes.

But immediately visible below this, is a call to getsn() to receive up to 48(0x30) bytes from the user. Filling the password buffer with the traditional overflow debugging payload of as many 'A's as you can fit in the entry point causes us to see the error insn address unaligned. Looking at PC, we see the two 'A's (4141) in the address. So far very similar to Cusco. However, we're up against the new improved HSM-2, so we can't just set PC to the unlock function. We'll have to make our own. Because there's no ASLR in place, our stack will exist in the same place every time, so we can just use the assembler to assemble push 0x7f; call INT;, place this at the start of our buffer, and place the address that our buffer is saved to in the word that overwrites the return pointer on the stack (i.e. the word after our local variable is stored). The password itself fails, but when the login function returns, it loads our return pointer and executes our unlock code.

Johannesburg

Weird behaviour here.

452c:  3150 eeff      add    #0xffee, sp
4530:  f140 d200 1100 mov.b  #0xd2, 0x11(sp)

We're told we're allowed a password of up to 16 characters. This is policed by setting up 18 bytes of stack room, and then setting the 17th byte to a hardcoded value (0xd2) which is checked later to make sure the user hasn't overwritten it. I guess this could be considered a sort of stack canary?

In any case we can mount a trivial overflow by making sure that when we overflow our variable to hijack the program counter, we set all the preceding bytes to d2 (saving the effort of worrying too much about which offset it's at).

Montevideo

Getting a little harder. We aren't writing directly to the stack any more, but to an address space somewhat further away. Our input is then being strcpyd onto the stack. One feature of strcpy is that it expects C-strings which terminate with a null (00) byte.

Solving this is largely the same as Whitehorse, except the null byte restriction requires us to be a bit more creative creating the payload. Pushing 0x007f onto the stack to unlock contains a null. However we can xor 0x007f with another value,

push $(0x007f ^ $val)
xor $val, SP(0)
call <INT>

Which works just fine.

Santa Cruz

This exercise has a far more complex checking mechanism which took me a while longer to understand. Similar to numerous previous exercises, getsn is called with a much larger value than is necessary, or even fitting for the spaces the user input is being stored (0x63 bytes). Further more there are some odd assignments early on in the login program:

mov.b     #0x0, -0x6(r4)
mov.b     #0x8, -0x19(r4)
mov.b     #0x10, -0x18(r4)

Experimentally throwing large inputs at the program, we find a couple of tight character counting loops which then try to compare the number of characters counted against these values. The length can be between 0x8 (8) and 0x10 (16) characters as described in the service banner.

However, we're perfectly capable of overflowing both of these inputs, and in the process re-writing the parameters for input size as well as the return pointer (which ultimately hands us our win).

We also need to set our second input to a smallish value within the parameters we have set: Before our return pointer is loaded, if loops counting string lengths fail tests, we're automatically failed, and our return pointer never gets loaded. Instead I created a very long username, a short password, and reversed the order of the max and min values so my counts needed to be higher than a big number or smaller than a lower number.

Because this is on HSM-1, all we need to do it control the program counter and send it to the unlock_door() function.

Addis Ababa

Printf format string vuln! [2]

Program getsn's some user data, strcpy's it somewhere apparently safe, and then printf's it directly back to the screen after checking the HSM-1 module to see if it is valid.

This version of printf isn't as fully featured as ones you'd normally see in C and related languages, but that's okay. Crucially, it implementes %n. Our payload consists of the (little endian encoded) address of the validity check byte, followed by %x (to bump the printf read pointer along a word so it points to our target address) followed by %n which instructs printf to write the number of chars printed so far to that byte.

Since any number other than 0 is recognised as a success, this is all we need to pop the lock.

[1]Arguably it's not actually a CTF, as there are no flags, and it's more of a standing wargame anyway. But it does have some fun challenges nonetheless.
[2]I knew about these but I never used one in practice before. This tutorial helped me a lot understanding the practice: https://crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf
Category: writeups
Tags: ctf embedded