1. 퍼징 (Fuzzing) 기본 개념
1.1 퍼징 (Fuzzing)
자동화된 소프트웨어 테스트 기법으로, 컴퓨터 프로그램에 무작위 데이터를 입력하여 소프트웨어 성능, 안정성 및 보안 취약점을 찾아내는 과정이다. 퍼징의 주요 목적은 프로그램에서 발생할 수 있는 예외 상황이나 오류를 찾아내어 프로그램 개발자가 문제를 수정하고 개선할 수 있도록 하는 것이다.
브라우저, 시스템 커널, API 및 기타 응용 프로그램 등 입력이 존재하는 거의 모든 소프트웨어가 퍼징의 대상이 될 수 있다. 퍼징은 복잡한 입력을 처리해야 하는 프로그램에 매우 효과적이다.
1.2 퍼저 (Fuzzer)
퍼저는 퍼징 테스트 과정에서 사용되는 주요 도구로서, 퍼징을 자동으로 실행하고 테스트한 뒤 이상 행위가 발생할 경우 해당 테스트 케이스를 저장하고 버그를 분류하는 "자동화 퍼징 도구"를 의미한다.
1.3 퍼징을 수행하는 이유!
퍼징을 진행하지 않은 기존 보안 프로세스는 보안 취약점이 드러난 후 대응하는 구조이기 때문에, 취약점이 악용되어 "Zero(0)-Day" 취약점이 발생할 가능성이 높다. 이러한 공격에 대한 대응은 취약점이 발견된 이후에 가능하기 때문에, 초기 방어가 어렵다.
하지만, 퍼징을 활용한 보안 프로세스는 알려지지 않은 취약점을 미리 찾아낼 수 있으며, 이는 "Zero(0)-Day" 공격에 대한 사전 방어 가능성을 높인다. 퍼징을 통해 사전에 취약점을 탐지하고 이를 수정함으로써, 공격자가 해당 취약점을 악용하기 전에 보안을 강화할 수 있다.
"Zero(0)-Day" 공격은 소프트웨어의 보안 취약점이 공개되거나 널리 알려지기 전에 발생하는 공격을 의미한다. 해당 용어는 보안 취약점이 발견되고 그 취약점에 대한 정보가 공개되기 전, 즉 Zero(0) 일째 발생하는 공격을 지칭한다. "Zero(0)-Day" 공격은 공격자들이 해당 취약점에 대한 패치나 보안 업데이트가 없는 상태에서 일을 악용하는 것을 말한다.
2. 퍼징 기법 분류
2.1 프로그램 정보를 활용하는 정도에 따른 분류
- White-box Testing
대상 프로그램의 내부 구조와 실행 중 발생하는 정보(동작)들을 사용하는 기법이다. 제품에 대한 함수 로직의 정상 동작 여부를 확인하기 위해 "내부 구조 기반 검증" 및 "결함 도출 테스트 기법"을 수행한다. 쉽게 설명하면 프로그램의 소스 코드, 알고리즘 시스템 아키텍처 등과 같은 내부 정보에 접근할 수 있는 상태에서 수행되는 기법을 의미한다.
소스 코드에 접근할 수 있기 때문에 다음과 같은 테스트 기법을 수행할 수 있다.
1. Control-Flow Test (제어 흐름 테스트)
2. Data Flow Test (데이터 흐름 테스트)
3. Brach Test (분기 테스트)
4. Prime Path Test (주요 경로 테스트)
5. Path Test (경로 테스트)
- Black-box Testing
대상 프로그램의 내부 정보를 사용하지 않고, 입력과 출력 데이터만 사용하는 기법이다. Black-box Testing은 소스 코드에 대한 접근 권한이 없기 때문에 주로 입출력 데이터를 기반으로 검증을 수행한다.
대표적으로 다음과 같은 테스트 기법을 수행한다.
1. Dumb Based Test
2. Decison Table Test (의사 결정 테이블 테스트)
3. Syntax Test
4. Use Case Test
5. User Story Test
- Gray-box Testing
대상 프로그램의 내부 정보 중 일부만 알고 진행하는 테스트 기법이다. 여기서 말하는 내부 정보의 일부란, 내부에서 사용하는 데이터 구조체 혹은 알고리즘에 대한 문서를 의미한다. 해당 기법은 Black-box와 White-box Test의 중간 형태이다.
대표적으로 다음과 같은 테스트 기법을 수행한다.
1. Matrix Text (행렬 테스트)
2. Regression Test (회귀 테스트)
3. Pattern Test (패턴 테스트)
4. Orthogonal Array Test (직교 배열 테스트)
2.2 입력 데이터를 생성하는 법에 따른 분류
- 생성 기반(Generation-based) 퍼징 기법
데이터의 구조 및 프로토콜을 이해하여 프로그램에 적합한 입력 데이터를 생성하는 기법이다. 파일의 포맷(형식)이나 프로토콜(규약) 등을 이해하여 적절한 INPUT을 생성하는 방식이다. 생성될 입력 데이터의 구조 및 프로토콜을 이해하기 때문에 유효한 입력 데이터를 잘 구성할 수 있지만, 많은 시간이 소요될 수 있다.
최근에는 생성 기반 퍼징 기법을 응용하여 프로그램에 적합한 입력 데이터를 생성하는 과정에서 특정 알고리즘, 휴리스틱(heuristic), 또는 기계 학습 기법 등을 사용하는 Intelligent Fuzzer를 많이 사용한다.
- 변이 기반(Mutation-based) 퍼징 기법
입력 데이터를 특정하여, 그 입력 데이터에 대해 조금씩 변이(Mutation)를 주어 새로운 입력 데이터를 생성하는 기법이다. 특정한 입력 데이터를 "시드(seed)"라고 부른다.
변이 기반 기법은 시드 데이터에 무작위적으로 변형을 주는 기법이기 때문에 많은 시간이 소요되지는 않지만, 유효하지 않은 입력 데이터가 생성되는 경우가 많아 "멍청한(Dumb) 퍼징" 기법이라고 불리기도 한다.
ex 1.) Bit Flipping 기법
데이터의 특정 영역의 값들을 임의로 바꿔치기(Replacing) 추가(Appending) 한다. 대표적인 도구로 Radamsa나, ZZUF 등이 있다.
3. Code Coverage 개념
Code Coverage는 퍼징을 진행할 때, 소프트웨어 테스트 케이스가 얼마나 충족되었는지 나타내는 지표를 의미한다. 즉, 얼마나 많은 코드가 테스트에 의해 실행되었는지를 나타낸다. 해당 지표는 소스 코드를 기반으로 수행하기 때문에 주로 White-box Testing을 통해 측정된다.
코드의 구조는 구문(Statement), 조건(Condition) 그리고 결정(Decision)으로 이루어져 있다. Code Coverage는 이러한 구조를 얼마나 커버했느냐에 따라 측정 기준이 달라진다.
3.1 구문 커버리지 (Statement Coverage)
구문 커버리지는 Line Coverage라고 부르기도 하며, 코드 한 줄이 한 번 이상 실행된다면 충족된다.
void foo (int x) {
system.out("start line"); // 1번
if (x > 0) { // 2번
system.out("middle line"); // 3번
}
system.out("last line"); // 4번
}
위 코드에서 "x = -1"을 테스트 데이터로 사용할 경우, if 문의 조건을 통과하지 못하기 때문에 3번의 코드는 실행되지 못한다. 총 4개의 라인에서 1, 2, 4번 라인만 실행된다. 따라서, 구문 커버리지는 3 / 4 * 100 = 75 (%)이다.
3.2 조건 커버리지 (Condition Coverage)
조건 커버리지는 프로그램 내의 모든 조건식에 대해, 그 내부 조건이 True와 False 각각의 결과를 가지는지 검증한다. 여기서 내부 조건은 조건식 내부 각각의 개별 조건을 의미한다. 예를 들어, "if (a > b && c < d)"와 같은 조건문이 있다면, "a > b"와 "c < d"는 두 개의 내부 조건으로 간주된다.
void foo (int x, int y) {
system.out("start line"); // 1번
if (x > 0 && y < 0) { // 2번
system.out("middle line"); // 3번
}
system.out("last line"); // 4번
}
위 조건 커버리지를 만족하기 위해서는 조건식 내의 각 조건이 독립적으로 True와 False가 되는 경우를 모두 테스트해야 한다. 따라서, 아래 4가지 조합을 모두 테스트해야 한다.
- "x > 0"이 True이고 "y < 0"이 True인 경우 (예: x = 1, y = -1)
- "x > 0"이 True이고 "y < 0"이 False인 경우 (예: x = 1, y = 1)
- "x > 0"이 False이고 "y < 0"이 True인 경우 (예: x = -1, y = -1)
- "x > 0"이 False이고 "y < 0"이 False인 경우 (예: x = -1, y = 1)
조건 커버리지는 위와 같은 방식으로 각 조건의 논리적 가능성을 완전히 탐색하여, 테스트가 코드의 논리적 구조를 얼마나 잘 커버하는지를 평가하는 것이다.
3.3 결정 커버리지 (Decision Coverage)
결정 커버리지는 분기 커버리지(Branch Coverage)라고도 불리며, 프로그램 내의 모든 결정 포인트(ex: if, for, while 문 등)가 True와 False를 가지게 되면 충족된다.
void foo (int x, int y) {
system.out("start line"); // 1번
if (x > 0 && y < 0) { // 2번
system.out("middle line"); // 3번
}
system.out("last line"); // 4번
}
위 코드에서 결정 커버리지를 충족시키기 위한 테스트 케이스는 다음과 같다.
- if 문이 참인 경우 (x > 0 && y < 0) -> "x = 1", "y = -1"과 같이 입력 -> 코드의 모든 라인이 실행된다.
- if 문이 거짓인 경우 -> "x = -1", "y = 1"과 같이 입력 -> 1번, 2번, 4번 라인이 실행되지만, 3번 라인은 실행되지 않는다.
결정 커버리지의 목표는 프로그램의 각 결정 포인트가 다양한 시나리오에서 어떻게 작동하는지 평가하는 것이다. 위 예시에서는 if 문이 True일 때와 False일 때의 두 가지 경우를 모두 테스트함으로써, 해당 결정 포인트에 대한 결정 커버리지를 만족시킬 수 있다.
- 코드 커버리지를 사용하는 목적!
코드 커버리지를 사용하는 주요 이유는 소프트웨어 테스트가 코드의 모든 중요 부분을 커버하고 있는지 확인하고, 이를 통해 숨겨진 오류를 찾아내며, 전반적인 테스트 품질을 향상시키기 위함이다. 이 과정은 인간의 실수를 줄이고, 소프트웨어의 신뢰성을 높이는 데 도움이 된다.
4. Fuzzer Architecture
퍼저의 아키텍처는 일반적으로 세 가지의 주요 구성요소로 분류된다.
4.1 TestCase Generator
대상 프로그램에 입력할 데이터를 생성하는 부분으로 생성 방식에 따라 2가지로 분류된다.
- Smart Fuzzer
프로그램의 입력 데이터 구조를 분석하여 해당 구조에 맞게 변형(Mutation)을 진행한다. 이는 더 지능적이고 구조적인 데이터 생성 방식을 의미한다.
- Dumb Fuzzer
임의의 랜덤 데이터를 생성하여 변형(Mutation)을 진행한다. 프로그램의 입력 데이터 구조에 대한 사전 지식 없이 무작위 데이터를 사용한다.
4.2 Logger
퍼징 과정 중 발생하는 버그나 이상 행위를 분석하기 위해 필요한 정보를 기록한다. Crash가 발생할 때의 데이터와 테스트 케이스를 저장하며, Code Coverage 기반의 퍼저를 사용할 경우에는 새로 발견된 Code Coverage 정보도 함께 저장한다. 이러한 로그를 분석하여 Crash의 원인을 분석한다.
4.3 Worker
TestCase Generator에서 생성한 입력 데이터를 받아 실행하고, 이상 행위를 탐지한다.
5. Fuzzing Tools
- Sanitizer
C/C++로 작성된 프로그램에서 발생하는 취약점을 탐지하고 로그에 기록하여 보여주는 도구이다.
- ASAN (Address Sanitizer)
메모리 관련 취약점을 탐지하는 도구로 다음과 같은 문제를 탐지한다.
- Use After Free
- Heap Buffer Overflow
- Stack Buffer Overflow
- Global Buffer Overflow
- Use After Return
- Use After Scope
- Initialization Order Bugs
- Bad Free
- Memory Leaks
- Thread Sanitizer
멀티스레딩 환경에서 발생하는 Data Race 문제를 탐지한다. 두 개의 스레드가 동시에 같은 데이터에 접근하여 수정할 때 발생하는 문제를 탐지한다.
- Memory Sanitizer
초기화되지 않은 메모리에의 접근을 탐지하는 도구이다. 이는 잠재적인 버그나 취약점이 원인이 될 수 있다.
- Radamsa
Sample 파일을 기반으로 자동으로 데이터를 변형(Mutation)하는 Mutation-Based-Fuzzer이다. 다양한 형태의 입력 데이터를 생성하여 프로그램의 안정성을 테스트한다.
5.1 AFL (American Fuzzy Lop) Fuzzer
AFL 퍼저는 가장 인기 있는 퍼저 중 하나이며 Gray-box 퍼저이다. 주로 Mutation-Based 방식을 사용하여 자동으로 입력 데이터를 변형시키고, 프로그램 실행 중 생성된 테스트 케이스의 Code Coverage를 분석하여 효과적인 테스트 케이스를 선별한다.
무엇보다, AFL은 사용 방법과 설정에 따라 Smart Fuzzer 또는 Dumb Fuzzer로 사용될 수 있다. 프로그램의 코드 커버리지를 분석하여 입력을 지능적으로 조정하고 새로운 코드 경로를 탐색할 경우 Smart Fuzzer의 역할을 하지만, 무작위 데이터만을 생성하여 기본적인 테스트를 수행할 때는 Dumb Fuzzer로 동작할 수 있다. 따라서, AFL을 사용하는 사용자가 어떤 전략과 설정을 적용하느냐에 따라, 그 성능과 역할이 달라질 수 있다.
AFL 퍼저는 GCC, Clang 등의 컴파일러와 상호 호환되며, 원하는 프로그램을 컴파일하는데 필요한 AFL 컴파일을 별도로 제공한다. AFL 컴파일러를 사용함으로써, 소스 코드 변경 없이 퍼징이 가능한 바이너리를 생성할 수 있다.
- AFL 전체 로직
- AFL-Fuzz에서 퍼징 하려는 프로그램을 실행한다.
- 프로그램이 처음 실행될 때, AFL-Fuzz와의 파이프 통신을 통해 fork 서버를 초기화한다. 이후, fork() 호출을 사용하여 타겟 인스턴스를 새롭게 실행한다.
- 표준 입력이나 파일로부터의 입력이 퍼징 대상 프로그램으로 전달된다.
- 실행된 결과는 공유 메모리에 기록되며, 프로그램은 실행을 완료한다.
- AFL-Fuzz는 공유 메모리에서 퍼징 대상이 남긴 실행 기록을 분석하고, 해당 정보를 기반으로 새로운 입력을 생성한다.
- 생성된 새로운 입력은 다시 프로그램에 전달되어 실행된다.
위에서 살펴본 퍼저들 외에도 AFLNET, Peach Fuzzer, LibFuzzer 등 다양한 퍼저들이 존재한다. 이렇게 다양한 퍼저들이 존재하는 이유는, 각각의 퍼징 도구가 특정 프로그램 구조, 기술 스택, 또는 특정 유형의 버그 발견에 더 적합하게 설계되었기 때문이다. 따라서, 퍼저를 사용하는 사용자들은 퍼징을 진행할 프로그램의 구조, 사용하는 기술 스택, 그리고 발견하고자 하는 버그의 유형을 고려하여 적절한 퍼저를 선택해야 한다.
6. 파싱과 파서 개념
6.1 파싱 (Parsing)
- 구문 분석
문장이나 데이터 스트림의 구성 요소를 분해하고, 그 성분의 위계 관계를 분석하여 구조를 결정하는 과정이다.
- 데이터 처리
데이터를 분해하고 분석하여, 원하는 형태로 조립하거나 추출하는 프로그램적 행위이다.
- 정보 가공
웹 상에서 주어진 정보를 원하는 형태로 가공하여 서버에서 불러들이거나, 문서에서 특정 데이터를 패턴이나 순서에 따라 추출해 가공하는 과정이다.
정리하자면, 파싱은 어떤 문장이나 데이터 스트림을 분석하거나 문법적 관계를 해석하는 행위를 말한다.
6.2 파서 (Parser)
파싱을 수행하는 구성 요소로, 입력된 데이터나 코드의 구문을 분석하는 역할을 수행한다.
- 구조 결정
분석 및 분해된 데이터나 코드의 구조를 목적에 맞게 결정한다.
- 컴파일러와 인터프리터
컴파일러나 인터프리터의 중요한 부분으로서, 원시 프로그램을 읽고 문장의 구조를 알아내어 문법적으로 올바른지 검사하는 데 사용된다.
이렇게 파싱과 파서는 프로그래밍 언어의 해석, 웹 크롤링, 데이터 처리 등 다양한 분야에서 중요한 개념으로 활용된다.
참고자료 및 출처
[퍼징 퍼저 개념]
- https://cpuu.postype.com/post/589162
- https://jangjongmin.oopy.io/b5b7c23b-1942-4108-b239-b1499827316e
- https://blog.naver.com/PostView.naver?blogId=autocrypt&logNo=222409538688
- https://csrc.kaist.ac.kr/blog/2022/11/21/화이트박스-테스팅-도구-기능-및-성능-분석-1부/
- https://csrc.kaist.ac.kr/blog/2021/02/26/퍼징-연구-안내서/
[코드 커버리지 개념]
- https://velog.io/@lxxjn0/코드-분석-도구-적용기-1편-코드-커버리지Code-Coverage가-뭔가요
- https://tecoble.techcourse.co.kr/post/2020-10-24-code-coverage/
[AFL 퍼저]
- Fuzzing like the Legendary Super Saiyan · TRX (theromanxpl0it.github.io)
- https://theromanxpl0it.github.io/articles/2019/03/29/fuzzing-talk.html
- https://thfist-1071.tistory.com/311
- https://barro.github.io/
- https://cinnamonc.tistory.com/342
- https://rond-o.tistory.com/214
- https://cpuu.postype.com/post/11671863
- https://cpuu.postype.com/post/9786737
- https://barro.github.io/
- https://brwook.github.io/posts/paper-afl/
'5. Fuzzing' 카테고리의 다른 글
AFL++를 이용하여 Xpdf 퍼징 실습 (0) | 2024.02.02 |
---|