카테고리 없음

에뮬레이터 이론 공부1

20308박동진 2024. 4. 8. 11:10

 

펌웨어(Firmware)

- *임베디드 시스템에서 구현되는 소프트웨어를 칭하는 단어.

 

- 하드웨어의 논리회로의 기능 들을 보강 및 대신할 수 있도록 만든 프로그램

 

- 기술이 발전함에 따라 소프트웨어가 하드웨어에게 전달해야하는 정보가 많아짐에 따라 하드웨어도 따로 부품을 교체하지 않고도 '업데이트'를 할 수 있게끔 하는 기술.

 

*임베디드 시스템 : 제한적인 일을 수행하는 시스템.

펌웨어 리호스팅(Firmware Re-hosting)

- 분석 도중 문제가 발생했을 경우 해당 테스트 베드를 아예 새로 구축해야한다는 문제가 있음.

 

- 이런 문제를 해결하기 위해 일종의 변환을 진행하는 것펌웨어 리호스팅이라고 함.

 

ex)  안드로이드 앱은 컴퓨터에서 실행할 수 없음. 당연히 컴퓨터와 핸드폰의 운영체제는 다르니까. 그러나 Nox, Bluestack 등의 에뮬레이터를 깔 경우 어플리케이션들을 실행할 수 있게 됨.

 

에뮬레이션(emulation)이란?

- 에뮬레이션 *호스트(host)라고 하는 시스템에서 지원하지 않는 소프트웨어, 즉 게스트(guest)를 실행하기 위해 에뮬레이터(emulator)라는 서포트 프로그램을 이용하는 방법.

 

-  고로 원본 코드를 호스트에 맞게 번역(traslate)해야 하고, 적절한 주변기기(peripheral), 타이머(timer), 가속기(accelerator), 센서(sensor) 역시 맞춰야 한다. 

 

- 스위치를 라우터로 사용하는 Vlan 과 같은 이유로 사용됨.(비용절감)

 

*호스트(host) : 데스크탑 PC와 같은 범용 컴퓨터 시스템

 

에뮬레이터(emulator)이란?

- 에뮬레이터(Emulator)는 한 시스템에서 다른 시스템을 복제하여 두 번째 시스템이 첫 번째 시스템을 따라 행동하는 것.

 

- 에뮬레이터 (Emulator) 로 한 컴퓨터를 다른 모델 또는 CPU 유형을 모방해서 다른 시스템에서 실행하도록 작성된 소프트웨어를 실행하도록 설계.

 

-  다른 장치와 유사하게 작동하도록 제작된 장치.

 

+

에뮬레이터 종류

Simics : 

- 여러 아키텍처들을 에뮬레이트하기 위해 생성됨.

- configuration 관리, scripting, 자동화된 디버깅 기능 제공하고, 다른 정적/동적 분석 툴을 사용해서 emulate 시스템을 구성할 수 있음.

 

QEMU : 

- emulation 속도를 개선하기 위해 정확도를 조금 포기.

- 오픈소스임.

 

Ghidra Emulator : 

- 오픈소스 리버스 엔지니어링 툴

- Sleigh라는 자체 processor modeling 언어와 register 전송 언어인 P-code를 사용.

 

에뮬레이터의 동작

- 에뮬레이터는 일반적으로 특정 *마이크로프로세서에 대한 명령 해석기(interpreter)로 구현.

 

- 기본 구조는 명령어를 페치(fetch)하고 *인터럽트를 관리(handle)하고, 그래픽, 소리, 사용자 입력, 타이머 동기화 등을 실행하는 while 루프로 생각할 수 있다.

 

ex)

while(!stop_emulation)
{
    executeCPU(cycles_to_execute);
    generateInterrupts();
    emulateGraphics();
    ...
}

 

*마이크로프로세서 : CPU 내부에 있는 매우 작은 처리 장치

*인터럽트 : 프로세스 실행 도중 예기치 않은 상황이 발생할 때 발생한 상황을 처리한 후 실행 중인 작업으로 복귀하는 것

 

 

 

유니콘 실습)

int strcmp(char *a, char *b) {
    // 문자열의 길이 구하기
    int len = 0;
    char *ptr = a;
    while (*ptr) {
        ptr++;
        len++;
    }
    
    // 문자열 비교
    for (int i = 0; i <= len; i++) {
        if (a[i] != b[i])
            return 1;
    }
    return 0;
}

__attribute__((stdcall)) int super_function(int a, char *b) {
    if (a == 5 && !strcmp(b, "batman")) {
        return 1;
    }
    return 0;
}

int main() {
    super_function(1, "spiderman");
}

 

--> 해석

#include <stdio.h>

// 문자열을 비교하여 동일한지 여부를 확인하는 함수
int strcmp(const char *a, const char *b) {
    // 두 문자열의 각 문자를 비교하며 끝까지 반복
    while (*a && *b) {
        // 현재 위치의 문자가 다를 경우 해당 문자들의 ASCII 값 차이를 반환
        if (*a != *b)
            return *a - *b;
        // 다음 문자로 이동
        a++;
        b++;
    }
    // 두 문자열 중 하나의 문자열이 끝날 때까지 모든 문자가 동일한 경우
    // 두 문자열의 길이가 같다면 0을 반환하고, 아니면 길이가 긴 문자열의 끝 문자를 기준으로
    // ASCII 값의 차이를 반환하여 순서를 정합니다.
    return *a - *b;
}

// 정수와 문자열을 받아 특정 조건일 때 1을 반환하는 함수
int super_function(int a, const char *b) {
    // a가 5이고 b가 "batman"일 때만 1을 반환
    if (a == 5 && !strcmp(b, "batman")) {
        return 1;
    }
    // 그 외의 경우에는 0을 반환
    return 0;
}

// 메인 함수
int main() {
    // super_function 호출하여 결과 출력
    int result = super_function(1, "spiderman");
    printf("Result: %d\n", result);
    return 0;
}

 

gcc function.c -m32 -o function

 

애뮬레이션 시켜서 return 1의 결과 값 가져오기

 for (int i = 0; i <= len; i++) {
        if (a[i] != b[i])
            return 1;
    }

해당 부분.

 

풀이코드. 

from unicorn import *
from unicorn.x86_const import *
import struct
from capstone import *
import sys

def read(name):
    with open(name) as f:
        return f.read()

def u32(data):
    return struct.unpack("I", data)[0]

def p32(num):
    return struct.pack("I", num)

BASE_ADDR = 0x8048000
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
DATA_ADDR = 0x2000000

CS = Cs(CS_ARCH_X86, CS_MODE_32)

def bypass(mu, address, size, user_data):
    mu.reg_write(UC_X86_REG_EIP, address + size)

def handler(mu, address, size, user_data):
    print(hex(address), end=" ")
    ins = CS.disasm(mu.mem_read(address, size), size)
    i = list(ins)[0]
    print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))

def solver():
    mu = Uc (UC_ARCH_X86, UC_MODE_32)

    mu.mem_map(BASE_ADDR, 1024*1024)  # code Area
    mu.mem_map(STACK_ADDR, STACK_SIZE)  # stack Area
    mu.mem_map(DATA_ADDR, 0x1000)       # DATA Area

    TARGET = read('./task2')    # Write Data
    mu.mem_write(BASE_ADDR, TARGET)
    mu.mem_write(DATA_ADDR, 'batman\x00')

    mu.reg_write(UC_X86_REG_ESP, int(STACK_SIZE)/3)   # Set ESP Register

    mu.hook_add(UC_HOOK_CODE, handler)    # Hook code insert
    mu.hook_add(UC_HOOK_MEM_WRITE_UNMAPPED, bypass)   # if program access unmapped area, bypass function execute
    mu.hook_add(UC_ERR_READ_UNMAPPED,bypass)

    r_esp = mu.reg_read(UC_X86_REG_ESP)   # Argument ARG1,2
    mu.mem_write(r_esp+4, p32(5))
    mu.mem_write(r_esp+8, p32(0x2000000))

    mu.emu_start(0x804845f, 0x8048489)
    print(mu.reg_read(UC_X86_REG_EAX,))
    sys.exit()

def main():
    print("[*] Unicorn Engine Tutorial")
    solver()

if __name__ == '__main__':
    main()

--> 해석 

# Unicorn 및 관련 모듈 가져오기
from unicorn import *
from unicorn.x86_const import *
import struct
from capstone import *
import sys

# 파일을 읽어오는 함수
def read(name):
    with open(name) as f:
        return f.read()

# 4바이트 데이터를 언팩하는 함수
def u32(data):
    return struct.unpack("I", data)[0]

# 4바이트 데이터를 팩하는 함수
def p32(num):
    return struct.pack("I", num)

# 메모리 주소 및 크기 정의
BASE_ADDR = 0x8048000
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
DATA_ADDR = 0x2000000

# Capstone 엔진 초기화
CS = Cs(CS_ARCH_X86, CS_MODE_32)

# 메모리 접근 오류를 우회하는 함수
def bypass(mu, address, size, user_data):
    mu.reg_write(UC_X86_REG_EIP, address + size)

# 명령어 실행을 처리하는 함수
def handler(mu, address, size, user_data):
    # 현재 주소 출력
    print(hex(address), end=" ")
    # 명령어 디스어셈블링
    ins = CS.disasm(mu.mem_read(address, size), size)
    # 첫 번째 명령어 가져오기
    i = list(ins)[0]
    # 명령어 출력
    print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))

# 바이너리를 실행하고 디버깅하는 함수
def solver():
    # Unicorn 엔진 초기화
    mu = Uc (UC_ARCH_X86, UC_MODE_32)

    # 메모리 매핑
    # 코드 영역 매핑
    mu.mem_map(BASE_ADDR, 1024*1024)
    # 스택 영역 매핑
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    # 데이터 영역 매핑
    mu.mem_map(DATA_ADDR, 0x1000)

    # 이진 파일 읽기
    TARGET = read('./task2')
    # 코드와 데이터 영역에 쓰기
    mu.mem_write(BASE_ADDR, TARGET)
    mu.mem_write(DATA_ADDR, 'batman\x00')

    # 스택 포인터 설정
    mu.reg_write(UC_X86_REG_ESP, int(STACK_SIZE)/3)

    # 명령어 훅 추가
    mu.hook_add(UC_HOOK_CODE, handler)

    # 메모리 접근 오류 우회 훅 추가
    mu.hook_add(UC_HOOK_MEM_WRITE_UNMAPPED, bypass)
    mu.hook_add(UC_ERR_READ_UNMAPPED,bypass)

    # 함수 인자 설정
    r_esp = mu.reg_read(UC_X86_REG_ESP)
    mu.mem_write(r_esp+4, p32(5))  # ARG1
    mu.mem_write(r_esp+8, p32(0x2000000))  # ARG2

    # 실행 및 디버깅 시작
    mu.emu_start(0x804845f, 0x8048489)

    # 결과 출력
    print(mu.reg_read(UC_X86_REG_EAX))
    sys.exit()

# 메인 함수
def main():
    print("[*] Unicorn Engine Tutorial")
    solver()

if __name__ == '__main__':
    main()

-----------------------

 

 

함수 정의:
read(name): 파일을 읽어들이는 함수입니다.
u32(data): 4바이트 데이터를 읽는 함수입니다.
p32(num): 4바이트 데이터를 쓰는 함수입니다.

 

메모리 및 상수 정의:
BASE_ADDR, STACK_ADDR, STACK_SIZE, DATA_ADDR: 메모리 주소 및 크기를 정의합니다.
CS: Capstone 엔진을 초기화합니다.
bypass 및 handler 함수 정의:
bypass(mu, address, size, user_data): 메모리 접근 에러를 우회하는 함수입니다.
handler(mu, address, size, user_data): 명령어를 디스어셈블하여 출력하는 함수입니다.

 

solver 함수 정의:
메모리 매핑 및 초기화: 이진 파일을 메모리에 로드하고 초기화합니다.
명령행에서 제공된 이진 파일을 읽고 메모리에 씁니다.
스택 포인터(ESP)를 설정합니다.
명령어 훅 설정: 명령어가 실행될 때마다 handler 함수를 호출하도록 설정합니다.
메모리 접근 에러를 우회하는 훅을 추가합니다.
함수에 전달할 인자를 스택에 씁니다.
Unicorn 엔진을 사용하여 이진 파일을 실행하고 디버깅합니다.

 

main 함수:
solver 함수를 호출하여 이진 파일을 실행하고 디버깅합니다.

-----------------------