
[ Fuzzer ] AFL Fuzzer 소스코드 분석
sangjun
·2022. 5. 10. 21:46
소스코드 분석이유
- 오늘부터 AFL/AFL++의 소스코드를 하나하나 분석해보겠다.
분석해보는 이유는 최종적으로 SangJun Fuzzer를 만들기 위해서다 LOL며칠전 p6 fuzzer를 보고 나도 나만의 퍼저를 만들기로 마음 먹었다.
+AFL-fuzzer를 분석하다보니 단기간 내에 coverage guided 기반 퍼저를 만들기엔 어렵다고 판단하여
AFL-fuzzer소스코드만 분석하고 무슨 기능하는지만 알아낸 다음 AFL/AFL++를 커스텀할 수 있을 정도까지만
분석해보고자 한다.
AFL++와 AFL 소스코드를 잠깐 봤는데 지금 수업듣고 있는 리눅스 시스템 프로그래밍 수업에서 배운
시스템콜과 함수들이 굉장히 많이 나와 공부차원에서도 도움될 것 같아서 시간 날 때마다 분석해보고자 한다.
소스코드를 보면서 어떻게 설계했는지 알아보지만 아직은 구글 형님들의 사고방식을 따라가기엔
힘드니 틀린 부분이 있을 수도 있다..
https://github.com/google/AFL
https://github.com/AFLplusplus/AFLplusplus
AFL/experimental/argv_fuzzing/argv-fuzz-inl.h
- AFL Fuzzer에서 target program ARGV인자로 전달해주기 위해 사용
사용법은 아래와 같다.
#include <stdio.h>
#include "argv-fuzz-inl.h"
int main(int argc, char *argv[]) {
AFL_INIT_SET0("a.out");
int i;
printf("argc = %d\n", argc);
for (i = 0; i < argc; i++)
printf("argv[%d] = '%s'\n", i, argv[i]);
return 0;
}
AFL/experimental/persistent_demo/persistent_demo.c
- 반드시 afl-clang-fast로 컴파일 해야된다.
- 바이너리가 계속 입력을 받는 상태로 대기할 때 어느 부분만 집중적으로 퍼징하고 싶을 때 이용한다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
/* Main entry point. */
int main(int argc, char** argv) {
char buf[100]; /* Example-only buffer, you'd replace it with other global or
local variables appropriate for your use case. */
/* The number passed to __AFL_LOOP() controls the maximum number of
iterations before the loop exits and the program is allowed to
terminate normally. This limits the impact of accidental memory leaks
and similar hiccups. */
while (__AFL_LOOP(1000)) {
printf("hello\n");
/*** PLACEHOLDER CODE ***/
/* STEP 1: Fully re-initialize all critical variables. In our example, this
involves zeroing buf[], our input buffer. */
memset(buf, 0, 100);
/* STEP 2: Read input data. When reading from stdin, no special preparation
is required. When reading from a named file, you need to close
the old descriptor and reopen the file first!
Beware of reading from buffered FILE* objects such as stdin. Use
raw file descriptors or call fopen() / fdopen() in every pass. */
read(0, buf, 100);
/* STEP 3: This is where we'd call the tested library on the read data.
We just have some trivial inline code that faults on 'foo!'. */
if (buf[0] == 'f') {
printf("one\n");
if (buf[1] == 'o') {
printf("two\n");
if (buf[2] == 'o') {
printf("three\n");
if (buf[3] == '!') {
printf("four\n");
abort();
}
}
}
}
/*** END PLACEHOLDER CODE ***/
}
char buf2[0x100];
scanf("%s",buf2);
sleep(2);
/* Once the loop is exited, terminate normally - AFL will restart the process
when this happens, with a clean slate when it comes to allocated memory,
leftover file descriptors, etc. */
return 0;
}
AFL/experimental/post_library/post_library.so.c
- 뭔가 자료가 부족하고 poc가 애매하다.
- AFL에서 페이로드를 보내기 전에 페이로드 앞에 헤더 부분들을 따로 붙여주는 기능같은데 조금 더 검색해봐야 정확한 기능을 알 수 있을 것 같다.
AFL/afl-fuzz.c
- afl-fuzz 바이너리를 만드는 afl-fuzz.c이다.
- 소스코드 라인은 약 8천라인 정도 되는데 main함수를 중심으로 무슨 동작을 하는지 분석해보겠다.
- afl-fuzzer에서 가장 중요한 것은 유전 알고리즘을 통한 testcase변형과 Forkserver라고 생각하니 이부분은 꼭
분석하자.
커스텀 헤더파일
- 매크로와 bitflip token이라는 것이 변경될 때 길이와 수가 이 파일에 저장된다고 한다.
- u8이라는 타입이 많이 나오는데 이것은 uint8_t로 커스텀 헤더파일에 정의 되어있다. ( s16 == int16_t )
- debug.h에 SAYF, ACTF 등 디버깅 매크로 지원
- 이 중에 debug.h, alloc-inl.h, types.h는 중요한 정보들이 많다.
1. SAYF 등 printf찍기 위한 매크로 정의, printf를 따로 이용하지 않은 이유는 로그에 색깔을 입히거나 printf를 매번 쓰면 소스코드가 길어져서 매크로로 치환해 사용하는 것으로 보임 ( 처음 퍼저 돌렸을 떄 나오는 로그표시 같은거 같다. 어딘가에 정의되어 있을거 같은데 찾지 못했다.)
- debug.h에 정의되어 있다.
2. access함수로 docs위치에 접근 가능한지 확인한다.
- 나중에 fuzz사용할 떄 잘못 사용하는거 같으면 docs위치/README.txt라는 메세지를 뿌리기 위함.
3. getopt로 옵션 받는다. i o M S f x t m b d B n T Q V 모드가 있다.
4. setup_signal_handlers()
SIGUP, SIGINT, SIGTERm에 대해서 핸들링함 -> handle_stop_sig로 핸들러함수 지정
--> various ways of saying "stop" 다양한 시그널들을 통해 멈추도록 함
SIGALRM -> hadle_timeout핸들러로 시그널 처리
-> 타임아웃 경고함
SIGWINCH
-> 윈도우 사이즈가 작을경우 Fuzzer UI잘 보이지 않기 때문에 handler_resize로 핸들링함
ㄴSIGTSTP, SIGPIPE에 대해서는 SIG_IGN로 시그널에 대한 무시처리를 한다.
5. check_asan_opts()
- asan과 msan옵션이 설정되어 있는지 환경변수를 확인 후 플래그를 설정한다.
6. sync_id가 설정되어 있으면 fix_up_sync()호출
- sync_id란 병렬처리 퍼징을 수행할 때 Master Fuzzer ID로 -M [퍼저ID] 로 옵션을 주면 sync_id가 생성된다.
- fix_up_sync()에서는 병렬처리시에 각 퍼저 ID별로 각자의 output디렉토리를 할당한다.
7. save_cmdline()
- AFL 퍼징 수행시 인자로 준 것들을 모두 저장한다. 저장 후 용도는 아직 파악 못함.
8. fix_up_banner()
- 퍼저 ID별로 별도의 (tests_config)에 해당하는 공간에 printf를 찍는다.
- 어떤 곳에 printf를 찍는지 못 찾음
9. check_if_tty
- ioctl시스템콜을 이용해 tty인지 확인한다. 환경변수로 tty가 아닌 상태에서 할 수 있도록 설정할 수 있는것 가탇. 아마 tty를 이용하지 않는 유저들을 위한 옵션이 있는것 같다.
10. get_core_count
- 사용가능한 CPU core의 걧수를 확인함.
11. check_crash_handling()
- "echo core > /proc/sys/kernel/core_pattern"이 되어있는지 확인한다.
- "시스템이 코어 덤프 파일(코어) 알림을 외부 프로그램에 보내도록 구성되어 있으면 충돌 정보를 Fuzzer로 보내는 사이의 지연이 증가하여 충돌이 시간 초과로 잘못 보고될 수 있습니다." 이렇게 되어 있는데 흠..시간초과로 오해하는걸 방지하기 위한 것으로 이해했다.
12. check_cpu_governor
- 잘 모르겠드아..
13. setup_post()
- 위의 AFL/experimental기능에서 post library를 로드한다. 즉, AFL이 돌아가기 전에 수행해야 하는 것을
post Library에 적어주면 이것을 실행하는것 같다.
14. setup_shm()
- 비트맵과 관련된 정보(virgin_bits, virgin_tmout, virgin_crash)를 0xff=255로 초기화 한다.
- trace_bits라는 변수를 통해 공유메모리를 관리한다.
- virgin_bits : 총 경로에 대한 정보 기록
virgin_tmout : 시간초과 사용 사례에 대해 기록
virgin_crash : Crash난 사례에 대해 기록
15. init_count_class16()
- count_class16을 초기화 한다.
-> 코드 분석해봤는데 로직을 모르겠다..
16. setup_dirs_fds()
- output으로 지정해준 디렉토리에 sync_id에 해당하는 폴더 아래에 크래시, hangs, queue등 폴더 및 파일 생성함
17. read_testcases()
- input으로 준 폴더에서 파일을 읽어 add_to_qeueue()함수로 큐로 넣는다.
18. load_auto()
- 자동으로 생성된 토큰을 로드하는 역할을 한다는데..아직 AFL에서 토큰이 무슨 역할 하는지 이해 못 했다.
19. pivot_inputs()
- output디렉토리에서 in디렉토리 파일들에 대한 하드링크를 생성한다.
- 하드링크를 생성하는 이유는 첫 큐에서 시드를 in에 있는 것을 그대로 사용하기 위함인것 같다.
20. detect_file_args()
- @@를 식별한다.
- 파일의 args에 문자열 대신 파일의 내용들을 넘겨주고 싶으면 @@를 써주면 되는것 같다.
21. check_binary(argv[optind])
- binary가 쉘 스크립트인지, 실행가능한지, ELF파일인지 등등 바이너리가 퍼징을 돌릴 수 있는 대상인지 확인한다
22. perform_dry_run(use_argv)
- afl-fuzz를 돌리면서 처음에 초기 테스트 케이스들이 전부 잘 돌아가는지 확인함
- 여기서 fork server를 생성한다.
- perform_dry_run에서 calibrate_case()를 부르는데 forkserver가 없다면 init_forkserver()를 부른다.
init_forkserver에서 재밌는 점은 fork를 2번 한다는 것이다.
현재 init_forkserver()를 호출하는 시점의 프로세스가 AFL-fuzzer라고 하자.
AFL-fuzzer에서 fork를 하고 execv("afl-gcc로 컴파일된 바이너리")를 실행한다.
--> 이것이 fork server의 역할을 한다.
--> 다시 말해 AFL-fuzzer에서 fork한 뒤 프로세스를 하나 만들고 execv()로 바이너리를 실행하면 이것이
fork server역할을 한다는 것이다. 어떻게 타겟 바이너리가 fork server 역할을 하냐??
--> afl-gcc 컴파일러가 포크서버 역할을 할 수 있는 코드를 넣어준다.
afl-gcc로 컴파일하면 코드의 분기문이나 함수 첫 부분마다 _afl_maybe_log라는 instrument가 들어간다.
_afl_maybe_log에서도 현재 상태를 fork한 뒤에 실제 바이너리를 테스트해보고 waitpid로
자식 프로세스가 끝날 때까지 forkserver는 기다린다. 이후에는 write로 파이프에 기록하면
AFL-fuzzer가 이것을 알 수 있는 구조이다.
23. cull_queue()
- 큐 대기열을 줄이는 역할을 한다.
24. show_init_stats()
- 테스트 케이스가 있는 디렉토리 정보들을 출력한다.
25. find_start_position
- 무슨 기능인지 아직 파악 X
26. write_stats_file(0, 0, 0)
- out/fuzzer_stats에 퍼저에 대한 정보를 기록하는거 같다.
27. save_auto
- 자동으로 생성된 토큰을 저장한다.
- ./out/queue/.state/auto_extras폴더에 저장하는거 같다.
지금까지는 AFL퍼저를 돌리기 위한 세팅을 했다.
이 코드 이후로는 afl이 돌아가고 fork server와 통신하거나, 병렬처리 퍼징을 하는등 본격적으로 퍼징이 돌아간다.
while(1)로 묶여 있어 무한 시간으로 퍼징을 한다.
while문에서 대표적으로 쓰이는 함수는 cull_queue, sync_fuzzers(use_argv), fuzz_one(use_argv)이 있다.
while문 이후로는 write_bitmap, write_status_file, save_auto, fclose, destroy_queue, destroy_extras 등등
fuzzer를 위해서 할당한 메모리를 해제하고, 퍼징수행기록을 저장하고 "Have a nice day!"라는 메시지를 끝으로
종료한다.
'Pwnable > Fuzzer' 카테고리의 다른 글
[ Fuzzer ] AFL Fuzzer 소스코드 분석 (0) | 2022.05.10 |
---|---|
AFL++논문 리뷰 (오픈소스) (0) | 2022.03.09 |
[ Fuzzing 101 ] Exercise 1 - Xpdf (CVE-2019-13288) (0) | 2022.02.24 |
[ Fuzzer ] Fuzzer 공부를 위한 자료들 (3) | 2022.01.22 |
0개의 댓글