[Paper] 유닛 테스트 케이스 코드를 활용한 C/C++ 라이브러리의 동적 퍼징 드라이버 생성
2022 한국소프트웨어종합학술대회 논문집_유닛 테스트 케이스 코드를 활용한 C/C++ 라이브러리의 동적 퍼징 드라이버 생성 (한동대학교 임성빈, 홍신)
을 읽고 정리한 정리본입니다.
🫧 용어 정리
[1] 퍼저(Fuzzer)는 소프트웨어 프로그램에 랜덤한 값을 입력하는 프로그램
[2] 퍼징 드라이버 : 오픈소스 프로그램에 Fuzzing[2]을 적용하기 위해 사용되는 드라이버[3]
[3] 퍼징(Fuzzing) : 소프트웨어의 취약점을 테스트하는 기법 중 하나. 퍼저를 사용하여 소프트웨어 프로그램의 문제를 테스트하는 기법이다.
[4] 드라이버 : OS, 하드웨어 간 통신을 도와주는 소프트웨어. 주로 커널 모드에서 동작함
🫧 서론
오픈소스의 발전에 따라 오픈소스 라이브러리에 대한 검증이 필요해졌다.
최근 linFuzzer, AFL과 같은 그레이박스 퍼저 (greybox fuzzer) 가 대표적인 라이브러리 검증 도구로 사용되고 있다.
시스템 수준 테스팅에서는 사용자가 일일이 퍼저가 생성하는 입력 데이터로 검증이 가능하지만, 시스템보다 더 널리 쓰이고 더 정확해야 하는 라이브러리 프로그램에서는 테스팅 자동화가 필수적이다.
테스팅을 자동으로 생성해 사용하기 위해서는 퍼징 드라이버(fuzzing drivere)
를 필수적으로 사용할 필요가 있다.
API 함수의 동작은 입렉 데이터와 API 함수 호출 순서에 따라 달라질 수 있으므로, 이 두 가지 사항을 고려해 다양한 퍼징 드라이버를 제공해야 한다.
여러 사항을 고려해 동적 퍼징 드라이버를 생성해줄 수 있는 기법인 GraphFuzz는 퍼저가 생성하는 입력 데이터로부터 다양한 실행 실행 시나리오를 라이브러리 구동 프로그램에 생성해 준다.
동적 퍼징 드라이버(dynamic fuzzing driver)를 활용할 경우, 의미 있는 테스트 시나리오를 선별하고 테스트 시나리오를 별개의 퍼징 드라이버 코드로 표현하는 과정을 자동화할 수 있으므로 라이브러리 프로그램 검증 비용을 줄일 수 있다.
이러한 장점에도 불구하고, GraphFuzz에는 비교적 단순한 형태의 라이브러리 프로그램에 대해서만 코드 생성을 지원한다는 한계가 존재한다.
검증 대상 클래스의 멤버 함수로만 테스트 시나리오를 생성하고, 그 외의 함수나 객체를 테스트 생성에 자동으로 고려하지 못한다.
이러한 한계로 인해 실제 사용 시 개발자가 수작업으로 테스트 코드를 짜야 한다는 부담감이 존재한다.
따ㄹ 본 연구는 C/C++ 라이브러리를 테스트하기 위해 기존 테스트 코드를 활용하여 동적 퍼징 드라이버를 생성하는 방법을 제안하고, 이를 GrahpFuzz를 기반으로 구현한 결과를 소개한다.
GraphFuzz가 다양한 테스트 시나리오를 구성할 수 있도록 단위 실행 블록을 추출하여 구성한다.
c-areas, cjson에 존재하는 총 네 개의 유닛 테스트 코드를 대상으로 제안한 기법을 적용한 결과, 기존 유닛 테스트보다 평균 4.09배 높은 분기 커버리지를 달성하는 테스트를 자동으로 생성할 수 있었다.
🫧 배경
✨ 라이브러리 프로그램을 위한 퍼징 드라이버
- 라이브러리 프로그램은 하나 혹은 적은 수의 파일 입력을 인터페이스로 갖는 어플리케이션 프로그램과 달리, 여러 개의 API 함수를 입력 인터페이스로 가진다.
이러한 라이브러리 프로그램은 다음과 같은 두 가지의 사항에 의해 결정된다.
- API 함수의 호출 순열
- 각 함수 호출 시 입력되는 값과 객체
라이브러리 프로그램을 검증하기 위해서는 위 두 가지 사항을 고려해야 하며, 여러 라이브러리 호출 순서와 함수 입력 값을 동시에 탐색하는 다양한 테스트 케이스가 필요하다.
이를 위해서는 퍼징 드라이버(fuzzing driver)
가 필요하다.
퍼징 드라이버는 퍼저가 생성한 입력 값을 검증 대상 API 함수에 전달한다.
이는 기능에 따라 정적 퍼징 드라이버 (static fuzzing driver)
와 동적 퍼징 드라이버 (dynamic fuzzing driver)
로 구분이 가능하다.
🌙 정적 퍼징 드라이버
API 함수 호출 시나리오를 고정한 상태에서, 들어오는 입력 값을 바탕으로 각 API 호출의 입력을 생성함으로써 테스트 생성을 수행한다.
API 함수 호출 시나리오를 고정했기 때문에 탐색 가능한 실행의 범위가 제한적이라는 한계점을 가진다.
이를 보완하기 위해 클라이언트 코드나 유닛 테스트 케이스로부터 정적 퍼징 드라이버를 자동으로 추출하는 기법이 제시되었다.
🌙 동적 퍼징 드라이버
동적 퍼징 드라이버는 퍼저로부터 받은 입력 값을 활용해 API 함수 호출 시나리오와 각 API 함수 호출에 사용되는 입력을 동시에 결정하여 테스트 코드를 생성한다.
정적 퍼징 드라이버의 경우와 달리 입력 값에 따라 다양한 API 함수 호출 시나리오를 탐색할 수 있어 비용 효과적이다.
✨ GraphFuzz
GraphFuzz는 검증 대상 C/C++ 라이브러리 프로그램의 코드로부터 동적 퍼징 드라이버 코드를 자동 생성한다.
GraphFuzz의 동작은 다음의 절차에 따라 이루어진다.
-
Endpoint schema 구조 생성
-> 동적 퍼징 드라이버 생성에 필요한 메타데이터 파일 (Endpoint schema) 생성
-> Endpoint schema : 검증대상 클래스 정의 + endpoint 정의
-> Endpoint : 입력 파라미터 리스트 + 출력 파라미터 리스트 + endpoint driver template
-> Endpoint driver template : 입력 파라미터들을 해당 API 호출의 입력으로 전달하고, API 함수 실행 결과를 출력 파라미터로 전달하는 코드 (실행 코드) -
사용자의 endpoint driver template 작성
-> 입력 변수와 출력 변수가 모두 검증대상 클래스 객체 또는 기본 타입인 경우에 한해 자동 생성해줌
-> 그 외의 함수는 자동 생성하지 않으므로 이에 대해서는 개발자가 수동으로 추가해야 함 -
동적 퍼징 드라이버 코드 생성
-> 완성된 endpoint schema를 입력 받아 이에 해당하는 동적 퍼징 드라이버를 C++ 코드로 자동 생성함
절차 상 GraphFuzz는 2. 사용자의 endpoint driver template 작성
부분에서 개발자가 직접 테스트 코드를 추가해야 하는 한계가 드러난다.
✨ 유닛 테스트 케이스 코드를 활용한 동적 퍼징 드라이버 생성
제안하는 기법에 대한 과정은 다음과 같다.
- 유닛 테스트 코드 전처리 (preprocessing)
- 유닛 테스트로부터 endpoint driver template 추출
- endpoint driver template로부터 endpoint 생성
제안하는 기법을 설명하기 위해 유닛 테스트 케이스 코드 예를 들어 설명하고자 한다.
유닛 테스트 케이스 코드 예
linked_list* l;
; = llist_aloc(5);
dateinfo d1 = { 2022, 1, 1 };
llist_insert(l, &d1);
for (int i=0; i<10; i++>) {
dateinfo *d2 = dateinfo_create(2022, 1, i);
llist_insert(l, d2);
}
EXPECT_EQ(11, llist_size(l));
이 예제는 linked_list 객체를 사용하는 라이브러리에 대한 유닛 테스트 코드로, 세 개의 API 함수 호출 (llist_alloc(), llist_insert(), llist_size()) 를 테스트 시나리오로 표현하고 있다.
🌙 1. 유닛 테스트 코드 전처리
전처리 과정에서는 주어진 유닛 테스트를 구성하는 코드 요소를 검사하여 endpoint schema에 정의되지 않은 것이 있다면 이를 사용할 수 있도록 추가하는 작업을 거친다.
또한, 코드 중 endpoint driver template으로 전환할 수 없는 코드 요소를 제거한다. 그 예로 코드 주석, assertion, 로그 생성 명령 등의 코드가 있다.
예시의 경우 dateinfo 구조체는 검증대상 라이브러리 함수가 아닌 다른 라이브러리에 선언된 경우로 이에 대한 정의를 추가하여 사용할 수 있도록 한다.
또한, 9번 줄에서 assertion에 해당하는 EXPECT_EQ를 제거하여 llist(size)만 남도록 변환한다.
🌙 2. 유닛 테스트로부터 endpoint driver template 추출
전처리를 거친 코드를 API 함수 호출을 기점으로 여러 구간으로 나누어 코드 블록으로 전환한다.
전처리를 거친 코드를 AST (abstract syntax tree)로 변환한 후, 최상위 수준의 구문 순열을 탐색하며 다음 API 함수 호출을 찾는다.
- 만일 다음 함수 호출이 발견되었다면 직전 API 함수 호출 직후부터 해당 API 함수 호출까지 코드를 하나의 코드 구간으로 추출한다.
- 만약 이전 API 함수 호출이 없었다면 처음 구문부터 해당 API 함수 호출까지의 코드를 하나의 구간으로 추출한다.
- 만약 마지막 API 호출 이후 구문이 남는다면 이를 종료 코드 블록을 지정한다.
추출한 각 코드 블록에서 지역 변수로 선언된 객체에 대해서도 새로운 동적 메모리를 할당하고 해당 변수의 값을 복사하여 사용하는 코드를 추가한다.
예시 코드의 경우 총 세 개의 코드 블록이 추출된다. (1-2행, 3-4행, 5-9행)
이때 7행의 llist_insert()의 경우 가장 바깥쪽 구문이 아니기 때문에 코드 블록으로 추출하지 않는다.
또한, 3-4행 코드 블록에서는 지역 변수(dateinfo d1) 가 선언되어 있으므로 아래와 같이 동적 메모리를 할당하여 사용하도록 변환한다.
🌙 3. Endpoint driver template으로부터 endpoint 생성
과정 2에서 도출한 endpoint driver template 코드 블록에 대해 입출력 파라미터를 결정하고, 이에 따라 endpoint 정의를 완성한다.
🫧 사례 연구
제안한 기법의 효용성을 평가하기 위해 본 연구에서는 오픈소스 C/C++ 라이브러리인 c-ares와 cjson을 대상으로 사례 연구를 수행했다.
제안한 동적 퍼징 드라이버 생성 기법은 C/C++ 유닛 테스트 케이스 코드를 srcML을 통해 코드의 AST 정보를 XML로 표현한 후, 이 데이터를 바탕으로 endpoint driver template 텍스트를 생성하는 Python 스크립트로 구현하였다.