본문으로 바로가기

CODEGATE2018 7amebox1

일단 요즘 분석능력을 기르기 위해서이기도 하고 일반적인 힙문제도 안나오는거같고 해서 vm문제를 좀 보려고 하는데 많은분들이 추천해주셔서 보게되었다.

문제를 구성하는 파일은 총 4개이다.

  • _7amebox.py : vm을 구현해둔 python 파일

    • 해당 파일에서 *.firm파일을 파싱

  • flag : 플래그

  • mic_check.firm : 바이트코드로 이루어짐

  • vm_original.py : 에물레이터


일단 특징을 보면, 21bit로 구현되어있다. (1byte = 7bit)

그리고 31개의 명령어와 6개의 syscall이 구현되어있다.

이제 vm_original에서 핵심 부분을 살펴보자. 먼저, EMU calssexecute이다.

def execute(self):
   try:
       while 1:
           cur_pc = self.register.get_register('pc')

           op, op_type, opers, op_size = self.dispatch(cur_pc)

           if not self.memory.check_permission(cur_pc, PERM_EXEC) or not self.memory.check_permission(cur_pc + op_size - 1, PERM_EXEC):
               self.terminate("[VM] Can't exec memory")
               self.register.set_register('pc', cur_pc + op_size)
               op_handler = self.op_hander_table[op]

               op_handler(op_type, opers)
               except Exception as e:
                   self.terminate("[VM] Unknown error")

dispatch함수에서 op, op_type, opers, op_size를 파싱해온다.


그러면 dispatch함수를 보자.

def dispatch(self, addr):
   opcode = self.bit_concat(self.read_memory(addr, 2))
   op     = (opcode & 0b11111000000000) >> 9
   if op >= len(self.op_hander_table):
       self.terminate("[VM] Invalid instruction")

       op_type = (opcode & 0b00000100000000) >> 8
       opers   = []
       if op_type == TYPE_R:
           opers.append((opcode & 0b00000011110000) >> 4)
           opers.append((opcode & 0b00000000001111))
           op_size = 2

           elif op_type == TYPE_I:
               opers.append((opcode & 0b00000011110000) >> 4)
               opers.append(self.read_memory_tri(addr+2, 1)[0])
               op_size = 5

               else:
                   self.terminate("[VM] Invalid instruction")

                   return op, op_type, opers, op_size

op_typeI인지, R인지에 따라서 파싱이 달라지는데, I는 인자가 상수일때, R은 인자가 레지스터일때이다.


이런 코드들을 대충 긁어서 op, op_type등을 출력시키면, 아래와 같은 정보를 얻을 수 있다.

0x0:  30 1 [13, 4] 5
0x5: 21 0 [0, 0] 2
0x7: 8 0 [0, 0] 2
0x9: 6 0 [11, 0] 2
0xb: 4 0 [11, 12] 2
0xd: 11 1 [12, 60] 5
0x12: 4 0 [5, 11] 2
0x14: 11 1 [5, 3] 5
0x19: 4 1 [6, 74565] 5
0x1e: 2 0 [6, 5] 2
0x20: 4 1 [0, 205] 5
0x25: 30 1 [13, 102] 5
0x2a: 4 1 [1, 66] 5
0x2f: 4 0 [5, 11] 2
0x31: 11 1 [5, 60] 5
0x36: 4 0 [0, 5] 2
0x38: 30 1 [13, 35] 5
0x3d: 4 1 [0, 211] 5
0x42: 30 1 [13, 73] 5
0x47: 4 0 [5, 11] 2
0x49: 11 1 [5, 3] 5
0x4e: 0 0 [6, 5] 2
0x50: 23 1 [6, 74565] 5
0x55: 28 1 [13, 2097067] 5
0x5a: 4 0 [12, 11] 2
0x5c: 7 0 [11, 0] 2
0x5e: 7 0 [13, 0] 2
0x60: 4 0 [3, 1] 2
0x62: 4 0 [2, 0] 2
0x64: 4 1 [1, 0] 5
0x69: 4 1 [0, 3] 5
0x6e: 8 0 [0, 0] 2
0x70: 7 0 [13, 0] 2
0x72: 6 0 [1, 0] 2
0x74: 6 0 [2, 0] 2
0x76: 6 0 [3, 0] 2
0x78: 4 0 [3, 1] 2
0x7a: 4 0 [2, 0] 2
0x7c: 4 1 [1, 1] 5
0x81: 4 1 [0, 2] 5
0x86: 8 0 [0, 0] 2
0x88: 7 0 [3, 0] 2
0x8a: 7 0 [2, 0] 2
0x8c: 7 0 [1, 0] 2
0x8e: 7 0 [13, 0] 2
0x90: 6 0 [0, 0] 2
0x92: 6 0 [1, 0] 2
0x94: 4 0 [1, 0] 2
0x96: 30 1 [13, 13] 5
0x9b: 5 0 [0, 1] 2
0x9d: 30 1 [13, 2097104] 5
0xa2: 7 0 [1, 0] 2
0xa4: 7 0 [0, 0] 2
0xa6: 7 0 [13, 0] 2
0xa8: 6 0 [1, 0] 2
0xaa: 6 0 [2, 0] 2
0xac: 21 0 [1, 1] 2
0xae: 21 0 [2, 2] 2
0xb0: 1 0 [2, 0] 2
0xb2: 24 1 [2, 0] 5
0xb7: 27 1 [13, 9] 5
0xbc: 17 0 [0, 0] 2
0xbe: 17 0 [1, 0] 2
0xc0: 29 1 [13, 2097131] 5
0xc5: 4 0 [0, 1] 2
0xc7: 7 0 [2, 0] 2
0xc9: 7 0 [1, 0] 2
0xcb: 7 0 [13, 0] 2
0xcd: 27 1 [6, 1662829] 5
0xd2: 0 0 [6, 2] 2
0xd4: 30 0 [14, 5] 2

이제, 이를 통하여 디스어셈블러를 만들면, 어셈블리 코드를 얻어낼 수 있고, BOF취약점이 있는 바이너리임을 알 수 있다.

NX가 비활성화되어있고, ret까지 덮을 수 있으며, stack의 주소가 고정이므로 ORW를 하는 어셈블리 코드를 작성하여 ret에 스택 주소를 덮어 ORW를 진행하면 된다.