[PaperReview] Evaluating Code Coverage for Kernel Fuzzers via Function Call Graph
커널은 시스템 권한을 갖기 때문에 좋은 공격 대상이고, 공격자보다 커널 버그를 먼저 찾아야 하는데 이때 퍼징이 보편적으로 사용된다. 대부분의 시스템 콜 퍼저는 퍼징에서 중요한 지표인 커버리지 성능 측면에서 평가되지 않았다. 따라서 본 연구에서는 Intel PT와 KCOV를 사용하여 커널 퍼저의 시스템 콜 관련 함수를 통해 코드 커버리지 성능을 평가하는 평가 시스템을 제안한다.
1) 시스템 콜
시스템 콜은 사용자 레벨 프로그램에서 커널에 더 높은 권한의 서비스를 요청할 때 사용된다.
2) 코드 커버리지 측정
커버리지 퍼징을 이용한 코드 커버리지 측정 방법 중 중요한 것은 적은 성능 오버헤드이다. 코드 커버리지 측정에 오버헤드가 크다면 제한된 시간에 적은 코드 블록이 실행될 것이고 더 적은 버그를 발견하게 된다. 다음은 리눅스 커널에서 커버리지 측정을 위한 인텔 프로세서 추적(PT)와 KCOV에 대한 설명이다.
1. Intel Processor Trace (PT)
인텔 프로세서 추적(Intel PT)은 프로세서의 하드웨어 장치를 사용하여 패킷 포맷 내 소프트웨어 실행 추적을 기록한다. Intel PT는 실행의 제어 흐름 변화시키는 명령어(Change of Flow Instructions, CoFI)가 있을 때 해당 패킷을 기록한다.
2. KCOV
KCOV는 리눅스 커널의 코드 커버리지 정보를 기록하는 도구이다. KCOV는 커널 전체에 실행된 함수를 측정하는 대신 시스템 콜 입력의 함수에 대한 기본 블록 레벨의 코드 커버리지를 측정하는 것을 목표로 한다. 따라서 시스템 콜을 다루는 사용자 스레드의 코드 커버리지 정보만 기록된다. KCOV는 코드 커버리지 측정을 위해 __sanitizer_cov_trace_pc() 함수를 삽입하는데, 이 함수는 모든 기본 블록 중 실행된 기본 블록의 프로세스 카운터를 추적한다. 이 추적된 프로세스 카운터는 커버리지 버퍼에 저장된다.
코드 커버리지 측면에서 시스템 콜 퍼저 평가하는 방법론
Figure 1시스템 구조
1) 시스템 콜과 연관된 함수와 베이직 블록 추출 및 리스트 생성
먼저, 소스 코드에서 시스템 콜을 관리하는 arch/x86/entry/syscalls/syscall_64.tbl를 사용해서 시스템 콜의 엔트리 함수를 구하고, System.map로부터 로드 된 커널 이미지 내 시스템 콜의 엔트리 함수의 커널 메모리 주소를 구한다. 이후 시스템 콜 함수와 관련된 모든 함수를 구하기 위해 각 엔트리 함수에 대한 함수 호출 그래프(FCG)를 생성한다. FCG는 리눅스 커널 이미지인 vmlinux 내에서Radare2를 이용하여 생성한다. 마지막으로 System.map를 이용해서 생성한 FCG 내 시스템 콜과 관련된 함수의 주소를 구하여 시스템 콜과 관련된 함수 주소 리스트를 생성한다. Radare2의 aaa 명령어를 통해 바이너리의 커널을 분석한 후, af 명령어로 System.map 내 리스트된 함수를 분석한다. agCj 명령어를 실행하여 FCG를 함수 이름, 함수 크기, 그리고 함수에서 사용된 심볼들을 포함하여 JSON 포맷으로 생성할 수 있다. 기본 블록 레벨에서 코드 커버리지를 사용하기 때문에, 시스템 콜 관련 함수에서 기본 블록 주소도 추출한다. Radare2의 afb 명령어를 사용하여 기본 블록의 시작과 끝, 그리고 크기를 추출할 수 있다.
2) 커널 퍼저를 이용하여 퍼징 수행 및 코드 커버리지 측정
리눅스 커널의 코드 커버리지는 정적 또는 동적 instrumentation으로 측정할 수 있다. 정적 instrumentation은 소스코드가 있어야 하기 때문에 다양한 운영체제에 적용하기 위해 동적 instrumentation을 사용했다. 동적 instrumentation에서 가상머신을 사용하는 방법은 성능 측면에서 오버헤드가 크기 때문에 Intel PT를 사용하여 코드 커버리지를 측정한다. 이때 Intel PT를 사용하는 kAFL의 변형된 버전을 사용했다. kAFL은 VM 인프라를 구성하고 있는데, VM 인프라는 하드웨어를 지원하는 가상화를 위한 VT-x와 실행 기록 추적을 위한Intel PT를 활용하는 게스트 운영체제의 제어 흐름을 추적한다. 함수 커버리지 측정을 위해 VM 인프라를 변형하고 시스템 콜 퍼저의 커버리지 추적을 위해 유저 에이전트를 변형했다. kAFL에서 인터럽트와 같은 비동기 이벤트를 처리하기 위해 TIP(Target IP) 패킷을 필터링한다. 게스트 운영체제 위에서 시스템 콜 퍼저를 실행하고, Intel PT를 활용하여 퍼저에서 실행된 커널 함수 정보를 로깅한다.
KCOV는 컴파일 시 커버리지 측정을 위한 코드를 삽입하여 코드 커버리지를 측정한다. 본 연구에서는 Syzkaller를 이용해서 구현했는데, Syzkaller는 KCOV를 이용해서 코드 커버리지 측정이 가능해서 Intel PT가 항상 필요하지 않기 때문이다. Syzkaller는 실행된 기본 블록의 비복잡한 주소를 커버리지 버퍼에서 관리하고 있어 syz-manager HTTP 포트 정보를 알고 있으면 커버리지 정보를 구할 수 있다.
3) 시스템 콜 퍼저 성능 평가
커널 퍼저의 커버리지 성능은 앞서 추출한 시스템 콜과 연관된 함수와 기본 블록 리스트와 퍼저에서 실행된 함수 리스트를 비교하여 퍼징 시 얼마나 많은 함수가 실행되었는지를 분석하여 평가한다. 이때 퍼저에서 실행 된 함수 리스트는 Intel PT에 의해 로깅 된 퍼징 시 실행된 함수이다.
1) 시스템이 많은 시스템 콜 관련 함수와 기본 블록 추출할 수 있는가?
Figure 2 리눅스 커널 내 전체 함수 수와 시스템 콜 관련 함수 수
그림 2는 추출된 시스템 콜과 관련된 함수 결과이다. 그림 2에서 리눅스 커널 내 32,000-47,000개의 함수가 구현되어 있고 시스템 콜 관련 함수는 9.7-15.2%임을 확인할 수 있다.
Figure 3 리눅스 커널 버전 별 시스템 콜 관련 기본 블록 수
그림 3은 시스템 콜 관련 기본 블록 수로, 시스템 콜과 관련된 기본 블록 수가 전체 기본 블록에서 11-18%를 차지하는 것을 확인할 수 있다.
2) 시스템 콜 관련 함수가 커널 퍼저의 개선을 나타낼 수 있는가?
Figure 4 Syzkaller 퍼저 24시간 실행 결과 시스템 콜 관련 함수와 기본 블록 수
시스템 콜 관련 함수만으로는 커널 퍼저가 직접적으로 실행할 수 있기 때문에 모든 관려 함수와 기본 블록 모두 커널 퍼저에서 테스트될 것이다. 따라서 시스템 콜 관련 함수가 퍼저에서 테스트되지 않았다면, 함수를 실행하지 못한 이유가 퍼저를 개선하기 위한 답일 것이다.
그림 4는 Syzkaller로 24시간 동안 퍼징을 수행했을 때 실행된 시스템 콜 관련 함수와 기본 블록 수를 보여준다. Syzkaller에서 40%의 시스템 콜 관련 함수가 테스트되지 못한 이유를 분석한 결과, 두 가지 이유로 함수 실행을 실패했다. 첫 번째로, 퍼저가 퍼징 시 관련 함수와 기본 블록의 큰 부분을 못 보고 넘어간 것이다. 두 번째 이유는 많은 실행된 함수(시스템 콜 관련 함수를 의미)가 커버리지 측정을 위한 KCOV에서 기록되지 않았기 때문이다. KCOV가 퍼징을 위해 설계되었기 때문에 함수 전체의 커버리지뿐만 아니라 시스템 입력 관련 함수의 커버리지도 측정하지 못했다.
Figure 5 Syzkaller 퍼저 24시간 실행 결과 실행하지 못한 함수 수
퍼저 개선을 위해, 실행하지 못한 함수를 식별하고 이 실행하지 못한 함수를 실행하도록 유도하는 것이 필요하다. 그림 5는 Syzkaller 퍼저를 24시간 동안 실행했을 때 실행하지 못한 함수 수를 보여주는데, Syzkaller는 30.000개가 넘는 함수를 실행하지 못했는데 이는 리눅스 커널 내 구현된 전체 함수의 60%를 차지한다.
Figure 6 Trinity, ext4 퍼저 24시간 실행 결과
그림 6은 Trinity와 ext4 퍼저를 24시간 동안 실행했을 때 실행된 시스템 콜 관련 함수와 기본 블록 수이다. 두 퍼저 모두 관련 함수는 36.5%와 32.2%를 실행하였고 기본 블록은 19.1%와 16.9%를 실행하여 Syzkaller와 비교하였을 때 적은 함수와 기본 블록을 실행했음을 확인할 수 있다. 시스템 콜 관련 함수로 실행되지 못한 함수를 쉽게 식별할 수 있기 때문에 시스템 콜 관련 함수를 퍼저를 개선하는데 사용할 수 있음을 알 수 있다.
3) 메트릭에 사용된 시스템 콜 관련 함수와 기본 블록이 커널 퍼저를 평가하는데 적합한가?
Figure 7 Trinity와 ext4 퍼저 24시간 실행 결과 실행된 함수와 기본 블록 수
그림 7a에서 Trinity가 ext4 퍼저와 비교했을 때 더 높은 함수 커버리지를 갖는 것을 확인할 수 있다. 그림 5b에서는 ext4 퍼저가 Trinity보다 기본 블록 커버리지가 더 높은 결과가 나왔는데, Trinity가 시스템 콜 관련 기본 블록은 더 많은 실행했음을 확인할 수 있다. 따라서 실행된 함수와 기본 블록 수만 비교하는 것보다 시스템 콜 관련 함수와 기본 블록을 사용하여 서로 다른 퍼저를 평가하는 것이 더 적합함을 확인할 수 있다.