목차

  • 요약

  • 1. 소개

  • 2. 배경과 관련 작업

    • 2.1 POSIX 장치 드라이버

    • 2.2 안드로이드 운영체제

    • 2.3 Fuzz Testing

    • 2.4 기타 분석

  • 3. 개요

    • 3.1 Example

  • 4.  인터페이스 복구

    • 4.1 시스템 계측 빌드

    • 4.2 ioctl 핸들러 식별

    • 4.3 장치 파일 탐지

    • 4.4 명령 값 결정

    • 4.5 인자 타입 식별

    • 4.6 구조체 정의 찾기

  • 5. 구조체 생성

  • 6. ON-DEVICE 실행

    • 6.1 포인터 고정

    • 6.2 실행

  • 7. 구현

    • 7.1 인터페이스 인식 퍼징

  • 8. 평가

    • 8.1 인터페이스 추출 평가

    • 8.2 평가 기준

    • 8.3 결과

    • 8.4 연구 사례 1 : Honor 8 설계 이슈

    • 8.5 연구 사례 2 : qseecom 버그

    • 8.6 커버리지 기반 퍼징 보강

  • 9. 토론

    • 9.1 약점

    • 9.2 향후 연구

  • 10. 결론

  • 부록 A. IOCTL 등록 구조

  • 부록 B. V4L2 드라이버

  • References

 

요약

장치 드라이버는 현대 유닉스 계열 시스템에서 하드 디스크 및 프린터에서 디지털카메라 및 블루투스 스피커에 이르기까지 물리적 장치에서 작업을 처리하는 데 필수적인 부분이다. 특히 모바일 기기에서 새로운 하드웨어의 급증으로 시스템 커널에서 장치 드라이버가 폭발적으로 증가한다. 이러한 많은 드라이버는 타사 개발자가 제공하며 보안 취약점에 취약하고 적절한 검증이 이루어지지 않는다. 불행하게도, 장치 드라이버에 대한 복잡한 입력 데이터 구조체는 퍼지 테스트와 같은 기존 분석 도구의 효율성을 떨어 뜨리며, 지금까지 커널 드라이버 보안에 대한 연구는 비교적 희박하다.

 

본 논문에서는 유효한 입력을 자동으로 생성하고 커널 드라이버의 실행을 트리거하는 인터페이스 인식 퍼징 도구인 DIFUZE를 제시한다. 정적 분석을 활용하여 사용자 공간에서 올바르게 구조화된 입력을 작성하여 커널 드라이버를 탐색한다. DIFUZE는 드라이버 핸들러 식별부터 장치 파일 이름에 매핑, 복잡한 인수 인스턴스 구성 등 완전히 자동화된 기능이며, 7개의 최신 안드로이드 스마트폰에 대한 접근 방식을 평가한다. 그 결과 DIFUZE는 커널 드라이버 버그를 효과적으로 식별할 수 있으며, 임의 코드를 실행할 수 있는 결함을 포함하여 이전에 알려지지 않은 32가지 취약점을 보고한다.

 

1. 소개

스마트폰과 다른 모바일 기기는 현재 생활의 중심 부분을 차지하고 있다. 그것들은 우리 중 많은 사람들이 밤에 대화하는 마지막 교류 수단이며 아침에 가장 먼저 도달하는 것이다. 우리는 금융 거래를 수행하고 가족, 친구, 동료들과 의사소통하고 위치, 오디오, 비디오를 녹음하는 데 사용한다. 점점 더 개인적이고 상업적인 목적으로만 사용되는 것이 아니라 정부 활동을 용이하게 하는 데에도 이용된다.

 

이러한 장치의 보안의 중요성은 명백하다. 만약 적이 연결된 세계로 가는 관문이 된 장치를 손상시키면 엄청난 힘을 얻게 될 것이다. 따라서 스마트폰의 보안을 보장하기 위해 많은 노력을 기울였다. 이 보안은 사용자 공간 응용 프로그램(예: 주소 공간 레이아웃 랜덤화, 데이터 실행 보호, SELinux 등)을 대상으로 많은 공격 완화 기법을 활용하고 보안을 1단계 개발 목표로 함으로써 정교한 응용 프로그램 샌드박스를 사용하여 달성한다. 그러나 모바일 장치의 커널 보안에는 약점이 있다.

 

여러 가지 취약점 완화 기술을 사용할 수 있는 사용자 공간 애플리케이션과 달리, 현대 운영 체제의 커널은 사용 가능한 보호 기능에도 불구하고 공격에 상대적으로 취약하다. 그 결과, 사용자 공간 애플리케이션의 취약점이 희박해짐에 따라 공격자는 커널에 초점을 맞춘다. 예를 들어, 최근 3년간 커널 코드에 있는 안드로이드 취약점 점유율은 4%(2014년)에서 39%(2016년)로 증가해 커널 버그를 탐지하고 제거하는 기법의 필요성을 부각했다.

 

커널 자체는 코어 커널 코드와 장치 드라이버의 두 가지 코드 유형으로 나눌 수 있다. 전자는 시스템 호출(syscall) 인터페이스를 통해 액세스 하여 사용자가 파일(open() 시스템 호출)을 열거나 프로그램 실행(execve() 시스템 호출)하는 등의 작업을 수행할 수 있다. 후자는 POSIX 호환 시스템(휴대폰 시장의 98% 이상을 차지하는 예: Linux/Android/FreeBSD/IOS)은 일반적으로 ioctl 인터페이스를 통해 액세스 된다. 특정 시스템 호출로 구현된 인터페이스는 입력 전송을 장치 드라이버에 의해 처리되도록 한다. 구글에 따르면 안드로이드 커널(Linux와 유사한 것)에 대해 보고된 버그 중 85%가 타사 기기 공급업체가 작성한 드라이버 코드에 있다. 모바일 장치의 수가 지속적으로 증가하고 보안이 중요해짐에 따라 공격자가 악용하기 전에 장치 드라이버의 취약점을 식별하는 자동화된 접근 방식이 매우 중요하다.

시스템 호출 인터페이스의 자동 분석은 관련 작업에 의해 철저히 검토되었지만 ioctls는 소홀히 취급되어 왔다. 이는 syscall과의 상호작용은 잘 정의된 통일된 사양을 따르는 반면 ioctls와의 상호작용은 해당 장치 드라이버에 따라 다르기 때문이다. 특히, ioctl 인터페이스는 각 유효한 명령 집합에 대한 구조화된 인수로 구성되며, 명령어와 데이터 구조체는 모두 드라이버에 의존한다. 이는 보안에 영향을 미치지만(즉, 이러한 구조체의 포인터, 동적 크기 필드, 공용체, 하위 구조체는 구조체의 구문 분석으로 인한 취약점의 가능성을 증가시킨다) 이러한 장치들을 분석하기 어렵게 만든다. 이러한 장치의 자동 분석은 반드시 인터페이스를 인식해야 하며, 효과적이기 위해서는 명령 식별자와 장치가 예상하는 데이터 구조를 사용하여 ioctls와 상호작용을 해야 한다.

본 논문에서는 인터페이스 인식 퍼징을 가능하게 하고 장치 드라이버가 제공하는 ioctl 인터페이스의 동적 탐색을 용이하게 하는 새로운 기술 조합인 DIFUZE를 제시한다. DIFUZE는 유효한 명령 및 관련 데이터 구조체를 포함하여 특정 ioctl 인터페이스를 복구하기 위해 커널 드라이버 코드의 자동 정적 분석을 수행한다. 복구된 인터페이스를 사용하여 ioctl 호출에 대한 입력을 생성하며, 사용자 공간 프로그램에서 커널로 전송될 수 있다. 이러한 입력은 드라이버가 사용하는 명령과 구조가 일치하여 ioctl을 효율적이고 심층적으로 탐색을 가능하게 한다. 복구된 인터페이스를 통해 퍼저는 데이터를 변경할 때 의미 있는 선택을 할 수 있다. 즉, 포인터, 열거 형, 정수와 같은 입력된 필드는 단순한 바이트의 시퀀스로 처리하면 안 된다. DIFUZE는 해당 드라이버의 가정을 강조하고 심각한 보안 취약점을 노출한다. 실험에서 7개의 최신 모바일 기기를 분석하여 36개의 취약점을 발견했는데, 그중 32개는 이전에 알려지지 않았으며(DIFUZE에 의해 발견된 4개의 취약점은 실험 과정에서 패치가 되었다), 문제의 장치를 크래시시키는 결함에서부터 공격자가 서비스 거부(DoS)를 완전히 제어할 수 있는 버그에 이르기까지 다양하다. 그 결과, 사용자 공간 애플리케이션의 취약점이 희박해짐에 따라 공격자는 커널에 초점을 맞춘다. 예를 들어, 최근 3년간 커널 코드에 있는 안드로이드 취약점 점유율은 4%(2014년)에서 39%(2016년)로 증가해 커널 버그를 탐지하고 제거하는 기법의 필요성을 부각했다.

 

요약하면, 논문은 다음과 같은 기여를 한다.

 

Interface-aware fuzzing
POSIX 시스템의 ioctl 커널 드라이버 인터페이스와 같은 인터페이스에 민감한 대상의 퍼징 하기 위한 새로운 접근 방식을 설계한다.

 

Automated driver analysis
장치의 커널 소스를 자동으로 분석할 수 있는 퍼징 프레임워크를 개발했다. 모든 드라이버에 대한 도구는 모든 ioctl 진입점과 해당 구조체 및 장치 파일 이름을 식별한다. 36가지 취약점을 식별하는 7가지 장치를 분석하기 위해서 기술을 적용한다. DoS에서 코드 실행 결함에 이르는 취약점은 접근 방식의 효과와 영향을 보여준다. 이러한 취약점을 해당 드라이버 공급 업체에 책임 있게 공개하고 있다.

 

DIFUZE prototype
DIFUZE를 오픈 소스 도구로 www.github.com/ucsb-seclab/difuze 에서 공개하여 향후 보안 연구원에게 유용할 것으로 기대된다.

 

2. 배경과 관련 작업

이 섹션에서는 반드시 극복해야 할 고유한 과제(이러한 과제로 인해 기존 시스템이 ioctl 퍼징에 적용되지 않은 이유)를 설명하고, 퍼징 도구가 작동하는 플랫폼(Android)을 소개하고, 프로그램 취약점을 찾아내는 이전의 작업을 비교한다.

 

2.1 POSIX 장치 드라이버

POSIX 표준은 사용자 공간 애플리케이션과 장치 드라이버의 상호작용을 위한 인터페이스를 명시한다. 이 인터페이스는 커널 상주 장치 드라이버의 사용자 공간 존재를 나타내는 특수 파일인 장치 파일을 통해 장치와 상호작용을 지원한다. 사용자 공간 애플리케이션이 open() 시스템 호출로 장치 파일에 대한 핸들을 획득한 후, 애플리케이션이 이러한 파일과 상호 작용을 할 수 있는 여러 가지 방법이 있다.

 

장치마다 기능을 수행하려면 다른 시스템 호출이 필요하다. 예를 들어, read(), write(), seek()는 하드 드라이브 장치 파일에 적용할 수 있다(하드 드라이브의 내용을 단일 파일로 표시). 오디오 장치의 경우 read()는 마이크에서 원시 오디오 데이터를 읽을 수 있으며 write()는 스피커에 원시 오디오 데이터를 쓸 수 있으며 seek()는 사용되지 않을 수 있다.

 

그러나 일부 기능은 기존 시스템 호출을 통해 구현될 수 없다. 예를 들어, 오디오 장치의 경우 사용자 공간 애플리케이션은 오디오를 녹음하거나 재생할 샘플링 속도를 어떻게 구성해야 할지? 이러한 대역 외 동작은 ioctl() 인터페이스 1을 통해 POSIX 표준에서 지원된다. 이 시스템 호출을 통해 드라이버는 기존 파일로 모델링하기 어려운 기능을 노출시킬 수 있다.

 

일반성을 지원하기 위해 ioctl() 인터페이스는 임의의 드라이버 지정 구조체를 입력으로 수신할 수 있다. C 프로토타입은 int ioctl(int file_descriptor, int request, ...)처럼 보이는데 여기서 첫 번째 인수는 열린 파일 설명자, 두 번째 인수는 일반적으로 명령 식별자로 알려진 정수이며 나머지 인수의 종류와 양은 드라이버와 명령 식별자에 따라 달라진다.

 

과제

앞에서 언급한 특성은 ioctl 시스템 호출을 특히 취약점에 취약하게 만든다. 첫째, read()와 write()와는 달리 ioctl() 호출에 제공된 데이터는 종종 극도로 복잡하고 비표준 데이터 구조체의 인스턴스들이다. 이러한 구조체를 파싱 하는 것은 쉬운 일이 아니며, 실수로 커널 컨텍스트에 치명적인 취약점이 생길 수 있다. 둘째, 데이터 구조체의 일반성으로 인해 분석가가 문제의 드라이버가 다른 명령 식별자를 처리하는 방법과 선택적 인수에 대해 예상되는 데이터 유형에 대한 지식이 있어야 하기 때문에 ioctl() 인터페이스의 분석도 어렵다.

 

이것들은 우리가 해결하고자 하는 핵심 문제들이다. DIFUZE가 명령 식별자와 구조체 정보를 자동으로 복구하고, 필요한 복잡한 데이터 구조체를 구축하며, 최소한의 사용자 개입으로 ioctl() 인터페이스가 있는 퍼지 장치를 통해 보안 취약점을 찾아낼 수 있도록 설계했다.

 

2.2 안드로이드 운영체제

안드로이드는 스마트폰의 운영체제로 설계되었다. 안드로이드가 2016년 3분기에 86.8%의 점유율로 스마트폰 OS 시장을 장악했다는 최근 보고서가 나왔다. 안드로이드 디자이너들은 기기들을 보호하기 위해 신중한 조치를 취하지만, 스마트폰 시스템에는 몇 가지 취약점이 있다. 안드로이드의 인기와 증가하는 보안 문제를 고려하여 분석 방식을 평가하기 위해 안드로이드 시스템을 주요 타깃 플랫폼으로 선택한다. DIFUZE는 다른 유닉스 계열 시스템에서도 작동한다.

 

안드로이드는 단일 아키텍처를 가진 리눅스 커널을 기반으로 한다. 커널 모듈(장치 드라이버 등)은 일정 수준의 모듈성을 제공하지만, 전체 커널이 하나의 메모리 공간에서 실행되고, 모든 부분이 동일한 권한이 부여된다는 점에서 설계 원리는 여전히 획일적이다. 따라서 장치 드라이버의 취약점은 전체 커널을 손상시킬 수 있다. 실제로 2016년 안드로이드 커널에 보고된 버그 중 80% 이상이 공급 업체가 작성한 드라이버 코드에서 나왔다. 안드로이드 오픈 소스 프로젝트는 공급 업체(예: Sony, HTC)는 디지털카메라, 가속도계, GPS 장치와 같은 새로운 하드웨어를 지원하기 위해 안드로이드 커널 드라이버를 사용자 정의할 수 있다. 보안은 종종 그러한 회사들의 시장 출시 시간을 단축시키기 때문에, 보안 프로세스의 도입 과정에서 보안 취약점이 발생하기 쉽다. 다행히도, 안드로이드 시스템의 개방성은 GNU(General Public License)에 따라 소스 코드를 공개적으로 이용할 수 있게 한다. 이는 드라이버에 대한 높은 수준의 의미론적으로 풍부한 정보에 대한 접근을 제공하기 때문에 우리의 접근 방식을 용이하게 한다.

 

2.3 Fuzz Testing

퍼징은 프로그램에 입력으로 무작위 데이터를 생성하여 프로그램 테스트를 하는 것으로 알려져 있다. SPIKE, Valgrind, PROTOS와 같은 많은 여구 관심을 끌었다.

 

Fuzzing

퍼징의 주요 전망은 "가장 유효한" 입력을 생성하여 대상 프로그램을 실행하고, 광범위한 기능을 수행하며, 취약점으로 이어지는 일부 코너케이스(corner case)를 트리거하는 것이다. 동적 특성 추적은 잠재적 입력을 생성하기 위해 널리 사용되는 전략이다. Dowser와 BuzFuzz는 특정 유형의 취약점을 유발할 가능성이 높은 입력을 생성하기 위해 오염 추적(taint tracking)을 사용한다. 그러나 매우 제한된 입력을 요구하는 ioctl 함수의 경우, 이러한 기법은 덜 효과적이다. 오염 분석에 기반한 접근법은 기본 프로그램에서 사용하는 입력 형식을 복구하기 위해 존재하지만, 특정 명령 식별자가 주어지면 ioctl 핸들러는 특정 유형의 추가 인수를 기대할 수 있다.

 

진화 기술은 퍼징 시스템에서 또 다른 일반적인 입력 생성 전략을 나타낸다. VUzzer와 SymFuzz는 정적 분석을 돌연변이 기반(mutation-based) 진화 기술과 결합하여 입력을 효율적으로 생성한다. 그러나 이러한 기술은 매우 제한된 입력을 생성하는 데 비효과적이다. DIFUZE는 가능한 ioctl 명령 값을 먼저 수집한 후 예상 입력 형식으로 퍼징하여 이 문제를 해결한다.

 

프로그램의 입력 형식을 알고 있으면, 유효한 입력 사양으로 퍼징을 향상시킬 수 있다. 피치(Peach)는 업계 표준 도구 중 하나이다. 그러나 실시간 데이터(즉, 다른 데이터에 대한 활성 포인터가 포함된 데이터)를 생성할 수 없으며, 8절에서 볼 수 있듯이 많은 장치 드라이버에는 포인터를 포함하는 입력 구조가 필요하다. 문법 기반 기술은 파일 형식, 인터프리터, 컴파일러를 퍼징하는 데 사용되었지만, 이러한 기술에는 입력이 고정된 형식을 갖도록 요구한다.

 

Kernel and driver fuzzing

운영 체제 인터페이스 또는 시스템 호출을 퍼징하는 것은 운영 체제 커널을 테스트하기 위한 실질적인 접근 방식이다. 대부분의 드라이버는 사용자 공간과 상호작용하기 위해 POSIX 표준인 ioctl 기능을 사용한다. 3.1절에서 논의한 바와 같이 ioctl는 복잡하며, 사용자가 생성한 특정 명령 값과 데이터 형식을 요구한다. 유효한 명령 값과 관련 데이터 구조체를 식별하는 것은 ioctl 퍼징의 두 가지 주요 문제이다. iofuzz, ioattack, ioctlbf, ioctlfuzzer와 같은 윈도우 커널용 ioctl 인터페이스를 테스트하기 위한 도구가 개발되었다. 그러나 이러한 도구는 윈도우 커널에서 제공하는 정보의 광범위한 로깅 및 추적뿐만 아니라 윈도우 고유의 ioctl 명령 형식에 의존한다. 또한, 이러한 도구들 중 많은 것들이 본질적으로 단순하다. 단순히 프로세스에 연결하고 윈도우 ioctl 호출을 연결하는 것을 포함한다. 일단 연결되면, 도구는 호출이 이루어질 때 값을 변경한다. 이것은 몇 가지 측면에서 부족하다. 예를 들어, 프로세스가 드라이버의 전체 기능을 발휘하지 못할 수 있으며 들어오는 데이터의 유형 정보를 알 수 없기 때문이다. 이 문제를 해결하기 위해 DIFUZE는 장치 드라이버의 소스 코드를 분석하여 유효한 명령과 해당 데이터 구조체를 식별한다. 우리가 사용하는 분석 기술은 실제 장치를 수정할 필요가 없다.

 

유효한 ioctl 명령어의 추출은 Stanislas 외에 시도되었지만, 최첨단 시스템은 실제 커널 모듈로 확장할 수 없었다. 반대로, 8절에서 보여지듯이, DIFUZE는 실제 장치의 대형 커널 모듈로 확장하고 취약점을 찾아낸다.

 

Trinity와 syzkaller는 리눅스 syscall 퍼징을 위해 특별히 개발되었다. 8절에서 알 수 있듯이 장치 드라이버의 ioctl 핸들러를 퍼징할 때 성능이 저하된다. syzkaller는 더 많은 버그를 탐지하기 위해 커널 Kernel Address Sanitizer(KASAN)와 같은 추가 계측 기법을 사용하지만, 이러한 기법은 분석가가 사용자 정의 펌웨어를 사용하여 장치를 재플래시해야하기 때문에 공급 업체 기기에서 직접 사용할 수 없다. 몇 가지 접근 방식은 특히 선택된 syscall 및 드라이버에 대한 퍼징에 중점을 두었다. 그러나 특정한 기능에만 초점을 맞추고 있으며 다른 시스템이나 드라이버로 일반화될 수 없다. DIFUZE는 수정되지 않은 커널을 실행하는 장치에서 모든 리눅스 커널 드라이버를 퍼징하도록 일반화할 수 있는 최초의 완전 자동화 시스템입니다.

 

2.4 기타 분석

퍼징 외에도 심볼릭 실행(symbolic execution)과 정적 분석(static analysis)이라는 두 가지 분석 기법이 있는데, 작업과 관련이 있다. 이러한 메커니즘을 소개하고 메커니즘이 설계에 미치는 영향을 설명할 것이다.

 

Symbolic execution

심볼릭 실행은 심볼릭 변수를 사용하여 제한된 입력을 생성하고 복잡한 검사를 만족시키는 기술이다.

 

DART, SAGE, Fuzzgrind, Driller는 심볼릭 실행을 무작위 테스트와 결합하여 코드 적용 범위를 증사시킨다. BORG는 버퍼 오버리드(Buffer over-reads)를 유발할 가능성이 높은 입력을 생성하기 위해 심볼릭 실행을 사용한다. 원시 장치에서 심볼릭 실행을 수행하는 엔지니어링 문제와 복잡한 시스템 커널에 의해 더욱 악화되는 근본적인 경로 폭발 문제(복잡한 시스템 커널에 의해 더욱 악화됨)는 이러한 기법을 커널 드라이버에 비실용적으로 만든다.

 

Static analysis

정적 분석은 해당 프로그램을 실행하지 않고 프로그램 취약점을 찾아내는 널리 사용되는 기술이다. 정밀도를 극대화하기 위해, 이러한 기법들도 분석을 수행하기 위해 일반적으로 소스 코드가 필요된다. 많은 시스템 커널(리눅스 커널 포함)과 장치 드라이버가 오픈 소스이기 때문에 커널 보안은 정적 분석에서 큰 이익을 얻을 수 있다. 예를 들어, Ashcraft 등은 리눅스와 OpenBSD 커널의 신뢰할 수 없는 소스에서 읽은 정수를 잡기 위해 컴파일러 확장을 개발했다. Post 등 리눅스 커널에서 교착 상태 및 메모리 누수를 찾기 위해 경계 모델 검사기를 사용했다. Ball 등 윈도우 드라이버의 정확성을 증명하기 위해 일련의 규칙으로 정적 분석 도구를 만들었다.

 

대부분의 정적 분석 도구의 한 가지 제한 사항은 많은 오탐지 생성입니다. 우리의 작업은 취약점 탐지 단계에서 퍼징을 활용하기 때문에, 확인된 모든 취약점은 실제 버그이며, 오탐지를 완전히 피할 수 있다. 정적 분석 기술의 또 다른 단점은 분석 시 보안 정책 및 규칙의 수동 사양이 필요하다는 것이다.

 

3. 개요

이번 절에서는 인터페이스 인식 퍼징 접근 방식과 ioctl 퍼징을 통한 장치 드라이버의 취약점 탐지에 대한 애플리케이션에 대한 개요이다.

 

[그림 1] DIFUZE 접근 다이어그램

DIFUZE는 유효한 ioctl 명령 및 인수 구조체 유형과 같은 드라이버 인터페이스 정보를 추출하기 위해 분석 구성을 사용하여 제공된 커널 소스를 분석합니다. 이러한 구조체의 예를 종합하여 대상 장치로 보내는데, 주어진 입력으로 ioctl 실행을 트리거하고 결국 장치 드라이버에서 크래시가 발견된다.

 

[그림 1]은 시스템의 높은 수준의 워크플로우를 보여준다. DIFUZE는 대상 호스트의 커널 소스 코드(장치 드라이버의 소스 코드를 포함)를 입력으로 요구한다. 리눅스는 GNU(General Public License)에 따라 라이선스가 부여되므로 커널 드라이브 인터페이스 코드와 같이 링크된 모든 소프트웨어도 릴리스해야 한다. 따라서 안드로이드 기기의 커널 소스는 쉽게 구할 수 있고 분석 또한 가능하다.

 

이 입력이 주어지면, DIFUZE는 여러 단계를 거쳐 장치 드라이버의 상호작용 인터페이스를 복구하고, 인터페이스를 실행하기 위한 정확한 구조체를 생성하고, 대상 호스트의 커널에 의해 이러한 구조체의 처리를 트리거한다. 커널 버그의 트리거는 시스템을 불안정하게 만드는 경우가 많아(중간 또는 재부팅으로 이어짐) DIFUZE의 최종 단계만 대상 호스트에서 수행해야 한다. 다른 단계는 외부 분석 호스트에서 실행되며, 그 결과는 로컬로 기록되고(버그가 트리커 된 경우 입력 재생을 위해) 네트워크 연결 또는 디버그 인터페이스를 통해 대상 호스트로 전송된다.

 

자세한 내용은 다음과 같다.

 

Interface recovery

첫 번째 단계에서 DIFUZE는 제공된 소스를 분석하여 대상 호스트에서 어떤 드라이버가 활성화되어 있는지, 어떤 장치 파일을 사용하여 상호작용할 수 있는지, 어떤 ioctl 명령을 받을 수 있는지, 이러한 구조체로 이들 명령에 전달 될 구조체를 탐지한다. 이 일련의 분석은 LLVM을 사용하여 구현되며 4절에 자세히 설명되어 있다. 이 단계의 최종 결과는 대상 드라이버, 대상 ioctl 명령어 및 구조체 유형 정의에 대한 장치 파일 이름의 튜플(Tuples) 집합이다.

 

Structure generation

각 구조체에 대해 DIFUZE는 구조체 인스턴스(Instance)를 지속적으로 생성하는데, 이는 이전 단계에서 복구된 유형 정보의 인스턴스화를 나타내는 매모리 내용이다. 이러한 인스턴스는 관련 대상 장치 파일 이름 및 대상 ioctl 명령 식별자와 함께 대상 호스트에 기록 및 전송된다. 이 단계는 5절에서 자세히 설명되어 있다.

 

On-device execution

실제 ioctl 트리거링 컴포넌트는 대상 호스트 자체에 있다. 대상 장치 파일 이름, 대상 ioctl 명령 및 생성된 구조체 인스턴스를 수신하면 실행자는 ioctl 실행을 트리거합니다. 이 단계는 6절에서 논의한다.

 

DIFUZE는 대상에 전송되는 입력 순서를 기록한다. 따라서, 버그가 트리거되고 대상 장치가 크래시날 때, 입력을 재현성과 수동 심사/분석에 사용될 수 있다.

 

3.1 Example

DIFUZE를 이해할 수 있도록 간단한 드라이버의 예시이다. 이 예는 [목록 1] 구조체 정의, [목록 2] 분석에 약간의 복잡성을 나타내는 copy_from_user 래퍼(wrapper) 함수, [목록 3] ioctl 핸들러, [목록 4] 주 드라이브 초기화 코드로 제시한다.

 

typedef struct {
	long sub_id;
	char sub_name[32];
} DriverSubstructTwo;

typedef union {
	long meta_id;
	DriverSubstructTwo n_data;
} DriverStructTwo;

typedef struct {
	int idx;
	uint8_t subtype;
	DriverStructTwo *subdata;
} DriverStructOne;

[목록 1] 실행 중인 예제의 구조체 정의. DIFUZE는 이를 자동으로 복구하여 대상 드라이버의 구조체 인식 퍼징 수행

 

int copy_from_user_wrapper(void *buff, void *userp, size_t size) {
	// copy size bytes from address provided by the user (userp)
	return copy_from_user(buff, userp, size);
}

[목록 2] 많은 실제 드라이버와 마찬가지로, 예제 드라이버는 copy_from_user 래퍼 함수 제공. 이와 같은 래퍼(더욱 복잡한) 때문에 DIFUZE는 중첩 함수 분석 지원

 

DriverStructTwo dev_data1[16];
DriverStructTwo dev_data2[16];
static bool enable_short; static bool subhandler_enabled;
long ioctl_handler(struct file* file, int cmd, long arg) {
	uint32_t curr_idx;
	uint8_t short_idx; void* argp = (void*)arg;
	DriverStructTwo* target_dev = NULL;
	switch (cmd) {
	case 0x1003:
		target_dev = dev_data2;
	case 0x1002:
		if (!target_dev)
			target_dev = dev_data1; // program continues to execute
		if (!enable_short) {
			if (copy_from_user_wrapper((void*)&curr_idx, argp,
				sizeof(curr_idx))) {
				return -ENOMEM; // failed to copy from user
			}
		}
		else {
			if (copy_from_user_wrapper((void*)&short_idx, argp,
				sizeof(short_idx))) {
				return -ENOMEM; // failed to copy from user
			}
			curr_idx = short_idx;
		}
		if (curr_idx < 16)
			return process_data(&(target_dev[curr_idx]));
		return -EINVAL;
	default:
		if (subhandler_enabled)
			return ioctl_subhandler(file, cmd, argp);
	}
	return -EINVAL;
}

long ioctl_subhandler(struct file* file, int cmd, void* argp) {
	DriverStructOne drv_data = { 0 };
	DriverStructTwo* target_dev;
	if (cmd == 0x1001) {
		if (copy_from_user_wrapper((void*)&drv_data, argp,
			sizeof(drv_data))) {
			return -ENOMEM; // failed to copy from user
		}
		target_dev = dev_data1;
		if (drv_data.subtype & 1)
			target_dev = dev_data2;
		// Arbitrary heap write if drv_data.idx > 16
		if (copy_from_user_wrapper((void*)&(target_dev[drv_data.idx]),
			drv_data.subdata,
			sizeof(DriverStructTwo))) {
			return -ENOMEM; // failed to copy from user
		}
		return 0;
	}
	return -EINVAL;
}

[목록 3] 명령 식별자에 대한 매우 구체적인 값을 예상하며 각 명령에 적합한 구조로 데이터가 표시될 것으로 예상하는 ioctl 핸들러. ioctl 처리는 여러 기능으로 나뉨

 

static struct cdev driver_devc;
static dev_t client_devt;
static struct file_operations driver_ops;
__init int driver_init(void)
{
    // request minor number
    alloc_chrdev_region(&driver_devt, 0, 1, "example_device");
    // set the ioctl handler for this device
    driver_ops.unlocked_ioctl = ioctl_handler;
    cdev_init(&driver_devc, &driver_ops);
    // register the corresponding device.
    cdev_add(&driver_devc, MKDEV(MAJOR(driver_devt), 0), 1);
}

[목록 4] 실행 중인 예제의 주 드라이버 초기화 기능. DIFUZE가 복구해야 하는 드라이버 파일을 동적으로 생성하고, 최상위 ioctl 핸들러를 등록하여 복구

 

4. 인터페이스 복구

장치 드라이버의 ioctl를 효율적으로 퍼지하기 위해 DIFUZE는 해당 드라이버의 인터페이스를 복구해야 한다. 장치 드라이버의 인터페이스는 장치와 통신하는 데 사용되는 장치 파일의 이름/경로, 해당 장치에 대한 ioctl 명령에 대한 유효한 값 및 다른 ioctl 명령어에 대한 ioctl 데이터 인수의 구조체 정의로 구성된다.

 

이 데이터를 복구하기 위해 DIFUZE는 LLVM에 구현된 분석 조합을 사용한다. 리눅스 커널은 LLVM을 이용한 분석(또는 컴파일)에 적합하기 않기 때문에 우선 대체 빌드 절차를 개발했다. 이 작업이 완료되면 장치 드라이버가 생성한 장치 파일의 파일 이름을 식별하고 ioctl 핸들러를 찾아 ioctl 명령 식별자의 유효한 집합을 복구하며 데이터 인수에 대한 구조체 정의를 ioctl 명령으로 검색한다.

 

4.1 시스템 계측 빌드

DIFUZE가 리눅스 장치 드라이버에서 LLVM 분석을 수행할 수 있도록 하기 위해 몇 가지 단계를 수행한다.

 

GCC 컴파일

먼저, GCC를 이용하여 컴파일을 위한 대상 호스트의 커널과 드라이버 소스를 설정하는 수동 단계를 수행해야 한다. 이것은 일반적으로 문서화가 잘되어있는 프로세스이지만, 모바일 장치 공급 업체는 GPL로 규정된 소스 코드 릴리스를 쉽게 컴파일할 수 있는 방법을 찾지 못하므로 일부 수동 구성 작업이 필요하다. 일단 소스 트리를 GCC로 컴파일할 수 있게 되면 전체 컴파일을 실행하고 실행된 모든 명령을 기록한다.

 

GCC-LLVM 변환

컴파일 단계에서 DIFUZE 용으로 만든 GCC-LLVM 명령 변환 유틸리티를 사용하여 실행된 명령 로그를 처리한다. 이 유틸리티는 GCC에 의해 예상되는 형식에서 LLVM 유틸리티에 의해 예상되는 형식으로 명령 행 플래그를 변환하고 LLVM을 통해 커널 소스를 컴파일할 수 있다. 컴파일 과정에서 LLVM은 각 소스 파일에 대한 바이트코드(Bitecode) 파일을 생성한다. 바이트코드 파일에 디버그 정보를 포함시킬 수 있으므로 4.6절에 설명된 구조체 정의를 추출하는 데 도움이 된다.

 

바이트코드 통합

DIFUZE가 수행하는 분석은 각 드라이버에서 별도로 적용된다. 따라서 다양한 바이트코드 파일을 통합하여 드라이버 당 단일 바이트코드 파일을 만든다. 이를 통해 단일 바이트코드 파일에 대한 인터페이스 복구 분석을 수행할 수 있어 분석이 단순화된다. 이 통합 바이트코드 파일은 다음 단계에서 분석을 수행하는 데 사용된다.

 

4.2 ioctl 핸들러 식별

2.1절에서 논의한 바와 같이, 기기 드라이버와의 상호작용의 대부분은 ioctl 인터페이스를 통해 이루어진다. 사용자 공간에서 애플리케이션은 ioctl() 시스템 호출을 호출하여 파일 설명자를 드라이버의 장치 파일, 명령 식별자 및 필요한 구조화된 데이터 인수에 전달한다. 이 시스템 호출이 커널 공간에서 수신되면 해당 드라이버의 ioctl 핸들러가 호출된다. 그럼 다음 핸들러는 명령 식별자에 따라 드라이버 내부의 다른 기능으로 요청을 전송한다. 실행 중인 예제의 경우 ioctl 핸들러 함수는 ioctl_handler이다.

 

유효한 명령 식별자와 추가 ioctl 인수의 구조체 정의를 복구하려면 먼저 DIFUZE가 최상위 ioctl 핸들러를 식별해야 한다. 각 드라이버는 각 장치 파일에 대해 최상위 ioctl 핸들러를 등록할 수 있으며, 리눅스 커널에서 이를 수행하는 몇 가지 방법이 있다. 그러나 이러한 모든 방법에는 구조체 집합 중 하나가 ioctl 핸들러에 대한 함수 포인터가 되어 이러한 목적을 위해 만들어진 구조체 집합 중 하나가 생성된다. 구조의 전체 목록과 커널 중 하나에 해당하는 필드 이름은 부록 A에 있다.

 

ioctl 핸들러를 식별하기 위한 분석은 간단하다 : LLVM의 분석 기능을 사용하여 드라이버에서 이러한 구조체의 모든 사용을 찾아 ioctl 핸들러 함수 포인터의 할당 값을 복구한다. 실행 중인 예제의 경우 file_operations 구조의 unlocked_ioctl 필드에 대한 쓰기를 식별한다.([목록4] 코드 9행) 그런 다음 ioctl_handler 함수를 ioctl 핸들러로 고려할 수 있다.

 

4.3 장치 파일 탐지

ioctl 핸들러에 해당하는 장치 파일을 판별하려면 ioctl 핸들러 등록에 제공된 이름을 식별해야 한다(예: 실행중인 예제에서 장치 파일은 [목록 4] 코드 7행부터 /dev/example_device).

 

장치의 종류에 따라 리눅스 커널에 파일 이름을 등록하는 방법에는 몇 가지 방법이 있다. 예를 들어, 문자 장치를 등록하면 alloc_chrdev_region 메서드를 사용하여 이름을 장치와 연결한다. proc 장치의 경우 proc_create 메서드가 파일 이름을 제공한다. 또한 3.1절에서 언급한 것처럼 장치 유형에 따라 장치 파일이 있는 디렉터리가 다를 수 있다.

 

ioctl 핸들러가 주어진 경우, 해당 장치 이름을 식별하기 위해 다음 절차를 사용한다.

(1) 부록 A에 나열된 운영 구조의 필드 중 하나에 주소를 저장하는 LLVM 저장소 지침을 검색한다.
(2) 등록 기능 중 운영 구조에 대한 참조를 확인한다.
(3) 기기 파일 이름에 대한 인수 값을 분석하여 상수일 경우 반환한다.

 

[목록 4]의 실행 예제의 경우, ioctl 핸들러 함수는 ioctl_handler로 미리 판단한다. ioctl_handler가 코드 9행(1단계)의 file_operations 구조(즉, dirver_ops)에 저장되어 있는지 확인한 다음 코드 10행(2단계)의 cdev_init 함수에 대한 매개 변수로 driver_ops의 사용법을 확인한다. cdev_add 기능은 장치가 문자 장치임을 나타낸다. 코드 7행에서 장치 메타 데이터(alloc_chrdrv_region)에 대한 할당 함수로 돌아가서 세 번째 인자는 장치 이름이고 상수 문자열로 검출하고, /dev/example_device를 장치 이름으로 반환한다.

 

VOS_INT __init RNIC_InitNetCard(VOS_VOID) {
	...
		snprintf(pstDev->name, sizeof(pstDev->name),
			"%s%s",
			RNIC_DEV_NAME_PREFIX,
			g_astRnicManageTbl[ucIndex].pucRnicNetCardName);
	...
}

[목록 5] 화웨이 Honor 기종의 RNIC 드라이버에서 동적으로 생성된 장치 이름. DIFUZE가 드라이버의 장치 이름을 찾지 못함

 

드라이버는 동적으로 생성된 파일 이름을 사용할 수 있다. 불행히도 정적 분석 고유의 제한 사항으로 인해 이러한 파일 이름이 누락되고 수동 분석으로 대체되어야 한다(물론 완전 자동화 상태를 유지하려면 이러한 장치를 무시해도 된다).

 

다음으로, ioctl 핸들러가 승인한 유효한 명령 식별자를 식별하는 작업을 진행한다.

 

4.4 명령 값 결정

ioctl 핸들러가 주어지면 cmd 값(즉, ioctl()의 두 번째 인자)에 대한 모든 동등 제약 조건을 수집하기 위해 정적 절차 간 경로 감지 분석을 수행한다. 그런 다음 범위 분석(Range Analysis)을 사용하여 비교 피연산자의 가능한 값을 복구한다. [목록 3] ioctl 예제의 경우 cmd == 0x1003(10행), cmd == 0x1002(12행), cmd == 0x1001(32~42행)의 제한 조건을 수집한다. 비교 피연산자가 상수이므로 범위 분석을 실행하면 각각 0x1003, 0x1002, 0x1001의 상수가 발생한다.

 

cmd 값에 대한 동등 제한 조건만 고려한다. 거의 모든 드라이버가 등식 비교를 사용하여 유효한 명령 ID를 확인할 수 있다. V4L2 드라이버와 같은 특수한 ioctl 기능이 존재하는데, 드라이버 별 기능은 다른 드라이버에 의해 중첩된 방식으로 호출된다. 부록 B의 이러한 사례에 대한 해결책을 제시한다.

 

4.5 인자 타입 식별

ioctl 명령 식별자와 해당 데이터 구조 정의는 다-대-다 관계를 가지고 있다. 각 ioctl 명령어는 여러 다른 구조(예: 전역 구성을 기반으로 함)를 취할 수 있으며, 각 명령 구조는 여러 ioctl 명령으로 전달될 수 있다. 이러한 구조를 찾기 위해, 먼저 리눅스 커널이 사용자 공간에서 커널 공간으로 데이터를 복사하는 데 사용하는 copy_from_user 함수에 대한 모든 경로를 식별한다(예: [목록 3] 코드 16행 → [목록 2] 코드 3행). 소스 피연산자(즉, copy_from_user의 두 번째 인수)가 ioctl 함수에 전달된 인수가 아닌 호출 사이트(call-site)는 무시한다. 이러한 경우 ioctl 인수 유형을 결정하는 데 도움이 되지 않는다.

 

포인터 형변환은 실제 구조 유형을 숨길 수 있다. [목록 2]의 코드 3행에 있는 copy_from_user가 ioctl 핸들러에서, [목록 3]의 ioctl_handler가 여러 경로(예: 16, 21, 32행 → 41행)에 도달할 수 있는 실행 예제를 고려해본다. 그러나 호출 사이트에서 소스 피연산자의 실제 유형은 void *이다. 또한 copy_from_user 함수는 래퍼 함수에 위치하여 ioctl 함수(예: [목록 3] 코드 16행 → [목록 2] 코드 3행)에 의해 간접적으로 호출될 수 있으며, 이는 다른 함수 또는 파일에 분산된다.

 

이를 처리하기 위해 각 경로에 있는 copy_to_user 함수의 소스 피연산자에 할당될 수 있는 가능한 모든 유형을 결정하기 위해 경로 민감형(path-sensitive type) 간 전파를 수행한다.

 

명령 식별자를 이러한 각 구조 유형에 연결하기 위해 유형 전파(type propagation)를 수행하는 동안 경로에 따라 동등한 제약 조건(4.4절에서 설명)을 수집한다. copy_from_user 함수에 도달하는 경로의 명령 값에 대한 제약 조건은 구조 유형과 관련된 가능한 명령 식별자를 나타낸다.

 

[목록 3]의 실행 예제에서는 먼저 copy_from_user 호출 사이트에 도달하는 모든 경로를 식별한다(실제 호출은 래퍼함수 copy_from_user_wrapper를 통해 발생). [표 1] 2열에는 모든 관련 경로가 표시된다. 간결성을 위해 cmd에 동일한 제약조건을 가지고 있고 같은 호출 사이트에 도달하는 경로를 무시했다.

 

소스 피연산자가 사용자 인수가 아니니 때문에 경로 6은 무시한다(즉, [목록 3]의 코드 49행에서 copy_from_user_wrapper의 두 번째 인수는 argp가 아님). 마지막으로 나머지 경로의 경우, 대상 copy_from_user 호출 사이트의 대상 피연산자 유형을 식별하여 명령 값 유형을 결정한다. 예를 들어, [표 1]의 Path 1에 대해 argp의 유형은 3의 16행에 있는 대상 피연산자 curr_idx와 동일하며 6행에 uint32_t로 정의된다. 각 명령 값에 대해 여러 유형을 얻을 수 있다. 예를 들어, [표 1]과 같이 Path 1과 Path 2는 cmd 제약 조건 값은 같지만 인수 유형은 다르다. 각 명령 값에 대해 가능한 모든 인수 유형을 연결한다. 예를 들어, [표 1]에서 명령 값 0x1003은 인수 유형 uint32_t 및 uint8_t와 연관될 수 있다. 다음으로, 인수의 구조체 정의를 추출할 필요가 있다.

 

Id Path cmd constraints Resolved command id User argument type
1 Line 10 → Line 11 → Line 16 → Line 3 (of Listing 2) cmd == 0x1003 0x1003 uint32_t
2 Line 10 → Line 11 → Line 21 → Line 3 (of Listing 2) cmd == 0x1003 0x1003 uint8_t
3 Line 12 → Line 16 → Line 3 (of Listing 2) cmd == 0x1002 0x1002 uint32_t
4 Line 12 → Line 21 → Line 3 (of Listing 2) cmd == 0x1002 0x1002 uint8_t
5 Line 30 → Line 32 → Line 41 → Line 3 (of Listing 2) cmd == 0x1001 0x1001 DriverStructOne
6 Line 30 → Line 32 → Line 49 → Line 3 (of Listing 2) cmd == 0x1001 0x1001 N/A

[표 1] ioctl 핸들러 [목록 3]에서 copy_from_user 호출 사이트까지의 관련 경로

 

4.6 구조체 정의 찾기

유형의 정의를 찾으려면 해당 유형으로 구성된 모든 유형의 정의를 찾아야 한다. [목록 1]에서 DriverStructOne 유형의 정의를 추출하려면 DriverStructTwo와 DriverSubstructTwo의 정의를 모두 추출해야 한다.

 

4.5절에서 식별된 각 유형에 대해 4.1절에서 계산된 디버그 정보를 사용하여 해당 copy_from_user 함수의 소스 파일 이름을 찾는다. 소스 파일을 알면 GCC-LLVM 파이프라인을 사용하여 해당 전처리 파일을 생성한다. 전처리된 파일에는 필요한 모든 유형의 정의가 포함되어야 하므로, 식별된 유형의 정의를 찾는다. 그런 다음 c2xml 도구를 실행하여 C 구조체 정의를 XML 형식으로 구문 분석하여 유형에 대한 필요한 정의를 추출한다.

 

5. 구조체 생성

DIFUZE는 ioctl 인터페이스를 복구한 후 on-device 실행 엔진에 전달할 구조체 인스턴스를 생성하기 시작할 수 있다. 이에 대한 절차는 다음과 같다. DIFUZE는 구조를 인스턴스화하고, 해당 필드를 랜덤 데이터로 채우고, 포인터를 적절하게 설정하여 ioctl에 복잡한 입력을 만든다.

 

Type-Specific Value Creation

특정 값은 다른 값보다 코드 적용 범위를 증가시킬 가능성이 더 높다. 예를 들어, 시스템 코드의 버퍼 길이는 비트 경계(즉, 크기가 128, 256 등의 버퍼)에 정렬되는 경우가 많으므로 비트 경계 바로 아래 또는 비트 경계에 있는 값은 코너 케이스를 트리거할 가능성이 더 높다(예: 부주의한 문자열 종단에 의한 단일 바이트 덮어쓰기). 이러한 점은 퍼징 커뮤니티에서 일반적인 정보이며, 이전 연구에서 널리 사용되어 있다. DIFUZE는 이 개념도 활용하며, 생성된 정수에서 2의 거듭제곱, 2의 거듭제곱보다 작거나 2의 거듭제곱보다 큰 정수를 선호한다(그러나 제한하지는 않는다).

구조화되지 않는 데이터(예: char * 포인터)나 구조체 정의를 복구할 수 없는 데이터(void *data)를 가리키는 일부 포인터가 있다. 이 데이터에 대해 DIFUZE는 랜덤 컨텐츠 페이지를 할당한다.

 

Sub-structure Generation

ioctl에 대한 입력은 종종 중첩된 구조의 형태를 취하는데, 여기서 최상위 구조에는 다른 구조체에 대한 포인터가 포함된다. DIFUZE는 이러한 구조체 인스턴스를 개별적으로 생성하여 on-device 실행 컴포넌트로 보낸다. 다음 단계에서 이러한 컴포넌트는 ioctl 자체로 전달하기 전에 중첩된 구조체로 병합한다.

 

6. ON-DEVICE 실행

DIFUZE의 이전 단계가 분석 호스트에서 실행되는 동안 ioctl의 실제 실행은 대상 호스트에서 이루어져야 한다. 이와 같이, 구조체 생성 컴포넌트는 대상 장치 드라이버 파일 이름 및 ioctl 명령 식별자와 함께 생성된 구조를 장치 내 실행 컴포넌트로 전달한다. 그런 다음 컴포넌트는 이러한 구조를 마무리하고 ioctl을 트리거한다.

 

6.1 포인터 고정

일부 구조는 포인터로 연결된 여러 개의 메모리 영역을 포함한다. 공간을 절약하기 위해 구조 생성 컴포넌트는 서로 다른 메모리 영역 인스턴스를 개별적으로 전송하는 방법에 대한 메타 데이터와 함께 전송하며, on-device 실행 컴포넌트는 데이터를 사용하여 전체 구조를 구축한다. 이렇게 하면 서로 다른 빌드 구조에 동일한 데이터를 사용할 수 있기 때문에 분석 호스트와 대상 호스트 간의 대역폭을 보존할 수 있다. 예를 들어, 트리 구조의 개별 노드는 개별적으로 전송되기 때문에, 이러한 노드를 사용하여 트리 구조의 다양한 최종 구성을 만들 수 있다.

 

일부 구조체는 재귀적이다. 예를 들어, 링크된 리스트 노드는 다음 링크된 리스트 노드에 대한 포인터를 포함할 수 있다. DIFUZE는 on-device 실행 컴포넌트가 작성하려고 시도하는 구조 조합의 수에 대한 제한을 설정하기 위해 이러한 주고의 재귀를 설정된 임계 값으로 제한한다.

 

6.2 실행

구조체가 생성되면 DIFUZE의 on-device 실행 컴포넌트는 적절한 장치 파일을 열고 ioctl 명령 식별자와 적절한 데이터 구조로 ioctl 시스템 호출을 트리거한다. 이때 DIFUZE는 대상 장치를 크래시시키는 커널의 모든 버그를 감시한다. 분석 호스트와 대상 호스트 간에 heartbeat 신호를 유지하면 된다. DIFUZE는 버그를 발견하면, 나중에 재생산 및 트리지를 위해 호스트 장치로 전송된 일련의 입력들을 기록한다.

 

시스템 재시작되는 버그가 트리거되면 대상 호스트가 일관되지 않은 상태이거나 크래시난 것이다. 전자의 경우, on-device 실행 컴포넌트는 다른 ioctl 명령과 다른 드라이버에서 퍼징을 다시 시작하기 전에 장치의 재부팅을 트리거한다. 후자의 경우, 크래시가 발생한 방식에 따라 장치가 자체적으로 다시 시작되는 경우가 있다. 그렇게 되면 DIFUZE는 분석가가 상호 작용 없이 다시 시작할 수 있다. 그렇지 않으면 분석가가 퍼징을 다시 시작되기 전에 장치를 재부팅해야 한다.

 

7. 구현

[그림 1]과 같이 시스템을 완전히 자동화하도록 설계했다. 사용자는 컴파일 가능한 커널 소스 아카이브를 제공하고, 대상 호스트(즉, 스마트폰)를 분석 호스트에 연결하고, 대상 호스트에서 장치 실행 컨포넌트를 시작한다. 그런 다음 한 번의 명령으로 전체 파이프라인이 실행된다.

 

Interface Extraction

LLVM 3.8을 사용하여 인터페이스 추출 기법을 구현했다. 인터페이스 추출의 모든 컴포넌트는 개별 LLVM 패스로 구현된다. 4.4절에서 언급했듯이, 유효한 명령 식별자를 복구하기 위해 범위 분석의 기존 구현을 사용했다.

 

7.1 인터페이스 인식 퍼징

5절과 6절의 구현은 MangoFuzz라고 한다. MangoFuzz는 분석 호스트에서 구조체 생성과 ioctl의 장치에서 실행을 결합하여 인터페이스 인식 퍼징을 달성한다. 결과에 영향을 줄 수 있는 다른 최적화 없이 인터페이스 인식 퍼징의 효과를 테스트하도록 설계된 의도적으로 단순한 프로토타입이다.

 

MangoFuzz는 실제 안드로이드 장치의 ioctl 시스템 호출을 목표로 한다. 5절에 설명된 방법을 사용하여 관련 구조와 함께 무작위 시퀀스를 생성하고 이를 대상 호스트에서 실행되는 장치 실행 컨포넌트로 전송한다.

 

"production-ready”" 접근 방식의 변형을 위해 DIFUZE를 최신 리눅스 시스템 call 퍼저인 syzkaller에 통합했다. 이 통합은 가능한 최고의 도구를 만드는 것을 목표로 하고 있는데, syzkaller의 오픈 소스 강화로서 커뮤니티에 기여할 것이다.

 

Syzkaller는 리눅스 시스템 호출 퍼저로, 분석가들이 시스템 호출 설명을(수동으로) 제공한 후 관련 시스템 호출을 퍼지한다. Syzkaller는 해당 형식을 수동으로 지정할 경우 시스템 호출 인수로 구조체를 처리할 수 있다. DIFUZE와 통합하기 위해 인터페이스 복구 단계의 결과를 syzkaller가 예상 한 형식으로 자동 변환하여 인터페이스를 인식합니다. Syzkaller는 일반적으로 커버리지 정보와 KASAN(또는 다른 메모리 액세스 검사기)으로 컴파일된 커널에 사용된다. 그러나 실제 수정되지 않은 안드로이드 기기에서 실행하기 위한 구성이 있는데, 이것은 목적에 따라 사용될 수 있다.

 

8. 평가

DIFUZE의 효과를 확인하기 위해 인터페이스 복구 및 버그 찾기 기능을 모두 평가한다. 평가는 가장 인기 있는 5개 업체의 7개 안드로이드 기기에서 실시되며, 광범위한 장치 드라이버를 포함한다. [표 2]는 칩셋 공급 업체와 함께 특정 스마트폰을 보여준다.

 

첫째, 인터페이스 복구가 시스템의 핵심 요소인 만큼, 인터페이스 복구의 효과와 효율성을 평가한다. 결과를 검증하기 위해 ioctl와 구조체의 랜덤 샘플링을 수동으로 분석하여 시스템의 복구된 인터페이스와 비교하여 확인한다. 그런 다음 MangoFuzz와 syzkaller의 개선 사항을 모두 사용하여 DIFUZE의 버그 찾기 기능에 대한 비교 평가를 수행한다.

 

8.1 인터페이스 추출 평가

인터페이스 추출의 모든 단계는 Ubuntu 16.04.2 LTS, Intel Xeon CPU E5-2690 (3.00 GHz)이 장착된 시스템에서 시연했다. 커널의 전체 인터페이스 추출 단계를 완료하는데 평균 55.74분이 걸렸다.

 

[표 2]에 나열된 장치의 커널에 대한 인터페이스 추출의 여러 단계의 효과를 평가한다. [표 3]은 다른 커널에서의 인터페이스 추출 결과를 보여준다. DIFUZE는 7개 장치의 커널에서 총 789개의 ioctl 핸들러를 식별했다. 핸들러의 수도 해당 스마트폰의 드라이버 수와 밀접하게 일치한다.

 

Device Vendor Chopset Vendor Android Kernel Version
Pixel Google Qualcomm 3.18
E9 Plus HTC Mediatek 3.10
M9 HTC Qualcomm 3.10
P9 Lite Huawei Huawei 3.10
Honor 8 Huawei Huawei 4.1
Galaxy S6 Samsung Samsung 3.10
Xperia XA Sony Mediatek 3.18

[표 2] 평가에 사용된 안드로이드 스마트폰(커널 버전은 실험 당시 각 스마트폰의 최신 안드로이드 커널 사용)

 

[그림 2] 유효한 명령 식별자 수에 대한 ioctl 핸들러 백분율의 CDF

 

Device Name Identification

장치 이름 식별에 대한 접근 방식(4.3절)은 다른 공급 업체별 장치에서 작동할 수 있다. DIFUZE는 ioctl 핸들러의 59.44%를 차지하는 469개의 장치 이름을 자동으로 식별할 수 있다. 대부분의 식별 실패는 커널 기본 라인 드라이버에서 발생한다. 예를 들어, Xperia XA의 공급 업체 드라이버에서만 이름을 복구하면 이름의 90% 이상을 복구할 수 있다. 이러한 불일치한 이유는 기본 드라이버가 동적으로 생성된 이름(5절 및 4.3절)을 사용하는 반면, 공급 업체 드라이버는 정적 이름을 사용하는 경향이 있기 때문이다. 동적으로 생성된 이름은 수동으로 추출했다.

Valid Command Identifiers

[표 3]의 네 번째 열에는 해당 커널의 모든 진입점에서 추출된 유효한 명령 식별자의 수가 표시된다. DIFUZE는 모든 커널의 모든 드라이버에서 3,565개의 유효한 명령 식별자를 발견했다. 유효한 명령 식별자의 수는 커널마다 상당히 다르다. [표 3]과 [표 5]에서 볼 수 있듯이, 퍼저가 발견한 크래시 수는 유효한 명령 식별자 수와 양의 상관관계가 있다.

 

  ioctl handlers Device Names Automatically Identified Valid Command Identifiers User Argument Types
no cpoy_from_user Scalar Struct Struct with pointers
Pixel 193 136 611 270 87 151 103
E9 Plus 77 36 610 272 101 195 42
M9 171 122 563 216 83 149 115
P9 Lite 71 30 384 187 56 118 23
Honor 8 86 33 376 208 70 87 11
Galaxy S6 106 70 364 243 23 67 31
Xperia 85 42 657 292 106 194 65
Total 789 469 3,565 1,688 526 961 390

[표 3] 여러 스마트폰 커널에서 DIFUZE가 복구한 인터페이스

 

[그림 2]는 ioctl 핸들러 당 유효한 명령 식별자 수의 분포를 보여준다. ioctl 핸들러의 11%는 어떠한 명령도 기대하지 않는다. 이 ioctl의 코드는 조건부로 컴파일되고 커널 구성에 의해 보호된다. 컴파일하는 동안 ioctl 핸들러 코드가 비활성화되므로 생성된 바이트코드 파일에서 해당 ioctl 핸들러가 비어있는 것으로 나타나서 명령 식별 프로세스에서 명령 값이 0이 된다. ioctl 핸들러의 50%가 단일 명령 식별자로 예상한다. 대부분 v4l2_ioctl_ops에 기인한다. [부록 B]에서 설명했듯이 이들은(단일) 특정 명령을 관리하는 중첩 처리기이다. ioctl 핸들러의 대다수(98.3%)는 유효한 명령 식별자가 20개 미만이다. 20개 이상의 명령 식별자를 가진 ioctl의 나머지(1.7%)를 수동으로 조사하여 ioctl 함수의 일부에 대한 함수 포인터에 근접하는 접근 방식을 알아냈다. 그러한 과도한 추정은 후속 퍼징 단계에서 유효하지 않은 퍼지 단위를 추가로 발생시키지만 전체 성능에 미미한 영향을 미친다.

 

User argument types

[표 3]의 마지막 4열은 사용자가 전달한 인수(ioctl 핸들러에 대한 세 번째 인수)가 처리되는 방식을 보여준다.

 

1,688개(47%)의 명령 식별자에 대해 copy_from_user를 찾을 수 없다. 이것은 두 가지 범주 중 하나에 넣는다.

(1) 사용자 인수는 C 유형으로 취급되므로 사용자 인수는 원시 값으로 취급되므로 user_from_user가 없으므로 인수 유형 식별이 필요하지 않다.

(2) 또는, copy_to_user가 있는데, 여기서 사용자는 커널이 사용자에게 정보를 복사할 특정 유형의 포인터를 제공하게 된다.

커널은 사용자 데이터를 처리하지 않기 때문에 여기서도 유형 식별에 신경 쓰지 않는다.

 

명령 식별자의 나머지 1,877(53%)에 대해 사용자 인수는 특정 데이터 유형에 대한 포인터가 될 것으로 예상된다. 즉, 데이터를 복사하기 위해 copy_from_user 호출을 사용해야 한다. 이러한 포인터 인수는 다음과 같은 세 가지 사례로 추가 분류할 수 있다.

 

(1) 명령 식별자의 526(15%)은 스칼라 포인터 인수를 예상한다. 예를 들어, [표 1]에서 표시한 바와 같이, 실행 사례에서 스칼라 타입 uint32_t 또는 uint8_t를 가리키는 사용자 인수를 예상하기 때문에 명령 ID 0x1003 및 0x1002는 범주에 속한다.

(2) 명령 식별자의 961(30%)은 사용자 인수가 내장된 포인터가 없는 C 구조체를 가리킬 것으로 예상한다. 예를 들어, [목록 1]의 DriverStructTwo.

(3) 명령 식별자의 390(11%)에 대해 날짜 유형은 내장된 포인터를 포함하는 C 구조체이다. 실행 예제의 경우 [표 1]에 표시된 것처럼 명령 ID 0x1001이 범주에 속하며 사용자 인수가 포함된 포인터([목록 1])를 포함하는 DriverStructOne을 가리킬 것으로 예상한다.

 

이러한 인수는 사용자 인수가 구조체를 가리키고, 그 자체에는 포인터(유효한 포인터도 포함되어야 함)가 있을 것으로 예상되기 때문에 인수 유형 정보 없이 효과적으로 퍼지하기 매우 어렵다. 다음 3가지 사례로 분류된다.

 

Random Sampling Verification

테스트 세트에서 7개의 안드로이드 스마트폰 각각에 대해 5개의 ioctl의 랜덤으로 추출하여 추출된 유형이 올바른지 수동으로 검증했다. 35개의 ioctl에는 총 327개의 명령을 가지고 있었는데, 그중 294개의 인수와 명령을 정확히 파악하여 90%의 정확도를 제공했다.

 

8.2 평가 기준

DIFUZE가 장치 드라이버에서 실제 버그를 얼마나 잘 찾을 수 있는지, 그리고 추출된 인터페이스 정보를 이 기능에 미치는 영향을 확인하기 위해 프로토타입 퍼저, MangoFuzz, syzkaller를 사용하여 테스트한다. DIFUZE(m) 및 DIFUZE(s) 식별자를 사용하여 퍼징 및 on-device 실행 구성 요소로 MangoFuzz 및 syzkaller를 각각 사용하는 경우 DIFUZE를 나타낼 것이다. 또한, syzkaller에서 제공하는 인터페이스의 양을 변경하여 시스템을 평가합니다. 이런 식으로, 인터페이스 추출이 결과에 어떤 영향을 미치는지 조사할 수 있다. 구체적으로 DIFUZE의 다음과 같은 구성을 실행한다.

 

Syzkaller base

syzkaller는 시스템 호출 open() (장치 파일 열기) 및 ioctl (ioctl 핸들러를 트리거하기 위해)을 사용하여 퍼지만 사용해야 한다고 지정한다. 기본 구성은 몇 개의 표준 장치 파일 이름과 일반적인 리눅스 장치용 ioctls와 함께 일부 표준 유형의 구조를 포함한다.

 

Syzkaller+path

이 구성에서는 syzkaller가 퍼징 하려고 하는 추출된 드라이버 경로의 사양을 추가한다. 그러나 나머지 인터페이스 정보는 제공되지 않는다.

 

DIFUZE(i)

여기서 장치 경로와 ioctls의 추출된 인터페이스 정보는 syzkaller와 함께 퍼지로 사용된다. 이 구성은 ioctl 명령 처리를 트리거할 수 있지만, 복잡한 구조를 처리하는 코드를 탐색할 수는 없다.

 

DIFUZE(s)

이 구성은 ioctl 인수 구조 형식의 자동 식별을 포함한 모든 인터페이스 복구와 syzkaller를 통합한다. 이것이 syzkaller에서 발견되는 많은 최적화들을 활용할 수 있기 때문에 최고의 성능의 구성이 될 것으로 기대한다.

 

DIFUZE(m)

최종 구성은 인터페이스 복구와 간단한 퍼저 프로토타입인 MangoFuzz를 통합한다. 이 구성은 다른 좋은 최적화가 없는 경우에도 인터페이스 인식 퍼징이 발견된 버그 수에 미치는 영향을 조사하기 위한 것이다.

 

현재 플래그쉽(flagship) 모델인 구글, 삼성, 소니, HTC를 포함하여 7개의 안드로이드 기기에서 시스템을 평가했다. 각 장치에 대해 먼저 사용 가능한 최신 버전으로 업데이트한 다음 장치를 루팅했다. On-device 구성 요소는 앱 레벨 권한으로 액세스 할 수 있는 드라이버뿐만 아니라 모든 드라이버를 퍼징할 수 있도록 루트에서 실행된다. 그러나 9절에서 논의된 바와 같이, 이 구성 요소는 표준 응용 프로그램의 형태를 취할 수도 있지만, 장치 파일(및 ioctl 핸들러)에 대한 액세스 가능성이 낮아진다. 이 설정에서는 커널을 다시 컴파일하고 컴파일된 커널을 플래시 해야 하므로 코드 커버리지 피드백이나 KASAN을 사용할 수 없다. 위에서 언급한 DIFUZE 구성은 모두 각 안드로이드 기기에서 5시간 동안 실행된다. 단일 드라이버에서 충돌이 자주 발생하는 경우 버그가 있는 ioctl 핸들러를 블랙리스트에 추가하여 스마트폰이 반복적으로 충돌하지 않고 재부팅이 실험에 방해가 되지 않도록 한다.

 

8.3 결과

모든 충돌 로그와 시스템 호출의 충돌 시퀀스를 수집하여 수동으로 분류하여 적은 수의 중복을 필터링했다. DIFUZE는 모두 7개의 안드로이드 기기에서 36개의 고유한 버그를 찾을 수 있었다. 발견된 버그에 대한 개요는 표5에 나와있다.

 

syzkaller가 Galaxy S6에서 작동하도록 할 수 없었고 DIFUZE(m)은 버그를 유발할 수 없었기 때문에 버그가 전혀없는 유일한 안드로이드 장치가 되었다. 다른 모든 기기에서 Xperia XA의 두 가지 취약점(Honor 8의 경우)에서 14가지 취약점을 찾아냈다.

 

syzkaller의 기본 구성(인터페이스 정보 없음)이 테스트에서 버그를 찾지 못했다. 올바른 드라이브 경로(syzkaller + path)를 제공하면 모든 장치에서 세번의 충돌만 발생했다. 이는 맹목적으로 퍼징하는 커널 드라이브가 그다지 효과적이지 않다는 것을 시사하며, 이는 이러한 장치를 구매자한테 배송전에 공급 업체가 테스트를 수행하기 때문일 수 있다.

 

추출된 ioctl 수의 형태로 부분 인터페이스 정보를 추가하면 DIFUZE(i)는 22개의 버그를 찾을 수 있다. 이 자체로는 인상적이지만, 인터페이스에 남아 있는 인터페이스 정보(ioctl 인수 구조체 정의)를 추가하면 발견된 버그 수가 54.5%나 증가해 총 34개 버그로 늘어났다. 이 결과는 인터페이스 인식 퍼징의 효과를 보여주며, 복구된 ioctl 명령 식별자와 ioctl 핸들러 분석에 대한 구조 정보의 중요성을 보여준다.

 

실험에서 특히 흥미로운 결과는 DIFUZE(m)이 DIFUZE보다 버그를 4개만 발견한 것이다. Syzkaller는 수많은 퍼징 전략과 최적화 기능이 내장된 새로운 도구인 반면, MangoFuzz는 6절에 설명된 것 외에 최적화가 없는 간단한 퍼징 프로토타입이다. 이것이 정확한 인터페이스 정보를 통한 퍼징하는 것이 매우 강력하다는 것을 보여준다.

 

각 충돌을 간단히 분류하고 장치 충돌 원인을 신속하게 분류했다. 이 결과는 [표 4]에 나와 있다. 충돌 자체가 양성으로 보이는 경우에는 종종 심각한 버그이다. 예를 들어, 악의적인 사용자가 보다 강력한 원시성을 얻기 위해 조심스럽게 조작할 수 있는 더 심각한 기본 버그에 의해 유발될 수 있다. 여기서 더해, 발견된 흥미로운 버그 중 하나는 우리가 직면한 대부분의 주장들을 우회할 수 있다는 것이다. 이러한 점검을 우회할 수 있게 됨으로써 많은 공격을 받을 수 있는 시나리오가 현실화될 수 있다. 결과의 심각성을 입증하기 위해 임의의 쓰기 취약점 중 하나를 이용하여 커널에서 코드를 실행하고 앱 레벨 권한에서 루트로 에스컬레이션했다.

 

현재 취약점을 공급 업체에 책임있게 공개하기 위해 노력하고 있다. 그렇게 하는 동안, 실험 과정에서 4개의 버그가 패치된 것을 발견했다. 아는 한 36개의 버그 중 나머지 32개는 제로데이이다.

 

다음 몇 개의 하위세션에서는 실험 중에 발견된 2개의 버그에 대한 연구 사례를 제시하여 버그들의 영향과 검출에서 인터페이스 정보의 필요성을 증명할 것이다.

 

Crasg Type Count
Arbitrary read 4
Arbitrary write 4
Assert Failure 6
Buffer Overflow 2
Null dereference 9
Out of bounds index 5
Uncategorized 5
Writing to non-volatile memory 1

[표 4] DIFUZE가 발견한 충돌 유형

 

8.4 연구 사례 1 : Honor 8 설계 이슈

가장 흥미로운 버그 중 하나는 (커널 버그는 일반적인 경우처럼) OS 충돌을 통해서가 아니라 대상 호스트로부터 매우 이상한 동작을 발견되었다. 화웨이 Honor 8에서 여러 번의 퍼징 라운드를 마친 후, 장치의 일변 번호가 변경되었다는 것을 발견했다([목록 6] 참조). 장치의 일련 번호는 (높은 EL3 권한 수준에서 실행되는) 부트 로더만 변경할 수 있는 읽기 전송 속성이어야 한다. 그러나 이 발생은 커널 드라이버를 이용하여 안드로이드의 사용자 공간 애플리케이션(최소 권한 레벨 EL0에서 실행)에서 실제로 일련 번호를 변경할 수 있음을 보여준다. 따라서 이것은 설계 수준의 취약점을 나타낸다.

 

이 버그는 드라이버 nve를 퍼지하면서 발견되었다. Honor 8은 flash, nvme에 파티션이 있어 장치 구성 정보를 저장한다. 이러한 구성 옵션 중 일부는 권한이 없으며 안드로이드에 의해 수정할 수 있다. 여기에는 기기 잠금 해체가 활성화되는지 여부와 램 덤프가 허용 여부도 포함되지만, 특히 보드 식별자 및 일변 번호와 같은 속성은 제외되며, 이는 부트 로더만 수정할 수 있어야 한다. 그러나 장치 /dev/nve의 ioctl 핸들러는 이러한 옵션을 읽고 쓸 수 있는 방법을 제공한다. 또한, 구성 옵션 유형을 확인하지 않으며 악의적인 사용자 공간 응용 프로그램은 권한 있는 구성 옵션을 읽거나 쓸 수 있다.

 

권한 구성 옵션의 대한 수정을 허용하지 않는 검사를 추가하면 이 문제가 해결될 수 있다. 권한 수준 EL1에서 실행 중인 안드로이드 커널이 더 높은 권한으로 실행 중인 부트 로더에 속하는 옵션을 읽거나 쓰는 것이 가능하지 않아야 한다. 물론, 이를 위한 해결책은 특권(privileged) 옵션과 특권이 없는 옵션을 분리하여 서로 다른 특권 코드가 접근할 수 있는 다른 파티션에 저장하는 것이다.

 

  syzkaller base syzkaller+path DIFUZE(i) DIFUZE(s) DIFUZE(m) total unique
Pixel 0 1 2 5 3 5
E9 Plus 0 0 4 6 6 6
M9 0 0 3 3 2 3
P9 Lite 0 0 2 5 5 6
Honor 8 0 0 1 2 2 2
Galaxy S6 - - - - 0 0
Xperia XA 0 2 10 13 12 14
Total 0 3 22 34 30 36

[표 5] 장치 별 퍼징 구성에서 발견된 버그

 

# before fuzzing 
HWFRD:/ $ getprop ro.serialno 
RNV0216811001641 
# after fuzzing 
HWFRD:/ $ getprop ro.serialno 
^RDO>l

[목록 6] nve 드라이버를 퍼징하는 동안 DIFUZE에서 발견된 설계 문제

 

8.5 연구 사례 2 : qseecom 버그

이번 절에서는 최고 수준의 인터페이스 추출(즉, 유형 복구/복합 구조 인스턴스화)에서만 발견된 버그의 예를 살펴본다. 관련 출처는 아래 제시되어 있으며, 이를 참조할 것이다.

 

static int qseecom_mdtp_cipher_dip(void __user* argp)
{
    struct qseecom_mdtp_cipher_dip_req req;
    u32 tzbuflenin, tzbuflenout;
    char* tzbufin = NULL, * tzbufout = NULL;
    int ret;
    do {
        ret = copy_from_user(&req, argp, sizeof(req));
        if (ret) {
            pr_err("copy_from_user failed, ret= %d\n",
                ret);
            break;
        }
        ...
            /* Copy the input buffer from
            userspace to kernel space */
            tzbuflenin = PAGE_ALIGN(req.in_buf_size);
        tzbufin = kzalloc(tzbuflenin, GFP_KERNEL);
        if (!tzbufin) {
            pr_err("error allocating in buffer\n");
            ret = -ENOMEM;
            break;
        }

        ret = copy_from_user(tzbufin, req.in_buf,
            req.in_buf_size);
        ...
    } while (0);
        ...
            return ret;
}

long qseecom_ioctl(struct file* file, unsigned cmd,
    unsigned long arg)
{
    int ret = 0;
    void __user* argp = (void __user*) arg;
    switch (cmd) {
        ...
    case QSEECOM_IOCTL_MDTP_CIPHER_DIP_REQ: {
            ...
                ret = qseecom_mdtp_cipher_dip(argp);
            break;
        }
        ...
    }
    return ret;
}

 

예로 CVE-2017-0612(이것은 실험 과정에서 패치된 4개의 버그 중 하나이다)의 버그는 구글의 대표 안드로이드 스마트폰인 픽셀에서 DIFUZE에 의해 발견되었다. 드라이버의 ioctl 기능은 코드 31행에서 시작하여 ioctl의 일반적인 설계를 따른다. 사용자 공간 응용 프로그램은 cmd 및 arg를 지정한다.

 

cmd QSECOM_IOCTL_MDTP_CIP_DIP_REQ가 있으면 코드 39행에 qseecom_mdtp_cipher_dip를 입력한다. 이 함수의 코드 9행에서 사용자 데이터가 구조체 Qseecom_mdtp_cipher_dip_req req에 복사된 것을 볼 수 있고, 코드 16번 줄에는 버그가 있다. tzbuflenin은 사용자 제어 값인 req.in_buf_size에 PAGE_ALING을 호출하여 계산한다. 사용자 공간 애플리케이션이 여기에 큰 값을 제공하면 PAGE_ALING이 오버플로우되어 req.in_buf_size보다 작은 값이 되며, 특히 0이 된다. 다음으로 코드 17번 라인에서 계산된 크기를 조정하려고 시도한다. 마지막 코드 24번 라인에서 드라이버는 구조체의 내장된 포인터를 할당된 버퍼에 copy_from_user하려고 한다. 이 copy_from_user는 버퍼 크기가 잘못 계산되었기 때문에 충돌이 발생할 것이다. 그러나 이 출동을 관찰하려면 사용자가 제공한 req.in_messages가 유효한 포인터여야 한다(이러한 copy_from_user는 정상적으로 실패하고 오류를 반환함).

 

8.6 커버리지 기반 퍼징 보강

커버지리 기반 퍼징(Coverage Guided Fuzzing)은 잘 연구된 기술이며, 좋은 커버리지를 달성하기 위한 효과적인 방법임이 입증되었다. 그러므로, 커버리지 가이드라인을 사용할 수 있다면 인터페이스 인식이 여전히 필요한가?라는 자연스러운 질문이 발생한다. 정답은 그렇다이다. 커버리지 안내 퍼징을 위한 인터페이스 정보를 제공하면 드라이버에 대한 성능이 크게 향상된다.

 

시연을 위해 x86-64 커널에서 syzkaller를 커버리지 기반 모드로 실행했고, ioctl SCSI_IOCTL_SEND_CORM(단순 인터페이스 포함)과 CDROM_SEND_PACKET(복잡한 인터페이스 포함)는 조합당 4시간 동안 구조 인터페이스 정보를 포함하거나 포함하지 않았다. [표 6]은 이러한 조합의 결과를 보여주며, 여기서 마지막 열은 인터페이스 정보가 제공되었을때 도달한 기본 블록의 증가율을 보여준다. 이는 인터페이스 정보가 여전히 커버리지 유도 퍼지 성능을 크게 향상시킬 것임을 보여준다.

 

kcomv를 역포팅하고 커널을 다시 플래시해야하기 때문에 이 평가를 상용장치로 확장하는 것은 어렵다. 이를 위해서는 프로젝트의 범위를 벗어난 상당한 엔지니어링 노력이 필요하다.

 

ioctl cmd ID Interface type Basic Blocks covered Percentage increase
No interface Interface Aware
SCSI_SCSI_IOCTL_SEND_COMMAND Simple Structure 3811 4629 21.46%
CDROM_SEND_PACKET Complex Structure 3956 5582 41.10%

[표 6] 인터페이스 정보 유무에 따른 커버리지 유도 퍼징 성능

 

9. 토론

인터페이스 인식 퍼지를 사용하면 DIFUZE가 잠재적으로 유해한 버그를 발견하여 커널 보안을 향상시킬 수 있음을 보여주었다. 그러나, 이러한 접근 방식과 개선 방향에는 아직 약점이 있는데, 이번 절에서 검토하기로 한다.

 

9.1 약점

퍼징하는 동안 발견한 1가지 문제는 버그가 있는 드라이버가 일찍부터 충돌하여 퍼저가 드라이버의 더 깊은 기능을 탐색하지 못하게 한다는 것이다. 단순히 이전 버그가 자주 발생하기 때문에 맞지 않는 버그가 있을 수 있으며 해당 버그를 칠 때마다 스마트폰이 재부팅된다. 현재의 기술로 유일한 해결책은 특정 명령 식별자 또는 때로는 전체 ioctl 처리기의 퍼징을 중지하고 다른 것으로 이동하는 것이다.

 

DIFUZE의 또 다른 약점은 인터페이스에서 구조체 사이의 복잡한 관계를 추출할 수 없다는 것이다. 구조체의 한 필드가 다른 필드와 관련되는 것은 드문 일이 아니다. 예를 들어, 길이 필드는 버퍼의 크기를 지정할 수 있다.

 

9.2 향후 연구

많은 최고의 퍼저에서 발견되는 유용한 퍼징 기법은 런타임 범위를 사용하여 퍼저를 사용하는 것이다. 현재 이 기법은 사용하지 않는다(8.6절에서 보여주듯이, 인터페이스 인식으로 커버리지 유도 기법을 크게 개선할 수 있다). 런타임 커버리지 정보를 사용하려면 커널을 장치에 다시 컴파일하고 플래시해야 하는데, 이는 몇 가지 과제를 제시한다. 첫째, 세분화된 커버리지 정보를 얻기 위해서는 개발 보드가 필요하다. 이것은 비쌀 수도 있고, 많은 경우에 실제 기기에서는 사용할 수 없을 수도 있다. 둘째, 재컴파일한 최신 커널 소스를 항상 찾을 수 있는 것은 아니다. 이는 사소한 커널 업데이트 간에 ioctl 인터페이스가 급격하게 변경될 가능성이 낮고, 대상 호스트의 최신 소프트웨어 버전에서도 실제 실행이 수행될 것이기 때문에 DIFUZE는 허용된다. 그러나 이전 커널을 대상 호스트에 플래시하면 결과적으로 발견된 버그는 이미 쓸모없게 될 수 있다. 마지막으로, 일부 공급 업체는 부트로더를 잠그고 다른 보안 검사를 수행하여 새로운 커널을 쉽게 플래시하지 못하게 한다.

 

이러한 이유로 코드 커버리지 측정 또른 KASAN(Kernel Address Sanitizer)을 삽입하기 위해 커널을 계측하지 않았다. KASAN 및 커버리지 정보는 DIFUZE의 결과를 더욱 향상시킬 수 있다. KASAN은 메모리 손상을 감지하고 어설션(Assertion) 오류를 발생시킴으로써 버그를 찾는 데 도움을 준다. 단지 손상된 메모리가 다른 기능에서 사용되지 않거나 중요한 데이터가 손상되지 않았기 때문에 장치가 충돌하지 않고 공격 가능한 버그가 트리거될 수 있다. 커버리지 정보는 이전에 무시되었던 드라이버 기능을 트리거하는 입력을 변경하려고 시도하므로 드라이버에 대한 심층 탐색을 가능하게 함으로써 시스템을 개선할 수 있다.

 

10. 결론

본 논문에서는 리눅스 커널 드라이버와 같은 인터페이스에 민감한 코드에 대한 자동 분석의 효과를 높이기 위해 인터페이스 인식 퍼징을 제안했다. 코드를 퍼징하기 위해 ioctl 인터페이스 사양을 복구하는 일련의 기술을 제공했다. 단일 명령으로 커널 소스 아카이브에서 직접 작동하는 자동화된 파이프라인에서 모든 기술을 구현했다. 기술의 대부분의 드라이버를 위한 인터페이스의 구성요소, 장치 파일 이름, 유효한 명령 식별자, 해당 인수 유형을 복구하는데 효율적이고 효과적이라는 것을 보여준다. 안드로이드 스마트폰의 7개 모델에 대해 여러 가지 다른 DIFUZE 구성을 사용하여 철저한 평가를 수행하여 인터페이스 인식 퍼저의 구현이 효과적이라는 것을 입증하며, 이 중 32개는 이전에 알려지지 않았던 취약점이다.

 

DIFUZE는 오픈 소스 커뮤니티에서 최신 모바일 기기의 안정성을 보장하는 도구를 제공하고 있다.

 

부록 A. IOCTL 등록 구조

리눅스 커널 드라이버가 ioctl 핸들러를 등록하는 데 사용할 수 있는 몇 가지 구조가 있다. [목록 7]은 화웨이 P9에서 실행되는 커널의 구조 목록이다.

 

struct.media_file_operations
struct.video_device
struct.v4l2_file_operations
struct.block_device_operations
struct.tty_operations
struct.posix_clock_operations
struct.security_operations
struct.file_operations
struct.v4l2_subdev_core_ops
struct.snd_pcm_ops
struct.snd_hwdep_ops
struct.snd_info_entry_ops
struct.adf_obj_ops
struct.net_device_ops
struct.kvm_device_ops
struct.ide_disk_ops
struct.ide_ioctl_devset
struct.hdlcdrv_ops
struct.uart_ops
struct.fb_ops
struct.proto_ops
struct.tty_ldisc_ops
struct.watchdog_ops
struct.atmdev_ops
struct.atmphy_ops
struct.atm_ioctl
struct.vfio_device_ops
struct.vfio_iommu_driver_ops
struct.rtc_class_ops
struct.usb_gadget_ops
struct.ppp_channel_ops
struct.cdrom_device_info
struct.cdrom_device_ops

[목록 7] ioctl 핸들러를 등록하는 데 사용할 수 있는 구조체 목록

 

부록 B. V4L2 드라이버

// v4l2_ioctl_ops initialized with required functions.
static const struct v4l2_ioctl_ops iris_ioctl_ops = {
.vidioc_querycap = iris_vidioc_querycap,
.vidioc_s_tuner = iris_vidioc_s_tuner
}
static const struct v4l2_file_operations iris_fops = {
    // here video_ioctl2, implemented by kernel
    // is the main ioctl handler.
     .unlocked_ioctl = video_ioctl2
};

static struct video_device iris_viddev_template = {
    //initialize file operations.
    .fops = &iris_fops,
    // initialize ioctl operations.
    .ioctl_ops = &iris_ioctl_ops
};

static int __init driver_init() {
    struct iris_device* radio;
    int radio_nr = -1;
    radio = kzalloc(sizeof(struct iris_device), GFP_KERNEL);
    if (!radio) {
        FMDERR(": Could not allocate radio device\n");
        return -ENOMEM;
    }
    // copy the video_device structure.
    memcpy(radio->videodev, &iris_viddev_template,
        sizeof(iris_viddev_template));
    // register the v4l2 device
    video_register_device(radio->videodev, VFL_TYPE_RADIO, radio_nr);
}

[목록 8] v4l2_ioctl_ops 초기화 및 v4l2 장치 등록의 예

 

vidioc_querycap:2154321408
vidioc_s_tuner:1079268894

[목록 9] <function name>:<command id> 형식의 항목을 포함하는 v4l2-function-mapping 예제

 

References

https://github.com/ucsb-seclab/difuze
http://swtv.kaist.ac.kr/publications/KSC2019pdf

'Fuzzing' 카테고리의 다른 글

DIFUZE Build  (0) 2020.05.29
WinAFL Build (Windows 10, Visual Studio 2019)  (0) 2020.01.18