확장 가능한 parallel fuzzing 인프라 구축 및 성능 비교 분석 (2-2) AFL++ LEVELDB FUZZ with Asan + Persistent Mode

OOSUlZ·2023년 2월 23일
0
post-thumbnail

3) AFL++로 LEVELDB Build 및 Fuzz

(1) LEVELDB Afl-Clang-Lto++ Build 사전작업

빌드 과정은 전과 비슷하나 CC=afl-clang-lto, CXX=afl-clang-lto++를 사용한다는 점이 다르다.
우선, LTO-mode를 사용하기 위해 선행해야하는 과정이 필요하다.
LTO mode에는 afl-clang-lto (C코드), afl-clang-lto++ (C++코드) 이 두개의 컴파일러가 있는데, 가장 빠르고 가장 좋은 커버리지를 제공한다는 장점이 있다.

(1) clang을 다운받는다.

우선 컴퓨터에 clang이 있는지 확인한다.

clang --version

만약 없다면 다음과 같은 문구가 뜰것이다.

clang이 없으면 아래와 같이 설치하면 된다.

sudo apt install clang

Ubuntu 22.04 기준, 설치하면 clang의 버전이 14로 설치될 것이다. llvm-14도 같이 설치된다. 우리는 llvm과 clang의 버전이 최소 11 이상인 것이 있어야 하므로 버전이 충족한다.

다시 clang을 쳐보면 아까와는 달리 no input files가 나오는 걸 볼 수 있다. clang이 컴퓨터에 정상적으로 설치되었다는 뜻이다.

💡 clang이란? gcc와 같은 C언어 계열의 컴파일러이다. LLVM project에 포함되어 있다.

clang의 버전을 확인해본다.

clang --version

버전이 11 이상이면 된다.

  1. lld+14를 설치한다.

lld도 설치해줘야 한다. clang의 버전과 같은 버전인 lld-14를 설치한다.

sudo apt install lld-14
  1. llvm-config-14의 경로를 환경변수에 추가한다.

설치를 해도 AFL++가 빌드 도중에 llvm을 찾지 못한다. LLVM_CONFIG를 설정하라고 한다. 환경변수 설정은 export를 사용한다.

export LLVM_CONFIG=/usr/bin/llvm-config-14

llvm-config-14가 다른 곳에 저장되어 있다면 그 경로에 맞춰서 써주면 된다. 보통 저 경로가 default 경로이다.

또한, 저장되어있는 모든 환경변수들을 확인하는 방법은 다음과 같다.

env
  1. AFL++ 를 빌드한다.

AFLplusplus 디렉토리에서 make를 실행한다.

make distrib 

정상적으로 작동했다면 다음과 같은 메시지가 make하는 와중에 있을 것이다.

make가 끝나면 afl-fuzz를 쳐서 잘 되는지 확인을 해야한다.

afl-fuzz

잘 되면 위와 같은 화면이 나온다.

(2) LEVELDB Afl-Clang-Lto++ Build & Fuzz

이제, LEVELDB를 다음과 같은 명령어로 빌드를 한다.

git clone --recurse-submodules https://github.com/google/leveldb.git
cd leveldb
mkdir -p build && cd build
CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ cmake -DCMAKE_BUILD_TYPE=Release ..
CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ cmake build .
# AFLPlusPlus의 경로에 맞게 컴파일러의 경로를 지정해 주어야 한다

leveldb 코드가 잘 컴파일될 수 있도록 헤더파일들의 위치를 옮겼다. (복사 붙이기를 활용)

cp -av ./include/leveldb /path/to/where/leveldb/is/ # build 디렉토리가 있는곳에 저장했다.
  1. test.cpp 작성

현재 디렉토리에 test.cpp코드를 다음과 같이 작성했다.

// test.cpp

#include <iostream>
#include <cassert>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
#include "leveldb/db.h"

using namespace std;

// C++에는 내장된 split함수가 없어서 직접 구현
vector<string> split(string str, char Delimiter) {
    istringstream iss(str);
    string buffer;

    vector<string> result;

    while (getline(iss, buffer, Delimiter)) {
        result.push_back(buffer);
    }

    return result;
}

int main(int argc, char *argv[]) {
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
    assert(status.ok());

    leveldb::Status s;

    string file_name = argv[1];  //.txt 파일의 이름이 들어갈 자리이다.
    std::ifstream file(file_name);

    if (file.is_open()) {
        string line;

        while (getline(file, line)) {
            vector<string> result = split(line, ' ');
            string key1 = result[0];
            string value1 = result[1];
            string value;
            
            s=db->Put(leveldb::WriteOptions(), key1, value1);
            s=db->Get(leveldb::ReadOptions(), key1, &value);

            cout << key1 << "'s value == " << value << endl;
        }
        file.close();
    } else {
        cout << "Failed to open file" << endl;
    }

    delete db;

    return 0;
}
  1. testcase 준비

AFLplusplus에 있는 testcase를 활용해도 되고, 자기가 새로 만들어도 된다. 대신에 코드가 일반적으로 돌아갈 수 있게끔 하는 input을 준비해야 한다. 에러가 고의적으로 발생하는 input을 준비하면 안된다. 우리의 코드에 부합하는 input을 새로 준비했다.

mkdir testcases && cd testcases
nano hello_world.txt
cd ..

여기서

nano hello_world.txt

를 치면 에디터가 나온다. 다음과 같이 입력하고 저장했다.

hello world
What's up
  1. test.cpp를 컴파일
/path/to/afl-clang-lto++ -o test ./test.cpp ./build/libleveldb.a -lpthread -I include -fsanitize=address

컴파일 성공하면 실행해보자.

./test ./testcases/hello_world.txt

코드가 잘 돌아가는걸 확인할 수 있다.

  1. afl-fuzz
afl-fuzz -i ./testcases -o ../out ./test -m @@

파일을 input으로 받을 경우, 뒤에 @@을 붙여줘야 한다. Input파일이 하나밖에 없지만, 안붙이면 에러가 난다.

직접 작성해본 코드를 성공적으로 실행시켰다.


4) AFL++로 LEVELDB Build 및 Fuzz (ASan 활용)

(1) ASan을 활용한 AFL++로 LEVELDB Build 및 Fuzz 실습

LTO mode 가 활성화되어 있다면 fuzzing하고자하는 프로그램을 빌드할 때 앞에 AFL_USE_ASAN=1 을 붙여주고 파일을 컴파일 시에는 -fsanitize=address 옵션을 추가해주면 된다.

ASan이란?

ASan은 Address Sanitizer의 줄임말로써 C/C++ 의 메모리 에러를 감지하는 도구이다. LLVM project에 포함되어 있어 clang으로 컴파일할 때 사용해서 memory error를 감지하는데 도움을 준다.

사용방법

clang으로 컴파일할 때 -fsanitize=address 옵션을 추가하면 된다.

예시)

clang++ -o test ./test.cpp -fsanitize=address # test와 test.cpp는 예시 파일이다.

AFL++에서 Asan을 활용하기 위해, Leveldb를 다시 빌드해야한다.

  1. 다음과 같은 명령어로 LevelDB를 빌드한다.
cd $HOME
mkdir fuzzing_leveldb && cd fuzzing_leveldb
git clone --recurse-submodules https://github.com/google/leveldb.git
cd leveldb
mkdir build && cd build

여기까지는 원래하던거와 똑같은데 다음 줄이 조금 다르다.

AFL_USE_ASAN=1 CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ cmake -DCMAKE_BUILD_TYPE=Debug ..
AFL_USE_ASAN=1 CC=/path/to/afl-clang-lto CXX=/path/to/afl-clang-lto++ cmake --build . 
cd ..

앞에 AFL_USE_ASAN=1을 추가했다.
export AFL_USE_ASAN=1 를 터미널에 입력하여 환경변수로 추가해주면 명시를 안해도 된다.
그리고 -DCMAKE_BUILD_TYPE=Debug 라고 썼다. 원래 하던 대로 -DCMAKE_BUILD_TYPE=Release를 하면 빌드를 하는 중간에 에러가 났기 때문이다.

빌드가 끝나면 현재 디렉토리에 테스트할 코드와 테스트 케이스를 준비한다.
레벨디비의 기능을 활용하기 위해 공식 문서에 있던 기능들을 몇가지 응용하였다.

테스트용 소스코드:

// leveldb_functions_test.cpp

#include <iostream>
#include <fstream>
#include <cassert>
#include <sstream>
#include <vector>
#include <string>
#include "leveldb/db.h"
#include "leveldb/write_batch.h"

using namespace std;

//split function
vector<string> split(string str, char Delimiter) {
    istringstream iss(str);
    string buffer;

    vector<string> result;

    while (getline(iss, buffer, Delimiter)) {
        result.push_back(buffer);
    }

    return result;
}

// loads multiple key-value pair written in the text file
void addKeyValues(leveldb::DB *db, std::ifstream& file) {
    if (file.is_open()) {
        leveldb::Status s;
        string line;
        string key;
        string value;

        while (getline(file, line)) {
            vector<string> result = split(line, ' ');
            key = result[0];
            value = result[1];

            s=db->Put(leveldb::WriteOptions(), key, value);
        }
    } else {
        cout << "File not opened" << endl;
        return;
    }
    return;
}

// iterate through all key-value pairs and print them out
void printAllKVPairs(leveldb::DB * db) {
    leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());

    for (it->SeekToFirst(); it->Valid(); it->Next()) {
        cout << it->key().ToString() << ": " << it->value().ToString() << endl;
    }
    assert(it->status().ok());

    delete it;
    return;
}

int main(int argc, char *argv[]) {
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
    assert(status.ok());

    leveldb::Status s;

    string filename = argv[1];
    std::ifstream file(filename);

    addKeyValues(db, file);  //custom function to load DB with key-value pairs
    printAllKVPairs(db);  // custom function to print all key-value pairs
    delete db;

    return 0;
}

테스트 케이스: (./testcases/helloworld.txt)

hello world

준비가 다 됐으면 위 테스트 코드를 컴파일해본다.

/path/to/afl-clang-lto++ -o ./test_leveldb ./leveldb_functions_test.cpp ./build/libleveldb.a -I ./include -fsanitize=address

컴파일할 때 뒤에 -fsanitize=address을 추가해줘야 컴파일이 된다.

퍼징한다.

afl-fuzz -i ./testcases -o ../out ./test_leveldb @@

crash가 너무 많이 생겨, 이중 하나로 id가 7번인 crash의 입력값을 살펴보았다

아래 사진은 vim 에디터로 7번 데이터를 열어본 것이다.

변환된 상태라 원래의 입력값이 보이지 않는다.

이 오류난 파일을 실행해보니, 어느 부분에서 어떠한 에러가 발생했는지가 정말 상세하게 확인할 수 있다


혹시 메모리 부족 문제일 거 같아서 -m 옵션을 하나 더 추가해줘서 퍼징해봤다.

afl-fuzz -i ./testcases -o ../out ./test_leveldb -m @@

이번엔 오히려 crash가 전혀 생기지 않았다. last new find와 run time의 시간이 같지 않을 걸 보니, 무언가 의미있는 mutated input을 발견한거 같아서 확인해보았다.

cd ..
cd out/default
cd queue
ls

하나씩 열어보았다.

nano ./id:000000,src:000000,time:1239,execs:105,op:havoc,rep:16

!

nano ./id:000002,src:000000,time:11965,execs:902,op:havoc,rep:16,+cov

이상한 텍스트로 변해있는 걸 확인할 수 있다. 또한 값이 공백이어도 들어간다.

실제로 mutate된 파일들을 실행해 봤다. 파일 이름은 언제나 다르다.

(2) AFL Persistent Mode

	// test.cpp

#include <iostream>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <ctime>
#include <string>
#include <cassert>
#include "leveldb/db.h"
#include "leveldb/write_batch.h"

using namespace std;

string put(leveldb::DB *db, string key, int value) {
    string val;
    leveldb::Status s;

    // check if the given key exists in the DB
    s = db->Get(leveldb::ReadOptions(), key, &val);

    if (!s.ok()) {
        s = db->Put(leveldb::WriteOptions(), key, to_string(value));
        s = db->Get(leveldb::ReadOptions(), key, &val);
    } else {
        leveldb::WriteBatch batch;
        batch.Delete(key);
        batch.Put(key, to_string(value));
        s = db->Write(leveldb::WriteOptions(), &batch);
        s = db->Get(leveldb::ReadOptions(), key, &val);
    }

    return val;
}

//generate random number from 1-10 for value
int getRandomNumber() {
    srand((unsigned int)time(NULL));

    int tmp = rand();
    tmp = (int) tmp % 10;
    if (tmp == 0) tmp = 1;

    return tmp;
}

int main(int argc, char *argv[]) {
    // create DB
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);

    // Persistent Mode
    __AFL_INIT();
    while (__AFL_LOOP(10000)) {
        // Open file
        string filename = argv[1];
        std::ifstream file(filename);

        if (file.is_open()) {
            string line;
            string key;
            int value;

            getline(file, line);
            key = line;
            value = getRandomNumber();
            cout << "value inserted: " << put(db, key, value) << endl;
        } else {
            cout << "File not Opened" << endl;
        }
    }

    delete db;

    return 0;
}

Persistent Mode를 적용하면, 속도가 상당히 빨라진걸 볼 수 있다.

(3) LevelDB에 이미지 파일 넣고, Fuzz하기

항상 txt파일에만 시도를 했으니, leveldb에 다른 형식의 파일도 넣어볼 시도를 해봤다.

문제점:

 Leveldb의 헤더 파일에 있는 Put 메소드를 보면, 인자로 Slice를 필요로 한다. Slice는 string으로는 자유자제로 형변환이 양방향으로 가능하지만, 그 외의 다른 형에 대해서는 가능하지 않다. 이미지 파일은 기본적으로 string형식의 파일이 아니기 때문에 일반적인 방법으로는 넣을 수 없다는 것이다. 

해결방안:

 이미지 파일을 binary로 읽어와서 그것을 통째로 string으로 변환한다음 value값에 넣어보기로 했다.

이번에는 key값으로 항상 unique한 현재 시스템 시간을 사용하기로 했다.

이번에 사용한 test.cpp코드는 다음과 같다.

// test.cpp

#include <iostream>
#include <fstream>
#include <sstream>
#include <cstdio>
#include <string>
#include <chrono>
#include <ctime>
#include <cassert>
#include <cstdlib>
#include "leveldb/db.h"

using namespace std;

int main(int argc, char *argv[]) {
    // Create DB
    leveldb::DB* db;
    leveldb::Options options;
    options.create_if_missing = true;
    leveldb::Status status = leveldb::DB::Open(options, "/testdb", &db);
    assert(status.ok());

    // Read File 
    string filename(argv[1]);
    ifstream fin;
    fin.open(filename, ios::binary);
    if (fin.fail()) {
        return -1;
    }
    // Put the file in stream
    ostringstream oss;
    oss << fin.rdbuf();
    string data(oss.str());

    // Get current time
    auto now = chrono::system_clock::now();
    time_t key = chrono::system_clock::to_time_t(now);

    // Check the data
    leveldb::Status s;
    cout << "Current Time: " << key << endl;
    cout << "Image: " << data << endl;

    // Put the key-value pair
    s = db->Put(leveldb::WriteOptions(), key, data);
    string val;
    if (s.ok()) s=db->Get(leveldb::ReadOptions(), key, &val);
    cout << val << endl;
    // Close DB
    delete db;
    // Close file
    fin.close();

    return 0;
}
키 값으로 시스템 시간으로 현재 시간을 사용했고, 이미지 파일을 binary 형식으로 열어서 levelDB에 추가하는 코드이다.

 이미지는 /AFLplusplus/testcases/images/jpeg에 있는 not_kitty.jpg를 활용했다. jpeg가 아닌 다른 파일들 (bmp, png)에 있는 이미지를 활용해도 동일하게 작동한다.

 이미지 파일을 사용하니 엄청 느리다. 그리고 CPU사용률이 거의 바닥을 친다. afl-gotcpu를 치면 연구실 컴퓨터에서 사용할 수 있는 CPU의 개수가 엄청 많다는 것을 볼 수 있다. 

40시간 후:

Saved Hang이 한개 생겨있다.

한번 실행해 봤지만 실행 자체는 정상적으로 되었다. fuzzing하는 과정이 우연히 오래걸린듯 하다.


profile
천천히,꾸준하게

0개의 댓글