Search

Firm-AFL 논문 리뷰

Category
논문
Column
2022/01/07 12:51
Tags
study

1. Abstract

IoT 펌웨어를 위한 높은 성능의 greybox 퍼저를 개발했다. Firm-AFL은 IoT 퍼징의 두가지 근본적인 문제를 해결하는 것에 초점을 뒀는데, 기존 IoT 퍼저의 문제점은 다음과 같다.
1.
호환성이 떨어짐
에뮬레이션이 가능한 POSIX 호환 펌웨어에 대해서 퍼징을 돌릴 수 있도록 해결함
2.
성능 저하(병목현상)
'augmented process emulataion' 이라고 부르는 기술로 시스템 모드 에뮬레이션에서 발생 할 수 있는 성능 저하 문제를 해결함
기본적으로 에뮬레이션 방식에는 시스템 모드, 유저 모드 두가지가 존재함. 시스템 모드는 커널을 포함하여 아키텍쳐 전체를 가상화 시키는 방식이다. 따라서 하드웨어 의존성에 따른 호환성 문제를 어느정도 해결 할 수 있지만, 기능이 좋지 않다.
반대로 시스템 모드 에뮬레이션은 커널 자체를 가상화시키는 방식이 아닌, 특정 바이너리를 해당 아키텍처에 맞게 실행시키는 방식으로 빠른 속도로 처리가 가능하지만 하드웨어 적인 호환성 문제는 해결 할 수 없다.
Firm-AFL은 이 두가지 모드(시스템 + 유저)를 결합하여 높은 호환성과 높은 처리량을 가지는 'augmented process emulation' 퍼저를 소개한다. 해당 퍼저의 결과로 일반적인 시스템 모드로 돌아가는 에뮬레이션 퍼저보다 8배 빠른 속도가 측정되었다.

1.2 Challenges in IoT firmware fuzzing.

일반적인 소프트웨어 퍼징과 다르게, IoT 펌웨어를 대상으로 하는 퍼징은 하드웨어 의존성이 매우 높기 때문에 퍼징을 바로 적용시키기가 어렵다.
이 때문에 IoTFuzzer, AVATAR, Firmadyne 등의 연구가 계속 진행되고 있다.
IoTfuzzer Device의 firmware 없이 memory corruption 취약점을 찾을 수 있는 Fuzzing Framework
AVATAR 하드웨어와 소프트웨어 에뮬레이션을 결합한 하이브리드 퍼저
firmadyne Linux 기반 임베디드 펌웨어의 에뮬레이션 및 동적 분석을 수행하기 위한 확장 가능한 자동화 시스템
IoT 펌웨어를 퍼징함에 있어 가장 중요한 키 포인트는 처리량이지만 전체 시스템 에뮬레이션의 경우 성능이 좋지않다. AFL은 유저 모드 에뮬레이션을 사용하는데 전체 시스템 에뮬레이션의 경우 AFL보다 약 10배 정도 느리다. 10배는 퍼징을 통해 취약점을 찾을 때 매우 좋지 않은 지표다.
분석 결과에 따르면, 전체 시스템 에뮬레이션에서 매우 큰 런타임 오버헤드가 발생한다. 첫 번째로 오버헤드 중 상당한 부분은 MMU 처리 부분에서 발생한다. MMU는 가상주소를 물리주소로 변환해주는 장치인데, 에뮬레이션 된 환경에서는 MMU를 소프트웨어 적으로 구현한다. 바로 이 소프트웨어 적으로 구현된 MMU의 동작에서 큰 오버헤드가 발생한다.
두 번째로 발생할 수 있는 오버헤드는 에뮬레이션된 system call 과정에서 발생한다.

1.3 Our solution: greybox fuzzing via augmented process emulation

퍼징을 하기 위해 따로 펌웨어를 수정하지 않아도 되는 transparency 설계 목표와, 전체 시스템 모드 에뮬레이션에서도 유저 모드 에뮬레이션처럼 높은 처리량(속도)를 가질 수 있게 efficiency 을 설계 목표로 잡았다.
요약 : generality from full-system emulation and efficiency from user-mode emulation.
본 논문에서 제안하는 기술의 이름은 'augmented process emulation' 이다. 메인 아이디어는 앞서 말한 것처럼 시스템 모드와 유저모드를 결합한 에뮬레이션이다.
Frim-AFL에서 퍼징될 프로그램은 높은 효율성을 달성하기 위해 주로 사용자 모드 에뮬레이션으로 실행되며, 필요한 경우에만 전체 시스템 에뮬레이션으로 전환되므로 일반화(호환)가 가능하다.
제안하는 기술을 평가하기 위해 전체 시스템 에뮬레이션 프레임워크은 Firmadyne 위에 Firm-AFL이라 불리는 프로토타입 시스템을 구현하였다. 이를 통해 일반적으로 AFL을 이용하여 퍼징하는 것처럼 IoT 펌웨어에서 동작하는 유저 바이너리를 퍼징할 수 있었다.

2 Background and Motivation

2.1 Fuzzing

일반적인 퍼징 이론 설명

2.2 QEMU

qemu는 basic 블록 단위로 에뮬레이션을 진행한다. qemu는 실행모드가 뭐냐에 따라서 주소 공간의 변환이 달라진다.
1.
시스템 모드 에뮬레이션인 경우 메모리 접근을 위해 MMU를 소프트웨어 적으로 구현한다. MMU는 GVA(Guset Virtual Addr) 를 HVA(Host Virtual Addr)와 매핑한다. 이 매핑 프로세스는 Guest OS의 Page Table을 통해 GVA-GPA 매핑을 설정하고 page fault를 처리할 수 있게 된다.(일반적으로 커널에서 수행되는 메모리 관리 프로세스와 동일).
QEMU는 GVA-GPA 변환 처리를 매 메모리 접근마다 삽입한다. 요놈의 속도를 높이기 위해 QEMU는 TLB를 이용한다(캐시 역할). 시스템 모드에서 HVA는 GVA+offset으로 계산한다.
2.
시스템 모드와 다르게 유저 모드 에뮬레이션에서 HVA는 GVA+상수로 계산한다. 그래서 GVA→HVA 변환 과정은 시스템 모드보다 더 빠르다.

2.3 Testing IoT Firmware

IoT 취약점 분석의 두가지 난제.
1.
compatibility(호환성) - 하드웨어 의존적임
2.
code coverage
AVATAR는 동적 분석을 목표로하는 도구이며 하드웨어 컴포넌트 지원이 가능하다. 하이브리드 실행 환경을 구축해 에뮬레이터와 실제 하드웨어 사이에서 AVATAR가 소프트웨어 프록시 역할을 한다. 하지만 속도가 매우 느리다.(시스템 모드 에뮬레이션 개념) - whitebox 퍼징
IoTFuzzer는 blackbox로 실제 하드웨어를 직접적으로 퍼징한다. 기존 블랙박스 퍼징과의 차이점은 타겟 디바이스에 제공되는 모바일 앱을 통해 퍼징을 수행한다. 따라서 좋은 테스트 케이스를 만들 수 있지만 초당 하나의 테스트 케이스를 생성하므로 속도가 느린 편이다.
Firmdadyne은 퍼징을 수행하는 프레임워크는 아니지만 시스템 모드 QEMU에서 IoT 펌웨어를 위해 하드웨어 서포팅을 지원한다. 이를 위해 Firmdadyne는 실제 하드웨어에서 필요한 부분이 없는 예외를 처리하기위해 커널과 드라이버를 수정함으로써 완전한 시스템 모드의 에뮬레이션을 지원한다.
Muench는 다양한 구성으로 blackbox 퍼저의 처리량을 비교함.(직접적으로 하드웨어에 input 넣어주기, 부분 에뮬레이션, 전체 에뮬레이션)
AFL은 greybox 퍼저이다. user mode qemu는 대부분 IoT 펌웨어를 에뮬레이팅 할 수 없다.
결론으로 위 존재하는 퍼저들은 만족스러운 결과를 도출하지 못한다.

2.4 Motivations

따라서 목표는 높은 처리랑을 가지는 greybox 퍼징을 IoT 펌웨어를 대상으로 수행하는 것이다. 타겟 프로그램에 user mode 에뮬레이션을 적용할 수 있다면 퍼징 성능이 좋아질 것이다. 다음으로 실행 시간 차이를 유발시키는 몇 가지 병목 현상이 있다.
Memory address translation
시스템 모드 에뮬레이션에서, MMU는 매 메모리 접근마다 VA→PA를 수행하지만 user mode에서는 주소변환을 더 간단히 수행한다. 따라서 사용자 모드 실행 시간만 고려하더라도 사용자 모드 에뮬레이션은 훨씬 적은 시간을 사용한다.
Dynamic code translation
유저 모드 에뮬레이션에서의 code translation은 full system 모드 보다 훨씬 빠르다. 전체 시스템 모드에서는 동일한 물리 페이지를 가져야지만 block chaining이 발생하므로 user 모드보다 더 자주 code translation이 호출된다.
Syscall Emulation
유저모드 에뮬레이션에서 시스템 콜은 직접적으로 host OS와 하드웨어에 의해 직접적으로 처리된다. 따라서 full-시스템 모드 에뮬레이션보다 훨씬 빠르다. 비록 하드웨어 에뮬레이션은 타겟 프로그램이 정확히 수행되야할 필요가 있지만, 모든 시스템 콜이 특정 하드웨어에 의존성이 있는 것은 아니다. 다른 말로 모든 시스템 콜이 에뮬레이션이 필요한 것은 아니다.
Firm-AFL은 IoT 펌웨어 퍼징을 위해 위 3가지 문제점을 해결하였다.

3. Augmented Process Emulation

3.1 Overview

목표는 user-mode 에뮬레이터에서 IoT 펌웨어를 정확하게 실행시키도록 하는 것이다. 이를 위해 다음의 두 가지를 만족시켜야함.
펌웨어는 system-mode QEMU에서 정확하게 에뮬레이팅 되야한다. 이는 Firmadyne 프레임워크를 이용한다. 해당 프레임워크는 많은 IoT Firmware 이미지를 제공한다
펌웨어는 POSIX OS 환경 안에서 동작 해야한다. (대부분 IoT 펌웨어들은 리눅스에서 동작하므로 이는 크게 신경 안 써도 될듯)
Augmented Process Emulation 목표
Transparency - 호환성
augmented process emulation 에서 동작하는 user-level 프로그램은 마치 system-mode emulator에서 동작하는 것처럼 행동해야한다.
High effciency - 성능
처리량이 퍼징의 주요 요소이기 때문에, aumented process emulation은 최대한 효율적으로 동작해야한다. 이상적으로는 user-mode emulation의 성능에 근접해야한다.
따라서 user-mode와 system-mode emulator를 새로운 방식으로 결합했다. Firm-AFL의 구조도는 다음과 같다.
처음에 IoT 펌웨어가 system-mode emulator에서 부팅되고 user-level 프로그램(퍼징 타겟 포함)이 emulator 내부에서 실행된다.
그 후, 퍼징된 프로그램이 미리 예견된 지점 (예를 들어 entry point 라던지, 첫 번째 네트워크 패킷을 받는 시점 등)에 도달하면 프로세스 실행은 높은 실행 속도를 위해 user-mode emulator으로 변경되어 실행된다.
간혹 드문 경우지만 오류가 있을 경우 실행의 정확성을 보장하기 위해 system-mode로 다시 변경되서 실행된다
user - system mode 변경에 드는 리소스를 최소화 하기 위해 메모리는 두 emulation mode사이에서 공유된다. 좀 더 자세히 말하자면 system mode emulator를 위한 가상 머신의 PA는 RAM File이라 불리는 memory-mapped file에 할당된다. 이 RAM file은 user-mode emulator의 주소 공간에 역시 매핑된다.
user-mode와 system-mode emulator는 서로 다른 방식으로 RAM file에 접근한다.
system mode emulator는 RAM file을 물리 메모리로 인식하여 처리하기 때문에 PA로 접근한다
user mode emulator는 공유된 메모리(RAM file)을 VA로 접근한다
따라서 RAM file의 physical page들은 page 단위로 VA에 의해 user-mode emulator 주소 공간에 매핑되어야 한다. 따라서 user-mode emulation에서 page 매핑이 설정되지 않은 경우, 프로세스의 실행은 syste-mode emulation으로 변경되서 실행되어야 하고, 그래야지만 page 매핑이 설정된다.
이러한 page 매핑이 잘 되었으면 프로세스는 user-mode emulation에서 정확하게 system call에 도달하기 전까지 정확하게 수행될 수 있다.
추가적으로 Host OS와 IoT 펌웨어의 OS가 다르고, 하드웨어 계층도 다르기 때문에 호스트 OS에서 로컬로 system call을 실행하는 것은 일반적으로 동작하지 않는다. 호환성을 보장하기 위해 system call을 처리하기 위해 실행 모드를 system-mode emulation으로 변경해야 한다. system call이 반환되면, 다시 실행 모드를 user-mode emulation으로 변경해야한다.

3.2 Memory Mapping

Bootstrapping

일반적으로 AFL로 퍼징을 수행할 때, 프로그램은 entry point 같이 미리 정의된 지점까지 수행하고 fork server와 통신하면서 fork를 통해 child 프로세스를 계속 생성하여 퍼징을 수행한다.
이와 유사하게 우리는 system mod emulation 환경에서 IoT 펌웨어를 부팅 시킬 것이고 해당 환경에서 IoT 프로그램을 동작시킬 것이다. 우리는 VMI를 이용하여 IoT 프로그램의 실행과정을 모니터 하고 실행이 미리 정해진 fork 지점에 도달하면 알림을 받을 수 있다.
VMI는 외부 모니터링 툴로써, VM의 런타임 상태에 대한 정보(Disk, memory, network, HW event, CPU reg.)를 읽는 일종의 그냥 디버깅 툴이라고 생각하면 된다.
이 순간에 지정된 프로세스의 page table을 탐색하고 가상-물리 페이지 매핑 정보를 확인하여 user-mode emulation 측으로 전송한다. 그런 다음 VA-PA 의 각 매핑에 대해, user-mode emulation 측에서 아래의 코드를 통해 매핑 정보를 설정한다.
mmap(va, 4096, prot, MAP_FILE, ram_fd, pa);
C
복사
addr(va) : 매핑할 메모리 주소
len(4096) : 메모리 공간의 크기
prot(prot) : 보호모드
flags(MAP_FILE) : 매핑된 데이터의 처리 방법을 지정하는 상수
fildes(ram_fd) : 파일 디스크립터 → RAM file
off(pa) : 파일 오프셋
fildes가 가리키는 파일에서 off로 지정한 오프셋부터 len 크기만큼 데이터를 읽어 addr이 가리키는 메모리 공간에 매핑한다. prot에는 읽기, 쓰기 등 보호 모드를 지정하고 flags에는 읽어온 데이터를 처리하기 위한 정보를 지정한다.
우리는 PA를 오프셋으로 사용하는 RAM file의 page를 지정된 VA로 매핑한다. 이 순간부터 syste-mode emulation의 동작은 멈추며, CPU state는 user-mode로 전송되고 그곳에서 다시 실행된다.

Page fault handling

user-mode emulation에서 프로세스가 실행되는 동안, 만약 접근한 메모리 주소가 이 주소 공간에 이미 매핑된 경우 실행이 성공적으로 진행되어야 한다. 그렇지 않으면 host 프로세스는 page fault를 유발시킨다. user-mode emulation에서 page fault를 위해 시그널을 등록함으로써, host OS가 page fault 이벤트를 use-mode emulation으로 전달한다.
이 시그널을 수신하면, user-mode emulation은 fault instruction시의 CPU state를 기록하고 실행을 일시중지한다. 그리고 CPU state를 system-mode emulation 측에 전달하여 page fault를 처리하도록 한다. 그리고 fault된 주소를 새롭게 페이지 테이블에 매핑시키는 작업을 한다.
system-mode emulation이 CPU state를 수신하고 실행을 재개하면 emulated된 프로세스는 page가 없기 때문에 page fault를 발생시킨다. (이는 생각해보면 당연함. 초기에 va-pa가 매핑되서 user-mode로 넘어갔다면 user-mode에서 page fault가 발생하지 않았을 것이다.)
IoT 펌웨어의 OS에 존재하는 page fault 핸들러는 이 page fault를 처리하고 새로운 매핑을 연결지을 것이다.(메모리에 해당 페이지를 올리는 작업). 여기서 중요한 질문은 페이지 매핑이 설정되었거나, 오류가 발생한 시점을 결정하여 실행 속도를 최대화하기 위해 user-mode emulation으로 다시 전환할 수 있도록 하는 것이다.
OS는 여러 작업을 동시에 처리하고 방대한 양의 context switching이 발생할 수 있기 때문에 그 해답을 찾기는 쉽지 않다.
페이지 매핑이 설정되는 시점을 알기 위해 각각의 basic block의 끝에 instrument를 삽입한다.(코드 삽입 - AFL 원리)
만약 실행이 지정된 프로세스나 쓰레드 내에 있는 경우, faulting instruction을 재개하기 위해 커널에서 유저 공간으로 실행이 되돌아왔다는 의미이다. 이 때에는 매핑 정보가 캐시 역할을 하는 TLB에 갱신되어야한다.
이 순간에 매핑 정보와 CPU state를 다시 user-mode emulation으로 전달하면 mmap을 호출하여 page table을 갱신하고 user-mode emulation에서 실행을 다시 시작한다. (예외가 발생하여 프로세스가 죽는 경우 VMI로 이를 모니터링하여 종료시킨다)

Preload page mapping

lazy mapping 이란 필요할 때 매핑 정보를 가져오는 방식을 뜻함
커널의 가상 주소 관리에서 메모리에 모든 주소를 다 올리지는 않고, page fault 즉, 첫 접근 시에 매핑 정보를 갱신하는 패턴을 lazy mapping이라고 한다. 이 패턴은 퍼징 성능에 악영향을 끼친다.
AFL에서 fork server는 퍼징 루틴 마다 반복적으로 fork 호출을 통해 child 프로세스를 생성하기 때문에 매핑되지 않은 page fault가 계속해서 발생한다. page fault 핸들링은 많은 오버헤드가 들기 때문에 목표로 하는 시스템에 악영향을 끼친다.
이를 해결하기 위해 주어진 프로세스의 code page들을 물리 메모리에 미리 로드하여 user-system mode 간의 매핑을 미리 수행한다. 이는 매 퍼징 루틴마다 반복적으로 코드 페이지를 로딩하는 것을 피하게 해주고, 퍼징 처리량을 높인다.

3.3 System Call Redirection

IoT 프로그램에서 system call과 구현은 IoT 하드웨어, 펌웨어 및 요구 사항으로 인해 일반적은 system call과 다르기 때문에, system call로 인한 예외가 제대로 처리되지 않으면 IoT 프로그램의 user-mode emulation이 실패할 수 있다.
따라서 실행 정확성을 보장하기 위해, user-mode 에서 syste-mode emulation으로 system call을 redirect 시켜야한다. 자세히 말하면 user-mode에서 system call을 만나면, 실행을 잠시 멈추고 현재 CPU state를 저장한 뒤 system-mode emulation으로 전송해야한다.
system-mode가 CPU state를 전달받으면 그 때 다시 동작을 재개한다. system-mode에서 즉 커널에서 다음 PC가 유저레벨 공간의 동작이라면 다시 실행을 일시 멈춤하고 CPU state를 저장한뒤, user-mode로 전달한다음 실행을 다시 재개한다.

Optimizing filesystem-related system calls.

IoT 프로그램의 system call은 file system과 많이 연관되어있다. IoT 프로그램은 파일이나 폴더에 접근을 시도한다. 따라서 펌웨어로부터 file system을 매핑하고 host OS에 디렉토리 처럼 마운트한다.(user-mode emulation이 직접 접근할 수 있게).
이 방법으로 user-mode emulation은 직접적으로 system call과 관련된 file-system을 syste-mode emulation으로 전달하는게 아닌 host OS에 직접적으로 전달할 수 있다.

4. Firm-AFL Design and Implementation

해당 절에서는 AFL과 augmented process emulation을 어떻게 결합했는지를 설명한다.

4.1 Workflow of AFL

AFL은 user-mode qemu를 이용해서 code coverage 정보를 수집하며 타겟 프로그램의 분기 전환의 코드 삽입을 하고 code coverage 정보를 bitmap에 저장한다.
퍼징 동안 반족적으로 타겟 프로그램을 실행해야 함으로, AFL은 fork를 이용해서 처리 속도를 높인다. AFL은 fork-server라 불리는 parent process에서 초기화 과정(딱 한번 수행되는 루틴들)을 수행한 뒤, fork로 child process를 생성하여 반복적으로 퍼징을 수행한다.
공유메모리(bitmap)을 이용하여 coverage를 넓혀가는 방식이 바로 AFL fuzzer이다.

4.2 AFL with Augmented Process Emulation

AFL 흐름은 유지하되, IoT 펌웨어를 대상으로 AFL을 통해 퍼징 할 것이다. 따라서 이를 위해 user-mode QEMU 를 augmented process로 대체하였으며 나머지 요소들은 그대로 두었다. 아래 사진이 바로 제안하는 Firm-AFL 구조도이다.
IoT 펌웨어를 퍼징하기 위해서, 펌웨어를 부팅시켜야 하고 시스템이 부팅 된 후 타겟 프로그램을 실행시켜야 한다. 해당 과정은 위에서 설명한 fork-server 에서 수행된다.
우리는 펌웨어를 올바르게 에뮬레이팅하기 위해 Firmadyne 프레임워크를 사용한다. 또한 VMI를 이용하여 시작 - 종료 순간의 정보를 모니터링 한다. 또한 타겟 프로그램이 언제 pre-determined 된 fork 지점에 도달했는지 알 수 있다.

Forking

AFL이 디폴트로 설정한 fork 지점은 main의 entry point이다. 우리 케이스에서는 네트워크 인터페이스를 통해 트리거되는 취약점을 찾는것에 관심이 있다. 그러므로 네트워크와 관련된 system call을 후킹하여 그러한 system call의 호출을 fork point로 설정하였다.
일반적인 AFL 흐름은, fork system call을 활용하여 자식 프로세스를 생성하고 다음 퍼징 인스턴스를 시작한다. 반면에 우리 케이스에서는 user-mode emulation을 위해 자식 프로세스를 생성하는 것 뿐만 아니라, system-mode emulation을 위해 새로운 가상머신 인스턴스를 생성해야한다.(두 모드가 서로 동기화되어야 하기 떄문에)
새로운 가상 머신을 fork하는 것은 많은 리소스가 든다. 따라서 우리는 대신에 fork 지점에서 가상머신 상태를 snapsot을 만들고 퍼징이 완료되면 스냅샷을 복원할 수 있다.(코드 커버리지 초기화) syste-mode QEMU는 CPU 레지스터와 특정 파일을 위한 메모리 공간을 저장할 수 있는 save_snapshop 함수를 제공한다. 그러나 file 읽기/쓰기 동작은 매우 느리다.
우리 시스템에서는 경량 copy-andwrite 원칙에 기반한 경량 snapshot 메커니즘을 구현한다. 자세히 말하면 Frim-AFL는 먼저 system-mode QEMU에 매핑된 RAM 파일을 읽기 전용으로 표시한다.
그리고 나서 memory write는 page fault를 발생시킨 뒤 page 복사본을 만들고 이를 write-able 상태로 표시한다.(memory write시 해당 페이지의 복사본을 만들고 거기다 write를 하는 방식인 CoW)
따라서 한 번의 퍼징 실행 중에 수정된 모든 메모리 페이지를 기록한다. snapshot을 복원할 때 이 기록된 페이지만 다시 write하면 된다.

Feeding input.

네트워크 인터페이스로부터 input을 받는 IoT 프로그램을 위해 user-mode emulation에서 직접적으로 네트워크와 관련된 system-call을 삽입한다. 따라서 우리는 system-call을 syste-mode emulation으로 redirect 할 필요가 없다.

Collecting coverage information.

대부분의 실행은 user-mode emulation에서 수행되고 system-mode emulation에서는 단지 page fault 및 일부 system-call을 처리하는데 필요하므로, user-mode QEMU에서 분기 전환 코드를 삽입하여 원래 AFL이 user-mode QEMU에서 수행하는 것처럼 user-mode QEMU에서 coverage bitmap을 게산할 수 있다.