• XSS.stack #1 – первый литературный журнал от юзеров форума

Fuzzing Заработайте 200 000 долларов на фаззинге на выходных: часть 1

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
Применив известные методы фаззинга к популярной цели, я обнаружил несколько ошибок, которые в общей сложности принесли более 200 тысяч долларов вознаграждения. В этой статье я покажу, насколько мощным может быть фаззинг применительно к программному обеспечению, которое еще не прошло достаточного тестирования.

Я призываю вас всех, даже тех, кто еще не пробовал свои силы в фаззинге, начинайте!

Экспозиция​

Несколько друзей и я запустили небольшой сервер Discord (теперь пространство Matrix), на котором мы обсуждали методы исследования безопасности и уязвимостей. Одна из вещей, которые мы запускаем на сервере, — это бот, который публикует все CVE по мере их появления. И да, я их много читал.

Однажды бот опубликовал то, что бросилось мне в глаза:
1653395734021.png


Это отмечает точку отсчета: 28 января. Я заметил этот CVE, в частности, по двум причинам:
  • это был BPF, который я считаю абсурдно крутой концепцией, поскольку он используется в ядре Linux (JIT-компилятор в ядре!!! что!!!)
  • это был JIT-компилятор, написанный на Rust
Этот CVE обнаружился почти сразу после того, как я разработал довольно интенсивный фаззинг для некоторого моего собственного программного обеспечения на Rust (в частности, ящик для проверки решений sokoban, где я наблюдал аналогичные проблемы и думал, что «это выглядит знакомо»). Зная то, что я узнал из своего опыта фаззинга собственного программного обеспечения, и то, что ошибки в программах на Rust можно довольно легко найти с помощью комбинации грузового фаззинга и произвольного , я подумал: «Эй, а почему бы и нет?».

Цель и ее тест​

Solana , как многие из вас, вероятно, знают, «представляет собой децентрализованный блокчейн, созданный для создания масштабируемых и удобных приложений для всего мира». В первую очередь они известны своей криптовалютой SOL, но также представляют собой блокчейн, который работает практически с любой формой смарт-контракта. rBPF — это самопровозглашенная «виртуальная машина Rust и JIT-компилятор для программ eBPF». Примечательно, что он реализует как интерпретатор, так и компилятор JIT для программ BPF. Другими словами: две разные реализации одной и той же программы, которые теоретически демонстрируют одинаковое поведение при выполнении. Мне посчастливилось пройти курс тестирования программного обеспечения в университете и быть частью исследовательской группы, занимающейся фаззингом (правда, мы занимались фаззингом аппаратного обеспечения, а не программного обеспечения, но концепции перекликаются). Концепция, за которую я особенно уцепился, — это идея тестовых оракулов — способ различить, что является «правильным» поведением, а что — нет в тестируемом проекте.
В частности, наличие интерпретатора и JIT-компилятора в rBPF особенно выделялось тем, что у нас, по сути, был совершенный псевдооракул; как пишет Википедия :

отдельно написанная программа, которая может принимать те же входные данные, что и тестируемая программа или система, чтобы можно было сравнить их выходные данные, чтобы понять, может ли возникнуть проблема для исследования.

Те из вас, у кого больше опыта в фаззинге, узнают эту концепцию как дифференциальный фаззинг , но я думаю, что мы часто можем упускать из виду, что дифференциальный фаззинг — это просто еще одно лицо псевдооракула. В этом конкретном случае мы можем выполнить интерпретатор, одну реализацию rBPF, а затем выполнить JIT-компилированную версию, другую реализацию с теми же входными данными (т. е. состоянием памяти, точкой входа, кодом и т. д. Если да, то одно из них обязательно должно быть неверным, согласно описанию rBPF: две реализации абсолютно одинакового поведения.

Написание фаззера

Для начала, давайте попробуем добавить в него кучу входных данных, не настраиваясь на что-то конкретное. Это позволяет нам проверить, действительно ли наша базовая реализация фаззинга работает так, как мы ожидаем.

Тупой фаззер

Во-первых, нам нужно выяснить, как выполнить интерпретатор. К счастью, есть несколько примеров этого, легко доступных в различных тестах. Я сослался на test_interpreter_and_jit макрос присутствует в ubpf_execution.rs в качестве основы для моего фаззера. Я предоставил последовательность компонентов, которые вы можете просмотреть по частям, прежде чем переходить ко всему фаззеру.

Шаг 1: определение наших входных данных

Мы должны определить наши входные данные так, чтобы они действительно были полезны для нашего фаззера. К счастью, arbitrary делает почти тривиальным получение входных данных из необработанных байтов.


Код:
#[derive(arbitrary::Arbitrary, Debug)]
struct DumbFuzzData {
    template: ConfigTemplate,
    prog: Vec<u8>,
    mem: Vec<u8>,
}

Если вы хотите увидеть определение ConfigTemplate, вы можете проверить его в common.rs , но все, что вам нужно знать, это то, что его цель — протестировать интерпретатор в различных конфигурациях выполнения.

Шаг 2. Настройка виртуальной машины

Затем следует настройка fuzz_target и виртуальной машины. Это позволит нам не только выполнить наш тест, но и позже проверить правильность поведения.


Код:
fuzz_target!(|data: DumbFuzzData| {
    let prog = data.prog;
    let config = data.template.into();
    if check(&prog, &config).is_err() {
        // verify please
        return;
    }
    let mut mem = data.mem;
    let registry = SyscallRegistry::default();
    let mut bpf_functions = BTreeMap::new();
    register_bpf_function(&config, &mut bpf_functions, &registry, 0, "entrypoint").unwrap();
    let executable = Executable::<UserError, TestInstructionMeter>::from_text_bytes(
        &prog,
        None,
        config,
        SyscallRegistry::default(),
        bpf_functions,
    )
    .unwrap();
    let mem_region = MemoryRegion::new_writable(&mut mem, ebpf::MM_INPUT_START);
    let mut vm =
        EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], vec![mem_region]).unwrap();

    // TODO in step 3
});

Шаг 3: Выполнение нашего ввода и сравнение вывода

На этом этапе мы просто запускаем виртуальную машину с предоставленным вводом. В будущих итерациях мы сравним вывод интерпретатора и JIT, но в этой версии мы просто выполняем интерпретатор, чтобы посмотреть, можем ли мы вызвать сбои.

Код:
fuzz_target!(|data: DumbFuzzData| {
    // see step 2 for this bit

    drop(black_box(vm.execute_program_interpreted(
        &mut TestInstructionMeter { remaining: 1024 },
    )));
});

Здесь я использую black_box, но не совсем уверен, что это необходимо. Я добавил его, чтобы гарантировать, что результат выполнения интерпретируемой программы не будет просто отброшен и, следовательно, выполнение не будет помечено как ненужное, но я вполне уверен, что это не будет независимо. Обратите внимание, что мы не проверяем, не произошло ли здесь выполнение. Если программа BPF не работает: нам все равно! Нам важно только, если виртуальная машина выйдет из строя по какой-либо причине.

Шаг 4: Соберем все вместе
Ниже приведен окончательный код фаззера, включая все биты, которые я не показал выше для краткости.

Код:
#![feature(bench_black_box)]
#![no_main]

use std::collections::BTreeMap;
use std::hint::black_box;

use libfuzzer_sys::fuzz_target;

use solana_rbpf::{
    ebpf,
    elf::{register_bpf_function, Executable},
    memory_region::MemoryRegion,
    user_error::UserError,
    verifier::check,
    vm::{EbpfVm, SyscallRegistry, TestInstructionMeter},
};

use crate::common::ConfigTemplate;

mod common;

#[derive(arbitrary::Arbitrary, Debug)]
struct DumbFuzzData {
    template: ConfigTemplate,
    prog: Vec<u8>,
    mem: Vec<u8>,
}

fuzz_target!(|data: DumbFuzzData| {
    let prog = data.prog;
    let config = data.template.into();
    if check(&prog, &config).is_err() {
        // verify please
        return;
    }
    let mut mem = data.mem;
    let registry = SyscallRegistry::default();
    let mut bpf_functions = BTreeMap::new();
    register_bpf_function(&config, &mut bpf_functions, &registry, 0, "entrypoint").unwrap();
    let executable = Executable::<UserError, TestInstructionMeter>::from_text_bytes(
        &prog,
        None,
        config,
        SyscallRegistry::default(),
        bpf_functions,
    )
    .unwrap();
    let mem_region = MemoryRegion::new_writable(&mut mem, ebpf::MM_INPUT_START);
    let mut vm =
        EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], vec![mem_region]).unwrap();

    drop(black_box(vm.execute_program_interpreted(
        &mut TestInstructionMeter { remaining: 1024 },
    )));
});

Теоретически актуальная версия доступна в репозитории rBPF .

Мини - итог:

Код:
$ cargo +nightly fuzz run dumb -- -max_total_time=300
... snip ...
#2902510    REDUCE cov: 1092 ft: 2147 corp: 724/58Kb lim: 4096 exec/s: 9675 rss: 355Mb L: 134/3126 MS: 3 ChangeBit-InsertByte-PersAutoDict- DE: "\x07\xff\xff3"-
#2902537    REDUCE cov: 1092 ft: 2147 corp: 724/58Kb lim: 4096 exec/s: 9675 rss: 355Mb L: 60/3126 MS: 2 ChangeBinInt-EraseBytes-
#2905608    REDUCE cov: 1092 ft: 2147 corp: 724/58Kb lim: 4096 exec/s: 9685 rss: 355Mb L: 101/3126 MS: 1 EraseBytes-
#2905770    NEW    cov: 1092 ft: 2155 corp: 725/58Kb lim: 4096 exec/s: 9685 rss: 355Mb L: 61/3126 MS: 2 ShuffleBytes-CrossOver-
#2906805    DONE   cov: 1092 ft: 2155 corp: 725/58Kb lim: 4096 exec/s: 9657 rss: 355Mb
Done 2906805 runs in 301 second(s)

После выполнения фаззера мы можем оценить его эффективность при поиске интересных входных данных, проверив его покрытие после выполнения в течение заданного времени (обратите внимание на использование -max_total_time флага). В этом случае я хочу определить, насколько хорошо он охватывает функцию, которая обрабатывает выполнение интерпретатора . Для этого я использую следующие команды:

Код:
$ cargo +nightly fuzz coverage dumb
$ rust-cov show -Xdemangler=rustfilt fuzz/target/x86_64-unknown-linux-gnu/release/dumb -instr-profile=fuzz/coverage/dumb/coverage.profdata -show-line-counts-or-regions -name=execute_program_interpreted_inner

Вывод команды rust-cov

Если вы не знакомы с выводом покрытия llvm, первый столбец — это номер строки, второй столбец — количество раз, когда эта конкретная строка была использована, а третий столбец — это сам код.
https://anonfiles.com/x7Tc2djdy5/_rust-cov_txt


К сожалению, этот фаззер, похоже, не обеспечивает ожидаемого охвата. Несколько инструкций пропущены (обратите внимание на покрытие 0 на некоторых ветвях совпадения), и нет переходов, вызовов или других инструкций, связанных с потоком управления. Во многом это связано с тем, что бросание случайных байтов в любой парсер просто не будет эффективным; большая часть вещей будет обнаружена на этапе проверки, и очень немногие действительно будут тестировать программу.
Мы должны улучшить это, прежде чем продолжить, или мы будем вечно ждать, пока наш фаззер найдет полезные ошибки.
На данный момент у нас около двух часов разработки.

Умный фаззер

eBPF — довольно простой набор инструкций; Вы можете прочитать полное определение всего на нескольких страницах. Зная это: почему бы нам не ограничить наш ввод только этими инструкциями? Этот подход обычно называют фаззингом с учетом грамматики из-за того, что входные данные ограничены некоторой грамматикой. Это очень мощная концепция и используется для тестирования множества больших целей, которые имеют строгие правила синтаксического анализа.
Чтобы создать этот фаззер с учетом грамматики, я изучил файл insn_builder.rs с услужливым названием , который позволил мне создать инструкции. Теперь все, что мне нужно было сделать, это представить все различные инструкции. Ссылаясь на документацию eBPF, мы можем представить каждую возможную операцию в одном перечислении. вы можете увидеть весь файлgram.rs в репозитории rBPF , но два наиболее важных раздела представлены ниже.

Определение перечисления, представляющего все инструкции

#[derive(arbitrary::Arbitrary, Debug, Eq, PartialEq)]
pub enum FuzzedOp {
Add(Source),
Sub(Source),
Mul(Source),
Div(Source),
BitOr(Source),
BitAnd(Source),
LeftShift(Source),
RightShift(Source),
Negate,
Modulo(Source),
BitXor(Source),
Mov(Source),
SRS(Source),
SwapBytes(Endian),
Load(MemSize),
LoadAbs(MemSize),
LoadInd(MemSize),
LoadX(MemSize),
Store(MemSize),
StoreX(MemSize),
Jump,
JumpC(Cond, Source),
Call,
Exit,
}


pub type FuzzProgram = Vec<FuzzedInstruction>;

pub fn make_program(prog: &FuzzProgram, arch: Arch) -> BpfCode {
let mut code = BpfCode::default();
for inst in prog {
match inst.op {
FuzzedOp::Add(src) => code
.add(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Sub(src) => code
.sub(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Mul(src) => code
.mul(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Div(src) => code
.div(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::BitOr(src) => code
.bit_or(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::BitAnd(src) => code
.bit_and(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::LeftShift(src) => code
.left_shift(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::RightShift(src) => code
.right_shift(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Negate => code
.negate(arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Modulo(src) => code
.modulo(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::BitXor(src) => code
.bit_xor(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Mov(src) => code
.mov(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::SRS(src) => code
.signed_right_shift(src, arch)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::SwapBytes(endian) => code
.swap_bytes(endian)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Load(mem) => code
.load(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::LoadAbs(mem) => code
.load_abs(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::LoadInd(mem) => code
.load_ind(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::LoadX(mem) => code
.load_x(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Store(mem) => code
.store(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::StoreX(mem) => code
.store_x(mem)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Jump => code
.jump_unconditional()
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::JumpC(cond, src) => code
.jump_conditional(cond, src)
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Call => code
.call()
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
FuzzedOp::Exit => code
.exit()
.set_dst(inst.dst)
.set_src(inst.src)
.set_off(inst.off)
.set_imm(inst.imm)
.push(),
};
}
code
}

Вы увидите, что наше поколение на самом деле не заботится о том, чтобы инструкции были действительными, а только о том, чтобы они были в правильном формате. Например, мы не проверяем регистры, адреса, цели перехода и т. д.; мы просто соединяем это вместе и смотрим, работает ли это. Это делается для предотвращения чрезмерной специализации, когда наши попытки фаззинга делают только «скучные» входные данные, которые не проверяют случаи, которые обычно считаются недействительными. Хорошо – давайте сделаем фаззер с этим. Единственная реальная разница здесь в том, что наш формат ввода теперь изменен, чтобы иметь наш новый тип FuzzProgram вместо необработанных байтов:

Код:
#[derive(arbitrary::Arbitrary, Debug)]
struct FuzzData {
    template: ConfigTemplate,
    prog: FuzzProgram,
    mem: Vec<u8>,
    arch: Arch,
}

Весь фаззер

Этот фаззер выражает определенную стадию развития. Дифференциальный фаззер существенно отличается в нескольких ключевых аспектах, которые будут обсуждаться позже.

#![feature(bench_black_box)]
#![no_main]

use std::collections::BTreeMap;
use std::hint::black_box;

use libfuzzer_sys::fuzz_target;

use grammar_aware::*;
use solana_rbpf::{
elf::{register_bpf_function, Executable},
insn_builder::{Arch, IntoBytes},
memory_region::MemoryRegion,
user_error::UserError,
verifier::check,
vm::{EbpfVm, SyscallRegistry, TestInstructionMeter},
};

use crate::common::ConfigTemplate;

mod common;
mod grammar_aware;

#[derive(arbitrary::Arbitrary, Debug)]
struct FuzzData {
template: ConfigTemplate,
prog: FuzzProgram,
mem: Vec<u8>,
arch: Arch,
}

fuzz_target!(|data: FuzzData| {
let prog = make_program(&data.prog, data.arch);
let config = data.template.into();
if check(prog.into_bytes(), &config).is_err() {
// verify please
return;
}
let mut mem = data.mem;
let registry = SyscallRegistry::default();
let mut bpf_functions = BTreeMap::new();
register_bpf_function(&config, &mut bpf_functions, &registry, 0, "entrypoint").unwrap();
let executable = Executable::<UserError, TestInstructionMeter>::from_text_bytes(
prog.into_bytes(),
None,
config,
SyscallRegistry::default(),
bpf_functions,
)
.unwrap();
let mem_region = MemoryRegion::new_writable(&mem, ebpf::MM_INPUT_START);
let mut vm =
EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], vec![mem_region]).unwrap();

drop(black_box(vm.execute_program_interpreted(
&mut TestInstructionMeter { remaining: 1 << 16 },
)));
});

Мини - итог v2

Давайте посмотрим, насколько хорошо эта версия теперь покрывает нашу цель.

Код:
$ cargo +nightly fuzz run smart -- -max_total_time=60
... snip ...
#1449846    REDUCE cov: 1730 ft: 6369 corp: 1019/168Kb lim: 4096 exec/s: 4832 rss: 358Mb L: 267/2963 MS: 1 EraseBytes-
#1450798    NEW    cov: 1730 ft: 6370 corp: 1020/168Kb lim: 4096 exec/s: 4835 rss: 358Mb L: 193/2963 MS: 2 InsertByte-InsertRepeatedBytes-
#1451609    NEW    cov: 1730 ft: 6371 corp: 1021/168Kb lim: 4096 exec/s: 4838 rss: 358Mb L: 108/2963 MS: 1 ChangeByte-
#1452095    NEW    cov: 1730 ft: 6372 corp: 1022/169Kb lim: 4096 exec/s: 4840 rss: 358Mb L: 108/2963 MS: 1 ChangeByte-
#1452830    DONE   cov: 1730 ft: 6372 corp: 1022/169Kb lim: 4096 exec/s: 4826 rss: 358Mb
Done 1452830 runs in 301 second(s)

Обратите внимание, что наше количество испробованных входных данных (самое левое число) составляет почти половину, но наши значения cov и ft значительно выше.
Давайте оценим это покрытие более конкретно:

Код:
$ cargo +nightly fuzz coverage smart
$ rust-cov show -Xdemangler=rustfilt fuzz/target/x86_64-unknown-linux-gnu/release/smart -instr-profile=fuzz/coverage/smart/coverage.profdata -show-line-counts-or-regions -show-instantiations -name=execute_program_interpreted_inner

Вывод команды rust-cov (v2)

https://anonfiles.com/P8La22jayc/_rust-cov_v2_

Теперь мы видим, что инструкции перехода и вызова действительно используются , и что мы выполняем содержимое цикла интерпретатора значительно чаще, несмотря на примерно одинаковое количество успешных вызовов функции интерпретатора. Из этого мы можем сделать вывод, что не только успешно выполняется больше программ, но и то, что из тех, которые выполняются, в целом они, как правило, имеют более правильные инструкции.
Хотя это не затрагивает каждую ветку, теперь оно затрагивает значительно больше — и с гораздо более интересными значениями.
Разработка этой версии фаззера заняла около часа, так что в общей сложности мы работаем один час.

JIT и дифференциальный фаззинг

Теперь, когда у нас есть фаззер, который может генерировать множество входных данных, которые нам действительно интересны, мы можем разработать фаззер, который может тестировать JIT и интерпретатор друг против друга. Но как мы можем проверить их друг против друга?

Выбор входов, выходов и конфигурации​

Как гласит определение псевдооракула: нам нужно проверить, обеспечивает ли альтернативная программа (для JIT, интерпретатор и наоборот) такой же «вход» такой же «выход». Итак, какие входы и выходы у нас есть?

Что касается входных данных, мы хотим изменить три важные вещи:
  • Конфигурация, которая определяет, как должна работать виртуальная машина (какие функции и т. д.)
  • Программа BPF для выполнения, которую мы сгенерируем, как в «умном фаззере»
  • Начальная память ВМ
После того, как мы разработали наши входные данные, нам также нужно подумать о наших выходных данных:
  • «Состояние возврата», сам код выхода или состояние ошибки
  • Количество выполненных инструкций (например, переполнение JIT-программы?)
  • Последняя память ВМ
Затем, чтобы выполнить JIT и интерпретатор, мы предпримем следующие шаги:
  • Те же шаги, что и для первых фаззеров:
    • Используйте проход проверки rBPF (называемый «проверка»), чтобы убедиться, что виртуальная машина примет входную программу.
    • Инициализировать память, системные вызовы и точку входа
    • Создайте исполняемые данные
  • Затем подготовтесь к дифференциальному тестированию.
    • JIT скомпилировать код BPF (если он не работает, тихо сбой)
    • Инициализировать интерпретируемую виртуальную машину
    • Инициализировать виртуальную машину JIT
    • Выполнение как интерпретируемых, так и JIT-виртуальных машин
    • Сравните состояние возврата, выполненные инструкции и конечную память и паникуйте, если что-то не совпадает.

Написание фаззера

Как и прежде, я разделил это на более удобные фрагменты, чтобы вы могли читать их по одному вне контекста, прежде чем пытаться интерпретировать их окончательный контекст.

Шаг 1: определение наших входных данных

Код:
#[derive(arbitrary::Arbitrary, Debug)]
struct FuzzData {
    template: ConfigTemplate,
    ... snip ...
    prog: FuzzProgram,
    mem: Vec<u8>,
}

Шаг 2. Настройка виртуальной машины

Код:
fuzz_target!(|data: FuzzData| {
    let mut prog = make_program(&data.prog, Arch::X64);
    ... snip ...
    let config = data.template.into();
    if check(prog.into_bytes(), &config).is_err() {
        // verify please
        return;
    }
    let mut interp_mem = data.mem.clone();
    let mut jit_mem = data.mem;
    let registry = SyscallRegistry::default();
    let mut bpf_functions = BTreeMap::new();
    register_bpf_function(&config, &mut bpf_functions, &registry, 0, "entrypoint").unwrap();
    let mut executable = Executable::<UserError, TestInstructionMeter>::from_text_bytes(
        prog.into_bytes(),
        None,
        config,
        SyscallRegistry::default(),
        bpf_functions,
    )
    .unwrap();
    if Executable::jit_compile(&mut executable).is_ok() {
        let interp_mem_region = MemoryRegion::new_writable(&mut interp_mem, ebpf::MM_INPUT_START);
        let mut interp_vm =
            EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], vec![interp_mem])
                .unwrap();
        let jit_mem_region = MemoryRegion::new_writable(&mut jit_mem, ebpf::MM_INPUT_START);
        let mut jit_vm =
            EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], vec![jit_mem_region])
                .unwrap();

        // See step 3
    }
});

Шаг 3: Выполнение нашего ввода и сравнение вывода

Код:
fuzz_target!(|data: FuzzData| {
    // see step 2

    if Executable::jit_compile(&mut executable).is_ok() {
        // see step 2

        let mut interp_meter = TestInstructionMeter { remaining: 1 << 16 };
        let interp_res = interp_vm.execute_program_interpreted(&mut interp_meter);
        let mut jit_meter = TestInstructionMeter { remaining: 1 << 16 };
        let jit_res = jit_vm.execute_program_jit(&mut jit_meter);
        if interp_res != jit_res {
            panic!("Expected {:?}, but got {:?}", interp_res, jit_res);
        }
        if interp_res.is_ok() {
            // we know jit res must be ok if interp res is by this point
            if interp_meter.remaining != jit_meter.remaining {
                panic!(
                    "Expected {} insts remaining, but got {}",
                    interp_meter.remaining, jit_meter.remaining
                );
            }
            if interp_mem != jit_mem {
                panic!(
                    "Expected different memory. From interpreter: {:?}\nFrom JIT: {:?}",
                    interp_mem, jit_mem
                );
            }
        }
    }
});

Шаг 4: Соберите это вместе
Ниже приведен окончательный код фаззера, включая все биты, которые я не показал выше для краткости.

https://anonfiles.com/j9If2cj3y2/Fuzzer3_txt

И вместе с этим у нас есть наш фаззер! На реализацию этой части фаззера ушло около трех часов (в основном из-за обнаружения нескольких проблем с фаззером и их отладки).

К этому моменту мы были уже около шести часов. Я включил фаззер и стал ждать:

$ cargo +nightly fuzz run smart-jit-diff --jobs 4 -- -ignore_crashes=1

И начались сбои. Появились две основные ошибки:

  1. Когда была ошибка интерпретатора, а не JIT, при записи на конкретный адрес (сбой через 15 минут)
  2. Сбой AddressSanitizer из-за утечки памяти, когда ошибка произошла сразу после того, как JIT-программа преодолела предел инструкций (сбой через два часа)

Перевод той статьи - https://secret.club/2022/05/11/fuzzing-solana.html

Репозитории rBPF
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх