Overview
비오비 교육 때 박세준 멘토님께서 내주셨던 과제를 정리할 겸 AFL을 이용해 1-day 취약점을 트리거하고 exploit하려 한다.
퍼징과 exploit은 Ubuntu Linux 18.04에서 진행한다.
아래와 같은 순서로 진행한다.
- AFL을 이용해 취약점이 있는 0.8.42 버전의 dact를 퍼징한다.
- crash를 기반으로 취약점을 분석하고 exploit 해본다.
AFL
afl은 honggfuzz, afl, libfuzzer와 함께 소프트웨어 퍼징을 위한 대표적인 gery-box fuzzer다.
자세한 내용은 따로 정리할 것이며 여기서는 간단하게만 다룬다.
현재 오픈소스로 제공하고 있으며 whitepaper도 확인할 수 있다.
[링크] : afl open source, afl whitepaper
아래 내용은 논문 [The Art, Science, and Engineering of Fuzzing: A Survey]를 기반으로 정리한 내용이다. 논문 리뷰는 이 링크에서 확인할 수 있다.
Summary
- Grey-box Fuzzer
- Open-sourced
- In-memory Fuzzing
- Seed Scheduling
- Mutation
- Coverage-baesd Crash Traige
- Evolutionary Seed Pool Update
- Seed Pool Culling
Instrumentation
AFL은 한 가지 이상의 instrumentation 방법을 사용하는 대표적인 퍼저다.
- static instrumentation
afl 전용 컴파일러를 이용해 소스 코드 수준의 정적 instrument 코드를 삽입한다.
- dynamic instrumentation
실행 가능한 코드를 PUT 내부에서 instrument 하거나(default 옵션) 실행 가능한 코드를 PUT나 외부 라이브러리에서 instrument 한다(AFL_INST_LIBS 옵션).
* AFL_INST_LIBS 옵션은 실행되는 코드를 모두 instrument 하는 것인데, 외부 라이브러리의 코드에 대한 커버리지 정보도 포함할 수 있어서, 더 완전한 커버리지 정보를 제공한다. 이는 AFL이 외부 라이브러리 함수에서 추가적인 경로를 퍼징하도록 도와준다.
AFL과 그 하위(AFL 계보 퍼저들)의 퍼저는 PUT의 모든 branch instruction을 instrument해서 branch coverage를 계산한다
In-memory fuzzing
GUI처럼 복잡한 프로그램은 프로세스를 생성하고 입력을 전달하기까지 수 초가 소요된다. 이런 프로그램을 퍼징하기 위해 GUI 초기화가 완료된 후에 메모리 스냅샷(snapshot)을 찍는 접근 방식이 존재한다. 새로운 테스트 케이스를 퍼징하기 위해, 테스트 케이스를 메모리에 집접 쓰고 실행하기 전에, 메모리 스냅샷을 복원할 수 있다. 클라이언트-서버간의 상호 작용이 많은 네트워크 응용프로그램을 퍼징할 때도 이런 접근 방식을 사용할 수 있다.
AFL은 초기 실행의 코스트를 줄이기 위해 fork server를 사용한다. fork server는 모든 fuzz iteration에 대해 새로운 프로세스를 fork 한다. 즉 각 fuzz iteration마다 이미 초기화 과정이 끝난 상태의 프로세스를 fork 하여 퍼징할 수 있다.
Execution Feedback
AFL의 minset은 각 branch에 지수적(:logarithm)인 카운터가 있는 branch coverage를 기반으로 한다. 그 이유는 branch 카운터가 몇 배 차이로 다를 때만 차이가 있다고 인지하도록 설정한 것이다.
[AFL whitepaper의 "2) Detecting new behaviors"에서 확인할 수 있다.]
seed trimming
AFL은 어떤 시드가 동일한 커버리지를 달성하도록 유지하면서, 그 시드의 일부분을 제거하는 코드 커버리지 instrumentation을 사용한다.
FCS Algorithms
AFL은 FCS problem에 EA(Evolutionary Algorithm)을 사용한다. 즉 fitness
의 가치가 있는 configuration set을 유지한다.
AFL은 control-flow edge를 실행하는 configuration 중, 가장 빠르고 가장 작은 configuration 입력을 포함하는 configuration을 가장 효과적(AFL에서는 favorite라는 용어를 사용)이라고 판단한다. AFL은 config 큐를 원형 큐(circular queue) 형태로 유지하면서 다음으로 효과적인 configuration을 선택하고, 일정한 횟수만큼 실행한다.
Mutation
AFL은 다양한 mutation 기법을 사용한다. 대표적인 방식은 다음과 같다.
Bit-Flipping, Arithmetic Mutation, Block-based Mutation, Dictionary-based Mutation
Triage (Coverage-based Deduplication)
AFL은 PUT의 각 실행에 대한 edge coverage를 기록하고 각 edge에 대한 대략적인 hit count를 측정하기 위해 효율적인 소스 코드 instrumentation을 사용한다. 주로 이 edge 커버리지 정보를 사용해 새로운 시드 파일을 선택한다.
게다가 이 방식은 독특한 중복 제거 기법으로 이어진다. 이전에 볼 수 없었던 edge를 커버했거나, 모든 이전 크래시들에 존재하던 edge를 커버하지 않은 경우 이를 unique한 크래시라고 간주한다.
Test case minimization
AFL은 crashing input의 크기를 줄이는 기술을 test case minimization라고 부른다.
AFL의 test case minimizer는 바이트를 0으로 설정하여 테스트 케이스의 길이를 줄인다.
Evolutionary Seed Pool Update
AFL은 branch에 hit 한 횟수를 기록하여 fitness function의 정의를 구체화했다.
dact
dact는 특히 출력 파일의 크기를 작게 만들기 위해 입력 데이터의 블록 크기를 고려하여 가장 적합한 알고리즘을 선택하는 압축 도구다.
dact의 manpage는 아래와 같다.
[dact manpage] https://manpages.debian.org/testing/dact/dact.1.en.html
Fuzzing
Build dact
먼저 취약점이 있는 dact-0.8.42 버전의 바이너리를 준비하자.
- dact-0.8.42 binary
cd ~
wget https://fossies.org/linux/privat/dact-0.8.42.tar.gz
tar -xvf dact-0.8.42.tar.gz
mv dact-0.8.42 dact_dir
아래와 같이 빌드할 수 있다.
- dact build
cd ~/dact_dir/ make clean ./configure make mv ~/dact_dir/dact ~/dact_original
Use dact
-help 옵션
으로 여러 옵션을 살펴볼 수 있다.
- dact usage
~/dact_original -help
몇 가지 주요 옵션만 살펴보자.
옵션 없이
입력 파일을 전달하면 압축해 준다.
-d 옵션
과 함께 입력 파일을 전달하면 압축을 해제해 준다.
-c 옵션
은 stdout으로 압축 또는 압축 해제 결과를 출력해 준다.
-f 옵션
은 강제로 압축(해제)를 진행하는 옵션이다.
직접 사용해보자.
- use dact
cd ~/
echo hello > hello.txt
~/dact_original ~/hello.txt
cat ~/hello.txt.dct
rm ~/hello.txt
~/dact_original -d ~/hello.txt.dct
cat ~/hello.txt
압축했는데 dct 파일 포맷으로 인해 크기가 오히려 증가한 것을 알 수 있다.
- size
$ ls -alh ~/hello.txt*
-rw-r--r-- 1 topcue topcue 6 Feb 17 17:10 hello.txt
-rw-r--r-- 1 topcue topcue 67 Feb 17 17:08 hello.txt.dct
- dct format
$ xxd ~/hello.txt.dct
00000000: 4443 54c3 0008 2a00 0000 0000 0000 0600 DCT...*.........
00000010: 0000 0100 0000 0b00 0000 001a 0400 0968 ...............h
00000020: 656c 6c6f 2e74 7874 0000 0412 e602 1f01 ello.txt........
00000030: 0004 12e6 021f 000b 6865 6c6c 6f0a 0000 ........hello...
00000040: 0000 00 ...
Build dact with scan-build
먼저 동적 분석 도구인 scan-view를 써보기 위해 scan-build를 이용해 빌드 하자.
- scan-build: static analysis tool
sudo apt-get install -y clang clang-tools
cd ~/dact_dir/
scan-build ./configure
scan-build make
그러면 /tmp 디렉토리 하위에 분석 결과를 확인할 수 있는 html 파일이 생성된다.
- scan-view
ls /tmp
scan-view /tmp/scan-build-*/
- output
의심 가는 버그 30가지를 알려주지만 심각한 취약점으로는 보이지 않는다.
build with AFL
AFL로 퍼징할 수 있도록 먼저 AFL을 빌드하자.
- build AFL Fuzzer
cd ~
wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
tar -xvf afl-latest.tgz
mv afl-2*/ afl_dir/
cd ~/afl_dir/
make
sudo make install
아래와 같이 AFL 컴파일러를 이용해 dact를 빌드 하자.
- compile dact w/ afl-compiler
cd ~/dact_dir/
make clean
CC=~/afl_dir/afl-gcc ./configure
make
mv ~/dact_dir/dact ~/
Fuzz with AFL
퍼징할 때 dact의 입력을 전달하기 위해 in이라는 디렉토리를 만들고 입력으로 쓸 hello.txt.dct를 전달하자.
- set input directory
mkdir ~/in
mv ~/hello.txt.dct ~/in/
그리고 코어 덤프 생성을 위해 아래 명령어를 사용할 수 있다.
- for core
sudo sysctl -w kernel.core_pattern=core
이제 직접 퍼징해보자.
- fuzzing with afl
~/afl_dir/afl-fuzz -i ~/in -o ~/out -- ~/dact_afl -dcf
-i
와 -o
는 각각 input/output 디렉토리를 지정해 준 것이다. --
은 타겟 프로그램의 이름과 인자를 전달하기 위함이다.
즉 -dcf 옵션
으로 dact를 퍼징하겠다는 의미다.
퍼징을 시작하면 아래와 같이 초기화 작업 이후 깔끔한 모습으로 상황을 보여준다.
아래 사진은 11분간 퍼징을 했을 때 상황이다.
unique crash가 27개 발견되었다.
이제 퍼징을 멈추고 취약점 종류를 살펴보자.
- see crash
ls ~/out/crashes/
퍼징에 사용한 dact_afl 바이너리도 reproduce에 사용할 수 있지만, 더 많은 정보를 제공하는 ASAN을 이용하겠다.
아래와 같이 ASAN instrumentation 코드를 삽입하여 dact를 빌드 하자.
- build dact with ASAN
cd ~/dact_dir/
make clean
CC="clang -fsanitize=address" CXX="clang++ -fsanitize=address" ./configure
make
mv ~/dact_dir/dact ~/dact_asan
이렇게 빌드 한 dact_asan으로 crashing input을 실행할 경우 crash가 발생한 이유를 자세히 확인할 수 있다.
crash 원인만 간단히 살펴보기 위해 dact_asan을 이용해 crash log를 생성하자.
- make crash log w/ asan
for file in ~/out/crashes/*; do;
echo Input: $file >> ~/crash.log;
~/dact_asan -dcf $file 2>> ~/crash.log;
done;
아래와 같이 grep을 이용해 ASAN이 분류해 준 취약점 원인을 확인할 수 있다.
- grep ERR
grep ERROR ~/crash.log
이들 중 스택 버퍼 오버플로우만 필터링하자.
- grep stack-buffer-overflow
$ grep ERROR ~/crash.log | grep stack
==1355737==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc9c54a180 at pc 0x0000004c9a75 bp 0x7ffc9c549870 sp 0x7ffc9c549868
==1355760==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffdec592a20 at pc 0x0000004c9a75 bp 0x7ffdec592110 sp 0x7ffdec592108
스택 버퍼 오버플로우 취약점을 트리거 하는 crashing input 두 개가 있다.
이들이 각각 어떤 파일인지 확인하기 위해 crash.log 파일을 열고 id를 확인하자.
- detail
vi ~/crash.log
# search string w/ command mode
/stack-buffer-overflow
==1355760==ERROR:
라인 위로 쭉 올라가면 파일명을 확인할 수 있다.
Input: /home/topcue/out/crashes/id:000026,sig:06,src:000027,op:havoc,rep:8
dact: read: No such file or directory
dact: read: No such file or directory
...
dact: read: No such file or directory
dact: read: No such file or directory
=================================================================
1721 ==1355760==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffdec592a20 at pc 0x0000004c9a75 bp 0x7ffdec592110 sp 0x7ffdec592108
1722 WRITE of size 8 at 0x7ffdec592a20 thread T0
1723 #0 0x4c9a74 in dact_process_file /home/topcue/dact-0.8.42/dact_common.c:478:40
1724 #1 0x4cdbf7 in main /home/topcue/dact-0.8.42/dact.c:689:8
1725 #2 0x7f38a1a330b2 in __libc_start_main /build/glibc-eX1tMB/glibc-2.31/csu/../csu/libc-start.c:308:16
1726 #3 0x41c4ad in _start (/home/topcue/dact_asan+0x41c4ad)
- crashing input filename
Input: /home/topcue/out/crashes/id:000026,sig:06,src:000027,op:havoc,rep:8
Input: /home/topcue/out/crashes/id:000017,sig:11,src:000000,op:havoc,rep:16
crash.log는 모든 crash의 정보가 포함되어 있기 때문에 보기 어렵다.
두 파일만 따로 가져와서 재현해보자.
$ ls ~/out/crashes/ | grep -e "id:000016" -e "id:000017"
id:000016,sig:06,src:000000,op:havoc,rep:4
id:000017,sig:11,src:000000,op:havoc,rep:16
- rename
cp ~/out/crashes/id:000016* ~/crash_1
cp ~/out/crashes/id:000017* ~/crash_2
crash_1과 crash_2로 스택 버퍼 오버플로우를 재현하면 아래와 같다.
- reproduce crash_1
~/dact_asan -dcf ~/crash_1
- reproduce crash_2
~/dact_asan -dcf ~/crash_2
/home/topcue/dact-0.8.42/dact_common.c:478:40
에 취약점이 있는 crash_2를 이용해 exploit할 것이다.
따라서 crash_2의 이름을 crash로 바꿔주자.
- rename crash
mv ~/crash_2 ~/crash
crash의 hex view는 다음과 같다.
- xxd crash
$ xxd ~/crash
00000000: 4443 54c3 0b08 2a00 0000 0000 0000 0605 DCT...*.........
00000010: 0003 0100 0000 0aeb 0000 b8b8 0400 0968 ...............h
00000020: 0000 6865 6c6c 6f0a 0000 0000 0004 1107 ..hello.........
00000030: 6865 6c6c 656c 6c6f 2e74 7874 0000 1013 hellello.txt....
00000040: 0402 1fe6 ff03 12e6 021f 0e0b 684f 6c12 ............hOl.
00000050: e602 1f00 0000 09
이제 crash를 이용해 다시 취약점을 트리거하고 화면에 출력된 결과를 분석해보자.
- reproduce crash
~/dact_asan -dcf ~/crash
결과는 아래와 같다.
output (crash)
$ ~/dact_asan -dcf ~/crash_2 dact: read: No such file or directory dact: read: No such file or directory ...skip... dact: read: No such file or directory dact: read: No such file or directory ================================================================= ==1356226==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffa42eed60 at pc 0x0000004c9a75 bp 0x7fffa42ee450 sp 0x7fffa42ee448 WRITE of size 8 at 0x7fffa42eed60 thread T0 #0 0x4c9a74 in dact_process_file /home/topcue/dact-0.8.42/dact_common.c:478:40 #1 0x4cdbf7 in main /home/topcue/dact-0.8.42/dact.c:689:8 #2 0x7f0baa9b90b2 in __libc_start_main /build/glibc-eX1tMB/glibc-2.31/csu/../csu/libc-start.c:308:16 #3 0x41c4ad in _start (/home/topcue/dact_asan+0x41c4ad) Address 0x7fffa42eed60 is located in stack of thread T0 at offset 2304 in frame #0 0x4c50ff in dact_process_file /home/topcue/dact-0.8.42/dact_common.c:249 This frame has 16 object(s): [32, 36) 'cipher.addr' [48, 192) 'filestats' (line 250) [256, 2304) 'file_extd_urls' (line 252) <== Memory access at offset 2304 overflows this variable [2432, 2433) 'algo' (line 253) [2448, 2449) 'ch' (line 254) [2464, 2472) 'hdr_buf' (line 255) [2496, 2499) 'version' (line 257) [2512, 2513) 'file_opts' (line 258) [2528, 2536) 'filesize' (line 260) [2560, 2564) 'blk_cnt' (line 261) [2576, 2580) 'file_extd_size' (line 261) [2592, 2596) 'blksize' (line 261) [2608, 2612) 'blksize_uncomp' (line 261) [2624, 2628) 'magic' (line 262) [2640, 2644) 'x' (line 265) [2656, 2664) 'offset' (line 266) HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow /home/topcue/dact-0.8.42/dact_common.c:478:40 in dact_process_file Shadow bytes around the buggy address: 0x100074855d50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100074855d60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100074855d70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100074855d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100074855d90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x100074855da0: 00 00 00 00 00 00 00 00 00 00 00 00[f2]f2 f2 f2 0x100074855db0: f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 01 f2 01 f2 0x100074855dc0: 00 f2 f2 f2 03 f2 01 f2 00 f2 f2 f2 04 f2 04 f2 0x100074855dd0: 04 f2 04 f2 04 f2 04 f2 00 f3 f3 f3 00 00 00 00 0x100074855de0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100074855df0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 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 ==1356226==ABORTING
하나씩 해석해보자.
dact: read: No such file or directory
dact: read: No such file or directory
...skip...
dact: read: No such file or directory
dact: read: No such file or directory
dact가 실행하면서 남긴 오류 메시지들이다.
=================================================================
address sanitizer가 출력한 구분선이며 이후 아래 모든 내용은 address sanitizer가 출력한 내용이다.
==1356226==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffa42eed60 at pc 0x0000004c9a75 bp 0x7fffa42ee450 sp 0x7fffa42ee448
ASAN이 감지한 취약점이 stack-based buffer overflow라는 사실을 알 수 있다.
또한 pc, bp, sp와 같은 레지스터 값을 출력해 준다.
WRITE of size 8 at 0x7fffa42eed60 thread T0
#0 0x4c9a74 in dact_process_file /home/topcue/dact-0.8.42/dact_common.c:478:40
#1 0x4cdbf7 in main /home/topcue/dact-0.8.42/dact.c:689:8
#2 ...skip...
어떤 thread에서 버그가 발생했는지 알려준다.
그 아래는 스택을 backtrace 해주며 취약점이 존재하는 정확한 라인 넘버를 제시한다.→ dact_common.c:478
Address 0x7fffa42eed60 is located in stack of thread T0 at offset 2304 in frame
#0 0x4c50ff in dact_process_file /home/topcue/dact-0.8.42/dact_common.c:249
This frame has 16 object(s):
[32, 36) 'cipher.addr'
[48, 192) 'filestats' (line 250)
[256, 2304) 'file_extd_urls' (line 252) <== Memory access at offset 2304 overflows this variable
[2432, 2433) 'algo' (line 253)
...skip...
[2640, 2644) 'x' (line 265)
[2656, 2664) 'offset' (line 266)
stack frame 정보를 제공한다. frame에 16개의 object들이 있으며 [256, 2304) 'file_extd_urls'의 256바이트부터 2303바이트까지 범위를 의미한다.
그런데 offset 2304에 접근하려 해서 overflow로 분류되었다.
스택에 guard page를 넣어뒀는데 [f2]에 접근하다가 다른 object를 침범했다.
Conclusion
AFL을 이용해 dact 바이너리를 퍼징하고 crash를 생성했다.
이제 crash가 발생한 원인을 살펴보고 이를 이용해 exploit 해보자.
Uploaded by Notion2Tistory v1.1.0