queenp's blog

Posted Wed 06 July 2016

MicroCorruption CTF part 2

Novosibirsk

Another HSM-2 challenge. Scanning down the disassembly view, we see function labels for putchar, getchar, getsn, puts, printf and strcpy. I'm largely treating this set of functions mapped after strings similarly to imports in conventional debugging: I avoid looking too closely at how they function besides knowing a variety of things that can go wrong with them in general.

The first thing this version of the software does is create a fairly large stack buffer, 500 bytes long. At this point I was finding the easiest way to understand things was to write pseudocode interpretations of the software behaviour.

void main()
{
    char local[500];
    printf("Enter your username below to authenticate.\n");
    printf(">> ");

    char* heapstring = 0x2400;
    getsn(heapstring, 500);

    strcpy(heapstring, local);
    printf(local);
    putchar("\n");

    if(conditional_unlock_door(local))
    {
       printf("Access Granted");
    } else {
       printf("That username is not valid");
    }
    exit(0);
}

int conditional_unlock_door(char* string)
{
    // Call conditional unlock interrupt with string
    return INT(0x7d, string);
}

So far so good. There's no obvious size mismatch between our inputs and where they're being stored, but we do have an unformatted printf of our input before the test happens. This is all we need.

Rooting through the disassembly, we can find the address of the instruction where the conditional_unlock_door() function pushes the interrupt code 0x7D.

Using the printf call to patch that instruction to push 0x7F instead, we can force the interrupt to open the door for us. To construct the format string, I first created a garbage string (I generated A1B2C3D4... quickly in a python console) 0x7f - 1 bytes long followed by "%c%n". This resulted with an error as the %n tried to write to an unaligned address (corresponding to the pair of bytes in my string it was taking as the address). I then placed the (encoded) address of the interrupt code instruction in this location (so that my format string was now printing 0x7f characters, and then writing that character count into my target address) and the lock popped.

Jakarta

Return of the HSM-1!

This one got a bit more subtle and the solution relies on matters that can't easily be pseudocoded into C.

In particular, separately a username and then a password are collected from the user, copied to another memory location, and checked for length before being used. The length test consists of a tight inc;tst.b 0;jmp loop looking for the first null byte. User input can be up to 32 bytes long in total (combining both the username and password).

The critical bug here, is that the comparison to limit the total characters of input possible is subject to an underflow/fencepost error. If we input 32 bytes of input in the username, this is treated as perfectly valid (we're not past our limit yet). And our full string length is 33 bytes (including the terminating NULL byte). When that is subtracted from 32 to find out how many characters we have left for the password, we get -1, (or in Two's Complement, 0xffff). This value is then ANDed with 0x1ff, so the value for how much password input we're allowed is now 0x1ff (511!). This is plenty to use the password field to mount a massive overflow on the return pointer. To save myself the effort of even worrying about where to place the return address of the unlock_door() function to line it up with the return pointer, I made a buffer repeating the pointer over and over with the python console (addr_bytes * (0x1ff/2)) and pasted it into the password field.

Algiers

In the manual for this one, we find no reference to Hardware Security Modules, but an Account Manager previously unmentioned. It's usually the changes and new features where the bugs are, so here goes.

Skimming the new functions, we have interestingly developed the use of malloc() and free(). Perhaps it's time to look for double-free vulnerabilities. There's no documentation in the manual for malloc or the Account Manager.

void login()
{
  char* username = malloc(0x10); // stored in r10
  char* password = malloc(0x10); // stored in r11
  puts("Enter your username and password to continue.");
  puts("Username >>");
  getsn(username, 0x30); // Wait a moment, that's not 0x10!
  puts("Username >>"); // Well this is a crappy bug!
  puts("(Remember: passwords are between 8 and 16 characters.)");
  getsn(password, 0x30); // Again, hark, a heap overflow!
  if(test_password_valid(password))
  {
    unlock_door();
    puts("Access granted.");
  } else {
    puts("That password is not correct");
  }
  free(username);
  free(password);
}

Nothing entirely controversial so far. Obviously there's the heap overflow, but, in the spirit of experimentalism, let's overflow it with abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV

Crashes in the middle of free() with:

load address unaligned: 7275
pc 4520 sp 4392 sr 0011 cg 0000
r4 0000 r5 5a08 r6 0000 r7 0000
r8 0000 r9 0000 r10 240e r11 2424
r12 1f9c r13 7674 r14 7271 r15 241e

Looking at r13 and r14, we can see that characters from our input have been loaded into the relevant registers. Most likely without reversing the free() function, this can be assumed to be due to our overflow from one heap buffer into another resulting in a double-free.

So we're going to have to look at what malloc()/free() do and how we can exploit it.

Seemingly, malloc sets up a heap in the form of a header

struct {
  int* heapbase;
  int* next;
  int sizeval; // 2 * length &=1;
  char buf[size];

With the next ll header initialised each time to an invalid value ready for allocation.

Free on the other hand, subtracts 6 from the address (the length of the header), does some other stuff, and interestingly writes values out from the header to the proximity of the address of the next pointer.

Specifically,

  • Subtract 6 from r15 (the pointer being freed)
  • Set r13 to the length word, square off the 0x1 bit from it.
  • Save the length word back in place now with the test failure flag set.
  • Save the next header location in r14.
  • Save the next length word in r12.
  • If in use, add 0x6 + r13 to r12 and re-save (@r14+4)
  • save r15+2 at @r14 + 2 and into r13.
  • save r14 to @r13.
  • store @r15 to r15
  • load @r15+2 into r14
  • load @r14+4 into r13

Tidy up and exit.

That seems plenty to write arbitrary data to a location of our choosing.

Assembling jmp unlock_door gives us ff3f

IMHO the easiest one to exploit is mov @r15+2, @r14+2

So we're going to overflow into the "next" pointer [nextp][jmp unlock] *4534 ff3f

Category: writeups
Tags: reversing crackmes ctf