에뮬레이션으로 IOT 기기에 접근하기
얼마 전까지 에뮬레이션(emulation)은 교훈적(didactic) 목적이나 MAME(Multiple Arcade Machine Emulator)같은 비디오 게임 목적으로 사용되었다. 최근에는 풀 시스템(full system) 에뮬레이션을 통해 분석이나 취약점 연구 목적으로 사용된다.
IoT와 임베디드 기기의 등장은 Avatar, Avatar2, PANDA 등의 툴 개발을 촉진했다. 파이썬 코드 기반인 덕에 새 프로젝트를 시작하고, 중단점을 제어하는 일련의 일을 매우 쉽게 해낼 수 있게 되었다.
사이버시큐리티에서 에뮬레이션을 도구로 활용하는 또다른 중요한 이유는 현대 프로그램이 난독화(obfuscation), 스톨링 코드(stalling code: 악성 코드의 실행을 지연시킴), 시한 폭탄(time bomb: 일정 시간이 지나면 동작 정지) 등에 의해 매우 복잡하기 때문이다. 풀 시스템 에뮬레이션은 하드웨어의 모든 면을 조작할 수 있고 이와 같은 보호 조치를 해제할 수 있다.
QEMU의 코드 구조
accel/
- QEMU 가속기(accelerator)를 구현하는 디렉토리다. KVM(Kernel-based Virtual Machine)을 지원하거나 Xen 하이퍼바이저(여러 운영체제를 동일한 하드웨어에서 동시에 실행할 수 있도록 허용함)의 콘텍스트 사용을 지원한다.
block/
- 블록 디바이스(block device, 블록 단위로 입출력) I/O와 관련된 루틴, 디스크 이미 생성/조작과 연관되어 있다.
chardev/
- 캐릭터 디바이스(character device, 문자 단위로 입출력)과 관련된 코드이다.
crypto/
- 암호화 루틴을 구현한다.
disas/
- 지원하는 아키텍처의 디스어셈블 명령이 담겨 있다.
docs/
- QEMU를 설명하는 문서 디렉토리이다.
fpu/
- IEEE-754 부동 소수점 연산 함수(floating-point arithmetic functions)를 구현한 소프트웨어이다.
hw/
- x86과 같은 특정 하드웨어 아키텍처의 에뮬레이션을 위한 코드다.
target/
- QEMU 타겟 아키텍처를 지원하는 코드다. CPU 세부사항의 정의와 명령어 집합(instruction set)을 TCG 중간 표현(IR)으로 번역하는 코드를 포함한다. 예를 들어 target/i386/tcg/translate.c 는 i386 명령어의 TCG 번역을 구현한다.
net/
- 네트워크 계층에 사용되는 코드다.
pc-bios/
- 일반적인 PC 바이오스의 바이너리 이미지이다.
tcg/
- QEMU의 TCG(tiny code generator)이다.
ui/
- 디스플레이 드라이버. GTK+ GUI 등 유저 인터페이스에 관련된 코드이다.
QEMU 에뮬레이션
QEMU는 본래 리눅스 커널의 동반자(companion)으로 탄생했지만, 거의 모든 종류의 코드를 실행할 수 있는 멀티 플랫폼 에뮬레이터가 되었다.
QEMU IR
QEMU는 내부적으로 동적 번역기(dynamic traslator)로 동작한다. 고수준(high-level) 관점에서 QEMU는 한 아키텍처의 바이너리를 받고, 풀 에뮬레이션 코드를 실행 중인 아키텍처의 코드로 번역한다. 각각의 아키텍처에 대응하는 모든 번역기를 갖는 문제(N job M machine 문제와 유사)를 해결하기 위해 QEMU는 번역 과정을 두 단계로 나눈다.
QEMU는 TCG에서 TB(Translation Block)이라고 부르는 것을 얻기 위한 전역 함수 tb_gen_code
가 포함되어 있다. 이 함수 내부에서 중간 코드를 만드는 gen_intermediate_code
와 호스트 머신을 위한 타겟 코드를 만드는 tcg_gen_code
가 있다. 두 함수는 아키텍처에 종속적이다.
void gen_intermediate_code(CPUState *cs, TranslationBlock *tb, int max_insns)
{
DisasContext dc = {};
const TranslatorOps * ops = &arm_translator_ops;
...
translator_loop(ops, &dc.base, cpu, tb, max_insns);
}
static const TranslatorOps arm_tr_ops = {
.init_disas_context = arm_tr_init_disas_context,
.tb_start = arm_tr_tb_start,
.insn_start = arm_tr_insn_start,
.translate_insn = arm_tr_translate_insn,
.tb_stop = arm_tr_tb_stop,
.disas_log = arm_tr_disas_log,
};
ops
변수는 각각의 아키텍처를 위한 특정 함수의 포인터이고, traslator_loop
는 IR을 얻기 위한 일반적인 호출을 할 수 있다.
각각의 아키텍처에 대해 명령어는 디스어셈블되고, IR 명령이 생성된다.
TB가 생성되면 QEMU는 tcg_gen_code
를 호출하고, 이 함수는 코드를 IR에서 호스트 머신에 맞는 코드로 변환할 것이다.
TB는 호스트가 이해할 수 있는 ISA(Instruction Set Architecture)로 다시 컴파일되고 코드는 즉시 실행된다. 이 실행을 위해 QEMU는 발생할 수 있는 모든 예외와 인터럽트를 관리할 것이다. QEMU는 직접 번역될 수 없는 명령어를 헬퍼 함수를 호출해 생성한다. IoT 주변장치가 존재하는지 확인하는 블록을 변환하려면 헬퍼 함수를 이용해 실행 흐름의 범위를 벗어나 처리해야 한다.
QEMU 아키텍처 파헤치기
QEMU는 다양한 아키텍처를 구현한다. 예를 들어 arm 아키텍처로 구현된 다양한 머신을 확인할 수 있다.
리스트 중
raspi0 Raspberry Pi Zero (revision 1.2)
가 있는데, 라즈베리 파이 머신임을 알 수 있다. CPU를 확인하면 다음과 같다.
아키텍처의 자세한 정보는 qemu/hw/avr/ 경로에서 확인할 수 있다. 라즈베리 파이 머신에 대한 자세한 정보는 raspi.c에 있다.
static const TypeInfo raspi_machine_types[] = {
{
.name = MACHINE_TYPE_NAME("raspi0"),
.parent = TYPE_RASPI_MACHINE,
.class_init = raspi0_machine_class_init,
}, {
.name = MACHINE_TYPE_NAME("raspi1ap"),
.parent = TYPE_RASPI_MACHINE,
.class_init = raspi1ap_machine_class_init,
}, {
.name = MACHINE_TYPE_NAME("raspi2b"),
.parent = TYPE_RASPI_MACHINE,
.class_init = raspi2b_machine_class_init,
#ifdef TARGET_AARCH64
}, {
.name = MACHINE_TYPE_NAME("raspi3ap"),
.parent = TYPE_RASPI_MACHINE,
.class_init = raspi3ap_machine_class_init,
}, {
.name = MACHINE_TYPE_NAME("raspi3b"),
.parent = TYPE_RASPI_MACHINE,
.class_init = raspi3b_machine_class_init,
#endif
}, {
.name = TYPE_RASPI_MACHINE,
.parent = TYPE_MACHINE,
.instance_size = sizeof(RaspiMachineState),
.class_size = sizeof(RaspiMachineClass),
.abstract = true,
}
};
위 코드는 arm 아키텍처의 모든 사용가능한 머신을 정의한다. 각각에 .name
, .class_init
필드가 있다. .class_init
필드에는 함수 초기화를 위한 포인터가 있고, MachineClass로 알려진 스트럭처를 초기화한다.
모든 구현된 머신은 각각의 특성(peculiarity)이 있어서 새 머신을 구현하는 것은 어려운 일인데, QEMU는 다양한 아키텍처를 위한 많은 CPU와 시스템을 미리 구현해두었다.
QEMU 익스텐션과 모드
C 코드를 다루는 일은 굉장히 어려운 일이다. 다행히 QEMU를 활용할 수 있는 다양한 확장 기능(extension)이 이미 만들어져 있다. Avatar와 Avatar², TriForceAFL, PANDA가 그 예이다.
Avatar²
Jonas Zaddach가 2014년 샌디에고에서 열린 Network and Dustributed System Security(NDSS) 심포지엄에서 Avartar의 첫 번째 버전을 발표했다. 당시에는 IoT 기기가 널리 보급되지 않아 영향력이 미미했다. Marius muench는 4년 후 Avatar²를 발표했다.
Avatar에서 가장 흥미로운 점은 임의의 코드를 삽입할 수 있다는 점이다. 이것이 이 프레임워크가 강력한 이유다. Avartar²는 FirmWire라 불리는 삼성 베이스밴드 에뮬레이터와 퍼저의 주춧돌이 되었다.
PANDA
PANDA는 동적 분석 프레임워크 오픈 소스 플랫폼이다. 아키텍처에 독립적이고 실행된 모든 코드와 에뮬레이트된 게스트 시스템에서 로드된 모든 데이터에 액세스할 수 있다. 또한 기록과 재생(record and replay)라는 기능이 추가되었다. 이를 통해 특정 메모리 구성과 관련된 다양한 버그나, 취약성을 유발하는 모든 것을 재현할 수 있다. FirmWire에서도 PANDA의 일부 기능을 사용해 기록과 재생 기능을 구현한다.
참고 자료
https://github.com/airbus-seclab/qemu_blog
Antonio Nappa , Eduardo Blázquez - Fuzzing Against the Machine
'보안 > fuzzing' 카테고리의 다른 글
[하드웨어 해킹] 퍼징 기술 (0) | 2024.01.15 |
---|---|
[하드웨어 해킹] 퍼징과 분석 기술 - 심볼릭 실행 (0) | 2024.01.11 |
[하드웨어 해킹] QEMU 실행 모드 (1) | 2024.01.11 |
[하드웨어 해킹] 에뮬레이션(emulation)이란? (1) | 2024.01.09 |
퍼징(fuzzing) 환경 구축하기 (1) | 2024.01.06 |