|
Fuzzing은 보안 취약점을 찾는데 아주 효과적인 테스트 방법입니다. 그런데 Library를 대상으로 Fuzzing을 적용할 때 추가적으로 생각해야 하는 것들이 Library Fuzzing 작업을 어렵게 만듭니다. 이 글을 통해 Library Fuzzing의 어려움이 무엇이고 어떻게 해결할 수 있을지 이해할 수 있습니다. |
들어가며
안녕하세요. Samsung Research Security Assurance Lab의 이하윤입니다.
Fuzzing은 소프트웨어 및 사이버 보안 취약점을 찾기에 아주 유용한 방법입니다. Fuzzing은 소프트웨어에 랜덤값을 입력한 후 그 입력에 대한 동작을 관찰하는 테스트를 반복하다가 소프트웨어가 입력값에 대한 처리가 미흡하여 비정상적인 동작이 발생하면 이를 분석하여 버그나 취약점을 찾는 기술입니다. 프로그래머가 모든 경우의 수를 고려하여 입력을 처리하기 어렵기 때문에 이런 방식의 랜덤 테스트는 사전에 예상하지 못한 버그 및 취약점을 찾는 데 매우 효과적입니다.
Fuzzing은 대체로 실행 가능한 프로그램(e.g. 미디어 플레이어)에 임의의 입력(e.g. 유효하지 않은 영상 파일)을 넣고 실행하여 프로그램을 테스트합니다. 그러나 이런 방식(end-to-end fuzzing)은 외부 입력을 처리하는 프로그램에만 적용할 수 있기에 실행 가능한 형태로 제공되지 않는 코드, 즉 Library를 테스트하기에는 적합하지 않습니다.
Library는 다양한 소프트웨어에서 쉽게 재사용될 수 있도록 특정 기능을 제공하는 코드의 모음입니다. openssl 오픈소스 프로젝트가 대표적인 예입니다. 이런 library 코드 내의 취약점이 있다면, 이 library를 사용하는 모든 소프트웨어에 이 취약점이 전파될 가능성이 있습니다. 실제로 openssl의 취약점인 Heartbleed가 큰 파장을 불러일으키기도 했죠. 앞서 언급한 Fuzzing을 library에도 적용할 수 있다면 library를 사용하는 다른 소프트웨어도 안전하게 만들 수 있어서 library fuzzing은 꼭 필요하다고 할 수 있습니다.
library fuzzing을 위해, fuzz driver 또는 fuzz hardness라고 불리는 구동 코드가 필요합니다. 이는 대상 library의 API를 호출하는 코드와 각 API 호출의 arguments에 랜덤한 입력값을 전달하는 방법을 기술하고 있습니다. fuzz driver는 library의 API에 랜덤 입력을 적절히 전달하는 기능, 반복 실행, 그리고 미리 정의한 이상 동작을 탐지할 수 있는 기능을 수행합니다.
사실 library의 API에 랜덤 입력을 전달하는 것이 가장 중요한데요. 설명으론 간단해 보이지만 fuzz driver를 만들기 위해선 library API 사용법은 물론, Fuzzing에 대한 이해도 필요하기 때문에 library fuzzing은 end-to-end fuzzing만큼 널리 적용되지 않고 있습니다.
Fuzz driver 생성의 어려움
fuzz driver 자동 생성을 위해 해결해야 하는 과제는 크게 두 가지로 요약될 수 있습니다.
(1) 유효한 API 호출 순서를 이해하고 작성하기 (즉, library API 중에 어떤 것을 선택하고 그것들의 순서는 어떻게 정할 것인가?)
(2) 유효한 입력값을 API에 맞게 어떻게 넣어줄 것인가? (즉, 선택된 API의 parameter들 중 어떤 parameter를 선택하고 어떤 값을 만들어서 넣을 것인가?)
이 두 가지 중 하나라도 제대로 처리하지 못하면 fuzz driver의 성능에 악영향을 미칩니다. 예를 들어, 그림 1처럼 fuzz driver가 initialize() API 이전에 terminate() API를 호출하면 crash가 발생하지만, 이는 API의 버그가 아니라 fuzz driver를 잘못 작성해서 발생한 결과이므로 전혀 도움이 되는 상황이 아닙니다. 또 다른 경우는, 서로 다른 API이지만 context에 해당하는 parameter는 같은 값이 들어가야 하거나 API parameter간 의존성이 있는 경우도 있습니다(e.g. 배열의 포인터와 길이). 이런 관계를 무시하고 랜덤한 값을 넣는다면 이 역시 시간 낭비가 되겠죠. 따라서, 단순히 랜덤 값과 함께 API를 무작위로 호출하는 것을 피하고 유효한 API, 유효한 값을 사용하도록 fuzz driver를 생성해야 합니다.
위에 말한 어려움으로 인해 fuzz driver는 대부분 수작업으로 신중하게 제작됩니다. Library 개발자나 보안 테스터는 시간을 들여 해당 library를 이해하고, fuzzing에 적합하다고 판단하는 API 호출 순서를 결정하며, API parameter에 입력값을 적절히 배치하는 과정을 진행합니다. 충분히 짐작할 수 있겠지만, 이러한 과정은 상당히 시간이 오래 걸립니다. 이는 앞서 언급한 대로 library fuzzing이 end-to-end fuzzing만큼 널리 퍼지지 못한 주요 이유이기도 합니다.
타이젠 오픈 소스 프로젝트는 수천 개의 API가 포함된 수백 개의 library를 갖고 있는데요. 단일 API의 올바른 사용법을 이해하는 데 30분이 걸린다고 가정하면, 1,000개의 API를 이해하는데 60일이 필요하게 됩니다.
그림2. Fuzzing Framework 예시: OSS-Fuzz (출처: https://github.com/google/oss-fuzz#overview)
그렇다고 Library Fuzzing을 안 할 순 없는 노릇이라 구글에선 OSS Fuzz 프로젝트를 운영하며 Library들을 Fuzzing하고 있습니다(https://google.github.io/oss-fuzz/). 다양한 오픈 소스 library를 위한 가장 성공적인 fuzzing 프로젝트죠.
그런데도 여기 등록된 library 대부분은 한두개의 fuzz driver만 갖고 있습니다. 아마도 올바른 fuzz driver를 만들기가 쉽지 않아 여러 driver로 넓은 범위를 테스트하기 어려운 이유도 크리라 생각합니다. 프로젝트당 몇 개의 fuzz driver만 가지게 되면 fuzz driver가 호출하지 않은 혹은 입력을 전달하지 않은 API에 있는 버그는 찾기 힘들 수 있습니다.
우리가 발견한 버그(예: CVE-2021-30473, CVE-2021-30474, CVE-2021-30475)는 OSS-Fuzz에 Library가 등록되어 있었지만, fuzz driver가 커버하지 못해서 남아있던 취약점들이었습니다. 따라서 library의 API 대부분을 fuzzing이 다루는 것이 중요한데 이는 수작업으로 해결되기 힘들기 때문에 자동으로 fuzz driver를 만들 수 있는 방법이 필요합니다.
Fuzz driver 자동 생성 방법
이를 해결하고자 우리 당사는 UTopia를 개발하였습니다. UTopia는 기존의 유닛 테스트(UT)를 확장성 있고 자동화된 방식을 통해 fuzz driver로 변환하는 기술입니다. UTopia의 핵심 아이디어는 1) UT의 속성을 활용해서 UT 코드 분석을 손쉽게 하고, 2) 적절한 fuzzing 입력값을 API parameter에 넣기 위해, root definition이란 개념을 제안하여 UT 코드상 어느 위치에 fuzzing 입력값을 넣어야 하는지 판단하고, 3) 각 argument가 해당 API의 내부에서 어떻게 쓰이는지 분석하여 fuzzing의 대상이 되는지 아닌지 등을 판단합니다. 이를 통해 UTopia는 코드를 깊이 있게 이해하고 유효하지 않은 API의 사용을 피할 수 있어 사람의 개입 없이 고품질의 fuzz driver를 자동으로 만들 수 있게 됩니다.
UTopia는 유닛 테스트와 대상 library 코드 모두를 분석하여 UT를 효과적인 fuzz driver로 변환하는데요. 그림 2는 UTopia가 어떻게 동작하는지 보여줍니다.
그림 3. fuzz driver를 생성하기 위한 UTopia의 동작
(1) UTopia는 UT framework의 구조적 특성을 미리 파악하여 UT framework를 구성하는 전체 코드를 분석하는 대신 개발자가 구현한 Unit Test Function만 분석하여 효율성을 높입니다.
(2) UTopia는 library를 분석하여 API parameter의 속성을 식별하고 parameter에 유효한 입력을 제공합니다. 우리는 주로 다음 다섯 가지 parameter 속성을 정의하고 찾습니다.
- output
- loop count
- allocation size
- file path
- array length
(3) 그 다음, 값을 전달하기 위해 root definition 분석을 수행합니다(그림 3). root definition을 찾고 그곳에 fuzzing input을 전달합니다. 그리고 UT 코드에 존재하던 기존값들을 추출하여 fuzzing할 때 초깃값으로 사용하고 랜덤하게 바꿔 갑니다.
(4) 마지막으로, 분석 결과를 기반으로 fuzz driver를 작성합니다. (2)에서 발견된 parameter 속성을 기반으로 root definition에 fuzzing 입력을 전달하는 코드로 변경시키죠. Output 속성을 가진 인수에는 어떠한 fuzzing 값도 넣지 않습니다. loop count와 allocation size의 속성을 가진 경우, 크기를 제한하여 fuzz driver가 메모리 부족 또는 타임아웃을 피할 수 있도록 합니다. file path 속성의 경우, parameter에 입력을 넣지 않고 해당 file에 fuzzing 입력을 채우게 됩니다. Array length 속성인 경우, 해당 배열을 찾아 ‘배열의 길이-1’ 값을 할당하고 배열의 끝에 null 종결자를 할당하여 fuzz driver의 유효성도 지키고 fuzzing 효율도 향상시킬 수 있습니다.
그림 4. 무작위로 생성된 fuzz driver(왼쪽) 대비 root definition 분석을 통해 원래 개발자가 의도한 대로 argument 간 관계를 지켜서 fuzzing하는 fuzz driver(오른쪽)
자동 생성된 fuzz driver로 버그를 찾아낼 수 있나요?
자동화 성능(즉, 확장성)을 평가하기 위해 우리는 다양한 출처로부터의 25개의 오픈 소스 library를 선택하여 평가를 진행했습니다(그림 4 참조). 이 25개의 library는 UTopia가 다양한 크기의 프로젝트에서도, 다양한 빌드 시스템에서도, 다양한 유닛 테스트 프레임워크를 가진 library에 모두 적용될 수 있는지 확인하기 위해 선택되었습니다.
25개 library에서 Fuzzing 적용 가능한 TC는 2,715개이고(e.g. output 속성의 parameter만 있는 API는 Fuzzing 불가), UTopia는 100% fuzz driver를 생성할 수 있었습니다. 또한, UTopia가 생성한 fuzz driver들이 기존 UT가 커버하지 못하는 영역을 탐색한다는 것을 알 수 있으므로(그림 4의 UCov) UT와 상호보완적으로 쓰일 수 있다는 것을 알 수 있습니다.
그림 5. UTopia에서 생성한 fuzz driver에 대한 평가 및 결과에 사용된 프로젝트들
Target Library:
SR = Source repository (O:OSSFuzz / G:GitHub / A:Anroid / T:Tizen), BS = Build system (cm:cmake / gn:gnu make / nj:ninja / bz:bazel), eFn = Exported functions in a library.
Unit Tests:
TF = Testing framework (G:gtest / B:boost), TcCov. = Region coverage of the target library with the test cases from which fuzz drivers are generated, TC = Total number of test cases.
UTopia-Generated Fuzz Drivers:
Oths. TCs implemented with macro functions other than TEST, TEST_F or BOOST_AUTO_TEST_CASE_FIXTURE, Ign. = TCs ignored by UTopia based on the exclusion criteria, AT = Per-core time to analyze library and unit test code, GT = Per-core time to generate fuzz driver code, UCov = Unique coverage of UTopia compared with TC Cov., AG = The ratio of the coverage with the aggregation of unique regions across all fuzzers to that of TCs from which the fuzzers were made, MG = The individual maximum growth ratio of a single fuzzer compared to execution with the initial seed.
UTopia에 대해 더 자세히 알 수 있나요?
Utopia를 자세히 소개하는 “UTopia: Automatic Generation of Fuzz Driver using Unit Tests”라는 제목의 논문이 2023 IEEE Symposium on Security and Privacy(SP) 학회에 채택되어 게재되었습니다(https://ieeexplore.ieee.org/document/10179394). 관련 오픈소스 역시 https://github.com/Samsung/UTopia에 공개되어 있습니다. 우리는 UTopia가 보안을 고려하는 library 메인테이너들에게 널리 사용되어 library fuzzing이 end-to-end fuzzing만큼 활성화되고, 향후 유사한 연구나 관련 연구에 도움이 되기를 바랍니다.
UTopia로 찾아내고, 신고하고, 수정한 버그들은 우리의 트로피 페이지(https://github.com/Samsung/UTopia/blob/main/Trophy.md)에 기록하고 있습니다.
그림 6. UTopia로 찾은 버그 및 Fuzz Driver의 예
만약 UTopia를 사용하면서 버그를 찾게 된다면 저희에게 꼭 알려 주세요. 해당 정보를 Trophy 페이지에 추가하고 널리 공유하려고 합니다. Utopia와 함께 버그를 찾는 즐거움을 경험해보세요!
Security Assurance Lab의 이하윤이었습니다. 감사합니다.
|
|
