LLVM instrumentation을 활용한 WinAFL 오픈소스 퍼징# 0. Intro 안녕하세요! LLVM instrumentation을 활용한 WinAFL 오픈소스 퍼징 주제로 블로그 포스팅을 하게 된 D0C70R입니다. 이 내용은 2026년 한국정보보호학회 하계학술대회에 투고한 논문의 내용이며, 아직 많이 부족한 점 양해 부탁드립니다. AFL 퍼저는 많은 분들이 한 번쯤 사용해보셨을 퍼저입니다. 주로 리눅스에서는 AFL이나 AFL++ 퍼저가 많이 활용되며, 특히 오픈소스에서 커버리지 계측을 위한 instrumentation 삽입을 주 목적으로 하는 afl-clang을 제공합니다. 반면 윈도우 환경에서는 WinAFL이 있어 윈도우 바이너리를 퍼징하는 데 활용되고 있습니다. WinAFL이 커버리지 계측을 위해 제공하는 방식은 Dynamic Binary Instrumentation(이하 DBI) 계열인 DynamoRIO와 TinyInst, Intel CPU 환경에서 트레이싱 기능을 활용한 계측 방식인 IntelPT, 그리고 x86 환경에서 정적 바이너리 계측 방식인 Syzygy 방식입니다. 하지만 오픈소스의 경우 DBI는 클로즈드 소스 퍼징과 차이점이 없으며, Intel CPU가 없을 경우 IntelPT 방식도 사용할 수 없습니다. 또한 x86 바이너리에만 적용되는 Syzygy로 인해 오픈소스라는 장점을 충분히 살리지 못하고 있습니다. 이 글에서는 리눅스 계열의 afl-clang을 윈도우 환경에 적용하여, 오픈소스 바이너리 빌드 시 instrumentation을 삽입하고 커버리지를 측정하는 방식으로 WinAFL의 오픈소스 퍼징 한계를 극복하는 내용을 작성했습니다. --- # 1. LLVM instrumentation 구현 및 적용 ### 1-1. instrumentation hook 삽입 모듈 설계 이 논문에서 설계한 clang wrapper는 C/C++ 컴파일 과정에서 LLVM pass 플러그인을 주입하고, 해당 pass가 중간언어(IR) 단계에서 각 Basic Block 시작점에 `__afl_trace`라는 instrumentation hook을 삽입합니다. 아래는 LLVM pass 플러그인으로 함수와 Basic Block을 모두 순회하며 `__afl_trace`를 삽입하는 코드 중 주요 부분만 가져온 것입니다. 해당 코드는 `coverage-pass.dll` 파일로 빌드됩니다. ```c // coverage-pass.dll FunctionCallee hook = M.getOrInsertFunction("__afl_trace", hookTy); // LLVM 모듈 안에 __afl_trace 없을 경우 추가 for (Function& F : M) { unsigned bb_index = 0; if (F.isDeclaration()) continue; if (F.getName().starts_with("__afl_trace")) continue; // __afl_trace가 존재할 경우 instrumentation hook 삽입 제외 for (BasicBlock& BB : F) { // 함수의 Basic Block 순회 auto insertPt = BB.getFirstInsertionPt(); if (insertPt == BB.end()) continue; builder.CreateCall(hook, { builder.getInt32(cur_loc) }); // Basic Block 시작점에 __afl_trace 삽입 } } ``` 아래 코드는 \__afl_trace 함수 내부 구조입니다. 현재의 Basic Block ID를 인자로 받아오며, coverage_standby() 함수를 호출해 임시로 만든 공유 메모리(WinAFL과는 별도로 만들어진 공유 메모리)를 불러옵니다. 이후 이전 Basic Block과 현재 Basic Block ID를 XOR하여 Edge coverage를 계산하고, AND 연산으로 coverage map에 반영한 뒤 bitmap을 증가시킵니다. ```c // coverage-rt.lib void __cdecl __afl_trace(uint32_t cur_loc) { uint32_t idx; coverage_standby(); idx = (cur_loc ^ g_prev_loc) & (MAP_SIZE - 1); // 이전 Basic Block ID 값과 현재 Basic Block ID 값으로 Edgce coverage 계산 g_state.area[idx]++; // Edge가 실행될 경우 coverage bitmap 증가 g_prev_loc = (cur_loc >> 1); // 현재 Basic Block ID를 이전 Basic Block ID로 swap } ``` --- ### 1-2. clang wrapper 설계 1-1에서 instrumentation hook을 삽입하고 커버리지 비트맵에 반영하기 위한 모듈들을 설명했습니다. 우리가 가진 모듈은 coverage-pass.dll, coverage-rt.lib 두 개입니다. 이제 이 두 라이브러리를 적용하기 위한 clang 기반의 wrapper만 설계해주면, instrumentation hook이 삽입된 바이너리로 컴파일할 수 있습니다. clang wrapper의 기능은 아주 간단합니다. clang 컴파일 흐름에서 IR 단계 때 coverage-pass.dll을 불러와 instrumentation hook을 삽입하며 object 파일을 생성합니다. 이후 링크 단계에서 coverage-rt.lib를 호출해 실제 구현된 `__afl_trace`를 연결하고, 커버리지 및 비트맵에 반영하는 기능을 링킹합니다. ```c // afl-clang.c main 함수 strcpy(tool_dir, sizeof(tool_dir), self); dirname_inplace(tool_dir); // 모듈 호출 snprintf(pass_dll, sizeof(pass_dll), _TRUNCATE, "%s\\coverage-pass.dll", tool_dir); snprintf(rt_lib, sizeof(rt_lib), _TRUNCATE, "%s\\coverage-rt.lib", tool_dir); ``` `afl-clang.exe`가 있는 폴더를 기준으로 라이브러리를 불러오며, 최종적으로 instrumentation hook이 삽입된 `target.exe`가 생성됩니다. --- ### 1-3.WinAFL 연동 일단 여차저차 afl-clang을 윈도우에도 그럴듯하게 구현했지만, WinAFL은 Intro에서 언급한 네 가지 방식만 지원하기 때문에 LLVM clang을 활용한 커버리지 계측 방식은 지원되지 않습니다. 하지만 WinAFL은 소스코드를 제공하기 때문에 일부 수정을 하면 이 논문에서 원하는 방식으로 퍼징이 가능합니다. WinAFL의 공유 메모리에 타겟이 직접 커버리지를 기록하는 역할을 담당할 dll을 제작합니다. 이 라이브러리는 퍼징할 때 `-l` 옵션만 추가하여 해당 라이브러리를 불러오고, coverage-rt.lib 내의 공유 메모리를 WinAFL에 반영하는 기능을 수행합니다. 먼저 아래 코드는 자식 프로세스와 공유할 coverage bitmap 공유 메모리를 만드는 과정입니다. `g_map_handle`에 공유 메모리(64KB)가 들어가고, `MapViewOfFile` 함수로 프로세스 주소 공간에 매핑해서 포인터로 접근하도록 설계했습니다. ```c // llvm_runner.dll static int create_map(void) { ... // 공유 메모리 영역 g_map_handle = CreateFileMappingA( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MAP_SIZE, g_map_name); g_map_view = (uint8_t*)MapViewOfFile(g_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, MAP_SIZE); // 현재 프로세스가 공유 메모리에 접근하도록 매핑 memset(g_map_view, 0, MAP_SIZE); // coverage map 초기화 return 1; } ``` 위 코드에서 `g_map_name`은 위에서 만든 coverage-rt.lib 모듈의 공유 메모리 환경변수 `AFL_COVERAGE_MAP`에 저장하여 별도의 공유 메모리에 접근해 기록하고 이후 한 사이클이 끝나면 WinAFL 공유 메모리에 반영하는 코드입니다. ```c // llvm_runner.dll 내 dll_run_target 함수 일부 if (!SetEnvironmentVariableA("AFL_COVERAGE_MAP", g_map_name) || // AFL_COVERAGE_MAP 환경변수에 g_map_name 저장 !SetEnvironmentVariableA("AFL_COVERAGE_CLEAR", "1") || !SetEnvironmentVariableA("AFL_COVERAGE_FILE", NULL) || !SetEnvironmentVariableA("AFL_STATIC_CONFIG", NULL)) { return FAULT_ERROR; } ... memcpy(trace_bits, g_map_view, copy_size); // WinAFL 공유 메모리에 임시 공유 메모리인 g_map_handle 복사 ``` 이제 WinAFL의 기능만 조금 손보면 됩니다. 아래는 위에서 언급한 대로 `-l` 옵션으로 설계한 instrumentation으로 퍼징하는 옵션을 추가하고 llvm_runner.dll을 로드하는 기능을 추가한 코드 일부입니다. ```c void load_custom_library(const char *libname){ HMODULE hLib = LoadLibraryEx(libname, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // llvm_runner.dll 내 coverage map 반영 함수 호출 dll_init_ptr = (dll_init)GetProcAddress(hLib, "dll_init"); dll_run_ptr = (dll_run)GetProcAddress(hLib, "dll_run"); dll_run_target_ptr = (dll_run_target)GetProcAddress(hLib, "dll_run_target"); } int main(int argc, char **argv){ // -l 옵션 추가 case 'l': custom_dll_defined = 1; load_custom_library(optarg); // llvm_runner.dll 로드 break; } ``` 모든 준비는 끝났습니다! 이제 LLVM instrumentation 삽입을 위한 afl-clang.exe, coverage-pass.dll, coverage-rt.lib 모듈과 WinAFL 퍼징을 위한 llvm_runner.dll, 그리고 커스터마이징된 afl-fuzz.exe 파일이 생성되었습니다. --- # 2. 1-Day 취약점 트리거 실험 퍼징을 위한 PC의 운영체제는 Windows 11 Education 25H2이며, CPU는 AMD Ryzen 5 5600 6-Core Processor로 IntelPT 방식을 사용할 수 없는 환경입니다. 따라서 테스트한 instrumentation은 DynamoRIO, TinyInst, Syzygy, 그리고 LLVM instrumentation인 afl-clang입니다. 선정한 1-Day 취약점은 CVE-2016-9297(tiffinfo), CVE-2017-9048(xmllint), CVE-2017-13028(TCPdump), CVE-2019-13109(Exiv2), CVE-2019-13288(Xpdf)입니다. Syzygy 비교도 함께 진행하기 위해 x86 환경으로 빌드했습니다. 모두 동일한 환경에서 3시간 퍼징을 진행했으며, 커버리지 확장부터 바이너리 실행 속도, 1-Day 취약점 트리거 여부 및 트리거 소요 시간을 비교했습니다. --- ### 최종 커버리지 확장 | CVE(Target) | afl-clang(Ours) | DynamoRIO | TinyInst | Syzygy | | --- | --- | --- | --- | --- | | CVE-2016-9297(tiffinfo) | 36% | 14% | 13% | 10.5% | | CVE-2017-9048(xmllint) | 35% | 18.5% | 17.5% | 0.3% | | CVE-2017-13028(TCPdump) | 54% | 38.5% | 10.5% | 6.5% | | CVE-2019-13109(Exiv2) | 2.45% | 1.55% | 0.35% | 0.08% | | CVE-2019-13288(Xpdf) | 13.5% | 13.0% | 8.5% | 9.3% | ### 바이너리 실행 속도(exec/s) | CVE(Target) | afl-clang(Ours) | DynamoRIO | TinyInst | Syzygy | | --- | --- | --- | --- | --- | | CVE-2016-9297(tiffinfo) | 91.2792 | 3.4808 | 1.0883 | 21.9930 | | CVE-2017-9048(xmllint) | 42.2399 | 1.2308 | 1.2798 | 123.2742 | | CVE-2017-13028(TCPdump) | 27.5253 | 1.3911 | 0.669 | 0.4461 | | CVE-2019-13109(Exiv2) | 17.8563 | 55.0731 | 11.469 | 55.2034 | | CVE-2019-13288(Xpdf) | 12.4583 | 30.9091 | 15.7029 | 33.0855 | ### 1-Day 취약점 트리거 시간(min) | CVE(Target) | afl-clang(Ours) | DynamoRIO | TinyInst | Syzygy | | --- | --- | --- | --- | --- | | CVE-2016-9297(tiffinfo) | 57.846 | N/A | N/A | N/A | | CVE-2017-9048(xmllint) | 124.836 | N/A | N/A | N/A | | CVE-2017-13028(TCPdump) | 136.698 | N/A | N/A | N/A | | CVE-2019-13109(Exiv2) | N/A | N/A | N/A | N/A | | CVE-2019-13288(Xpdf) | 2.5 | 5.516 | N/A | N/A | 전반적으로 afl-clang의 커버리지 확장이 가장 우수한 것을 볼 수 있었습니다. Exiv2는 비교적 복잡하여 최종 2.45%까지만 도달했으나, TCPdump에서는 54%까지 도달한 것을 확인할 수 있었습니다. Xpdf는 afl-clang과 DynamoRIO가 비슷한 성능으로 나타났습니다. 한편 실행 속도 측면에서는 afl-clang이 무조건적으로 우세하진 않았습니다. 오히려 Syzygy가 xmllint에서 초당 123.2742로 빠르게 나타났으며, Exiv2와 Xpdf에서는 DynamoRIO보다 느리고 TinyInst와 거의 비슷한 실행 속도를 보였습니다. 1-Day 취약점 트리거 결과는 afl-clang이 Exiv2를 제외한 모든 타겟에서 성공했으며, afl-clang을 제외하고는 DynamoRIO가 유일하게 Xpdf에서 취약점 트리거에 성공했습니다. --- # 3. 결론 및 향후 연구방향 본 논문은 리눅스 AFL의 LLVM clang instrumentation 방식을 모방해 윈도우의 WinAFL 환경에 적용해보는 실험을 진행했습니다. 기존 WinAFL의 instrumentation 방식보다 LLVM instrumentation이 커버리지 증가율은 높았고, 같은 타겟에서 준수한 실행 속도를 보였으며, 취약점 트리거 성공률도 준수했습니다. 구현된 afl-clang은 대조군보다 오버헤드가 적고, 컴파일의 중간언어 단계에서 Basic Block 시작점에 직접 `__afl_trace`를 삽입해 퍼징 대상의 모든 소스코드에 대한 커버리지를 정확히 측정할 수 있다는 장점을 증명했습니다. 향후 연구에서는 설계한 Clang wrapper를 LTO 모드로 구현해 퍼징 타겟과 링킹하는 방식으로 빌드하고, 오픈소스를 대상으로 0-Day 취약점 발견을 목표로 실험해볼 예정입니다. 긴 글 읽어주셔서 감사합니다! --- # 4. Reference Fioraldi, A., Mantovani, A., Maier, D., & Balzarotti, D. (2023). Dissecting american fuzzy lop: a fuzzbench evaluation. ACM transactions on software engineering and methodology, 32(2), 1-26. Fioraldi, A., Maier, D., Eißfeldt, H., & Heuse, M. (2020). {AFL++}: Combining incremental steps of fuzzing research. In 14th USENIX workshop on offensive technologies (WOOT 20). Bruening, D., & Amarasinghe, S. (2004). Efficient, transparent, and comprehensive runtime code manipulation. Chen, Y., Mu, D., Xu, J., Sun, Z., Shen, W., Xing, X., ... & Mao, B. (2019, July). Ptrix: Efficient hardware-assisted fuzzing for cots binary. In Proceedings of the 2019 ACM Asia Conference on Computer and Communications Security (pp. 633-645). winafl, TinyInst, Syzygy, Fuzzing101,