상세 컨텐츠

본문 제목

What the fuzz 번역

Fuzzing/Theory

by RACC8N 2020. 8. 5. 21:10

본문

https://labs.f-secure.com/blog/what-the-fuzz/

 

What the Fuzz

 

labs.f-secure.com

What is fuzzing?

Fuzzing or Fuzz testing은 프로그램 내 잠재적 취약점을 찾기위해 사용되는 자동화된 소프트웨어 테스트 기법이다. 프로그램을 Fuzzing하는 것은 랜덤한 input을 제공하고, 그로인해 발생하는 모든 case (crash, memory corruption)를 기록한다. Fuzzing은 broute force 기법과 비슷하다. 브라우저, 파일, 시스템 커널, 대부분의 API 및 기타 프로그램 인터페이스, 드라이버, 네트워크 데몬, 웹 애플리케이션 등 어떤 종류의 입력을 기대하는 거의 모든 소프트웨어를 Fuzzing 할 수 있다. 간단한 Fuzzing 기법은 프로그램의 어떤 정보도 필요하지 않는 black box testing 기법이다. 하지만 source code나 binary를 분석, 계측하여 프로그램의 input을 맞춤화하 고급 Fuzzing 기법도 있다.

 

Why fuzzing?

Fuzzing은 자동으로 버그를 찾는 방법이다. 이상적으로 Fuzzer는 혼자서 작동할 수 있다. 때문에 많은 수동 상호작용이 필요하지 않아 싼 testing 기법이 될 수 있다. 반면 수동 테스트는 발견되는 bug의 양이 tester의 skill에 많이 달려있다.

두가지 기법 모두 보통 다른 종류의 버그를 드러내고 다른 기법을 교육할 수 있기 때문에 Fuzzing과 수동 테스트의 조합이 최적이다.

 

복잡한 응용프로그램일수록 더 많은 버그를 갖게 되고, 복잡성이 증가함에따라 attack surface도 넓어진다. 수동 테스트는 프로그램이 복잡하면 많은 시간과 좋은 testing skill이 필요하다. 예시로, 만약 회사가 브라우저를 수동 테스트만 testing하기를 결정했다면, 다음 릴리즈 전까지 전체 응용프로그램을 테스트할 수 있으려면 숙련된 tester들이 많이 필요하다. 하지만 Fuzzer는 출시되기전 부터 테스트 할 수 있으며 확장에도 용이하다. (많은 기계, 많은 코어로 더 빠르게 돌릴 수 있다.) Fuzzer는 이미 알려진 bug를 다시 도입하지 않는지 확인하기 위한 regression testing에도 사용된다.

 

Common issue with fuzzing

Fuzzer가 훌륭하고 싼 방법이지만 항상 최적의 것은 아니다. Fuzzer가 찾아야할 원하는 버그 유형과 응용프로그램에 따라 설정 시간이 많이 걸리 수 있다. 특히 입력 구조를 알고 있는 self-written smart fuzzer는 사용 가능한 결과가 나올 때까지 어느 정도 작업이 필요하다.

 

Fuzzer는 다른 보안 조치를 대신할 수 없다. 수많은 취약점을 드러내는 가장 큰 Fuzzer를 가지고 있다고 해도 프로그램의 모든 취약점을 찾을 것 같지는 않다. 그리고 만약 Fuzzer가 더 이상 취약점을 찾지 못한다면 그 프로그램이 완벽하게 안전하다는 것을 의미하지 않는다. Fuzzer는 정기적인 code audit과 다른 안전한 개발 원칙과 같은 다른 보안 조치들에 대한 좋은 보완책일 뿐이다.

 

Fuzzer는 많은 false positive와 unexploitable bug와 duplicate를 만들어낼 수 있다. 따라서 검토 프로세스는 수동 또는 자동 중 하나로 필수적이며, 자원이 필요하다. 입력이 복잡할수록 검토 과정은 더 복잡해진다.

 

Fuzzer의 효율성을 극대화하는 것은 Fuzzer의 가장 어려운 부분 중 하나이다. Fuzzer의 가치는 일반적으로 그것이 얼마나 많은 이전에 알려지지 않은 bug를 찾느냐에 따라 결정된다. 더 많은 bug를 찾기 위해 Fuzzer는 생성된 테스트 케이스의 품질에 크게 의존하는 테스트 케이스당 높은 crash 비율을 가져야 한다. 목표물에 /dev/urandom을 파이핑하는 것은 쉽지만, 테스트 사례당 충돌 비율은 너무 낮아서 적절한 시간대에서 어떤 것도 찾을 수 없을 것이다. 동시에 단일 테스트 사례의 실행 시간은 가능한 한 낮아야 한다.

 

What is needed for fuzzing?

Fuzzing을 시작하려면 입력을 하는 대상이 필요하다. 응용 프로그램이 복잡할수록 Fuzzer로 버그를 찾을 가능성이 높다. 활용 사례에 따라 in-house or third-party 소프트웨어일 수 있다. 찾으려는 버그의 종류에 대해 생각해 볼 필요가 있다. Fuzzer는 언제 버그를 발견했는지 알아야 하기 때문에 가장 쉽게 발견되는 버그는 잠재적 보안 취약성을 나타낼 수 있는 충돌로 이어지는 버그들이다.

 

What types of bugs can a fuzzer find?

이론적으로 Fuzzer를 통해 가능한 모든 버그 종류를 찾는 것이 가능하다. Fuzzer는 프로그램의 행동을 의도하지 않았거나 악의적인 것으로 분류하기 위한 탐지 메커니즘이 필요하다. 그러므로 어떤 벌레 수업은 명백한 오행을 낳기 때문에 다른 사람들보다 훨씬 쉽게 찾을 수 있다. 예를 들어 trigger 시 충돌로 이어지는 memory corruption bug는 쉽게 감지할 수 있다. 반면에 logic bug는 프로그램이 명백히 잘못된 것이 아니며 logic bug를 감지하기 위한 규칙을 사전 정의하는 것은 까다로울 수 있기 때문에 탐지하기가 매우 어려울 수 있다.

 

Architecture of a typical fuzzer

전형적인 Fuzzer는 세 개의 다른 부분으로 구성되어 있다. Fuzzer에 있는 프로그램에 공급하기 위해 input을 생성하는 test case generator, 주어진 입력으로 프로그램을 실행하고 예상치 못한 동작을 인식하는 worker, 흥미로운 test case와 버그를 분석하는 데 필요한 모든 것을 기록하는 logger. 대부분의 Fuzzer에는 다른 세 부분을 조정하고 그들 사이의 통신을 관리하는 서버/마스터도 포함된다.

 

Test case generator

test case generator는 주로 새로운 test case를 만드는 역할을 한다. smart fuzzer는 새로운 test case를 만들기 위해 입력 구조를 인식할 수 있다. 반대로 입력 구조를 알지 못하는 test case generator를 dumb fuzzer이라고 부른다. test case는 처음부터 생성하거나 기존 test case에서 변형할 수 있다. 정교한 fuzzer는 test case의 생성과 mutation을 모두 결합할 수 있다.

 

Guided fuzzing

test case generator에 의해 생성된 test case는 품질 면에서 큰 차이가 있다. 따라서 test case generator가 어떤 test case를 mutate하거나 generate할 지를 결정하기 위해서는 일종의 guidance가 필요하다. 가장 일반적인 guidance heuristic 은 code coverage이다. 이론적으로, 더 많은 code coverage가 있을수록 fuzzer은 더 많은 버그에 도달한다. test case generator는 새로운 coverage를 밝힌 test case와 유사한 test case를 만들 수 있도록 구성할 수 있다. coverage guided fuzzing과 유사하게 기본적으로 더 많은 버그를 찾는 것과 관련된 모든 metric은 예를 들어 data flow guidance와 같은 test case generator의 guidance로 사용될 수 있다.

 

Dumb fuzzing

새로운 test case를 생성하기 위해 입력 구조의 모델이 필요하지 않은 test case generator를 dumb fuzzer라고 부른다. dumb fuzzer의 장점은 입력 구조에 대한 정보가 필요하지 않고 따라서 큰 조정 없이 여러 가지 프로그램을 fuzzing하기에 적합하다는 것이다. dumb fuzzing의 가장 큰 단점은 대부분의 입력이 사전 정의된 구조를 필요로 하거나 checksum을 포함하며, fuzzer는 유효한 입력을 생성하는데 어려움을 겪을 것이므로 대부분 애플리케이션의 parsing code를 테스트하고 있다는 것이다.

 

Smart fuzzing

dumb fuzzer와 반대되는 것은 smart fuzzer로, test case generator가 입력 파일의 구조를 알고 있는 것이다. 입력 파일의 구조를 입력 모델이라고 한다. 예를 들어 입력 모델은 프로그래밍 언어의 문법이나 데이터 형식 모델이 될 수 있다. smart fuzzer의 장점은 주로 유효한 입력 파일을 만들어 실제 프로그램에서 더 높은 코드 커버리지를 유도한다는 것이다. 그러나 smart fuzzer는 일반적으로 특정 입력 유형에 매우 전문화되어 있으며 버그를 유발할 가능성이 있는 test case를 만들 수 있는 우수한 입력 모델이 필요하다.

 

Mutation based fuzzing

mutation base fuzzer은 알려진 테스트 케이스를 mutate하여 새로운 테스트 케이스를 만든다. 일반적인 mutation에는 비트 플립(bit fliping)이 있으며, 입력의 랜덤 비트가 플립되거나 입력의 데이터 블록을 이동, 삭제 또는 반복한다. mutation base fuzzer의 가장 큰 장점은 설치하는 데 필요한 작업이 적다는 것이다. mutation base fuzzer의 중요한 부분은 mutate될 흥미로운 테스트 케이스를 선택하는 것이다. 그들은 퍼저가 버그를 더 잘 드러낼 수 있도록 가능한 한 많은 코드 적용 범위에 도달할 수 있도록 가능한 한 구별되어야 한다. 예를 들어, 대상이 사진을 보고 편집하기 위한 애플리케이션이고 변경하기 위해 선택한 테스트 케이스가 PNG 형식에만 있는 경우 퍼저가 다른 파일 형식을 처리하는 코드에 도달할 가능성은 낮을 것이다.

Generation based fuzzing

generation based fuzzer는 처음부터 새로운 테스트 케이스를 생성한다. generation based fuzzer는 입력 파일의 구조를 알아야 하며 그렇지 않으면 무작위 바이트를 만들 것이다. 일반적으로 설정하기 위한 작업이 필요하며 특정 입력 유형에 따라 전문화된다. 그러나 일단 가동되고 실행되면 mutation based fuzzer보다 코드 적용 범위가 더 많은 경향이 있으므로 뚜렷한 버그를 발견할 가능성이 더 높다.

 

Test case minimization

시간이 지남에 따라 mutation 의해 생성된 테스트 케이스는 크기와 복잡성이 커지는 경향이 있다. 이로 인해 실행 시간이 길어지고 흥미로운 테스트 케이스를 분석하기가 어려워진다. 작은 테스트 케이스도 Fuzzer가 있는 소프트웨어의 흥미로운 부분에 fuzzer를 집중시키는 것이 좋다. 따라서 fuzzer의 test case generator 엔진도 테스트 케이스를 최소화할 수 있어야 한다. 테스트 케이스를 최소화하는 목적은 최초 테스트 케이스가 트리거된 동일한 행동을 트리거하면서 가능한 가장 작은 테스트 케이스를 찾는 것이다. 동일한 행동은 이 경우에 동일한 충돌이 발생하거나 동일한 지침 요건이 충족되는 것을 의미한다. 예를 들어 coverage guided fuzzing의 경우 최소화된 테스트 케이스에서 동일한 흥미로운 code block이 적중됨을 의미한다. 이는 테스트 케이스의 일부를 제거하고 변경된 입력으로 대상을 이송하여 동작이 여전히 동일한 요건을 충족하는지 관찰함으로써 수행될 수 있다.

Worker

worker는 test case generator가 공급하는 test case 실행을 책임진다. 예상치 못한 행동도 인식할 필요가 있다.

일반적으로 테스트 케이스를 실행하는 것은 직진이다. worker는 주어진 입력으로 대상에게 먹이를 주고 잘못된 행동을 감지한다. 간단한 경우는 C-family Language에서 컴파일된 이진법, 즉 메모리 손상 문제가 발생하기 쉬운 이진법을 실행하는 것이다. 그 경우에 crash는 쉽게 감지될 수 있는 전형적인 비행이다. AddressSanitizer와 같은 도구와 함께 컴파일러 계측기를 사용하면 non-crashing  memory safety bugs를 탐지하는 데 도움이 될 수 있다.

 

Logger

logger는 발견된 모든 crash와 각각의 테스트 케이스를 기록하거나 저장한다. 만약 Fuzzer가 커버리지에 의해 안내된다면, 새로운 코드 커버리지를 발견한 모든 테스트 케이스도 저장할 수 있다. logging은 stack traces of crash로 crash의 분석을 용이하게 할 수 있다. crash로 이어지는 테스트 케이스를 기록하는 것은 매우 중요하다. 그렇지 않으면 crash는 재현될 수 없기 때문이다.

 

Summary

Fuzzing은 버그를 찾는 자동화된 연속적인 방법이다. 개발자들에게 fuzzing은 매우 일찍, 때로는 bug가 나오기 전부터 bug를 발견할 수 있기 때문에 매우 도움이 될 수 있다. fuzzing은 reintroduced bugs를 찾기 위한 regression testing 도구로도 사용될 수 있다. 보안 연구자들에게 fuzzing은 수동 audit/침투 테스트를 통해 발견되지 않은 버그를 찾아낼 수 있고 다른 작업을 하는 동안 계속 실행될 수 있기 때문에 버그를 찾는 훌륭한 도구다.

fuzzing의 가장 어려운 부분은 fuzzer의 효율을 극대화하는 것이다. fuzzer의 효율성은 테스트 케이스당 crash와 초당 테스트 케이스에 의해 주어진다. fuzzer의 전체적인 목표는 가능한 최소 시간 내에 많은 crash를 찾는 것이다.

일반적인 fuzzer는 입력 파일을 생성하기 위한 test case generator, 테스트 케이스를 실행하고 프로그램의 예기치 않은 동작을 인식하는 worker, 흥미로운 테스트 케이스를 기록하고 crash 또는 악의적인 행동의 흔적을 잠재적으로 쌓는 logger의 세 가지 구성 요소로 구성된다.

 

댓글 영역