[PAPER] Automatic Firmware Emulation through Invalidity-guided Knowledge
실제 하드웨어를 사용해서 지원하지 않는 주변장치를 지원하게 하는 방법으로, 큰 스케일의 동적 분석에 부적절하다.
HALucinator는 HAL(Hardware Abstraction Layer) API를 펌웨어에 매칭시켜서 교체하는 방식을 사용한다.표준화가 필요하고 custom SoC에는 적용 불가능하다.
실제 하드웨어 없이 펌웨어 전체를 에뮬레이트하는 방법으로, high fidelity를 가진다. P2IM는 주변장치에 적절한 응답을 찾기 위해 전문가 기반 휴리스틱 방법과 알지 못하는 주변장치의 접근 패턴을 추론한 interaction 모델을 사용한다. Laelaps는 symbolic execution을 사용하여 가능한 모든 branch를 탐색하고 휴리스틱 방법을 사용하여 따라가기 좋은 경로를 예측한다.
ARM MCU 펌웨어의 task 코드에서 버그를 찾기 위한 동적 분석 도구인 uEmu를 제안한다. uEmu는 invalidity-guided symbolic execution을 기반으로 펌웨어 이미지를 에뮬레이트하여 필요한 지식을 추론한다. 평가 결과 70개의 unit test 중 93% 통과했고, AFL을 결합하여 만든 fuzzing 도구를 리얼월드 펌웨어 샘플의 task 코드에 적용한 결과 새로운 버그 발견할 수 있었다.
peripheral 문맥 기반으로 hardware state machine을 만들고, 현재 hardware state machine 기반으로 peripheral는 값을 반환한다. 문맥은 cache entry를 매칭하는데 사용되는데, peripheral 접근 매칭되는 rule을 매칭해보고 만약 틀렸으면 더 복잡한 execution 문맥을 사용해서 업그레이드된 rule을 매칭한다. 응답에 사용될 값을 찾기 위해 cache entry는 점진적으로 더 많은 문맥 정보를 사용한다.
simple matching rule은 symbolic execution의 탐색 공간을 줄일 수 있는데, 만약 simple matching rule로 복잡한 문제를 처리하지 못하면 context-aware matching rule이 사용된다.
knowledge extraction phase
invalid-guided symbolic execution을 사용하여 peripheral access에 적절히 응답하기 위한 knowledge base(KB)를 구축하는 과정이다. Dynamic Analysis phase에서 주어진 peripheral access를 처리할 값이 KB에 없는 경우, knowledge extraction phase를 다시 진행한다.
tiered caching strategy
symbolic execution 과정
새로운 peripheral access인 경우(unknown peripheral access), 이 unknown peripheral access를 심볼화하고 symbolic execution 수행한다. 한 branch에서 이 심볼화된 값이 branch target에 영향을 주는 경우, 해당 peripheral access에 대한 디폴트 branch target으로 선택하고 이 solved 값을 KB에 캐싱한다. KB에 캐싱된 값은 symbolic execution 과정 동일한 peripheral access가 발생하는 경우, 대응하는 값을 사용해서 branch target을 결정할 수 있도록 한다.
depth-first search (DFS) algorithm 사용
현재 execution state가 invalid하면, symbolic execution engine은 다른 branch target을 선택하여 matching rule을 업데이트하고 KB 내 branch target에 대응하는 cache entry를 선택한다. 한 branch 내 branch target에서 execution state가 모두 invalid한 경우, parent branch로 rollback하여 탐색하지 않은 target을 선택하여 탐색을 계속 진행한다. 펌웨어 실행이 종료하거나 더 이상 새로운 basic block을 찾을 수 없을 때까지 알고리즘이 실행된다.
dynamic analysis 접근법을 사용하여 펌웨어를 실행 시 peripheral access가 발생할 때, KB를 참조해서 적합한 응답값을 리턴한다.
그림 1의 가장 왼쪽 그림인 펌웨어 이미지에서 3개의 실행 추적이 있을 때, 각 branch는 node로 표현된다. 각 branch node는 하나의 branch target을 결정하는데 대응되는 peripheral register의 주소가 표시된다.
그림 2의 가장 오른쪽 그림에 해당한다. 이 과정에서 uEmu는 peripheral register access에 대해 KB에 query를 해서 KB 내 어떤 Entry에 매칭되는지 확인한다.
uEmu는 S2E 2.0 기반으로 개발되었다. S2E 2.0에서는 symbolic execution engine에서 hypervisor 의존성을 없애기 위해 KVM interface로 바꿨는데, KVM interface에서 모든 아키텍처를 관리하는 것은 어렵기 때문에 몇 가지 아키텍처 지원이 없어졌고, 그 중 ARM이 지원 중단되었다.
ARM을 지원하도록 S2E에서 변경한 2가지
uEmu 기능 위한 4개의 플러그인
InvalidStateDetection
: invalid state detection pluginKnowledgeExtraction
: invalidity-guided KB extraction pluginInterruptControl
: interrupt injection pluginFuzzerHelper
: fuzzer integration plugin
T0는 peripheral register의 storage model로, peripheral register는 가장 최근에 적힌 값을 저장하고 read operation 시 저장한 값으로 응답해준다. T0은 다른 caching rule 전에 사용되고, 만약 T0가 틀렸으면 caching rule T1으로 업그레이드된다.
T1 matching rule은 greedy 알고리즘 특성을 반영하여 가장 많은 peripheral access를 매칭하도록 했고, 이것으로 path explosion 문제를 피했다. 문맥을 고려하지 않고 PC(pc
)와 peripheral address(addr
)로만 값을 결정하는데 사용된다.
KB에서 entry는 T1_addr_pc_NULL_value
와 같이 인코딩된다. T1_0x40023800_0x10000_NULL_0x00은 펌웨어가 PC 0x10000에서 주소 0x40023800를 읽으면, 유망한 branch target에 도달하기 위해 0x00 값이 사용되어야 한다.
만약 T1이 틀렸다고 입증되면, caching rule T2로 업그레이드된다.
동일한 peripheral register의 리턴값이 실행 문맥에 따라 바뀌어야 할 때와 같은 복잡한 상황을 T1 matching rule은 처리하지 못한다.
T2 matching rule에서는 peripheral에 접근 시 T1에서 고려한 PC와 peripheral register에 추가로 실행 문맥을 고려한다. 따라서 실행 문맥을 이어서 만든 hash value를 cache entry 인코딩 시 사용한다. entry는 T2_addr_pc_contextHash_value
로 표현된다.
만약 T2가 틀렸다고 입증되면, caching rule T3로 업그레이드된다.
디바이스가 부팅되지 않은 상태인 디바이스 초기화 코드와 같은 경우에는 T2 matching rule을 사용할 수 없다.
1 rf_read_buf(&buf, len);
2 if(strncmp((const char*)&buf, "OK\r\n", 4))
3 while(1);
Code Listing 4에서 실행 문맥이 동일하기 때문에 T2는 UART data register 에서 4개의 read operation을 구별하지 못한다. 따라서 T3로 업그레이드되고, T3는 라인 2에서 "OK\r\n"을 얻기 위해 rf_read_buf()에서 얻어야 하는 UART data register 의 4개의 바이트를 모두 cache entry에 저장한다. 즉, caching rule T3는 T3_addr_pc_null_{v1, v2, ...}
과 같이 인코딩된다. 펌웨어 dynamic analysis phase에서는, 배열 내 값들이 순서대로 사용 되어서 실행 시 동일한 흐름을 따를 수 있다.
평가 결과, 외부 데이터를 수신하는데 사용하는 레지스터 등이 아니면 T3 caching rule은 거의 활성화되지 않았다.
이 섹션에서는 InvalidStateDetection
plugin이 어떻게 invalid state를 탐지하고, KnowledgeExtraction
plugin에 탐지된 invalid state를 알리는지 설명한다.
펌웨어 실행 시 회복 불가능한 에러가 발생하면, 간단한 infinite loop을 돌려서 스스로 실행을 중단한다. 따라서 infinite loop이 탐지가 된다는 것은 캐싱된 peripheral reading이 잘못되었다는 것이다.
InvalidStateDetection
plugin은 각 execution path마다 control flow를 기록하고, 만약 control flow에서 반복적인 cycle이 발견되면 loop이 탐지된 것으로 간주한다.
uEmu는 마지막 BB#_INV1
수의 translation block에서만 infinite loop 발생 여부를 검사한다.
문맥 상에 최소 하나의 symbol이 포함된 경우에만 infinite loop detection이 활성화된다.
펌웨어는 peripheral이 특정 연산을 마쳤는지 확인하기 위해 timeout 메커니즘을 통해 peripheral register의 특정 값을 기다릴 수 있다. Code Listing 1에서와 같이 register가 조건에 맞지 않는 값을 가지면 long loop에 들어가서 몇 초 동안 register에 원하는 값이 있을 때까지 기다린다.
InvalidStateDetection
은 long loop을 식별하기 위해 반복되는 사이클을 카운팅해서 지정된 값을 초과하면 long loop으로 간주한다. long loop를 확인하기 위해 지정된 값은 BB#_INV2
로 디폴트 값은 2000이다.
문맥 상에 최소 하나의 symbol이 포함된 경우에만 long loop detection이 활성화된다.
invalid memory region은 주소 공간에 매핑되지 않은 메모리 지역을 의미한다. 주소 공간에 매핑된 지역은 ROM, RAM, system region, external peripheral region이 있으며 이외의 지역은 매핑되지 않은 것이다.
펌웨어가 매핑되지 않은 주소에 접근하는 경우
InvalidStateDetection
plugin이 invalid state로 보고하게 된다.실행되지 않아야 하는 execution point를 분석가가 invalid point로 정의할 수 있다.
Input :KB
Input :selected_target
Output:KB
symbol <- get_symbol();
KB_Update(KB, symbol);
do
targets[] <- execute_BB(selected_target);
if meet termination condition then
return KB;
end
if current state is invalid then
break;
end
if sizeof(targets) == l then
selected_target <- next_BB(selected_target);
else
selected_target <- favorable_target(targets);
other_target <- non_favorable_target(targets);
unexplored.push(other_target);
symbol <- get_symbol();
KB_Update(KB, symbol);
end
while true;
// switch execution state
selected_target <- unexplored.pop();
KB_Learn(selected_target);
Algorithm 1은 DFS 기반의 KB_Learn() 함수로, basic block과 현재 KB를 입력으로 받고 심볼릭하게 실행한다. 출력은 학습 후 업데이트된 KB이다.
KB_Learn() 과정
Input:KB
Input:Symbol
new_entry <- solver(symbol);
if new_entry conflicts with KB then
// upgrade caching rules
if type(symbol) == T0 then
type(symbol) <- T1;
else if type(symbol) == T1 then
type(symbol) <- T2;
else if type(symbol) == T2 then
type(symbol) <- T3;
end
replace the conflicting entry with new_entry;
else
KB <- KB|new_entry;
end
Algorithm 2인 KB_Update()는 knowledge base update algorithm이다. symbolic execution engine은 현재 branch target로 실행을 이끌 수 있는 symbol의 concrete 값을 계산해서 리턴한다. 리턴값은 새로운 cache entry를 구성하는데 사용되는데, 이 새로운 cache entry가 현재 KB 내에서 충돌되지 않으면 현재 KB에 추가되고, 충돌이 발생하는 경우 caching rule을 업그레이드한다.
리얼월드 펌웨어는 외부 이벤트에 응답하기 위해 infinite loop에서 실행된다. 따라서 knowledge extraction 과정이 끝나지 않을 수 있기 때문에 가장 마지막에 실행된 30,000개의 basic block을 모니터링해서 새로운 basic block에 도달하지 않으면 해당 knowledge extraction 과정이 종료된다. 모니터링하는 basic block 수는 BB#_Term
으로 조절할 수 있다.
현재 KB에 매칭될 수 없는 특정 레지스터나 context hash/PC가 cache entry에 없으면 다른 knowledge extraction을 수행하는 방식으로 학습하는 강화 학습 방식이 사용된다.
주변장치가 외부와 통신하기 위해 interrupt가 필요하다.
InterruptControl
plugin
cache 메커니즘은 한 번에 한 경로만 실행될 수 있기 때문에 에뮬레이트된 경로가 고정되는 문제가 있다. 이 문제를 해결하기 위해 interrupt handler 내부에서 서로 다른 경로를 실행하도록 한다. uEmu는 실행 문맥을 모니터링해서 interrupt context가 탐지되면, symbolic execution engine은 모든 가능한 경로를 탐색한다. valid state로 이끄는 peripheral register에 대한 읽기는 cache entry에 저장된다.
펌웨어 dynamic analysis phase에서는 각 cache entry 내 값이 랜덤으로 선택되어서 interrupt handler 내에서 경로가 랜덤으로 실행되도록 한다.
peripheral에서 status register는 보통 control register에 의존성이 있는데, control register는 일반적으로 T0으로 인식된다. 따라서 interrupt event prediction의 정확도를 높이기 위해 uEmu는 T0 타입의 peripheral register부터 살펴본다.
FuzzerHelper
plugin은 AFL이 uEmu와 연결될 수 있도록 하고, 자동으로 fuzzing input point를 찾아서 데이터를 feed 해준다.
AFL은 test-case generation에만 사용하고, FuzzerHelper
plugin에서 coverage instrumentation, fork server, crash/hang detection을 수행하도록 구현했다.
coverage instrumentation
fork server
crash detection
HardFault
를 crash indicator로 사용 → unrecoverable errorhang detection
fuzzing 시 공격자가 제어할 수 있는 input channel이 있는데, MCU의 경우 peripheral data register에 해당한다. 따라서 data register를 식별하기 위한 data register의 특성을 다음과 같이 정의했다.
data register 특징
만약 한 register가 위의 특성 중 하나를 가지면 data register로 체크된다.
Evaluation point
실험 환경
P2IM 실험에서 사용한 70개의 펌웨어 샘플 대상으로 실험을 진행했다. 각 unit test마다 먼저 knowledge extraction phase를 돌리고 dynamic analysis phase를 진행했다.
실험 결과
모든 유닛 테스트에서 knowledge extraction phase는 한 라운드 당 1분 이내에 완료된 것으로 knowledge extraction algorithm이 높은 효율성을 보임을 알 수 있다. 70개 샘플 중 5개의 유닛 테스트만 실패하여, passing rate가 93%으로 P2IM(79%)보다 높음을 확인할 수 있다.
Failed Tests in P2IM
섹션 5.3에서 실패 원인을 mis-categorization(MC), invalid assumption(IA), limited exploration(LE)로 분류하여 설명한다.
Failed Tests in uEmu
uEmu에서는 invalidity checking이 가장 중요한 기능으로, 모든 실패한 테스트의 경우 invalid로 인식되지 못한 예상하지 못한 경로를 에뮬레이트하는 경우였다. 70개의 샘플 중 5개의 실패한 테스트의 경우 이 상황에 해당했다. 이 문제는 펌웨어에서 에러 코드를 탐지해서 바로 처리하도록 해서 해결할 수 있다. 각 실패한 샘플에서 invalid point를 추가 후 다시 테스트했을 때 100% passing rate가 나왔다.
평가 시 사용한 펌웨어 (21개)
테스트 방법
실험 과정
각 펌웨어 샘플마다 knowledge extraction 수행 후, 24시간 동안 fuzzing 진행
결과 평가 관점
1. Knowledge Extraction Performance
KB extraction에서 강화 학습에 필요한 round 수와 KB extraction 전체 시간 측정를 측정했다.
2. Coverage Improvement
uEmu 사용했을 때의 path coverage, uEmu 사용하지 않았을 때의 path coverage를 비교했을 때, QEMU에서 peripheral emulation 없을 때보다 10배부터 140배까지 code coverage 증가했다.
3. Fuzzing
식별된 data register를 입력으로 펌웨어 샘플 내 task code를 퍼징하는데 uEmu 사용한 결과, Steering_Control
에서 double-free로 인한 arbitrary write 취약점과 uTaskerUSB
에서 out-of-bound write 취약점을 새롭게 발견했다. S2E에서의 실행 속도 때문에 P2IM보다 uEmu이 더 느렸다.
False Crashes/Hangs
Steering_Control
,GPS_Tracker
,3DPrinter
에서 발견된 False positive의 경우 uEmu에서의 Direct Memory Access (DMA) 부족이 원인이었다.
P2IM에서는 control bit와 status bit가 겹치지 않는다는 가정으로 control register와 status register를 결합한 레지스터를 모델링해서 사용한다. 하지만 이 가정은 항상 맞지 않는다. 따라서 P2IM에서의 잘못된 인식으로 펌웨어가 중간에 멈추게 한다.
P2IM에서의 휴리스틱 방법으로는 status register의 값을 적절히 찾을 수 없기 때문에 explorative execution을 사용한다. explorative execution는 레지스터 읽기 지점에서 멈춰서 실행을 기록하는 것이다. explorative execution는 모든 검색 공간을 대상으로 적용할 수 없어서 single bit set만을 가진다고 보고 조사를 하는데, 복잡한 주변장치의 경우 multi-bit status register가 흔하기 때문에 이 multi-bit status register에 대해서는 예상값을 찾을 수 없고, 펌웨어가 멈추게 한다.
논문에서는 펌웨어 내 task code에서 잘못된 I/O interface 입력에 의한 버그를 찾는 목적으로 펌웨어를 에뮬레이트하는 도구인 uEmu를 제안했다.
invalid checking으로 펌웨어가 invalid state에 가지 않게 하고, symbolic execution을 통해 새로운 경로를 탐색한다. knowledge extraction phase에서 peripheral access에 적합한 값을 학습하고 KB에 저장하고, dynamic analysis phase에서 peripheral reading 연산 시 적절한 값을 KB에서 찾아서 응답해준다. 이러한 기술을 사용해서 uEmu는 실제 하드웨어 없이 unknown peripheral 접근 시 적합한 응답을 자동으로 찾아서 펌웨어를 실행할 수 있게 한다.
퍼징 플러그인을 결합해서 퍼징한 결과 리얼월드 펌웨어에서 새로운 버그를 찾을 수 있었다.