[ Fuzzer ] AFL Fuzzer 소스코드 분석

sangjun

·

2022. 5. 10. 21:46

반응형

https://zhuanlan.zhihu.com/p/266432033

소스코드 분석이유

- 오늘부터 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라고 생각하니 이부분은 꼭

  분석하자.

 

출처 : https://wogh8732.tistory.com/272 까망눈연구소님 블로그

커스텀 헤더파일

 

- 매크로와 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!"라는 메시지를 끝으로
종료한다.

 

 

 

 

 

 

반응형

0개의 댓글