使用 protobuf & AFLplusplus 进行简易 CTF 自动化 fuzz

一、简介

fuzz 的结构感知 时遇到了 protobuf,觉得很有意思,于是尝试使用 protobuf 来进行快速简易的 CTF fuzz。

以下以 TCTF2021-babyheap2021 为例,来简单说明一下自动化步骤。

这里主要用到以下项目:

需要注意的是,该 fuzz 目前处于实验性版本,可能不太稳定,仅作为学习研究使用。

二、操作流程

1. 下载依赖

git clone 下 AFL++ 和 afl-libprotobuf-mutator (链接在上面)即可。

2. 配置 afl-libprotobuf-mutator

  • 首先,用 ida64 打开 babyheap2021, F5阅读伪代码并总结其输入模板,最后用 protobuf 描述输入结构:

    这类菜单题的输入模板大体上比较固定,下面的代码随便改改就能换一道题目用用。

    代码编写完成后,覆盖保存至 afl-libprotobuf-mutator/gen/out.proto。注意路径必须完成一致,若遇到重名文件 out.proto 则直接替换。

    如果不会写 protobuf 描述的话,可以看看这个 Protocol Buffers Tutorials

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // out.proto
    syntax = "proto2";
    package menuctf;

    message AllocChoice {
    required int32 choice_id = 1 [default=1];
    required int32 size = 2;
    required string content = 3;
    }

    message UpdateChoice {
    required int32 choice_id = 1 [default=2];
    required int32 idx = 2;
    required int32 size = 3;
    required string content = 4;
    }

    message DeleteChoice {
    required int32 choice_id = 1 [default=3];
    required int32 idx = 2;
    }

    message ViewChoice {
    required int32 choice_id = 1 [default=4];
    required int32 idx = 2;
    }

    message ExitChoice {
    required int32 choice_id = 1 [default=5];
    }

    // Our address book file is just one of these.
    message ChoiceList {
    message Choice {
    oneof the_choice{
    AllocChoice alloc_choice = 1;
    UpdateChoice update_choice = 2;
    DeleteChoice delete_choice = 3;
    ViewChoice view_choice = 4;
    ExitChoice exit_choice = 5;
    }
    }
    repeated Choice choice = 1;
    }
  • 到了这里,我们需要理一理思路。对于CTF题来说,大多都是直接从 stdin 中获取输入的文本数据。因此首先,我们需要编写 Protobuf::Message常规输入字符串的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    void ProtoToDataHelper(std::stringstream &out, const google::protobuf::Message &msg) {
    const google::protobuf::Descriptor *desc = msg.GetDescriptor();
    const google::protobuf::Reflection *refl = msg.GetReflection();

    const unsigned fields = desc->field_count();
    // std::cout << msg.DebugString() << std::endl;
    for (unsigned i = 0; i < fields; ++i) {
    const google::protobuf::FieldDescriptor *field = desc->field(i);

    // 对于单个 choice
    if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) {
    // 如果当前是 choice list
    if (field->is_repeated()) {
    const google::protobuf::RepeatedFieldRef<google::protobuf::Message> &ptr = refl->GetRepeatedFieldRef<google::protobuf::Message>(msg, field);
    // 将每个 choice 打出来
    for (const auto &child : ptr) {
    ProtoToDataHelper(out, child);
    out << "\n";
    }
    // 如果当前是某个子 choice
    } else if (refl->HasField(msg, field)) {
    const google::protobuf::Message &child = refl->GetMessage(msg, field);
    ProtoToDataHelper(out, child);
    }
    }
    // 对于单个 field
    else if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_INT32) {
    out << refl->GetInt32(msg, field);
    if(i < fields - 1)
    out << " ";
    }
    else if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING) {
    out << refl->GetString(msg, field);
    if(i < fields - 1)
    out << " ";
    }
    else {
    abort();
    }

    }
    }
  • 之后,参照 AFL++ 的 Custom Mutators in AFL++,完成一些必要的 custom mutate 函数。

    这里我们需要完成以下几种函数:

    • void *afl_custom_init(void *afl, unsigned int seed):在执行 custom mutate 前需要执行的初始化操作,这里只需初始化一下随机种子。
    • size_t afl_custom_fuzz(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf, unsigned char *add_buf, size_t add_buf_size, size_t max_size) :变异逻辑,在该代码中编写自己的变异逻辑。
    • size_t afl_custom_post_process(void* data, uint8_t *buf, size_t buf_size, uint8_t **out_buf):将 protobuf::Message 格式的二进制数据转换成 target 可读的数据。
    • void afl_custom_deinit(void *data):变异完成后需要做的事情,目前没有什么事情需要在这里进行处理。
    • int32_t afl_custom_init_trim(void *data, uint8_t *buf, size_t buf_size):自定义 trim 逻辑的初始化。为了防止 trim 逻辑破坏 protobuf::Message 的二进制数据,影响正常的 Parse 过程,这里可以让该函数直接返回0,跳过每次的 trim 阶段。
    • size_t afl_custom_trim(void *data, uint8_t **out_buf):自定义 trim 逻辑。由于afl_custom_init_trim函数返回0,因此实际上该函数不会被调用,但我们仍然必须声明该函数以启用自定义 trim 逻辑。

    需要注意的是,这一整个 extern "C" 的代码以及内部用到的 ProtoToDataHelper 函数的代码,必须全部放在 afl-libprotobuf-mutator/src/mutate.cc 中。

    由于 afl-libprotobuf-mutator 较为久远,因此大部分 AFL++ 相关的接口需要修改亿下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    // AFLPlusPlus interface
    extern "C" {
    static std::default_random_engine engine_pro;
    static std::uniform_int_distribution<unsigned int> dis(0, UINT32_MAX);

    void *afl_custom_init(void *afl, unsigned int seed) {
    #pragma unused (afl)
    engine_pro.seed(seed);
    return nullptr;
    }

    void afl_custom_deinit(void *data) {
    assert(!data);
    }

    // afl_custom_fuzz
    size_t afl_custom_fuzz(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf,
    unsigned char *add_buf, size_t add_buf_size, size_t max_size) {
    #pragma unused (data)
    #pragma unused (add_buf)
    #pragma unused (add_buf_size)

    static uint8_t *saved_buf = nullptr;

    assert(buf_size <= max_size);

    uint8_t *new_buf = (uint8_t *) realloc((void *)saved_buf, max_size);
    if (!new_buf) {
    *out_buf = buf;
    return buf_size;
    }
    saved_buf = new_buf;

    memcpy(new_buf, buf, buf_size);

    size_t new_size = LLVMFuzzerCustomMutator(
    new_buf,
    buf_size,
    max_size,
    dis(engine_pro)
    );
    *out_buf = new_buf;
    return new_size;
    }

    size_t afl_custom_post_process(void* data, uint8_t *buf, size_t buf_size, uint8_t **out_buf) {
    #pragma unused (data)
    // new_data is never free'd by pre_save_handler
    // I prefer a slow but clearer implementation for now

    static uint8_t *saved_buf = NULL;

    menuctf::ChoiceList msg;
    std::stringstream stream;
    // 如果加载成功
    if (protobuf_mutator::libfuzzer::LoadProtoInput(true, buf, buf_size, &msg)) {
    ProtoToDataHelper(stream, msg);
    }
    else {
    // printf("[afl_custom_post_process] LoadProtoInput Error\n");
    // std::ofstream err_bin("err.bin");
    // err_bin.write((char*)buf, buf_size);

    // abort();

    // 如果加载失败,则返回 Exit Choice
    /// NOTE: 错误的变异 + 错误的 trim 将会导致 post process 加载失败,尤其是 trim 逻辑。
    /// TODO: 由于默认的 trim 会破坏样例,因此需要手动实现一个 trim,这里实现了一个空 trim,不进行任何操作
    ProtoToDataHelper(stream, menuctf::ExitChoice());
    }
    const std::string str = stream.str();

    uint8_t *new_buf = (uint8_t *) realloc((void *)saved_buf, str.size());
    if (!new_buf) {
    *out_buf = buf;
    return buf_size;
    }
    *out_buf = saved_buf = new_buf;

    memcpy((void *)new_buf, str.c_str(), str.size());

    return str.size();
    }

    int32_t afl_custom_init_trim(void *data, uint8_t *buf, size_t buf_size) {
    /// NOTE: disable trim
    return 0;
    }

    size_t afl_custom_trim(void *data, uint8_t **out_buf) {
    /// NOTE: unreachable
    return 0;
    }

    }
  • 当然,编写上面的代码需要做一次又一次的测试,这里放上笔者的测试代码片段。这部分测试代码位于 afl-libprotobuf-mutator/src/dump.cc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    inline std::string slurp(const std::string& path) {
    std::ostringstream buf;
    std::ifstream input (path.c_str());
    buf << input.rdbuf();
    return buf.str();
    }

    extern "C" {
    void *afl_custom_init(void *afl, unsigned int seed);
    size_t afl_custom_fuzz(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf,
    unsigned char *add_buf, size_t add_buf_size, size_t max_size);
    size_t afl_custom_post_process(void* data, uint8_t *buf, size_t buf_size, uint8_t **out_buf);
    void afl_custom_deinit(void *data);
    }

    int main(int argc, char *argv[]) {
    menuctf::ChoiceList msg;

    if (argc == 2) {
    std::string data = slurp(argv[1]);
    if(!protobuf_mutator::libfuzzer::LoadProtoInput(true, (const uint8_t *)data.c_str(), data.size(), &msg)) {
    printf("[afl_custom_post_process] LoadProtoInput Error\n");
    abort();
    }

    // 测试变异逻辑
    void* init_data = afl_custom_init(nullptr, time(NULL));
    for(int i = 0; i < 30; i++) {
    uint8_t *out_buf = nullptr;
    size_t new_size = afl_custom_fuzz(init_data, (uint8_t*)data.c_str(), data.size(),
    &out_buf, nullptr, 0, data.size() + 100);
    uint8_t *new_str = nullptr;
    size_t new_str_size = afl_custom_post_process(init_data, out_buf, new_size, &new_str);
    std::string new_str_str((char*)new_str, new_str_size);
    std::cout << i << ": " << new_str_str << std::endl;
    }
    afl_custom_deinit(init_data);
    } else {
    // alloc 12 "[menuctf::AllocChoice]"
    {
    auto choice = new menuctf::AllocChoice();
    choice->set_size(12);
    choice->set_content("[menuctf::AllocChoice]");

    msg.add_choice()->set_allocated_alloc_choice(choice);
    }

    // update 2 20 "[menuctf::UpdateChoice]"
    {
    auto choice = new menuctf::UpdateChoice();
    choice->set_idx(2);
    choice->set_size(20);
    choice->set_content("[menuctf::UpdateChoice]");

    msg.add_choice()->set_allocated_update_choice(choice);
    }

    // DeleteChoice 3
    {
    auto choice = new menuctf::DeleteChoice();
    choice->set_idx(3);

    msg.add_choice()->set_allocated_delete_choice(choice);
    }

    // ViewChoice 4
    {
    auto choice = new menuctf::ViewChoice();
    choice->set_idx(4);

    msg.add_choice()->set_allocated_view_choice(choice);
    }

    // ExitChoice
    {
    auto choice = new menuctf::ExitChoice();

    msg.add_choice()->set_allocated_exit_choice(choice);
    }

    std::ofstream output_file("output.bin", std::ios::binary);
    // 这里保存的 Serialize 必须使用 Partial 保存,
    msg.SerializePartialToOstream(&output_file);
    output_file.close();
    }

    // std::cout << "msg DebugString: " << msg.DebugString() << std::endl;
    std::stringstream stream;
    ProtoToDataHelper(stream, msg);
    std::cout << stream.str() << std::endl;

    return 0;
    }
  • 接下来只需在 afl-libprotobuf-mutator 文件夹下执行 ./build.sh && make即可,完成后,在当前工作路径下将会生成 dumperlibmutator.so以及mutator三个文件。我们可以利用 dumper 对上面的代码进行测试,libmutator.so 用于 afl++ 中的自定义变异。

3. 配置 AFL++

现在压力来到了 AFL++ 这里(笑),我们先试试看能不能马上跑起来。

尝试执行以下命令来构建 AFL++:

1
2
3
4
5
6
7
8
9
10
11
12
# 构建 AFLplusplus
# 1. 安装依赖项
sudo apt-get update
sudo apt-get install -y ninja-build build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
# try to install llvm 11 and install the distro default if that fails
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev
# 2. 开始构建
cd AFLplusplus
make distrib # 这一步要等一段时间
# sudo make install # 将 AFL++ 安装至本机
# 如果不需要了可以使用 sudo make uninstall 卸载

4. 运行

执行以下命令运行 AFL++:

1
2
3
4
5
6
7
8
9
10
11
# AFL++ 构建完成后,进入 workdir 配置语料
mkdir workdir
[配置语料等等...]

# 设置相关环境变量
export AFL_CUSTOM_MUTATOR_ONLY=1 # 禁用除自定义 mutator 以外的其他自带 mutator
export AFL_CUSTOM_MUTATOR_LIBRARY=../afl-libprotobuf-mutator/libmutator.so # 指定自定义路径
export AFL_USE_QASAN=1 # 启用 QASAN

# 运行 AFL++
AFLplusplus/afl-fuzz -i workdir/fuzz_input -o workdir/fuzz_output -Q -- ./babyheap

别忘记在 workdir 中放点输入语料,语料可以通过 afl-libprotobuf-mutator/dumper 来随便生成一点。

运行时如果遇到 afl-quemu-trace 不存在,则单独执行AFLplusplus/qemu_mode/build_qemu_support.sh 构建即可。

三、源代码

相关源代码以及构建方式已开源至 github 上。

四、可改进的地方

  1. libprotobuf-mutator 的变异效果一般,最好手动改进一下
  2. 需要实现一下 trim 逻辑,空的 trim 逻辑可能会产生 样例爆炸

五、一些需要注意的点

如果在运行 AFL++ 后,发现 fuzz 始终无法发现新路径,即路径始终只有一个,那么就必须考虑目标CTF文件是否可执行。以当前的 babyheap2021 为例,笔者在测试时初始 AFL++ 状态如下:

image-20210927082401830

尝试直接执行 babyheap,发现 Permission Denied无法执行。但即便赋以 excutable 权限,仍然无法执行,报错 no such file or directory

image-20210927082630107

这一看,要么是架构问题,要么是 libc.so / ld.so 的问题。因此执行以下命令以更新 babyheap 所使用的 libc.so & ld.so,之后便可以正常执行。

1
2
patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 ./babyheap
patchelf --replace-needed libc.so libc.so.6 ./babyheap

跑起来效果,还行?(不是很懂.jpg)

image-20210927112534183

六、补充说明

补充于 2022/8/25 晚。

发现这篇文章好像有挺多人看的,而且还动手实践了(震惊)。之前想的是做一个 toy 出来玩玩,没想到有挺多人有这方面的需求。既然看的人多,那我得补充一些说明上去。

  • 第一点,也是最重要的一点,我当初选择这个 babyheap 作为例子是一个非常错误的想法。babyheap 本身有一些坑,例如上面说的要执行一些命令来修正;还有内部 mmap 定向内存分配在 qemu 中是无法满足的,prctl 调用也会失败,会被直接 exit 掉,需要做一些 patch 操作,详情可查看评论区。

  • 第二点,喂入 AFL 的 testcase 必须是 protobuf bin 格式的数据。即需要事先用 afl-libprotobuf-mutator/dumper 将明文输入转换为 protobuf bin 格式的数据,再来喂给 AFL;直接把用户的明文数据喂入 AFL 会导致异常。

  • 第三点,AFLplusplus 的更新频率比我想象的要快很多。我当时使用的版本为 2021年10月的,现在过了这么久,很多接口和代码都发生了变动,需要注意这点!

  • 第四点,有好几个师傅反应这个变异效果,有那么忆点点拉跨呀。这是因为 libprotobuf-mutator 的源码中内置了两种变异,一种是自己本身的变异逻辑,再一种是使用 libfuzzer 的变异逻辑。但关键是 libfuzzer 的变异逻辑的实现是空的,变异函数返回一个0…

    1
    2
    3
    4
    5
    // https://github.com/google/libprotobuf-mutator/blob/e5869dd9690c3f4dfb842fb90bd07a5a9ee32172/src/libfuzzer/libfuzzer_mutator.cc#L55

    LIB_PROTO_MUTATOR_WEAK_DEF(size_t, LLVMFuzzerMutate, uint8_t*, size_t, size_t) {
    return 0;
    }

    但 protobuf fuzzer 用的就是 libfuzzer 的变异逻辑,因此得改一下代码。在每次变异之前,变异器会先获取 mutator,但这个 mutator 效果拉跨,因此需要修改这一句代码:

    1
    2
    3
    4
    5
    6
    // https://github.com/google/libprotobuf-mutator/blob/e5869dd9690c3f4dfb842fb90bd07a5a9ee32172/src/libfuzzer/libfuzzer_macro.cc#L126

    Mutator* GetMutator() {
    static Mutator mutator; // <---
    return &mutator;
    }

    现在用的是派生类的 Mutator,得把它换成基类 Mutator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Randomly makes incremental change in the given protobuf.
    // Usage example:
    // protobuf_mutator::Mutator mutator(1);
    // MyMessage message;
    // message.ParseFromString(encoded_message);
    // mutator.Mutate(&message, 10000);
    //
    // Class implements very basic mutations of fields. E.g. it just flips bits for
    // integers, floats and strings. Also it increases, decreases size of
    // strings only by one. For better results users should override
    // protobuf_mutator::Mutator::Mutate* methods with more useful logic, e.g. using
    // library like libFuzzer.
    class Mutator {
    public:
    ...

    一直以为基类这个变异器才是 protobuf 变异的正统,那个派生变异器是个啥…

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~