Defcon CTF Quals 2011 – Retro 400

This challenge was on remote exploiting. The binary is for FreeBSD.

The program is some kind of a Virtual Machine, with it’s own stack and memory.
binary

Summary: memory address check mistake, write shellcode and overwrite _exit function pointer

Quick look at the programs code (main is sub_80496F0):

  v5 = "HistoryRepeatsItself\n";
  v9 = &a1;
  v4 = a1;
  v6 = buf;
  v7 = a2;
  fwrite("Password: ", 1, 10, STDOUT);
  fgets(buf, 256, STDIN);
  v3 = 22;
  do
  {
    if ( !v3 )
      break;
    v2 = *v6++ == *v5++;
    --v3;
  }
  while ( v2 );
  JUMPOUT(!v2, *error);
  load_code(v4, v7);
  while ( 1 )
    execute(v4);

We see a password check and then loading and executing virtual machine code. load_code is rather complicated function, but after close look it simply does the following things:

  • it reads code from a file, passed through argv[1] or from stdin
  • the code is parsed as a some sort of whitespace: 0x20 (space) is 1, 0x09 (tab) is 0:
    • opcodes are 7 such ‘bits’
    • operands are 32 such ‘bits’
  • instructions with arguments must end with \n

Ok, now we can pass some code for the VM. Let’s analyze execute function (sub_08048DE0). There are many opcodes and we can easily guess most of them, like arithmetic operands:

      case 0u:
        v13 = sub_8048290();
        v14 = sub_8048290();
        return sub_80482C0(v13 + v14);
      case 1u:
        v15 = sub_8048290();
        v16 = sub_8048290();
        return sub_80482C0(v16 - v15);
      case 2u:
        v17 = sub_8048290();
        v18 = sub_8048290();
        return sub_80482C0(v17 * v18);
      case 3u:
        v19 = sub_8048290();
        v20 = sub_8048290();
        return sub_80482C0(v20 / v19);
      case 4u:
        v21 = sub_8048290();
        v22 = sub_8048290();
        return sub_80482C0(v22 % v21);

But the most interesting instruction is this:

case 0x1F:
  v13 = get_argument();
  if ( (unsigned int)v13 > 0x7F )
    fwrite("Register out of range.\n", 1, 23, STDOUT);
  result = get_argument();
  dword_80E49C0[v13] = result;
  return result;

It reads two arguments and copies arg2 to mem[arg1]. And here is the mistake: arg1 is checked, but if it’s too large, nothing bad happens – only a message is shown and the instruction is executed.

So, we can overwrite any writeable place in program with any data. I decided to put shellcode at the first virtual memory addresses. And then overwrite some callable pointer with our shellcode address. There is a nice one, at 0x080E4FF0 (it points to a some sort of terminating function). It’s has index of 396 ( (0x080E4FF0-0x080E49C0)/4 ):

vals = struct.unpack("<%dI" % (len(SC)/4), SC)

code = ""
for i, val in enumerate(vals):
    # PUT SHELLCODE TO VM's MEM
    code += instr(31, i, val)
# MAKE POINTER POINT TO SHELLCODE
code += instr(31, 396, sc_addr)

The final exploit is here.

Also, there were no binaries there, even /bin/sh. So I wrote a shellcode, which opened file “key” and printed it’s contents.

The flag: grace slick is a badass bitch!

1 comment

    • frank on February 17, 2012 at 17:01
    • Reply

    there’s. little chance that arg1 can get saturated

Leave a Reply to frank Cancel reply

Your email address will not be published.