목차
RS-OS/App팀 dgkim(dong) 연구원
1. 개요
수 많은 테스트 케이스와 코드 리뷰만으로는 현실 세계 소프트웨어의 모든 버그 및 취약점을 찾을 수 없다. 이때 Fuzzing은 가장 효과적인 방법이 될 수 있다. Fuzzing은 의도적으로 조작된 비정상적인 데이터를 대량으로 프로그램에 입력하여 숨겨진 결함과 보안 취약점을 찾아내는 자동화된 테스트 기법이다.
From Zero to 0-day 시리즈에서는 Fuzzing의 기본 개념, 실제 환경에서 어떻게 활용하는지에 대한 가이드를 제공한다. Windows/Linux Application Fuzzing과 Windows Driver Fuzzing의 과정을 단계별로 실습하며 실제로 CVE를 획득하기까지의 과정을 공유한다.
1부에서는 Fuzzing의 개요, Linux Application Fuzzing을 실습한다.
2. Fuzzing 기법
2.1. Fuzzing 개요
Fuzzing은 테스트 대상 프로그램의 내부 정보를 얼마나 활용하는지에 따라 크게 Black-box Fuzzing, White-box Fuzzing, Grey-box Fuzzing 방식으로 나뉜다.
2.1.1. Black-box Fuzzing
Black-box Fuzzing은 프로그램의 내부 구조를 전혀 보지 않고, 입력 값을 무작위로 생성하고 출력 및 예외 처리를 모니터링하여 취약점을 찾는 방법이다.
•
특징
◦
내부 구조를 보지 않음.
◦
입력/출력(I/O) 기반: 프로그램에 데이터를 입력하고 그 결과를 관찰하는 방식으로만 취약점을 찾음
◦
데이터 중심적 : 프로그램 자체보다는 입력 데이터의 구조를 분석하고 변형하는 데 집중하여 테스트 케이스를 만듦
◦
대표적인 예: funfuzz, Peach 등
2.1.2. White-box Fuzzing
White-box Fuzzing은 프로그램 내부에 대한 정보를 수집한 후, 입력 데이터를 생성하여 취약점을 찾는 방법이다.
동적 기호 실행(DSE: Dynamic Symbolic Execution) 또는 Concolic Testing (Concrete + Symbolic) 기법을 사용하여 실행 경로를 효과적으로 탐색한다.
•
특징
◦
프로그램 내부 분석.
◦
체계적인 경로 탐색: 프로그램의 논리 구조를 기반으로, 코드의 다양한 실행 경로를 체계적으로 탐색하여 다양한 버그 탐색 가능
◦
높은 오버헤드 : 내부 분석을 위해 추가적인 연산을 수행하므로, Black-box 방식에 비해 훨씬 느리고 더 많은 컴퓨팅 자원을 소모하는 단점 존재
◦
주요 기술: 동적 기호 실행(DSE: Dynamic Symbolic Execution), 또는 Concolic Testing(Concolic Testing)
◦
대표적인 예: Driller, SAGE 등
2.1.3. Grey-box Fuzzing
Grey-box Fuzzing은 Black-box와 White-box 기법의 중간 형태로, 프로그램 내부 정보를 일부 활용하여 취약점을 찾는 방법이다.
주로 코드 커버리지 정보를 수집하여, 새로운 코드 경로를 탐색하는 방향으로 Fuzzing을 진행한다.
•
특징
◦
실행 정보(예: 코드 커버리지, 기본적인 실행 흐름 분석)를 활용하여 효율적으로 입력을 조작
◦
White-box Fuzzing보다 빠르고, Black-box Fuzzing보다 효과적인 취약점 탐색 가능
◦
대표적인 예: AFL, VUzzer, EFS 등
2.2. Linux Fuzzing 도구
2.2.1. AFL++
AFL++은 대표적인 Grey-box Fuzzing의 도구인 AFL(American Fuzzy Lop)을 기반으로 한 Fuzzing 도구로, Linux 환경에서 Fuzzing을 지원한다.
•
AFL++ (Linux)
◦
컴파일 시 계측 코드 삽입
◦
Forkserver를 활용한 빠른 Fuzzing
2.2.2. Sanitizer
Sanitizer는 프로그램 실행 중 메모리 및 정의되지 않은 동작(Undefined Behavior) 관련 오류를 실시간으로 감지하는 런타임 검증 도구이다.
이를 통해 Fuzzing 과정에서 발생하는 Buffer Overflow, Use-After-Free(UAF), Double-Free, Integer Overflow, Use of Uninitialized Memory 등을 탐지하여 발견하기 힘든 미묘한 버그들까지 발견할 수 있도록 도와준다.
Sanitizer는 컴파일 과정에서 소스 코드에 버그를 탐지하기 위한 코드를 삽입하는 방식으로 동작하기 때문에, 분석 대상 프로그램의 소스 코드가 필요하다.
2.2.3. Address Sanitizer 예제
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 10
#define STACK_SIZE SIZE*2
int main() {
uint32_t input_size;
uint8_t stack_buf[STACK_SIZE] = { 0 };
printf("stack_buf = %p\nsize = %d\n", stack_buf, STACK_SIZE);
memset(stack_buf, 0x41, STACK_SIZE);
uint8_t* heap_buf = (uint8_t*)malloc(SIZE);
printf("heap_buf = %p\nsize = %d\n", heap_buf, SIZE);
scanf("%d", &input_size);
printf("memcpy(heap_buf, stack_buf, %d)", input_size);
memcpy(heap_buf, stack_buf, input_size);
free(heap_buf);
return 0;
}
C++
복사
Address Sanitizer 사용 샘플 : 코드-1
$ gcc subtle_bug.c -o subtle_bug_without_sanitizer
$ ./subtle_bug_without_sanitizer
stack_buf = 0x7ffccc9f4d30
size = 20
heap_buf = 0x61b4b9a346b0
size = 10
10
memcpy(heap_buf, stack_buf, 10)
$ echo $?
0
$ ./subtle_bug_without_sanitizer
stack_buf = 0x7ffe3c1fd240
size = 20
heap_buf = 0x64dc42e356b0
size = 10
11
memcpy(heap_buf, stack_buf, 11)
$ echo $?
0
Bash
복사
Address Sanitizer없이 비정상적인 입력 : 비정상 종료 되지 않음
그러나 코드-1을 AddressSanitizer와 함께 컴파일(-fsanitize=address 옵션 활용)하고 heap_buf 크기(10)보다 더 큰 값(11)을 입력하면 AddressSanitizer에 의해 프로그램이 비정상 종료된다.
$ gcc subtle_bug.c -o subtle_bug_with_sanitizer -fsanitize=address
$ ./subtle_bug_with_sanitizer
stack_buf = 0x75c146600040
size = 20
heap_buf = 0x75e1479e0010
size = 10
10
memcpy(heap_buf, stack_buf, 10)
$ echo $?
0
Bash
복사
Address Sanitizer 정상적인 입력 : 정상 종료됨.
$ ./subtle_bug_with_sanitizer
stack_buf = 0x70b166200040
size = 20
heap_buf = 0x70d1675e0010
size = 10
11
=================================================================
==3495881==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x70d1675e001a at pc 0x74b168b1e307 bp 0x7ffeb3773b70 sp 0x7ffeb3773318
WRITE of size 11 at 0x70d1675e001a thread T0
// ...
Bash
복사
AddressSanitizer를 활성화 한 후 Heap Overflow 탐지 : 비정상 종료
AddressSanitizer는 11바이트 데이터를 쓰는 과정에서 발생한 Heap Buffer Overflow를 탐지했다. 이처럼 ASan을 적용하면, 당장 프로그램의 Crash를 유발하지 않더라도 메모리를 손상시키는 잠재적인 버그까지 정확히 찾아낼 수 있다.
AddressSanitizer가 이러한 버그를 탐지 할 수 있는 이유
3. 환경 구성 : Linux AFL++ with docker
•
환경 정보
◦
OS : Ubuntu 24.04 (WSL 또는 Bare-metal)
◦
AFL++ 버전 : 4.33a
$ docker run --name linux_fuzzing -ti aflplusplus/aflplusplus
$ docker start linux_fuzzing
linux_fuzzing
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64482c79abe8 aflplusplus/aflplusplus "/bin/bash" About a minute ago Up 2 seconds linux_fuzzing
$ docker exec -it linux_fuzzing /bin/bash
[AFL++ 64482c79abe8] /AFLplusplus $ cd ~
[AFL++ 64482c79abe8] ~ $ ls
[AFL++ 64482c79abe8] ~ $ afl-fuzz
[+] Enabled environment variable AFL_SKIP_CPUFREQ with value 1
[+] Enabled environment variable AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES with value 1
[+] Enabled environment variable AFL_TRY_AFFINITY with value 1
afl-fuzz++4.33a based on afl by Michal Zalewski and a large online community
afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]
Bash
복사
docker를 이용한 AFL++ 설치
4. Fuzzing Target 선정
4.1. Fuzzing Target 선정 기준
본 장에서는 Fuzzing Target 선정 기준에 대해 공유한다. 이를 통해 Fuzzing Target의 내부 구조에 대한 깊은 이해 없이 취약점을 빠르게 찾는 것을 목표로 한다.
1.
Fuzzing Harness 작성 난이도 : 낮음
Harness 작성이 쉬울수록 더 빠르게 Fuzzing을 시작할 수 있다. 이미지(PNG, JPG), 문서(PDF, XML) 등 정해진 구조의 파일 포맷을 파싱하는 라이브러리는 입력과 출력이 명확하고, 특정 파일을 처리하는 단일 함수만 호출하면 되는 단순한 구조인 경우가 많다. 따라서 이는 Harness 작성이 쉬워 이상적인 Target이 된다.
2.
Fuzzing Target의 구현 및 알고리즘의 복잡성 : 복잡함
코드의 로직이나 알고리즘이 복잡할수록 개발자가 예측하지 못한 Edge Case가 존재할 확률이 증가한다. Fuzzer는 개발자가 생각하기 힘든 비정상적인 값을 연속적으로 입력하여, 이러한 복잡한 로직의 허점을 효과적으로 찾아낸다.
3.
파급력 : 큼
인기 있는 소프트웨어는 많은 시스템에 설치되어 사용된다. 이러한 소프트웨어에서 취약점이 발생하면, 해당 소프트웨어를 사용하는 시스템이 잠재적인 위험에 노출되므로 그 파급력이 매우 크다.
따라서 파급력이 높은 소프트웨어를 Fuzzing Target으로 삼는 것은, 잠재적 위협으로부터 광범위한 시스템을 보호한다는 점에서 큰 의미를 가진다. 또한, 인기 있는 소프트웨어는 문서화가 잘 이루어져 있어 Harness 작성의 난이도를 낮출 수 있다.
4.
오픈소스 또는 SDK
소스 코드가 공개되어 있거나 분석 가능한 SDK가 제공되면 훨씬 효율적인 Fuzzing이 가능하다. 소스 코드가 있으면 AFL++ 등의 도구를 활용한 컴파일 시점의 커버리지 기반 Fuzzing이 가능하다. 그러나 소스 코드가 없더라도 SDK가 존재하면 함수 정보나 사용법을 명확히 알 수 있어 Harness 작성에 큰 도움이 된다.
4.2. Fuzzing Target 선정 및 근거
4.2.1. Linux/Windows Target : libarchive
1.
Fuzzing Harness 작성 난이도 : 낮음
2.
Fuzzing Target의 구현, 알고리즘의 복잡성 : 복잡함
libarchive는 다양한 포맷을 지원하므로 알고리즘, 구현이 복잡할 가능성이 높다.
3.
파급력 : 큼
libarchive는 3.2K의 Github Star를 가지고 있으며, Windows 내장 압축 해제 라이브러리로 활용되고 있어 취약점 발생 시 파급력이 크다.
4.
오픈소스
소스 코드가 공개되어 있어 AFL++를 활용해 Fuzzing이 가능하다.
5. Fuzzing 진행
5.1. libarchive (Linux) Harness 작성
1.
AFL++ docker 환경에서 libarchive를 위한 종속성 패키지를 설치한다.
[AFL++ 64482c79abe8] ~ $ apt-get update && apt-get install -y libbz2-dev liblzma-dev libb2-dev liblz4-dev libzstd-dev libssl-dev
Bash
복사
2.
libarchive Repository를 clone 한다.
[AFL++ 64482c79abe8] ~ $ git clone https://github.com/libarchive/libarchive
[AFL++ 64482c79abe8] ~ $ cd libarchive
Bash
복사
3.
libarchive의 Fuzzing 효율을 높이기 위해, Code coverage의 증가를 방해하는 CRC 검사 기능을 제거(-DDONT_FAIL_ON_CRC_ERROR=ON)한다. 빌드에는 afl-clang-fast 컴파일러를 사용하며, 메모리 오류 탐지를 위해 Sanitizer를 활성화(USE_AFL_ASAN=1)한다.
[AFL++ 64482c79abe8] ~/libarchive $ cmake -S . -DDONT_FAIL_ON_CRC_ERROR=ON -DCMAKE_C_COMPILER=afl-clang-fast -DCMAKE_CXX_COMPILER=afl-clang-fast++ -DENABLE_WERROR=OFF -B libarchive_build
[AFL++ 64482c79abe8] ~/libarchive $ cd libarchive_build
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build $ USE_AFL_ASAN=1 make -j `nproc`
Bash
복사
Harness 작성 시 OSS-Fuzz를 위한 Fuzzing Harness(libarchive_fuzzer.cc)를 사용할 수도 있으나, bsdtar를 이용한다. OSS-Fuzz를 위한 Fuzzing Harness는 내부 검증 로직이 상대적으로 적어 특정 조건에서는 실제 취약점으로 연결되지 않는 경우가 존재한다. bsdtar는 사용자에게 바이너리 형태로 직접 배포되므로, 이곳에서 발견된 취약점은 실제 사용자에게 더 큰 영향을 미친다. 따라서 Fuzzing 결과를 실제 취약점 시나리오와 효과적으로 연결하기 위해 bsdtar를 Target으로 사용한다.
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ ls
bsdcat bsdcat_test bsdcpio bsdcpio_test bsdtar bsdtar_test bsdunzip bsdunzip_test libarchive_test
Bash
복사
Fuzzing Harness : bsdtar
•
효율적인 Fuzzing을 위한 bsdtar 옵션 분석
Fuzzing 과정에서 생성되는 수많은 파일은 디스크 용량을 채워 Fuzzing을 방해하는 요인이 될 수 있다. 이를 해결하기 위해 bsdtar의 옵션을 분석하여 압축 해제 시 파일이 디스크에 생성되지 않도록 설정한다.
$ ./bsdtar -h
bsdtar: manipulate archive files
First option must be a mode specifier:
-c Create -r Add/Replace -t List -u Update -x Extract
Common Options:
-b # Use # 512-byte records per I/O block
-f <filename> Location of archive (default /dev/st0)
-v Verbose
-w Interactive
Create: bsdtar -c [options] [<file> | <dir> | @<archive> | -C <dir> ]
<file>, <dir> add these items to archive
-z, -j, -J, --lzma Compress archive with gzip/bzip2/xz/lzma
--format {ustar|pax|cpio|shar} Select archive format
--exclude <pattern> Skip files that match pattern
--mtime <date> Set modification times for added files
--clamp-mtime Only set modification times for files newer than --mtime
-C <dir> Change to <dir> before processing remaining files
@<archive> Add entries from <archive> to output
List: bsdtar -t [options] [<patterns>]
<patterns> If specified, list only entries that match
Extract: bsdtar -x [options] [<patterns>]
<patterns> If specified, extract only entries that match
-k Keep (don't overwrite) existing files
-m Don't restore modification times
-O Write entries to stdout, don't restore to disk
-p Restore permissions (including ACLs, owner, file flags)
bsdtar 3.9.0 - libarchive 3.9.0dev zlib/1.2.11 liblzma/5.2.5 bz2lib/1.0.8 liblz4/1.9.3 libzstd/1.4.8 libxml2/2.9.13 openssl/3.0.2 libb2/system
Bash
복사
1.
파일 생성 방지 옵션
bsdtar의 도움말(-h 옵션)을 분석한 결과 아래와 같은 정보를 얻을 수 있다.
•
-x: 압축 해제 모드
•
-f <파일명>: 대상 압축 파일 지정
•
-O: 결과를 디스크가 아닌 표준 출력(stdout)으로 전송
2.
CRC 검사 비활성화 옵션
libarchive의 소스 코드(archive_read_support_format_zip.c) 분석을 통해 ignorecrc32 옵션의 존재를 확인할 수 있다.
이 옵션을 활성화하면 ZIP 파일 압축 해제 시 불필요한 CRC검사를 생략하여 더 높은 커버리지와 성능을 높일 수 있다.
// ...
} else if (strcmp(key, "ignorecrc32") == 0) {
/* Mostly useful for testing. */
if (val == NULL || val[0] == 0) {
zip->crc32func = real_crc32;
zip->ignore_crc32 = 0;
} else {
zip->crc32func = fake_crc32;
zip->ignore_crc32 = 1;
}
return (ARCHIVE_OK);
}
// ...
C
복사
archive_read_support_format_zip.c 내부 ignorecrc32 설정
•
--options "zip:ignorecrc32=1": ZIP 포맷에 한해 CRC32 검사를 비활성화한다.
3.
최종 테스트
위 옵션들을 조합하여 압축 해제 테스트를 진행한다.
[AFL++ 64482c79abe8] $ ls
test1 test2 bsdtar
[AFL++ 64482c79abe8] $ cat test1
test1
[AFL++ 64482c79abe8] $ cat test2
test2
[AFL++ 64482c79abe8] $ zip test.zip test1 test2
[AFL++ 64482c79abe8] $ rm test1 test2
[AFL++ 64482c79abe8] $ ./bsdtar -O --options "zip:ignorecrc32=1" -xf ./test.zip
test1
test2
[AFL++ 64482c79abe8] $ ls
bsdtar test.zip
Bash
복사
파일이 디스크에 생성되지 않고 파일 내용(test1, test2)만 표준 출력으로 나오는 것을 확인할 수 있다.
5.2. Seed Corpus 수집
Coverage-guided Fuzzing에서 가장 중요한 요소는 양질의 초기 입력 데이터, 즉 Seed corpus를 확보하는 것이다.
Seed corpus가 높은 코드 coverage를 달성할수록, Fuzzer가 더 깊은 로직에 도달하여 취약점을 발견할 확률이 높아진다.
5.3. libarchive (Linux) Fuzzing
1.
Fuzzing 환경 준비
Linux에서 장시간 안정적으로 Fuzzing 세션을 유지하기 위해 tmux를 사용하는 것을 추천한다. tmux를 사용하면 터미널 연결이 끊어져도 Fuzzing 작업을 계속 실행할 수 있다.
[AFL++ 64482c79abe8] $ apt install tmux -y
[AFL++ 64482c79abe8] $ tmux new-session -t libarchive_fuzzing
Bash
복사
2.
초기 corpus 최적화
2.1. 중복 corpus 제거 (afl-cmin)
afl-cmin은 원본 corpus 집합에서 서로 중복되는 코드 경로를 실행하는 파일들을 제거하여, 최소한의 파일 집합만 남기는 도구이다.
[AFL++ 64482c79abe8] $ cd ~/libarchive/libarchive_build/bin
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-cmin -i original_seed -o cmin_seed -- ./bsdtar -O --options "zip:ignorecrc32=1" -xf @@
Bash
복사
2.2. corpus 파일 크기 최소화 (afl-tmin)
afl-tmin은 동일한 Instrumentation 결과 혹은 Crash를 유지하면서 입력 파일의 크기를 줄이는 도구이다.
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ cat tmin.sh
#!/bin/bash
# 입력 및 출력 디렉터리 설정
INPUT_DIR="cmin_seed"
OUTPUT_DIR="tmin_seed"
# 출력 디렉터리가 없으면 생성
mkdir -p "$OUTPUT_DIR"
# 입력 디렉터리의 모든 파일에 대해 반복
for f in "$INPUT_DIR"/*
do
# 파일이 존재하는지 확인 (빈 디렉터리 등의 경우)
if [ -f "$f" ]; then
# 입력 파일의 기본 이름(경로 제외)을 가져옴
BASENAME=$(basename "$f")
# 각 입력 파일에 대한 출력 파일 이름 지정
OUTPUT_FILE="$OUTPUT_DIR/$BASENAME.min"
echo "Processing $f -> $OUTPUT_FILE"
# afl-tmin 명령어 실행
afl-tmin -i "$f" -o "$OUTPUT_FILE" -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
fi
done
echo "All done."
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ ./tmin.sh
Bash
복사
3.
Fuzzing 실행
corpus 최적화가 완료되면, 최종적으로 생성된 시드(tmin_seed)를 사용하여 Fuzzing을 시작한다. 또한, 여러 CPU 코어를 활용하여 Fuzzing 효율을 극대화하기 위해 Master-Slave 구조를 사용한다.
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-fuzz -i ./tmin_seed -o output -M master -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-fuzz -i ./tmin_seed -o output -S slave1 -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-fuzz -i ./tmin_seed -o output -S slave3 -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-fuzz -i ./tmin_seed -o output -S slave4 -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
[AFL++ 64482c79abe8] ~/libarchive/libarchive_build/bin $ afl-fuzz -i ./tmin_seed -o output -S slave5 -- ./bsdtar --options "zip:ignorecrc32=1" -O -xf @@
Bash
복사
1.
진행 상황 (Process & Cycle)
•
run time: Fuzzer가 총 실행된 시간. (현재 5분)
•
last new find: 새로운 코드 경로를 마지막으로 발견한 시점. 이 시간이 짧을수록 Fuzzer가 활발하게 새 경로를 찾고 있다는 의미. (현재 : 0초)
•
cycles done: Corpus을 한 번씩 모두 테스트한 횟수. 0은 아직 첫 번째 사이클을 진행 중이라는 뜻.
2.
성능 및 커버리지 (Performance & Coverage)
•
exec speed: 초당 실행 횟수. Fuzzing에서 가장 중요한 성능 지표로, 높을수록 좋음. (현재 초당 231.5회)
•
corpus count: 현재까지 수집된 의미 있는 입력의 총 개수. (현재 538개)
•
map density: 전체 코드 중 얼마나 많은 부분을 탐색했는지 나타내는 커버리지 지표. (현재 약 8.74%)
3.
결과 (Overall Results)
•
saved crashes: 발견된 고유한 Crash의 수. Fuzzing의 주된 목표로, 이 숫자를 높이는 것이 중요. (현재 0개)
6. libarchive (Linux) Heap Buffer Overflow 취약점 분석
6.1. 취약점 동적 분석
Fuzzer가 발견한 crash를 재현하고 원인을 파악하기 위해, AddressSanitizer를 활성화하여 libarchive를 다시 빌드한다.
•
CMakeLists.txt
# relaxed somewhat in final shipping versions.
IF (CMAKE_C_COMPILER_ID MATCHES "^GNU$" OR
CMAKE_C_COMPILER_ID MATCHES "^Clang$" AND NOT MSVC)
SET(CMAKE_REQUIRED_FLAGS "-Wall -Wformat -Wformat-security -fsanitize=address")
...
C
복사
•
tar/CMakeLists.txt
...
# Add sanitizer option
TARGET_COMPILE_OPTIONS(bsdtar PRIVATE -fsanitize=address)
TARGET_LINK_OPTIONS(bsdtar PRIVATE -fsanitize=address)
...
C
복사
이후 아래 명령어로 빌드를 수행한다.
$ cmake -S . -DENABLE_WERROR=OFF -B libarchive_build
$ cd libarchive_build
$ make -j `nproc`
Bash
복사
•
crash 재현
$ ./bin/bsdtar -xvf ./heap_of.tar
=================================================================
==25253==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61a000000b13 at pc 0x7eb518cd5aa7 bp 0x7fffe21a4150 sp 0x7fffe21a38f8
READ of size 1172 at 0x61a000000b13 thread T0
#0 0x7eb518cd5aa6 in __interceptor_strlen ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:389
#1 0x56e474ef680a in archive_mstring_copy_mbs /root/libarchive/libarchive/archive_string.c:4096
#2 0x56e474e9f33e in archive_entry_set_link /root/libarchive/libarchive/archive_entry.c:1172
#3 0x56e474ee0bff in header_gnu_longlink /root/libarchive/libarchive/archive_read_support_format_tar.c:1149
#4 0x56e474ee0167 in tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:829
#5 0x56e474edf7e8 in archive_read_format_tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:546
#6 0x56e474ea6aff in _archive_read_next_header2 /root/libarchive/libarchive/archive_read.c:646
#7 0x56e474ea6bed in _archive_read_next_header /root/libarchive/libarchive/archive_read.c:684
#8 0x56e474ef7e8c in archive_read_next_header /root/libarchive/libarchive/archive_virtual.c:148
#9 0x56e474e8ef4a in read_archive /root/libarchive/tar/read.c:241
#10 0x56e474e8e100 in tar_mode_x /root/libarchive/tar/read.c:93
#11 0x56e474e8b341 in main /root/libarchive/tar/bsdtar.c:993
#12 0x7eb518a98d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
#13 0x7eb518a98e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f)
#14 0x56e474e85764 in _start (/root/libarchive/linux_build/bin/bsdtar+0x10764)
0x61a000000b13 is located 0 bytes to the right of 1171-byte region [0x61a000000680,0x61a000000b13)
allocated by thread T0 here:
#0 0x7eb518d4cc38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 0x56e474eeee94 in archive_string_ensure /root/libarchive/libarchive/archive_string.c:316
#2 0x56e474ee0e41 in read_bytes_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1225
#3 0x56e474ee1016 in read_body_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1277
#4 0x56e474ee0be9 in header_gnu_longlink /root/libarchive/libarchive/archive_read_support_format_tar.c:1148
#5 0x56e474ee0167 in tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:829
#6 0x56e474edf7e8 in archive_read_format_tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:546
#7 0x56e474ea6aff in _archive_read_next_header2 /root/libarchive/libarchive/archive_read.c:646
#8 0x56e474ea6bed in _archive_read_next_header /root/libarchive/libarchive/archive_read.c:684
#9 0x56e474ef7e8c in archive_read_next_header /root/libarchive/libarchive/archive_virtual.c:148
#10 0x56e474e8ef4a in read_archive /root/libarchive/tar/read.c:241
#11 0x56e474e8e100 in tar_mode_x /root/libarchive/tar/read.c:93
#12 0x56e474e8b341 in main /root/libarchive/tar/bsdtar.c:993
#13 0x7eb518a98d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:389 in __interceptor_strlen
Shadow bytes around the buggy address:
0x0c347fff8110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fff8120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fff8130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fff8140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fff8150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c347fff8160: 00 00[03]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fff8170: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fff8180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fff8190: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fff81a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fff81b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==25253==ABORTING
Bash
복사
AddressSanitizer 로그를 통해 strlen 함수가 호출되는 과정에서 할당된 Heap Buffer의 경계를 넘어 데이터를 읽는 Out-of-Bounds Read가 발생했음을 확인 할 수 있다.
6.2. 취약점 정적 분석
AddressSanitizer가 보고한 Call Stack을 역추적하여 코드의 취약점을 분석한다.
1.
취약점 발생 지점: archive_mstring_copy_mbs
archive_mstring_copy_mbs 함수 내부에서 발생한다. 이는 strlen(mbs)를 호출하는 과정에서 mbs 문자열이 널 종료(\0)되지 않아, strlen 함수가 할당된 버퍼를 넘어서까지 문자열의 길이를 계산하려 시도했기 때문이다.
int
archive_mstring_copy_mbs(struct archive_mstring *aes, const char *mbs)
{
if (mbs == NULL) {
aes->aes_set = 0;
return (0);
}
return (archive_mstring_copy_mbs_len(aes, mbs, strlen(mbs)));
}
C
복사
2.
archive_entry_set_link 함수
archive_entry_set_link 함수는 archive_mstring_copy_mbs를 호출할 때 target 포인터를 인자로 전달한다. 취약점은 바로 이 target 문자열에서 발생한다.
void
archive_entry_set_link(struct archive_entry *entry, const char *target)
{
archive_mstring_copy_mbs(&entry->ae_linkname, target);
if ((entry->ae_set & AE_SET_SYMLINK) == 0) {
entry->ae_set |= AE_SET_HARDLINK;
}
}
C
복사
3.
header_gnu_longlink 함수
이 함수는 read_body_to_string을 통해 파일 링크 경로를 읽어 linkpath라는 문자열 변수에 저장한다. 이후 이 변수를 archive_entry_set_link 함수에 전달한다.
static int
header_gnu_longlink(struct archive_read *a, struct tar *tar,
struct archive_entry *entry, const void *h, size_t *unconsumed)
{
int err;
struct archive_string linkpath;
archive_string_init(&linkpath);
err = read_body_to_string(a, tar, &linkpath, h, unconsumed);
archive_entry_set_link(entry, linkpath.s);
archive_string_free(&linkpath);
return (err);
}
C
복사
4.
취약점의 근본 원인: read_bytes_to_string
•
read_body_to_string 내부에서 호출되는 read_bytes_to_string 함수에 취약점의 핵심 원인이 존재한다.
•
__archive_read_ahead를 호출하여 조작된 tar 파일 데이터를 읽으려 시도한다.
•
__archive_read_ahead 함수가 비정상적인 데이터로 인해 실패하여 NULL을 반환한다.
•
이때 함수는 오류 처리 로직에 따라 즉시 ARCHIVE_FATAL을 반환하며 종료된다.
•
이 과정에서 성공적으로 데이터를 읽었을 때만 실행되는 널 문자 삽입 코드(as->s[size] = '\\0';)가 실행되지 않는다.
static int
read_body_to_string(struct archive_read *a, struct tar *tar,
struct archive_string *as, const void *h, size_t *unconsumed)
{
// ...
r = read_bytes_to_string(a, as, size, unconsumed);
*unconsumed += 0x1ff & (-size);
return(r);
}
static int read_bytes_to_string(...) {
// ...
// 데이터 읽기
src = __archive_read_ahead(a, size, NULL);
if (src == NULL) { // 데이터 읽기 실패 시
// ... 오류 설정
return (ARCHIVE_FATAL); // 널 문자를 삽입하지 않고 즉시 리턴
}
// 아래 코드는 실행되지 않음
memcpy(as->s, src, (size_t)size);
as->s[size] = '\\0'; // <- 널 문자 삽입 로직
// ...
return (ARCHIVE_OK);
}
C
복사
5.
결론
조작된 tar 입력 파일로 인해 read_bytes_to_string 함수가 문자열 Buffer에 널 문자(\0)를 삽입하지 않은 채로 반환하는 것이 취약점의 근본적인 원인이다. 이렇게 비정상적으로 생성된 문자열이 상위 함수로 계속 전달되어, 최종적으로 strlen 함수에서 할당된 메모리 경계를 넘어 데이터를 읽게 되면서 Heap Buffer Overflow가 발생한다.
7. 취약점 제보
7.1. CVE (cve.org)
1.
github issue Open
2.
cve.org 방문 및 Report/Request
3.
MITRE 클릭
4.
아래의 내용을 적절히 채우고 제보한다.
7.2. 결과
libarchive : CVE-2024-57970
8. libarchive 패치 분석
err가 ARCHIVE_OK가 아닌 경우에만 archive_entry_set_link함수를 호출하도록 패치되었다.
CVE-2024-57970 패치
또한 이 취약점은 특정 커밋에서 기존에는 존재하지 않았던 버그가 생긴 것으로 추정된다. 2d8a576 커밋 이전에는 read_body_to_string 함수 호출 이후 err값을 확인하여 그대로 리턴하는 로직이 존재했지만, 커밋 이후에는 해당 로직이 없어진 것을 확인 할 수 있다.
2d8a576 : Parse tar headers incrementally
9. 결론
1부에서는 Fuzzing의 개요, Linux 환경에서 오픈소스(libarchive)를 대상으로 Fuzzing을 수행하여 실제 Heap Overflow 취약점을 발견했으며, 이 취약점은 CVE를 획득했다. 이 과정을 통해 Fuzzing이 단순한 이론을 넘어, 실제 제품의 보안 취약점을 찾아내는 강력하고 실효성 높은 기법임을 입증했다.
78ResearchLab RedSpider-OS/APP 연구팀은 이처럼 알려지지 않은 0-Day 위협을 탐지하는 사이버 보안 신기술을 연구·개발하여 고객의 제품과 서비스에 선제적인 보안 솔루션을 제공한다.