확장 가능한 parallel fuzzing 인프라 구축 및 성능 비교 분석 (1-3) AFL++ with Docker

OOSUlZ·2023년 2월 22일
0

(7) Docker를 활용한 AFL++ 분석

Docker란?

Docker는 container라는 독립된 공간에서 프로그램을 개발하고 구동하고 배포할 수 있게 하는 플랫폼이다. 우리는 흔히 개발할 때 다른 사람들의 코드를 가져오기도 하고 여러 프로그램을 다운받아서 사용하기도 한다. 하지만 그때마다 우리는 프로그램들끼리의 호환성 문제(버전이 안맞아서 작동이 안된다든가 아주 예전에 조작한 환경변수가 이제와서 에러를 만든다든가 등등)라든가, 그저 프로그램을 구성하는 코드들이 사방으로 흩어져 있어서 관리하기 어렵다든가 하는 문제들이 발생한다. 이 상황에서 우리는 독립된 공간이 있어서 되는 것들만 딱 모여있는 공간들로 구분해서 작업을 할 수 있다면 얼마나 좋을까하는 생각을 가지게 되는데 이게 Docker의 목적이다. Container라는 독립적인 공간을 제공해서 우리들의 작업을 보다 용이하게 해준다. 요즘에는 수많은 애플리케이션들이 컨테이너 단위로 개발되고 배포되고 있기 때문에 Docker와 같은 container 관련 기술들을 잘 다룰 수 있는 건 매우 중요하다고 본다.

=컨테이너

 컨테이너는 하나의 독립된 공간이다. 컨테이너 안에는 자기가 개발하고자하는 애플리케이션과 이와 관련된 모든 라이브러리나 환경 변수와 같은 모든 dependency들이 들어있다. 그리고 컨테이너 내에 있는 모든 것들은 컨테이너 외부에 있는 프로그램들과 전혀 영향을 끼치지 않는다. 간단한 예시를 들자면 자신의 운영체제에 python3.10이 있고 컨테이너 안에는 python 3.5가 있다면, 컨테이너 안에서는 python 3.5를 사용할 것이다. 

 컨테이너의 중요한 특징 중 하나는 바로 굉장히 가볍다는 것이다. 용량이 가벼운 이유는 다른 Virtual Machine처럼 자체적인 커널을 가지지 않고 오로지 application만 가지고 있기 때문이다.

위 그림을 보면 도커는 Host Operating System 위에서 구동되는 것을 볼 수 있다. 즉, Host OS는 그대로이고, 그 위에서 작게 컨테이너들이 동작하는 것을 알 수 있다. 반면에 Virtual Machine(우리가 현재 쓰고 있는 VirtualBox가 대표적)은 자체적인 application과 함께 자체적인 커널도 함께 구현하기 때문에 용량이 엄청 클 수밖에 없다. 즉, Host 위에서 host와 무관한 완전히 별개의 컴퓨터를 사용한다고 보면 될 듯하다

💡 **커널(kernel)은 운영체제를 작동시키는 핵심으로 kernel 위에 있는 application과 kernel 밑에 있는 물리적인 장치들간의 소통을 해준다.(예를 들어 변수를 저장하려고 하면 커널에서 그 변수를 실제 하드웨어에서 메모리의 어느 위치에 저장할지 고려해서 넣는다) Ubuntu, Centos, Fedora, Debian 모두 Linux라는 하나의 카테고리에 묶이는 이유가 이들이 전부 Linux Kernel을 기반으로 application이 구축되었기 때문이다.**
 컨테이너의 또하나의 중요한 특징으로는 Portable하다는 것이다. 흔히 개발을 하고 이걸 테스트할 때 각자의 컴퓨터로 이 작업을 수행하려고 하면 분명 어디선가 문제가 생길 것이다. 개발하는 사람들이 텍스트든 말이든 아무리 잘 설명해줘도 개발자 측에서 중요한 요소를 실수로 까먹고 안 알려준다든가 아니면 테스트 측에서 잘못 이해해서 잘못된 방향으로 테스트하는 등의 문제가 생길 수 있다. 하지만 개발자 측에서 자신들이 개발한 애플리케이션을 컨테이너 형태로 한꺼번에 테스트측으로 보내준다면, 테스트측에서는 컨테이너 안에있는 내용물만 테스트하면 되는 것이다. 테스트측에서 따로 무언가를 추가로 설치한다든가 설정을 변경할 필요가 없다는 얘기다. 

이러한 장점들 때문에 많은 사람들이 자신들의 애플리케이션을 컨테이너화(containerize)시켜서 작업하는 일이 많아졌다. 그렇다면 컨테이너는 어떻게 만드는 건가? 이걸 알기 위해서는 도커의 이미지(Image)에서 알아야 한다.

=이미지

 이미지(Image)는 컨테이너를 만드는 방법이 설명되어 있는 명세서이다. 즉, 이미지에 있는 내용에 따라 컨테이너가 생성된다. 사실 컨테이너는 이미지를 구현하는 instance에 불과하다. 객체 지향 프로그래밍 느낌으로 말하자면 이미지는 클래스의 개념이고 컨테이너는 객체의 개념과 유사하다. 이미지 안에는 필요한 프로그램과 환경변수 설정 등이 전부 기록이 되어 있고 이들을 실제로 구현한 것이 바로 컨테이너이다. 

 이미지는 이미지 레지스트리(Image Registry)라는 곳에 저장된다. 수많은 이미지 레지스트리가 있으며 가장 대표적인 곳은 Docker Hub이다. 

Docker Hub Container Image Library | App Containerization

 Docker Hub에는 이미 수많은 유명한 애플리케이션들(ubuntu, python, java, node, 등등)이 공식 이미지로 저장이 되어있고, 우리는 그걸 가져다 사용하기만 하면 된다. Docker Hub에 있는 공식 이미지들은 전부 문제없이 가동되고 심지어 예전 버전에 대한 이미지들도 상당수 제공하기 때문에 우리가 따로 환경구축을 할 필요없이 그저 가져다 쓰면 된다.

 Docker Hub 이외에도 많은 레지스트리가 있다. 깃허브, AWS 등 굵직한 곳들 모두 그들만의 레지스트리가 있다. 다만 이들은 Docker Hub처럼 모든 게 public하지 않고 비용부담도 있기 때문에 Docker Hub처럼 편하게 사용하기는 쉽지 않다. 다만 자신의 이미지가 남들에게 드러나서는 안된다면 private한 레지스트리를 제공하는 곳을 사용해야 할것이다. 

= 이미지 생성방법

 컨테이너는 이미지를 바탕으로 구현이 된다는 걸 알았으니, 이미지를 만드는 방법에 대해서 알아야 한다. 이미지를 만드는 방법은 Dockerfile을 작성하는 것이다. 

 Dockerfile은 GNU의 Makefile과 거의 비슷하다. 애플리케이션을 빌드할 때 make를 사용하면 해당 디렉토리 내에서 Makefile을 찾아서 거기에 쓰여진 지시사항 그대로 빌드를 하는 것처럼 이미지를 빌드할 때에도 디렉토리 내에 있는 Dockerfile을 찾고 거기에 쓰여있는 지시사항에 따라서 이미지를 생성한다. Dockerfile은 도커가 이미지를 생성할 때 참고하는 파일로, 이미지를 구성하는 방식에 대해 단계적으로 쓰여있다. 즉, Dockerfile만 작성한다면 우리도 이미지를 만들어낼 수 있다. 하지만 이거보다 더 중요한 기능이 있으니, 바로 이미지가 어떤 환경에서 세팅이 되었고 어떤 프로그램이 필요한지를 거의 전부 알 수 있다는 점이다.

= AFL++을 활용한 Dockerfile 설명

다음은 AFL++의 Dockerfile이다.

#
# This Dockerfile for AFLplusplus uses Ubuntu 22.04 jammy and
# installs LLVM 14 for afl-clang-lto support.
#
# GCC 11 is used instead of 12 because genhtml for afl-cov doesn't like it.
#

FROM ubuntu:22.04 AS aflplusplus
LABEL "maintainer"="afl++ team <afl@aflplus.plus>"
LABEL "about"="AFLplusplus container image"

### Comment out to enable these features
# Only available on specific ARM64 boards
ENV NO_CORESIGHT=1
# Possible but unlikely in a docker container
ENV NO_NYX=1

### Only change these if you know what you are doing:
# LLVM 15 does not look good so we stay at 14 to still have LTO
ENV LLVM_VERSION=14
# GCC 12 is producing compile errors for some targets so we stay at GCC 11
ENV GCC_VERSION=11

### No changes beyond the point unless you know what you are doing :)

ARG DEBIAN_FRONTEND=noninteractive

ENV NO_ARCH_OPT=1
ENV IS_DOCKER=1

RUN apt-get update && apt-get full-upgrade -y && \
    apt-get install -y --no-install-recommends wget ca-certificates apt-utils && \
    rm -rf /var/lib/apt/lists/*

RUN echo "deb [signed-by=/etc/apt/keyrings/llvm-snapshot.gpg.key] http://apt.llvm.org/jammy/ llvm-toolchain-jammy-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.list && \
    wget -qO /etc/apt/keyrings/llvm-snapshot.gpg.key https://apt.llvm.org/llvm-snapshot.gpg.key

RUN apt-get update && \
    apt-get -y install --no-install-recommends \
    make cmake automake meson ninja-build bison flex \
    git xz-utils bzip2 wget jupp nano bash-completion less vim joe ssh psmisc \
    python3 python3-dev python3-pip python-is-python3 \
    libtool libtool-bin libglib2.0-dev \
    apt-transport-https gnupg dialog \
    gnuplot-nox libpixman-1-dev \
    gcc-${GCC_VERSION} g++-${GCC_VERSION} gcc-${GCC_VERSION}-plugin-dev gdb lcov \
    clang-${LLVM_VERSION} clang-tools-${LLVM_VERSION} libc++1-${LLVM_VERSION} \
    libc++-${LLVM_VERSION}-dev libc++abi1-${LLVM_VERSION} libc++abi-${LLVM_VERSION}-dev \
    libclang1-${LLVM_VERSION} libclang-${LLVM_VERSION}-dev \
    libclang-common-${LLVM_VERSION}-dev libclang-cpp${LLVM_VERSION} \
    libclang-cpp${LLVM_VERSION}-dev liblld-${LLVM_VERSION} \
    liblld-${LLVM_VERSION}-dev liblldb-${LLVM_VERSION} liblldb-${LLVM_VERSION}-dev \
    libllvm${LLVM_VERSION} libomp-${LLVM_VERSION}-dev libomp5-${LLVM_VERSION} \
    lld-${LLVM_VERSION} lldb-${LLVM_VERSION} llvm-${LLVM_VERSION} \
    llvm-${LLVM_VERSION}-dev llvm-${LLVM_VERSION}-runtime llvm-${LLVM_VERSION}-tools \
    $([ "$(dpkg --print-architecture)" = "amd64" ] && echo gcc-${GCC_VERSION}-multilib gcc-multilib) \
    $([ "$(dpkg --print-architecture)" = "arm64" ] && echo libcapstone-dev) && \
    rm -rf /var/lib/apt/lists/*
    # gcc-multilib is only used for -m32 support on x86
    # libcapstone-dev is used for coresight_mode on arm64

RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-${GCC_VERSION} 0 && \
    update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-${GCC_VERSION} 0 && \
    update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${LLVM_VERSION} 0 && \
    update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${LLVM_VERSION} 0

RUN wget -qO- https://sh.rustup.rs | CARGO_HOME=/etc/cargo sh -s -- -y -q --no-modify-path
ENV PATH=$PATH:/etc/cargo/bin

ENV LLVM_CONFIG=llvm-config-${LLVM_VERSION}
ENV AFL_SKIP_CPUFREQ=1
ENV AFL_TRY_AFFINITY=1
ENV AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1

RUN git clone --depth=1 https://github.com/vanhauser-thc/afl-cov && \
    (cd afl-cov && make install) && rm -rf afl-cov

WORKDIR /AFLplusplus
COPY . .

ARG CC=gcc-$GCC_VERSION
ARG CXX=g++-$GCC_VERSION

# Used in CI to prevent a 'make clean' which would remove the binaries to be tested
ARG TEST_BUILD

RUN sed -i.bak 's/^	-/	/g' GNUmakefile && \
    make clean && make distrib && \
    ([ "${TEST_BUILD}" ] || (make install && make clean)) && \
    mv GNUmakefile.bak GNUmakefile

RUN echo "set encoding=utf-8" > /root/.vimrc && \
    echo ". /etc/bash_completion" >> ~/.bashrc && \
    echo 'alias joe="joe --wordwrap --joe_state -nobackup"' >> ~/.bashrc && \
    echo "export PS1='"'[afl++ \h] \w$(__git_ps1) \$ '"'" >> ~/.bashrc

FROM [image]

FROM ubuntu:22.04 AS aflplusplus
LABEL "maintainer"="afl++ team <afl@aflplus.plus>"
LABEL "about"="AFLplusplus container image"
 **모든 이미지는 FROM으로 시작해야한다**(예외도 있지만 지금은 넘어가자). FROM은 base image를 설정한다. Base image는 가장 밑바탕이 되는 이미지로, 구축하고 싶은 가장 기본적인 환경이라고 생각하면 된다. 여기서 알 수 있는 것은 이미지가 다른 이미지 위에 만들어진다는 것이다. 위 예시를 보면 FROM ubuntu:22.04라고 쓰여있는데, 이는 ubuntu라는 이미지 위에 이미지를 생성하겠다는 뜻이다. ‘:’ 뒤에 있는 22.04는 태그(tag)라고 하는데 쉽게 말하자면 버전과 비슷한 거다. 

 다음은 Docker Hub에 있는 ubuntu의 공식 이미지이다.

ubuntu - Official Image | Docker Hub

 위 사이트를 들어가면 다음과 같이 여러개의 지원하는 태그들이 나열되어 있다.

 보통 공식 이미지들은 태그를 버전명으로 생성하는 경우가 많다. 만약 태그를 명시하지 않았다면 도커는 자동적으로 default인 latest 태그를 붙여서 찾는다.

LABEL

 LABEL은 특별한 기능이 있는건 아니고 Dockerfile에 대한 설명을 추가한다. #으로 주석을 달아줄 수 있지만, LABEL을 사용하면 이미지를 사용하는 사람이 해당 문구들을 볼 수 있다. 주로 코드의 저자나 관리자 등의 내용들을 달아놓는 편이다.

ENV

ENV NO_CORESIGHT=1
ENV NO_NYX=1

ENV LLVM_VERSION=14
ENV GCC_VERSION=11

ENV NO_ARCH_OPT=1
ENV IS_DOCKER=1
 ENV는 이미지의 환경변수를 설정한다. 예시로 위에 보면 LLVM_VERSION=14로 설정해 놓은 것을 볼 수 있다. 

RUN

RUN apt-get update && apt-get full-upgrade -y && \
    apt-get install -y --no-install-recommends wget ca-certificates apt-utils && \
    rm -rf /var/lib/apt/lists/*

RUN echo "deb [signed-by=/etc/apt/keyrings/llvm-snapshot.gpg.key] http://apt.llvm.org/jammy/ llvm-toolchain-jammy-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.list && \
    wget -qO /etc/apt/keyrings/llvm-snapshot.gpg.key https://apt.llvm.org/llvm-snapshot.gpg.key

RUN apt-get update && \
    apt-get -y install --no-install-recommends \
    make cmake automake meson ninja-build bison flex \
    git xz-utils bzip2 wget jupp nano bash-completion less vim joe ssh psmisc \
    python3 python3-dev python3-pip python-is-python3 \
    libtool libtool-bin libglib2.0-dev \
    apt-transport-https gnupg dialog \
    gnuplot-nox libpixman-1-dev \
    gcc-${GCC_VERSION} g++-${GCC_VERSION} gcc-${GCC_VERSION}-plugin-dev gdb lcov \
    clang-${LLVM_VERSION} clang-tools-${LLVM_VERSION} libc++1-${LLVM_VERSION} \
    libc++-${LLVM_VERSION}-dev libc++abi1-${LLVM_VERSION} libc++abi-${LLVM_VERSION}-dev \
    libclang1-${LLVM_VERSION} libclang-${LLVM_VERSION}-dev \
    libclang-common-${LLVM_VERSION}-dev libclang-cpp${LLVM_VERSION} \
    libclang-cpp${LLVM_VERSION}-dev liblld-${LLVM_VERSION} \
    liblld-${LLVM_VERSION}-dev liblldb-${LLVM_VERSION} liblldb-${LLVM_VERSION}-dev \
    libllvm${LLVM_VERSION} libomp-${LLVM_VERSION}-dev libomp5-${LLVM_VERSION} \
    lld-${LLVM_VERSION} lldb-${LLVM_VERSION} llvm-${LLVM_VERSION} \
    llvm-${LLVM_VERSION}-dev llvm-${LLVM_VERSION}-runtime llvm-${LLVM_VERSION}-tools \
    $([ "$(dpkg --print-architecture)" = "amd64" ] && echo gcc-${GCC_VERSION}-multilib gcc-multilib) \
    $([ "$(dpkg --print-architecture)" = "arm64" ] && echo libcapstone-dev) && \
    rm -rf /var/lib/apt/lists/*
    # gcc-multilib is only used for -m32 support on x86
    # libcapstone-dev is used for coresight_mode on arm64

RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-${GCC_VERSION} 0 && \
    update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-${GCC_VERSION} 0 && \
    update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${LLVM_VERSION} 0 && \
    update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${LLVM_VERSION} 0

RUN wget -qO- https://sh.rustup.rs | CARGO_HOME=/etc/cargo sh -s -- -y -q --no-modify-path
 가장 중요한 커맨드 중 하나인 RUN은 말그대로 주어진 커맨드를 실행한다. apt install 과 같은 모든 명령들을 RUN에 명시해서 실행시킬 수 있다. 위 예시를 보면, 관련된 모든 dependency들을 설치하고 관련 작업들을 수행하는 걸 볼 수 있다. 중간에 git clone이나 build, make, 등 모든 것이 다 가능하다.

WORKDIR

WORKDIR /AFLplusplus
 컨테이너 내에서 작업할 디렉토리를 설정한다. 여기서 알 수 있는 사실은 image는 자신만의 고유한 파일 시스템을 가지고 있다는 것이다. 즉, 내부에서 cd나 ls와 같은 명령어를 통해 image안에 어떤 파일들이 있는지 탐색할 수 있다는 것이다.

 컨테이너 내에서 root 디렉토리는 다음과 같다.
/
 하나의 Dockerfile내에서 WORKDIR는 여러번 바꿀 수 있다. 다만 상대 경로로 주어져있을 경우, 현재 디렉토리에 이어서 상대 경로를 표현한다.

COPY [src dir][dest dir]

COPY . .
 COPY는 “**호스트**”의 [src dir]에 있는 모든 파일과 디렉토리를  “**컨테이너**”의 [dest dir]로 복사한다. 위의 예시는 호스트에 있는 현재 디렉토리 (.) 에 있는 모든 파일과 디렉토리를 컨테이너의 현재 디렉토리(.)에 복사한다는 뜻이다. 

(8) Docker를 활용한 AFL++ 설치 및 실습

Docker 설치

Ubuntu 22.04 버전 기준으로 설치한다. (2023년 1월 기준)

설치하기 전 주의사항: VM의 메모리 용량이 충분한지 확인해야 한다. (VM 권장용량 ⇒ 80GB이상)

linux에 도커를 설치하는 방법(공식 사이트):

Install on Linux

위 사이트에서도 확인할 수 있다.

시스템 요구사항

  • 64비트 kernel 및 virtualization이 가능해야함
  • KVM Virtualization Support.

확인방법:

sudo apt install cpu-checker
kvm-ok

성공했다면 다음을 입력한다.

ls -al /dev/kvm
sudo usermod -aG kvm $USER
  • RAM 최소 4GB 필요

Docker 설치

  1. 예전 버전들을 전부 제거한다. (첫 다운시 생략가능)
sudo apt remove docker-desktop
rm -r $HOME/.docker/desktop
sudo rm /usr/local/bin/com.docker.cli
sudo apt purge docker-desktop
  1. 다음 작업들을 수행한다.
sudo apt-get update
sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
  1. 도커 엔진을 설치한다. 총 4개의 프로그램을 설치한다.
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
  1. 잘 설치되었는지 확인한다.
sudo docker run hello-world
  1. 해당 사이트에서 DEB package를 다운받는다

Install on Ubuntu

  1. 다운받은 DEB package를 설치한다. Docker Desktop을 설치하는 과정이다.
sudo apt-get update
 sudo apt-get install ./docker-desktop-<version>-<arch>.deb 
 # 다운로드 받은 위치를 잘 기술해야한다.

아마 다운받은 파일이 Downloads 폴더에 있을테니 DEB package의 경로를 잘 명시하고 버전을 고쳐서 위 줄을 실행하면 된다.

  1. 설치가 완료되면 다음을 실행한다.
systemctl --user start docker-desktop

Docker Desktop을 실행시켜준다. 당연한 말이지만 약관 동의 안하면 프로그램 사용 못하니까 동의해주자.

  1. 나머지 프로그램들도 다 잘 설치되었는지 확인한다.
docker compose version
docker --version
docker version

추가로 컴퓨터를 키자마자 Docker Desktop의 실행을 원하면 다음과 같이 입력한다.

systemctl --user enable docker-desktop

설치가 완료되었다.

AFL++ Image를 가져와서 컨테이너 생성하기

도커를 무사히 설치했다면 다음을 실행해서 AFL++를 컨테이너에 실행할 수 있다.

docker pull aflplusplus/aflplusplus
docker run -ti -v /location/of/your/target:/src aflplusplus/aflplusplus

여기서 pull은 Docker Hub에서 AFL++ Image를 가져온다는 의미이다.

/location/of/your/target:/src 부분에서 /location/of/your/target 부분을 $HOME을 제외하고 원하는 디렉토리를 써넣으면 된다.

mkdir AFLplusplus
docker run -ti -v $HOME/AFLplusplus:/src aflplusplus/aflplusplus

완료되면 이런식으로 컨테이너안에 AFL++가 있는 모습을 볼 수 있다.

Docker Desktop을 열어보면 container가 돌아가고 있는걸 확인할 수 있다.

컨테이너 내에서 빌드를 할 수 있다.

make distrib

엄청 오래걸리지만, 오류없이 빌드가 될 것이다. (되는 걸 image로 만들어놨기 때문이다.)

빌드가 끝났으면 afl++ 안에 있는 예시 코드와 testcase를 이용해 afl-fuzz가 잘 작동하는지 확인해보자.

afl-clang-lto -o ./test-instr ./test-instr.c -fsanitize=address  # compile
afl-fuzz -i ./testcases/others/text/ -o ./out ./test-instr @@

잘 돌아간다.

컨테이너에서 나가려면 Ctrl + d를 해주면 된다.

컨테이너 안의 file system으로 다시 접근하고 싶으면 다음을 실행한다.

docker exec -t -i <container_name> /bin/bash

현재 작동하고 있는 컨테이너를 확인하고 싶으면 다음을 실행한다.

docker ps

Container ID를 이용해서 container를 멈추거나 재가동하거나 제거할 수 있다.

docker stop <Container ID>  # stop container
docker restart <Container ID>  # restart container
docker rm -f <Container ID>  # remove container (Caution: 여태까지 했던게 다 날아가니 주의)

(9) AFL++ Persistent Mode

AFL++ 에는 persistent mode가 있다. Persistent mode는 퍼징의 대상이 라이브러리처럼 안으로 계속 파고드는 형태라면, 해당 프로그램의 코드에서 가장 핵심이 되는 메소드를 골라서 그 메소드만 반복시키는 mode이다. 전체 코드를 전부 보는게 아닌 특정 메소드만 확인하기 때문에 속도가 더 빠르다는 장점이 있다.

 Persistent mode의 예제로 libxml2를 퍼징했다.

Home · Wiki · GNOME / libxml2 · GitLab

 Libxml2는 XML을 parse해주는 무료 소프트웨어이다. 여기에 xml을 넣어서 fuzzing을 해볼 것이다.

AFL-fuzz (default)


  1. 우선 libxml2를 다운받는다.
git clone https://gitlab.gnome.org/GNOME/libxml2.git
cd libxml2
  1. configure한다.
./autogen.sh
./configure --enable-shared=no
  1. Sanitizer를 활성화한다. 이번에 사용할 sanitizer는 ASan과 UBSan이다.
export AFL_USE_ASAN=1
export AFL_USE_UBSAN=1
  1. make한다.
make CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ LD=/path/to/afl-clang-lto
  1. 우선은 원래 하던 기본적인 방법으로 fuzzing을 준비한다. Libxml2가 실제로 사용하는 실행파일은 xmllint이다.
mkdir fuzz      # 이미 있다면 그냥 넘어간다
cp xmllint fuzz/xmllint_cov    # persistent mode랑 비교하기 위해 따로 디렉토리를 만들었다.

mkdir fuzz/in   # input 데이터 디렉토리
cp test/*.xml fuzz/in

cd fuzz
  1. afl-system-config를 사용한다. 이걸 사용하면 fuzzing의 효율이 증가하지만 보안문제에 더 취약해진다고 한다. 지금은 보안이 딱히 상관없으므로 사용한다.
sudo /path/to/afl-system-config
  1. Fuzzing한다
afl-fuzz -i ./in -o ./out -- ./xmllint_cov @@

아느정도 시간이 지나니까 exec_speed가 느려지는 걸 볼 수 있다.

AFL-fuzz (Persistent Mode)


  1. xmllint.c를 찾아서 열어본다
nano /path/to/xmllint.c
  1. main함수를 찾는다. 파일 가장 마지막에 쓰인 함수가 main함수이다. main 함수를 살펴보면 한 화면에 담을 수 없을 정도로 매우 길가는 것을 확인할 수 있다. 우리는 이 main함수를 수정해서 간결하게 만들어줄 필요가 있다.

  2. main함수 바로 위에 다음 main함수를 추가하고 원래 있던 main함수의 이름을 old_main으로 바꿔서 이전 main함수를 무용지물로 만든다.

int main(int argc, char** argv) {
    if (argc < 2) return 1;

    while (__AFL_LOOP(10000)) {          // AFL++에게 persistent mode를 쓰라고 알려준다
        parseAndPrintFile(argv[1], NULL);
    }

    return 0;
}
 위에 while문을 보면 __AFL_LOOP(10000)이 있다. __AFL_LOOP이 있으면 AFL++는 fuzzing할 때 persistent mode를 사용한다. 즉, while문 안에 있는 parseAndPrintFile() 메소드를 무한히 돌린다는 얘기다. 여기서 10000은 10000번째 실행마다 다시 재정비를 하라는 뜻이다. 반복만 하게되면 쓸모 없는 메모리가 생기는데 이를 방지하기 위해서 일정 횟수마다 재정비를 하게끔 한다.

 여기서 parseAndPrintFile이 선정되었는데, 그 이유는 해당 함수가 libxml2의 가장 핵심적인 기능을 담당한다고 판단되기 때문이다(XML을 받아서 parse하고 print해주는 게 이 프로그램의 핵심이다). 
  1. 원래 있던 xmllint를 지우고(xmllint.c가 아니다!!) 다시 compile해준다.
cd ..
rm xmllint
make CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ LD=/path/to/afl-clang-lto
cp xmllint fuzz/xmllint_persistent
  1. Fuzzing한다.
cd fuzz
afl-fuzz -i ./in -o ./out -- ./xmllint_persistent @@

속도가 무려 4508/sec (네자리수)인걸 볼 수 있다. 속도가 정말 빨라진 것을 확인 할 수 있다.

profile
천천히,꾸준하게

0개의 댓글