본문 바로가기

카테고리 없음

Unicorn-Engine, VMware RPC backdoor

Unicorn engine을 사용하면 임의 코드를 에뮬레이팅 할 수 있다. 간단한 어셈블리 코드를 만들어서 테스트 해볼 수 있다.

주로 악성코드로부터 얻는 쉘코드를 분석하는데 많이 사용하는 것 같다. 나는 주로 파이썬을 사용하기 때문에 파이썬 바인딩을

설치해 쓰고 있지만 unicorn.js에서 자바스크립트로 포팅해놓은 프로젝트도 찾을 수 있었다. 심지어 AFL과 연동해 취약점 찾는

도구로 활용한 afl-unicorn 프로젝트도 있다.

아래는 EAX 레지스터 값을 변경하는 예제 코드다.

from keystone import *
from unicorn import *
from unicorn.x86_const import *

def hook_code(mu, address, size, user_data):  
    print '>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)     

    r_eax = mu.reg_read(UC_X86_REG_EAX)
    print '>>> Tracing EAX: 0x%X' % r_eax


if __name__ == "__main__":
    code = ''
    code += b'mov eax, 0x1;'
    code += b'add eax, 0x1;'
    code += b'add eax, 0x1;'
    code += b'add eax, 0x1;'
    code += b'mov eax, 0x0;'
    code += "mov eax, 0x20;"
    code += "mov eax, esp;"
    code += "add eax, 1;"
    code += "mov eax, 0x41424344;"
    code += "nop;"
    code += "nop;"
    code += "nop;"

    opcode = ''
    try:
        ks = Ks(KS_ARCH_X86, KS_MODE_32)
        encoding, count = ks.asm(code)    
        opcode = ''.join(map(chr, encoding))
    except KsError as e:
        print "ERROR: %s" % e

    try:
        mu = Uc(UC_ARCH_X86, UC_MODE_32)
        BASE = 0x400000
        STACK_ADDR = 0x0
        STACK_SIZE = 1024*1024

        mu.mem_map(BASE, 1024*1024)
        mu.mem_map(STACK_ADDR, STACK_SIZE)    

        mu.mem_write(BASE, opcode)
        mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 1)

        mu.hook_add(UC_HOOK_CODE, hook_code)
        mu.emu_start(BASE, BASE+len(opcode))

    except UcError as e:
        print "ERROR: %s" % e

unicorn을 사용해보게 된 계기는 Host OS <-> Guest OS 사이의 Vmware RPC Backdoor 작동을 살펴보기 위해서였다. 간단한 RPC 요청 어셈블리 코드를 구성하고 실행하는데 적합해 보였기 때문이다. 하지만, Guest OS에서 Host OS 클립보드에 있는 내용에 접근하는 예제를 만들어 에뮬레이팅 해봤는데 원하는 결과를 얻지 못했다.

code = ''
code += b"INT3;"
code += b'mov eax, 0x564D5868h;'
code += b'mov ecx, 0x6;'    # BDDOR_CMD_GETSELLENGTH
code += b'mov dx, 0x5658;'
code += b'in eax, dx;'

code += b'mov eax, 0x564D5868h;'
code += b'mov ecx, 0x7;'   # BDDOR_CMD_GETNEXTPIECE
code += b'mov dx, 0x5658;'
code += b'in eax, dx;'

code += b'mov eax, 0x564D5868h;'
code += b'mov ecx, 0x7;'   # BDDOR_CMD_GETNEXTPIECE
code += b'mov dx, 0x5658;'
code += b'in eax, dx;'

Host OS에서 AAAABBBBCCCC를 클립보드에 복사한 후 Guest OS에서 위 코드를 실행하면 첫 번째 'in' 명령어 실행 후 EAX 레지스터에 0xC(12)가 들어올 것을 예상했는데 EAX 레지스터에 0이 들어가 있는 것으로 확인됐다.

0x40000F를 트레이싱 할 때 EAX 레지스터가 0을 가지는 것을 볼 수 있다. 제대로 작동하지 않았다는 의미다.

이유는 'in' 명령어에 있다. 유저모드 프로그램이 'in' 명령어를 호출하면 권한에러에 의해 인터럽트가 발생한다. vmware Backdoor는 이렇게 발생한 인터럽트를 캐치해서 무엇을 하려고 했는지 확인한 후 그에 상응하는 데이터를 돌려주는 방식으로 작동한다.

이런 신호는 Host OS의 vmware-vmx.exe 프로세스에서 처리한다. unicorn 엔진은 에뮬레이터 안에서 발생한 이상 작동을 꿀꺽해버렸고 이 때문에 신호가 Host OS까지 전달되지 못했다. 인터럽트 발생할 시 UC_HOOK_INTR 코드에 대한 콜백함수를 통해 사용자 코드가 개입할 수 있지만 지금 내 경우에선 콜백함수로 가지 않는다. 그냥 꿀꺽. 지금 시점에는 모르는 상황이지만 나중에는 알 게 되길.. 어쨌든 RPC backdoor랑 놀기에 unicorn 엔진은 적합하지 않은 것 같다.

현 상황에서는 unicorn 보다는 ctypes으로 win32 api를 사용하는 것이 더 나은 선택인 것 같다.

#-*- coding: utf-8 -*-
from keystone import *
from ctypes import *

def execute_opcode_windows(opcode, ret_type = c_int, *arg):
    kernel32 = windll.kernel32
    opcode_ = bytearray(opcode)
    opcode_length = len(opcode_)

    ptr = kernel32.VirtualAlloc(0, opcode_length, 0x3000, 0x40)        
    buf = (c_char * opcode_length).from_buffer(opcode)
    kernel32.RtlMoveMemory(ptr, buf, opcode_length)

    print "Address: 0x%X" % ptr

    func = CFUNCTYPE(ret_type)(ptr)
    result = func(*arg)

    freed = kernel32.VirtualFree(ptr, 0, 0x8000)
    return result


if __name__ == "__main__":
    code = ''
    code += b'mov eax, 0x564D5868h;'
    code += b'mov ecx, 0x6;'    # BDDOR_CMD_GETSELLENGTH
    code += b'mov dx, 0x5658;'
    code += b'in eax, dx;'

    # code += b'mov eax, 0x564D5868h;'
    # code += b'mov ecx, 0x7;'   # BDDOR_CMD_GETNEXTPIECE
    # code += b'mov dx, 0x5658;'
    # code += b'in eax, dx;'

    # code += b'mov eax, 0x564D5868h;'
    # code += b'mov ecx, 0x7;'   # BDDOR_CMD_GETNEXTPIECE
    # code += b'mov dx, 0x5658;'
    # code += b'in eax, dx;'
    code += b'ret;'

    try:
        ks = Ks(KS_ARCH_X86, KS_MODE_32)
        encoding, count = ks.asm(code)    
        opcode = ''.join(map(chr, encoding))

        result = execute_opcode_windows(opcode)        
        print "result: 0x%X" % result      # result: 0xC
    except KsError as e:
        print("ERROR: %s" %e)