1. 환경설정 및 설치
대상(LibXML2) 다운로드
# 디렉토리 생성
cd $HOME
mkdir Fuzzing_libxml2 && cd Fuzzing_libxml2
# 다운로드 및 압축해제
wget http://xmlsoft.org/download/libxml2-2.9.4.tar.gz
tar xvf libxml2-2.9.4.tar.gz && cd libxml2-2.9.4/
LibXML2 빌드 및 설치
# Libxml2 빌드 및 설치
sudo apt-get install python-dev
CC=afl-clang-lto CXX=afl-clang-lto++ CFLAGS="-fsanitize=address" CXXFLAGS="-fsanitize=address" LDFLAGS="-fsanitize=address" ./configure --prefix="$HOME/Fuzzing_libxml2/libxml2-2.9.4/install" --disable-shared --without-debug --without-ftp --without-http --without-legacy --without-python LIBS='-ldl'
make -j$(nproc)
make install
CC=afl-clang-lto: C 컴파일러를 afl-clang-lto로 설정한다.
CXX=afl-clang-lto++: C++ 컴파일러를 afl-clang-lto++로 설정한다.
CFLAGS="-fsanitize=address": C 컴파일러게 ASAN을 활성화하도록 지시한다.
CXXFLAGS="-fsanitize=address": C++ 컴파일러에게 ASAN을 활성화하도록 지시한다.
=> C, C++ 코드를 컴파일 할 때, ASAN에 필요한 코드를 삽입한다.
LDFLAGS="-fsanitize=address": 링커에게 링크 과정에서 ASAN 라이브러리를 링크하도록 지시한다.
=> 프로그램을 실행할 때, ASAN 라이브러리를 링크해 ASAN과 함께 실행하도록 한다.
--prefix: 프로그램을 설치할 디렉토리를 지정한다.
--disable-shared: 공유 라이브러리 생성을 비활성화한다.
--without-debug: 디버그 정보 기능을 빌드에서 제외한다.
--without-ftp: FTP(File Transfer Protocol)기능을 빌드에서 제외한다.
--without-http: HTTP기능을 빌드에서 제외한다.
--without-legacy: 레거시(과거에 사용되던 기술, 소프트웨어, 시스템)기능을 빌드에서 제외한다.
--without-python: Python 관련 기능을 빌드에서 제외한다.
LIBS='-ldl': -ldl라이브러리를 링크한다.
+) '-ldl' 라이브러리는 프로그램이 실행 중에 동적으로 라이브러리를 로드하는 데 필요한 함수들을 사용할 수 있도록 한다.
make -j$(nproc): 현재 시스템의 프로세서 코어의 개수에 맞추어 병렬로 빌드를 실행하도록 한다.
Fuzzing101에서 제공하는 Seed corpus 사용
# Fuzzing101에서 제공한 SampleInput.xml 가져오기
mkdir afl_in && cd afl_in
wget https://raw.githubusercontent.com/antonio-morales/Fuzzing101/main/Exercise%205/SampleInput.xml
cd ..
Dictionary 다운로드
# Dictionary(xml.dict) 다운로드
mkdir dictionaries && cd dictionaries
wget https://raw.githubusercontent.com/AFLplusplus/AFLplusplus/stable/dictionaries/xml.dict
cd ..
- Dictionary는 text-based file format(ex) XML)을 Fuzzing할 때 사용되며, Fuzzer가 대상의 메모리 파일을 변형할 때 사용하는 단어나 값들의 집합을 의미한다.
- Dictionary는 주로 Override, Insert 작업에 사용된다.
- Override: 특정 위치를 Dictionary 길이만큼 다른 값으로 대체함으로써 파일의 구조와 문법을 변형한다.
- Insert: Dictionary 항목을 현재 파일 위치에 삽입함으로써 파일의 구조와 크기를 변형한다.
2. Fuzzing 단계

# 마스터 인스턴스 예시
./afl-fuzz -i afl_in -o afl_out -M Master -- ./program @@
# 노예 인스턴스 예시
./afl-fuzz -i afl_in -o afl_out -S Slave1 -- ./program @@
./afl-fuzz -i afl_in -o afl_out -S Slave2 -- ./program @@
...
./afl-fuzz -i afl_in -o afl_out -S SlaveN -- ./program @@
- 시스템에 여러 개의 CPU 코어가 있으면, 여러 개의 인스턴스를 사용해 병렬적으로 Fuzzing을 효율적이게 수행할 수 있다.
- 이러한 공유 인스턴스 방식은 마스터 인스턴스와 노예 인스턴스가 있는데, 각각의 인스턴스는 옵션을 통해 구분할 수 있다.
- ‘-M’로 마스터 인스턴스를 설정하고 ‘-S’로 노예 인스턴스를 설정할 수 있는데, 마스터 인스턴스는 모든 노예 인스턴스를 제어하고 작업을 관리하며, 노예 인스턴스는 독립적으로 각각 마스터 인스턴스로부터 받은 테스트케이스를 변형하고 Fuzzing을 수행한다.
- Fuzzing으로 해당 crash를 찾지 못해 crash를 다운받아 진행했습니다. 이점 참고해주시면 감사하겠습니다 :)
3. 결과 분석

xmlValidateElementContent() :
// valid.c - xmlValidateElementContent()
#endif /* LIBXML_REGEXP_ENABLED */
if ((warn) && ((ret != 1) && (ret != -3))) {
if (ctxt != NULL) {
char expr[5000];
char list[5000];
expr[0] = 0;
xmlSnprintfElementContent(&expr[0], 5000, cont, 1);
- xmlValidateElementContent() 함수에서 호출한 xmlSnprintfElementContent() 함수에서 stack buffer overflow가 발생했다.
// DTD 구조
<?xml version="1.0"?>
<!DOCTYPE a [
<!ELEMENT a (ppppppp:llllllll)>
]>
<a/>
- 위와 같은 XML 문서의 구조를 정의하는 구조를 DTD(Document Type Definition)구조라고 하는데, 이러한 DTD 구조를 통해 새로운 문서 형식을 만들 수 있다.
- 여기서 ppppppp는 prefix를, lllllll은 name을 의미한다고 한다.

// valid.c - xmlSnprintfElementContent()
void
xmlSnprintfElementContent(char *buf, int size, xmlElementContentPtr content, int englob) {
int len;
if (content == NULL) return;
len = strlen(buf);
if (size - len < 50) {
if ((size - len > 4) && (buf[len - 1] != '.'))
strcat(buf, " ...");
return;
}
if (englob) strcat(buf, "(");
switch (content->type) {
case XML_ELEMENT_CONTENT_PCDATA:
strcat(buf, "#PCDATA");
break;
case XML_ELEMENT_CONTENT_ELEMENT:
if (content->prefix != NULL) {
if (size - len < xmlStrlen(content->prefix) + 10) {
strcat(buf, " ...");
return;
}
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (size - len < xmlStrlen(content->name) + 10) {
strcat(buf, " ...");
return;
}
if (content->name != NULL)
strcat(buf, (char *) content->name);
break;
- content를 gdb로 확인해 보면, XML_ELEMENT_CONTENT_ELEMENT을 충족하므로 위 코드의 switch (content->type) case 조건에 맞게 된다.
- 위 코드에서는 앞에서 보았던 prefix와 name을 strcat()을 통해 buf에 붙여주는데, 여기서 문제가 발생한다.

// valid.c - xmlValidateElementContent()
#endif /* LIBXML_REGEXP_ENABLED */
if ((warn) && ((ret != 1) && (ret != -3))) {
if (ctxt != NULL) {
char expr[5000];
char list[5000];
expr[0] = 0;
xmlSnprintfElementContent(&expr[0], 5000, cont, 1);
- python으로 길이를 확인해 보면 6001로, 앞서 xmlSnprintfElementContent()에서 expr, list의 크기가 5000이기 때문에 문제가 발생할 수 있다.
- 그리고 이러한 취약점의 원인으로는 prefix, name을 xmlSnprintfElementContent()에서 크기를 검증하는 과정에 있다.
CVE-2017-9047
// valid.c - xmlSnprintfElementContent()
void
xmlSnprintfElementContent(char *buf, int size, xmlElementContentPtr content, int englob) {
int len;
if (content == NULL) return;
len = strlen(buf);
if (size - len < 50) {
if ((size - len > 4) && (buf[len - 1] != '.'))
strcat(buf, " ...");
return;
}
if (englob) strcat(buf, "(");
switch (content->type) {
case XML_ELEMENT_CONTENT_PCDATA:
strcat(buf, "#PCDATA");
break;
case XML_ELEMENT_CONTENT_ELEMENT:
if (content->prefix != NULL) {
if (size - len < xmlStrlen(content->prefix) + 10) {
strcat(buf, " ...");
return;
}
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (size - len < xmlStrlen(content->name) + 10) {
strcat(buf, " ...");
return;
}
if (content->name != NULL)
strcat(buf, (char *) content->name);
break;
/**
* xmlStrlen:
* @str: the xmlChar * array
*
* length of a xmlChar's string
*
* Returns the number of xmlChar contained in the ARRAY.
*/
int
xmlStrlen(const xmlChar *str) {
int len = 0;
if (str == NULL) return(0);
while (*str != 0) { /* non input consuming */
str++;
len++;
}
return(len);
}
- 코드를 보면, xmlStrlen()은 단순히 문자열의 길이를 반환하는 함수임을 알 수 있다.
- 여기서 size(5000)를 단순히 prefix와 name을 각각 분리해서 비교하고, 업데이트가 안된 len을 strlen(buf) 대신 비교함으로써 prefix와 name의 크기 조건문을 통과시키면 stack buffer overflow가 발생하게 된다.
CVE-2017-9048
// xmlSnprintfElementContent() 이어서
if (englob)
strcat(buf, ")");
switch (content->ocur) {
case XML_ELEMENT_CONTENT_ONCE:
break;
case XML_ELEMENT_CONTENT_OPT:
strcat(buf, "?");
break;
case XML_ELEMENT_CONTENT_MULT:
strcat(buf, "*");
break;
case XML_ELEMENT_CONTENT_PLUS:
strcat(buf, "+");
break;
}
- 다음 코드는 CVE-2017-9047 이후에 바로 이어지는 코드이다.
- 여기서 buf에 최대 길이가 2인 문자가 들어갈 수 있기 때문에, 해당 코드에 도달하기 전에 미리 buf의 크기를 확인해야 한다.
- 만약 그렇지 않으면, 마찬가지로 overflow가 발생하게 된다.
4. 취약점 패치
CVE-2017-9047

- prefix, name을 각자 비교하지 않고, 길이를 더해 한꺼번에 비교하도록 패치되었다.
CVE-2017-9048

- overflow가 발생하는 로직 전에 buf의 크기가 3이상으로 충분한지 확인하도록 패치되었다.