커널은 시스템 권한을 갖기 때문에 좋은 공격 대상이고, 이러한 위협은 공격자보다 커널 버그를 먼저 찾아야 하는데 이때 퍼징이 보편적으로 사용된다. 대부분의 시스템 콜 퍼저는 퍼징에서 중요한 지표인 커버리지 성능 측면에서 평가되지 않았다. 따라서 본 연구에서는 가상화와 인텔 프로세서 추적(Intel Processor Trace, PT)을 결합한 방식을 사용하는 시스템 콜 퍼저의 코드 커버리지 성능을 평가하는 방법론을 제안한다.
방법론
1. 시스템 콜에 의해 실행될 수 있는 커널 내 모든 함수를 추출한다. (함수 리스트 생성)
2. 시스템 콜 퍼저로 퍼징을 수행하고 Intel PT를 활용하여 커버리지 정보를 기록한다.
3. 시스템 콜 퍼저를 평가 -> 퍼징 수행 동안 Intel PT에 의해 로깅 된 실행된 함수를 시스템 콜과 관련이 있는 함수 리스트(앞서 생성한 함수 리스트)와 비교
퍼징은 커널 내 버그를 찾는데 잘 사용된다. 대부분의 커널 퍼저는 프로세스 제어, 파일 관리, 장치 관리를 포함하지만 이에 국한되지 않는 이 커널로 서비스를 요청하는 시스템 콜을 대상으로 한다. 이 상황에서 다양한 시스템 콜 퍼저가 만들어졌지만 퍼징의 성능 측면에서 평가가 부족하다. 퍼저의 주요 목표는 버그를 더 많이 찾는 것이지만, 퍼저가 발견한 버그 리스트는 퍼징 성능을 이해하기에 충분하지 않다. 퍼저를 엄밀히 평가하려면, 버그 수와 코드 블록 수를 모두 고려해야 한다. 따라서 코드 커버리지가 퍼저가 적절히 작동하는지 알 수 있는 좋은 지표이기 때문에 코드 커버리지 분석이 필요하다. 커널 버그를 트리거할 때 커널 패닉이 발생할 수 있는데, 이는 버그가 트리거되었는지 아는데 더 쉽다. 하지만 코드 커버리지는 즉각적인 피드백 정보가 만들어지지 않기 때문에 측정이 어렵다. 이를 통해 코드 커버리지를 이용해서 시스템 콜 퍼저의 성능을 평가하고 퍼저에 대한 통찰력을 얻어야겠다는 동기를 얻을 수 있었다.
1) Intel Processor Trace (PT)
인텔 프로세서 추적(PT)은 프로세서의 하드웨어 장치를 사용하여 패킷 포맷 내 소프트웨어 실행 추적을 기록한다. Intel PT는 실행의 제어 흐름 변화시키는 명령어(Change of Flow Instructions, CoFI)가 있을 때 해당 패킷을 기록한다. 이때 패킷에는 TNT(Taken Not-Taken) 패킷, TIP(Target IP) 패킷, 그리고 FUP(Flow Update) 패킷이 있다. TNT 패킷은 분기 조건문에서 참 또는 거짓 결과를 기록한다. TIP 패킷은 상대 주소를 사용하는 간접 점프 명령어가 실행되었을 때 다음 명령어의 주소를 기록한다. FUP 패킷은 인터럽트나 트랩과 같은 비동기 이벤트와 관련된 정보를 기록한다.
2) 시스템 콜
시스템 콜은 사용자 레벨 프로그램에서 커널에 더 높은 권한의 서비스를 요청할 때 사용된다. 만약 사용자 공간에서 시스템 콜과 연관된 read() 함수가 호출되면, 커널은 시스템 테이블에서 해당 함수와 관련된 엔트리 함수를 찾아서 해당 엔트리 함수를 실행하여 사용자 요청을 처리한다.
코드 커버리지 측면에서 시스템 콜 퍼저 평가하는 방법론
1) 시스템 콜과 연관된 함수 추출 및 함수 리스트 생성
커널 내 함수 중 시스템 콜과 연관된 함수를 추출하고 해당 함수가 메모리에 로드되었을 때 주소를 찾는다. 이때 함수는 시스템 콜이 호출되면 실행되는 엔트리 함수에 대한 함수 콜 그래프를 구성하는 커널 함수를 참조하는 시스템 콜과 연관된 함수를 추출한다. 리눅스 소스 코드에서 시스템 콜을 관리하는arch/x86/entry/syscalls/syscall _64.tbl를 사용해서 시스템 콜의 엔트리 함수를 구한다. System.map에는 커널 이미지 심볼과 관련 주소 정보를 포함하고 있어서, 로드된 커널 이미지에서 시스템 콜의 엔트리 함수의 커널 메모리 주소를 구할 수 있다. System.map은 시스템 콜에서 호출되지 않는 함수도 갖고 있어서 앞서 추출한 시스템 콜 엔트리 리스트 내 엔트리 함수의 주소를 구할 수 있다. 시스템 콜 함수와 관련된 모든 함수를 구하기 위해 각 엔트리 함수에 대한 함수 호출 그래프(FCG)를 생성한다. FCG는 리눅스 커널 이미지인 vmlinux를 통해 구할 수 있다. FCG를 생성하는데 Radare2를 이용했다. System.map로부터 생성한 FCG 내 시스템 콜과 관련된 함수의 주소를 구할 수 있다. 이 과정에서 시스템 콜과 관련된 함수 주소 리스트를 생성할 수 있다.
2) 가상머신 위에 운영체제 구축 후 시스템 콜 퍼저를 이용하여 퍼징 수행 및 코드 커버리지 측정
리눅스 커널의 코드 커버리지는 정적과 동적으로 측정할 수 있다. 정적 방식으로 코드 커버리지를 측정하기 위해 컴파일 단계에서 원본 코드에 커버리지 측정을 위한 코드가 추가된다. 하지만 정적 방식은 소스코드가 있어야 하기 때문에 윈도우와 같은 운영체제 커널에서는 적용하기 어렵다. 따라서 다양한 운영체제에 적용될 수 있도록 동적 방식으로 코드 커버리지를 측정했다.
Intel PT를 활용하여 퍼저에서 실행된 함수인 커버리지 정보를 기록한다.
3) 퍼저에서 실행된 함수 리스트를 시스템 콜과 연관된 함수 리스트와 비교하여 시스템 콜 퍼저 성능 평가
시스템 콜 퍼저의 커버리지 성능은 앞서 추출한 시스템 콜과 연관된 함수 리스트와 퍼저에서 실행된 함수 리스트를 비교하여 평가한다. 이때 퍼저에서 실행된 함수 리스트는 Intel PT에 의해 로깅된 실행된 함수이다.
시스템 콜 퍼저를 평가하는 시스템을 1) 시스템 콜 관련 함수 추출, 2) 코드 커버리지 측정하는 과정을 통해 테스트하였다.
1. 시스템 콜 관련 함수 추출
Figure 2 리눅스 커널 내 전체 함수 수와 시스템 콜 관련 함수 수
그림 2는 추출된 시스템 콜과 관련된 함수 결과이다. 리눅스 커널 4.4.87 버전에서 전체 커널 함수 중 19.5%만 시스템 콜을 통해 실행될 수 있음을 확인할 수 있다. 시스템 콜을 통해 호출되는 함수는 이미 결정 되어있기 때문에 이 함수 내에서 시스템 콜 퍼저의 함수 커버리지를 측정한다.
2. 코드 커버리지 측정
Figure 3 시스템 콜 퍼저가 실행한 함수 수
그림 3은 시스템 콜 퍼저인 Trinity와 IDLE를 24시간 동안 실행했을 때 실행한 함수 수로, 이를 통해 Trinity가 IDLE보다 더 많은 시스템 콜과 시스템 콜과 연관된 함수를 실행했음을 확인할 수 있다.