WEB3DEV

Cover image for Apresentando Trdelnik: Framework de Teste de Fuzz para Solana e Anchor - Ackee Blockchain
Banana Labs
Banana Labs

Posted on

Apresentando Trdelnik: Framework de Teste de Fuzz para Solana e Anchor - Ackee Blockchain

Os desenvolvedores precisam testar a confiabilidade e a segurança de seus programas antes da implantação. Testes unitários tradicionais muitas vezes falham em revelar vulnerabilidades de casos extremos. O Trdelnik aborda isso introduzindo testes de fuzz para programas Solana, que geram um grande conjunto de entradas aleatórias e ordens de chamada para sondar fraquezas inesperadas.

Por que criamos o Trdelnik

Programas para a blockchain Solana são escritos principalmente na linguagem de programação Rust. Há uma boa razão para usar Rust, pois garante a segurança da memória sem comprometer o desempenho, como é o caso de outras linguagens de programação que usam coleta de lixo (garbage collection). O Rust, por outro lado, usa um novo conceito único de propriedade. A desvantagem é que Rust pode se tornar muito complexo e a curva de aprendizado é íngreme.

Mas espere, você também ouviu que “apenas os melhores programadores podem escrever em Rust e os melhores cometem menos erros”? Bem, mesmo que isso seja parcialmente verdadeiro, existem maneiras melhores de prevenir bugs e uma delas é o teste. Testes extensivos e sistemáticos são uma parte integrante de qualquer desenvolvimento de software e são uma ótima maneira de descobrir bugs precocemente durante o desenvolvimento.

Como é possível que a maioria dos projetos tenha apenas testes básicos ou nenhum teste? Acredite ou não, escrever bons testes não é tão fácil quanto parece e, em alguns casos, pode levar ainda mais tempo do que o desenvolvimento do produto em si. No mundo acelerado da criptografia, os prazos muitas vezes são muito curtos e a pressão para lançar novos projetos é extremamente alta.

É por isso que desenvolvemos o Trdelnik, nosso framework de teste baseado em Rust que fornece várias ferramentas convenientes, escritas em Anchor, para desenvolvedores testarem programas Solana. O objetivo principal do Trdelnik é simplificar a configuração do ambiente de teste, fornecer uma API gerada automaticamente para enviar instruções para programas personalizados e acelerar o processo de teste.

Teste de Fuzz

Uma nova funcionalidade recentemente introduzida no Trdelnik é o teste de fuzz. É uma técnica automatizada de teste de software que fornece dados de entrada gerados aleatoriamente, inválidos ou inesperados para o seu programa. Isso ajuda a descobrir bugs e vulnerabilidades desconhecidos e pode prevenir exploits zero-day do seu programa.

Existem vários fuzzers usados para programas Rust, no entanto, quando se pesquisa por testes de fuzz Solana, não aparece nenhum resultado e é por isso que decidimos integrar essa funcionalidade em nosso framework. Sob o capô, o Trdelnik usa um fuzzer bem conhecido chamado honggfuzz desenvolvido pelo Google.

No texto a seguir, descreveremos passo a passo como usar o Trdelnik para testes de fuzz.

# Na raiz do seu projeto Anchor, execute estes comandos:
cargo install trdelnik-cli
cargo install honggfuzz
trdelnik init
# agora vá para ./trdelnik-tests/src/bin/fuzz_target.rs e edite o template de teste fuzz
# para executar o teste fuzz, substitua <TARGET_NAME> por fuzz_target
trdelnik fuzz run <TARGET_NAME>
# para depurar uma passagem de travamento em um arquivo travado *.fuzz com o seguinte caminho trdelnik-tests/hfuzz_workspace/<TARGET_NAME>/<CRASH_FILE>.fuzz
trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>
Enter fullscreen mode Exit fullscreen mode

Fuzzing com Trdelnik Passo a Passo

Neste tutorial, passaremos pela configuração completa envolvendo estas etapas:

1.Configurando um novo projeto Anchor
2.Criando um programa que contém bugs para detectar
3.Inicializando o framework de teste Trdelnik
4.Escrevendo um teste fuzz simples
5.Executando o teste de fuzz
6.Depurando nosso programa usando arquivos de falha de teste fuzz
7.Configurando um novo projeto Anchor

Para fins deste tutorial, criaremos um novo projeto Anchor. Se você ainda não tem o framework Anchor, instale a versão 0.28.0.

Abra um terminal e vá para a pasta do seu projeto onde criaremos o novo projeto e verifique se o projeto pode ser construído:

anchor init my-trdelnik-fuzz-test
cd my-trdelnik-fuzz-test
anchor build
Enter fullscreen mode Exit fullscreen mode

Criando um programa que contém bugs para detectar

A seguir, criaremos um programa simples onde introduziremos intencionalmente bugs que tentaremos encontrar com nosso fuzzer. Abra o arquivo de origem do seu programa programs /my-trdelnik-fuzz-test/src/lib.rs e substitua tudo após a macro declare_id!() pelo seguinte código:

const MAGIC_NUMBER: u8 = 254;

#[program]
pub mod my_trdelnik_fuzz_test {
   use super::*;

   pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
       let counter = &mut ctx.accounts.counter;

       counter.count = 0;
       counter.authority = ctx.accounts.user.key();

       Ok(())
   }

   pub fn update(ctx: Context<Update>, input1: u8, input2: u8) -> Result<()> {
       let counter = &mut ctx.accounts.counter;

       msg!("input1 = {}, input2 = {}", input1, input2);

       // comente isso para consertar o black magic panic
       if input1 == MAGIC_NUMBER {
           panic!("Black magic not supported!");
       }

       counter.count = buggy_math_function(input1, input2).into();
       Ok(())
   }
}

pub fn buggy_math_function(input1: u8, input2: u8) -> u8 {
   // comente a instrução if para causar div-by-zero ou subtrair com overflow panic
   if input2 >= MAGIC_NUMBER {
       return 0;
   }
   let divisor = MAGIC_NUMBER - input2;
   input1 / divisor
}

#[derive(Accounts)]
pub struct Initialize<'info> {
   #[account(init, payer = user, space = 8 + 40)]
   pub counter: Account<'info, Counter>,

   #[account(mut)]
   pub user: Signer<'info>,

   pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
   #[account(mut, has_one = authority)]
   pub counter: Account<'info, Counter>,
   pub authority: Signer<'info>,
}

#[account]
pub struct Counter {
   pub authority: Pubkey,
   pub count: u64,
}
Enter fullscreen mode Exit fullscreen mode

É um programa simples com duas instruções: initialize e update. A instrução de inicialização (initialize) criará a conta de dados necessária e a instrução de atualização (update) atualizará os dados em cadeia. Ele também contém uma macro panic! intencional que encerrará imediatamente o programa, simulando uma falha se a primeira entrada do programa for igual à constante MAGIC_NUMBER.

Agora, você pode verificar novamente se seu programa foi construído com sucesso usando anchor build.

Inicialize o framework de teste Trdelnik

Para usar o Trdelnik e o fuzzer, temos que instalá-los:

cargo install trdelnik-cli
cargo install honggfuzz

Enter fullscreen mode Exit fullscreen mode

Se você já tinha o Trdelnik instalado, precisa atualizar para a versão 0.5.0.

Depois disso, devemos inicializar o framework Trdelnik em nosso projeto usando o comando:

trdelnik init
Enter fullscreen mode Exit fullscreen mode

Este comando gerará automaticamente a pasta .program_client com uma API para o nosso programa, adicionará as dependências necessárias aos arquivos de configuração e gerará modelos de testes Trdelnik.

Escreva um teste de fuzz simples

O modelo de destino de teste de fuzz está localizado no arquivo trdelnik-tests/src/bin/fuzz_target.rs. Este arquivo pode ser modificado de acordo com suas necessidades.

Agora, você só pode substituir seu conteúdo pelo seguinte código:

use my_trdelnik_fuzz_test::entry;
use program_client::my_trdelnik_fuzz_test_instruction::*;
const PROGRAM_NAME: &str  = "my_trdelnik_fuzz_test";
use assert_matches::*;
use trdelnik_client::fuzzing::*;

#[derive(Arbitrary)]
pub struct FuzzData {
   param1: u8,
   param2: u8,
}

fn main() {
   loop {
       fuzz!(|fuzz_data: FuzzData| {
           Runtime::new().unwrap().block_on(async {
               let program_test = ProgramTest::new(PROGRAM_NAME, PROGRAM_ID, processor!(entry));

               let mut ctx = program_test.start_with_context().await;

               let counter = Keypair::new();

               let init_ix =
                   initialize_ix(counter.pubkey(), ctx.payer.pubkey(), SYSTEM_PROGRAM_ID);
               let mut transaction =
                   Transaction::new_with_payer(&[init_ix], Some(&ctx.payer.pubkey().clone()));

               transaction.sign(&[&ctx.payer, &counter], ctx.last_blockhash);
               let res = ctx.banks_client.process_transaction(transaction).await;
               assert_matches!(res, Ok(()));

               let res = fuzz_update_ix(
                   &fuzz_data,
                   &mut ctx.banks_client,
                   &ctx.payer,
                   &counter,
                   ctx.last_blockhash,
               )
               .await;
               assert_matches!(res, Ok(()));
           });
       });
   }
}

async fn fuzz_update_ix(
   fuzz_data: &FuzzData,
   banks_client: &mut BanksClient,
   payer: &Keypair,
   counter: &Keypair,
   blockhash: Hash,
) -> core::result::Result<(), BanksClientError> {
   let update_ix = update_ix(
       fuzz_data.param1,
       fuzz_data.param2,
       counter.pubkey(),
       payer.pubkey(),
   );

   let mut transaction = Transaction::new_with_payer(&[update_ix], Some(&payer.pubkey()));
   transaction.sign(&[payer], blockhash);

   banks_client.process_transaction(transaction).await
}
Enter fullscreen mode Exit fullscreen mode

Para fins deste tutorial e para simplificar, estamos fazendo o fuzzing apenas dos parâmetros de instrução. Nomeadamente, os dois parâmetros input1 e input2 da instrução update.

Se você olhar para o código alvo de fuzz, poderá ver que a função principal (main) contém um loop infinito com a macro fuzz!. Ela que tem um _closure_ onde passamos a estruturaFuzzData`.

Vamos dissecar o código um pouco. O ponto de entrada do alvo de fuzz é a função principal (main), onde chamamos a macro fuzz! repetidamente. Essa macro executa o código na closure e em cada loop os fuzz_data passados são diferentes.

Estamos usando o crate arbitrary que nos permite transformar facilmente dados aleatórios não estruturados em dados estruturados como definimos na estrutura FuzzData. O código do closure é executado e se o programa não falhar, o teste de fuzz continua com nova iteração do loop e o fuzz_data passado para o closure é modificado. Se a variável fuzz_data contiver dados que produzem uma falha em nosso programa, o fuzzer armazena automaticamente os dados em um arquivo de falha de fuzz separado ./trdelnik-tests/hfuzz_workspace/<TARGET_NAME>/<CRASH_FILE_NAME>.fuzz. Isso é útil especialmente para depuração subsequente.

O código executado no fechamento em nosso alvo de fuzz primeiro cria um novo TestProgram e adiciona nosso programa ao ambiente de teste. Em seguida, o cliente de teste é iniciado, o que nos permite enviar transações para nosso programa. Tendo feito isso, temos que inicializar nosso programa primeiro. Criamos a instrução de inicialização com a ajuda do Trdelnik, que gerou automaticamente a função initialize_ix para nós. Finalmente, criamos uma nova transação, que assinamos e enviamos via cliente para o ambiente de teste. Ótimo, nosso programa está inicializado!

Agora chamamos a função fuzz_update_ix para fornecer dados aleatórios à instrução de atualização em que queremos fazer o fuzz e onde queremos encontrar bugs. Novamente, construímos a instrução de atualização usando a função update_ix gerada automaticamente e fornecemos os parâmetros gerados aleatoriamente da estrutura FuzzData. Finalmente, criamos a transação, assinamos e enviamos. E é isso, nosso alvo de fuzz está pronto para ir!

Execute o teste de fuzz

O Trdelnik fornece uma maneira conveniente de executar os testes de fuzz. Em qualquer lugar do seu projeto Anchor, você pode executar o comando:

trdelnik fuzz run <TARGET_NAME>

Então, em nosso caso, se substituirmos pelo nome real, obtemos o seguinte:

trdelnik fuzz run fuzz_target

Depois de executar este comando, todo o projeto deve ser construído com instrumentação para fuzzing, por isso leva algum tempo. Após a conclusão da construção, o fuzzer é iniciado automaticamente e se parece com isso:

saída

Na parte superior, você pode ver as estatísticas de fuzzing. Especialmente quantas vezes seu programa falhou, quantas dessas falhas foram únicas, quantas iterações foram feitas e assim por diante.

Para interromper o fuzzing, você pode simplesmente pressionar CTRL + C. Como o Trdelnik usa o Honggfuzz sob o capô, você também pode passar parâmetros diretamente para o fuzzer usando variáveis de ambiente.

Por exemplo:


\\ Time-out: 10 secs
\\ Number of concurrent fuzzing threads: 1
\\ Number of fuzzing iterations: 10000
\\ Display Solana logs in the terminal
\\ Exit upon crash
HFUZZ_RUN_ARGS="-t 10 -n 1 -N 10000 -Q --exit_upon_crash" trdelnik fuzz run fuzz_target

Se você executar o comando acima, o fuzzer será interrompido após a primeira falha encontrada e a passagem da flag -Q nos permite ver os logs do Solana. Em nosso programa de exemplo Solana, usamos a macro msg! para exibir o valor dos parâmetros de instrução de atualização input1 e input2 que você pode ver no log que têm valores 254 e 255, respectivamente.

saída

Depurar nosso programa usando arquivos de falha de teste fuzz

Como você pode ver no log acima, os dados que causaram a falha de nosso programa foram armazenados no arquivo fuzz de falha na pasta ./trdelnik-tests/hfuzz_workspace/fuzz_target. Agora é possível usar este arquivo de falha e “reproduzir” a falha no depurador para inspecionar o bug. O comando correspondente é:

trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>

Então, em nosso caso, se substituirmos e pelos valores reais,. O nome do arquivo de falha pode ser diferente, então você terá que modificá-lo adequadamente.

Em nosso caso, o comando resultante é o seguinte:


trdelnik fuzz run-debug fuzz_target ./trdelnik-tests/hfuzz_workspace/fuzz_target/SIGABRT.PC.7ffff7c8e83c.STACK.1b3a7a7882.CODE.-6.ADDR.0.INSTR.mov____\%eax,\%ebx.fuzz

Depois de executar este comando, todo o projeto deve ser construído para depuração. Depois disso, o fuzzer executa seu programa com os parâmetros fornecidos pelo arquivo de falha e simula a falha novamente para inspeção.

Agora você pode ver no depurador que a thread entrou em pânico em ‘Black magic not supported!’, programs/my-trdelnik-fuzz-test/src/lib.rs:27:13

saída

Este é o resultado esperado porque introduzimos intencionalmente um pânico em nosso programa se a variável input1 for igual ao nosso MAGIC_NUMBER que é 254.


if input1 == MAGIC_NUMBER {
panic!("Black magic not supported!");
}

Enquanto estiver no depurador, você pode executar um comando de ajuda (help) para outras ações ou, execute o comando q, para sair.

Se você quiser tentar encontrar outro bug no programa, pode descomentar a instrução if:


if input2 >= MAGIC_NUMBER {
return 0;
}

Na função buggy_math_function no programa Solana em programs/my-trdelnik-fuzz-test/src/lib.rs execute o fuzzer novamente. Depois de algum tempo, uma nova falha única deve ser encontrada, que você pode analisar novamente usando o depurador, como mostrado anteriormente.

Conclusão

Mostramos como usar o framework Trdelnik para escrever testes de fuzz para programas Solana escritos em Anchor. Você pode encontrar o projeto de exemplo completo no repositório do GitHub do Trdelnik.

O objetivo do Trdelnik não é implementar um novo fuzzer, mas sim fornecer uma maneira conveniente de usar o fuzzer honggfuzz existente sem a inconveniência de configurar o ambiente de teste.

É tão simples quanto inicializar o Trdelnik em seu projeto e você está pronto para o fuzz!

Os próximos passos serão os testes de fluxo de contas e instruções do fuzz.

Fique atento a mais tutoriais no futuro!

Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui .

Oldest comments (0)