A equipe Skyfall da CertiK descobriu recentemente várias vulnerabilidades em nós RPC baseados em Rust em vários blockchains, incluindo Aptos, StarCoin e Sui. Como os nós RPC são componentes críticos de infraestrutura que conectam dApps e o blockchain subjacente, sua robustez é fundamental para uma operação contínua. Os designers de Blockchain sabem da importância de serviços RPC estáveis, por isso adotam linguagens seguras para memória, como Rust, para evitar vulnerabilidades comuns que podem destruir os nós RPC.
A adoção de uma linguagem segura para memória, como Rust, ajuda os nós RPC a evitar muitos ataques baseados em vulnerabilidades de corrupção de memória. No entanto, por meio de uma auditoria recente, descobrimos que mesmo as implementações Rust com segurança de memória, se não forem cuidadosamente projetadas e verificadas, podem ser vulneráveis a certas ameaças de segurança que podem interromper a disponibilidade dos serviços RPC.
Neste artigo, apresentaremos nossa descoberta de uma série de vulnerabilidades por meio de casos práticos.
Função do nó Blockchain RPC
O serviço de chamada de procedimento remoto (RPC) do blockchain é o principal componente de infraestrutura do blockchain da Camada 1. Ele fornece aos usuários um importante front-end de API e atua como um gateway para a rede blockchain de back-end. No entanto, o serviço RPC blockchain é diferente do serviço RPC tradicional, pois facilita a interação do usuário sem autenticação. A disponibilidade contínua do serviço é crítica e qualquer interrupção no serviço pode afetar gravemente a disponibilidade do blockchain subjacente.
Perspectiva de auditoria: servidor RPC tradicional VS servidor RPC blockchain
A auditoria de servidores RPC tradicionais concentra-se principalmente na verificação de entrada, autorização/autenticação, falsificação de solicitação entre sites/falsificação de solicitação do lado do servidor (CSRF/SSRF), vulnerabilidades de injeção (como injeção de SQL, injeção de comando) e vazamento de informações.
No entanto, a situação é diferente para servidores RPC blockchain. Desde que a transação seja assinada, não há necessidade de autenticar o cliente solicitante na camada RPC. Como front-end do blockchain, um dos principais objetivos do serviço RPC é garantir sua disponibilidade. Se falhar, os usuários não podem interagir com o blockchain, impedindo-os de consultar dados on-chain, enviar transações ou emitir funções de contrato.
Portanto, o aspecto mais vulnerável de um servidor RPC blockchain é a "disponibilidade". Se o servidor cair, os usuários perdem a capacidade de interagir com o blockchain. O mais grave é que alguns ataques se espalharão na cadeia, afetarão um grande número de nós e até levarão à paralisação de toda a rede.
Por que o novo blockchain usará RPC com segurança de memória
Alguns blockchains de Camada 1 conhecidos, como Aptos e Sui, usam a linguagem de programação segura para memória Rust para implementar seus serviços RPC. Graças à sua forte segurança e rigorosas verificações de tempo de compilação, o Rust torna os programas virtualmente imunes a vulnerabilidades de corrupção de memória, como estouros de pilha e vulnerabilidades de desreferência de ponteiro nulo e re-referência após liberação.
Para proteger ainda mais a base de código, os desenvolvedores seguem rigorosamente as práticas recomendadas, como não introduzir código inseguro. Use #![forbid(unsafe_code)] no código-fonte para garantir que o código não seguro seja bloqueado e filtrado.
Exemplos de desenvolvedores de blockchain implementando práticas de programação Rust
Para evitar o estouro de número inteiro, os desenvolvedores geralmente usam funções como check_add, check_sub, saturating_add, saturating_sub, etc. em vez de simples adição e subtração (+, -). Reduza o esgotamento de recursos definindo tempos limite apropriados, limites de tamanho de solicitação e limites de itens de solicitação.
Ameaças de RPC de segurança de memória no Blockchain de camada 1
Embora não sejam vulneráveis à insegurança de memória no sentido tradicional, os nós RPC são expostos a entradas facilmente manipuladas por invasores. Em uma implementação de RPC com segurança de memória, há várias situações que podem levar a uma negação de serviço. Por exemplo, a amplificação de memória pode esgotar a memória do serviço, enquanto problemas de lógica podem introduzir loops infinitos. Além disso, as condições de corrida podem representar uma ameaça em que as operações simultâneas podem ter uma sequência inesperada de eventos, deixando o sistema em um estado indefinido. Além disso, dependências gerenciadas incorretamente e bibliotecas de terceiros podem introduzir vulnerabilidades desconhecidas no sistema.
Neste post, nosso objetivo é chamar a atenção para maneiras mais imediatas pelas quais as proteções de tempo de execução do Rust podem ser acionadas, fazendo com que os serviços sejam interrompidos.
Explicit Rust Panic: Uma maneira de encerrar os serviços RPC diretamente
Os desenvolvedores podem introduzir código de pânico explícito, intencionalmente ou não. Esses códigos são usados principalmente para lidar com condições inesperadas ou excepcionais. Alguns exemplos comuns incluem:
assert!(): Use esta macro quando uma condição deve ser atendida. Se a condição declarada falhar, o programa entrará em pânico, indicando que há um erro grave no código.
panic!(): Esta função é chamada quando o programa encontra um erro do qual não pode se recuperar e não pode continuar.
unreachable!(): Use esta macro quando um pedaço de código não deve ser executado. Se esta macro for invocada, indica um erro lógico grave.
não implementado!() e todo!(): Essas macros são espaços reservados para funcionalidades não implementadas. Se esse valor for atingido, o programa falhará.
unwrap(): Este método é usado para os tipos Option ou Result.Quando uma variável Err ou None for encontrada, o programa falhará.
Vulnerabilidade 1: Acione a declaração no Verificador de Movimento!
A blockchain Aptos usa o verificador de bytecode Move para realizar análises de segurança de referência por meio de uma interpretação abstrata do bytecode. A função ute() faz parte da implementação do trait TransferFunctions e simula a execução de instruções de bytecode em blocos básicos.
A tarefa da função ute_inner() é interpretar a instrução de bytecode atual e atualizar o estado de acordo. Se tivermos executado até a última instrução no bloco básico, conforme indicado por index == last_index, a função chamará assert!(self.stack.is_empty()) para garantir que a pilha esteja vazia. A intenção por trás desse comportamento é garantir que todas as operações sejam balanceadas, o que também significa que cada push tenha um pop correspondente.
No fluxo normal de execução, a pilha é sempre balanceada durante a interpretação abstrata. Isso é garantido pelo Stack Balance Checker, que verifica o bytecode antes de interpretá-lo. No entanto, uma vez que ampliamos nossa perspectiva para o reino dos intérpretes abstratos, vemos que a suposição de equilíbrio da pilha nem sempre é válida.
Patch para a vulnerabilidade analise_função no AbstractInterpreter
Em sua essência, um interpretador abstrato emula bytecode no nível de bloco básico. Em sua implementação original, encontrar um erro durante ute_block solicitaria que o processo de análise registrasse o erro e continuasse a execução no próximo bloco no gráfico de fluxo de controle. Isso pode criar uma situação em que um erro em um bloco de execução pode fazer com que a pilha fique desbalanceada. Se a execução continuar neste caso, uma verificação assert! será feita se a pilha não estiver vazia, causando pânico.
Isso dá aos invasores uma oportunidade de explorar. Um invasor pode disparar um erro projetando um bytecode específico em ute_block() e, em seguida, ute() pode executar um assert se a pilha não estiver vazia, fazendo com que a verificação do assert falhe. Isso causará mais pânico e encerrará o serviço RPC, afetando sua disponibilidade.
Para evitar isso, a correção implementada garante que todo o processo de análise seja interrompido quando a função ute_block encontra um erro pela primeira vez, evitando assim o risco de falhas subsequentes que podem ocorrer ao continuar a análise devido ao desequilíbrio da pilha devido a erros. Essa modificação remove as condições que podem causar pânico e ajuda a melhorar a robustez e a segurança do interpretador abstrato.
** Vulnerabilidade 2: Acione o pânico no StarCoin! **
O blockchain Starcoin tem seu próprio fork da implementação do Move. Neste repositório Move, há um pânico no construtor do tipo Struct! Se o StructDefinition fornecido tiver informações de campo Native, o pânico será acionado explicitamente! .
Pânico explícito para estruturas inicializadas em rotinas de normalização
Esse risco potencial existe no processo de redistribuição dos módulos. Se o módulo publicado já existir no armazenamento de dados, a normalização do módulo será necessária para o módulo existente e o módulo de entrada controlado pelo invasor. Durante esse processo, a função "normalized::Module::new" constrói a estrutura do módulo a partir dos módulos de entrada controlados pelo invasor, disparando um "pânico!".
Pré-requisitos para a rotina de normalização
Esse pânico pode ser acionado enviando uma carga especialmente criada do cliente. Portanto, agentes mal-intencionados podem interromper a disponibilidade dos serviços RPC.
Patch de pânico de inicialização de estrutura
O patch Starcoin apresenta um novo comportamento para lidar com o caso Nativo. Agora, em vez de entrar em pânico, ele retorna um ec vazio. Isso reduz a possibilidade de os usuários enviarem dados causando pânico.
Pânico de ferrugem implícito: uma maneira facilmente negligenciada de encerrar serviços RPC
Os pânicos explícitos são facilmente identificáveis no código-fonte, enquanto os pânicos implícitos têm maior probabilidade de serem ignorados pelos desenvolvedores. Pânicos implícitos geralmente ocorrem ao usar APIs fornecidas por bibliotecas padrão ou de terceiros. Os desenvolvedores precisam ler e entender completamente a documentação da API, ou seus programas Rust podem parar inesperadamente.
Pânico implícito no BTreeMap
Vamos usar o BTreeMap do Rust STD como exemplo. BTreeMap é uma estrutura de dados comumente usada que organiza pares chave-valor em uma árvore binária classificada. O BTreeMap fornece dois métodos para recuperar valores por chave: get(&self, key: &Q) e index(&self, key: &Q).
O método get(&self, key: &Q) recupera o valor usando a chave e retorna uma Option. A opção pode ser Some(&V), se a chave existir, retorna a referência do valor, se a chave não for encontrada no BTreeMap, retorna None.
Por outro lado, index(&self, key: &Q) retorna diretamente uma referência ao valor correspondente à chave. No entanto, há um grande risco: acionará um pânico implícito se a chave não existir no BTreeMap. Se não for tratado adequadamente, o programa pode travar inesperadamente, tornando-se uma vulnerabilidade em potencial.
Na verdade, o método index(&self, key: &Q) é a implementação subjacente da característica std::ops::Index. Esta característica é uma operação de índice em um contexto imutável (ou seja, contêiner [index] ) fornece açúcar sintático conveniente. Os desenvolvedores podem usar diretamente btree_map [key] , chame o método index(&self, key: &Q). No entanto, eles podem ignorar o fato de que esse uso pode entrar em pânico se a chave não for encontrada, representando assim uma ameaça implícita à estabilidade do programa.
Vulnerabilidade 3: Acionar um pânico implícito no Sui RPC
A rotina de liberação do módulo Sui permite que os usuários enviem cargas úteis do módulo via RPC. O manipulador RPC usa a função SuiCommand::Publish para desmontar diretamente o módulo recebido antes de encaminhar a solicitação para a rede de verificação de back-end para verificação de bytecode.
Durante essa desmontagem, a seção code_unit do módulo enviado é usada para criar um VMControlFlowGraph. O processo de construção consiste na criação de blocos básicos, que são armazenados em um BTreeMap denominado "'blocks'". O processo inclui a criação e manipulação do Mapa, onde um pânico implícito é acionado sob certas condições.
Aqui está um código simplificado:
Pânico implícito ao criar VMControlFlowGraph
Nesse código, um novo VMControlFlowGraph é criado percorrendo o código e criando um novo bloco básico para cada unidade de código. Os blocos básicos são armazenados em um bloco nomeado BtreeMap.
O mapa de blocos é indexado usando block[&block] em um loop que itera sobre a pilha, que foi inicializada com ENTRY_BLOCK_ID. A suposição aqui é que há pelo menos um ENTRY_BLOCK_ID no mapa de blocos.
No entanto, essa suposição nem sempre é válida. Por exemplo, se o código confirmado estiver vazio, o "mapa de bloco" ainda estará vazio após o processo "criar bloco básico". Mais tarde, quando o código tentar percorrer o mapa de blocos usando for succ em &blocks[&block].successors , um pânico implícito poderá surgir se a chave não for encontrada. Isso ocorre porque a expressão blocks[&block] é essencialmente uma chamada para o método index(), que, conforme mencionado anteriormente, entrará em pânico se a chave não existir no BTreeMap.
Um invasor com acesso remoto pode explorar a vulnerabilidade nessa função enviando uma carga de módulo malformada com um campo code_unit vazio. Essa solicitação RPC simples trava todo o processo JSON-RPC. Se um invasor continuar enviando essas cargas malformadas com esforço mínimo, isso resultará em uma interrupção contínua do serviço. Em uma rede blockchain, isso significa que a rede pode não ser capaz de confirmar novas transações, resultando em uma situação de negação de serviço (DoS). A funcionalidade da rede e a confiança do usuário no sistema serão severamente afetadas.
Correção do Sui: remova a desmontagem da rotina de problemas de RPC
Vale a pena notar que o CodeUnitVerifier no Move Bytecode Verifier é responsável por garantir que a seção code_unit nunca esteja vazia. No entanto, a ordem das operações expõe os manipuladores RPC a possíveis vulnerabilidades. Isso ocorre porque o processo de validação ocorre no nó Validator, que é um estágio após o RPC processar os módulos de entrada.
Em resposta a esse problema, a Sui resolveu a vulnerabilidade removendo a função de desmontagem na rotina RPC de liberação do módulo. Essa é uma maneira eficaz de impedir que os serviços RPC processem bytecode potencialmente perigoso e não validado.
Além disso, vale a pena observar que outros métodos RPC relacionados a pesquisas de objetos também contêm recursos de desmontagem, mas não são vulneráveis ao uso de células de código vazias. Isso ocorre porque eles estão sempre consultando e desmontando os módulos publicados existentes. Os módulos publicados devem ter sido verificados, portanto, a suposição de células de código não vazias sempre é válida ao criar um VMControlFlowGraph.
Sugestões para desenvolvedores
Depois de entender as ameaças à estabilidade dos serviços RPC em blockchains de pânicos explícitos e implícitos, os desenvolvedores devem dominar estratégias para prevenir ou mitigar esses riscos. Essas estratégias podem reduzir a possibilidade de interrupções de serviço não planejadas e aumentar a resiliência do sistema. Portanto, a equipe de especialistas da CertiK apresenta as seguintes sugestões e as lista como melhores práticas para programação Rust.
Abstração do Rust Panic: Sempre que possível, considere o uso da função catch_unwind do Rust para detectar pânicos e convertê-los em mensagens de erro. Isso evita que todo o programa trave e permite que os desenvolvedores lide com os erros de maneira controlada.
Use APIs com cautela: pânicos implícitos geralmente ocorrem devido ao uso indevido de APIs fornecidas por bibliotecas padrão ou de terceiros. Portanto, é crucial entender totalmente a API e aprender a lidar com possíveis erros de maneira adequada. Os desenvolvedores devem sempre presumir que as APIs podem falhar e se preparar para tais situações.
Tratamento adequado de erros: use os tipos Resultado e Opção para tratamento de erros em vez de recorrer ao pânico. O primeiro fornece uma maneira mais controlada de lidar com erros e casos especiais.
Adicione documentação e comentários: certifique-se de que seu código esteja bem documentado e adicione comentários às seções críticas (incluindo aquelas em que podem ocorrer pânicos). Isso ajudará outros desenvolvedores a entender os riscos potenciais e lidar com eles de forma eficaz.
Resumir
Os nós RPC baseados em ferrugem desempenham um papel importante em sistemas blockchain como Aptos, StarCoin e Sui. Como eles são usados para conectar DApps e o blockchain subjacente, sua confiabilidade é fundamental para o bom funcionamento do sistema blockchain. Embora esses sistemas usem a linguagem segura de memória Rust, ainda existe o risco de um design ruim. A equipe de pesquisa da CertiK explorou esses riscos com exemplos do mundo real que demonstram a necessidade de um design cuidadoso e cuidadoso na programação segura para a memória.
Ver original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
Crashing RPC: análise de um novo tipo de vulnerabilidade em nós RPC de blockchain com segurança de memória
A equipe Skyfall da CertiK descobriu recentemente várias vulnerabilidades em nós RPC baseados em Rust em vários blockchains, incluindo Aptos, StarCoin e Sui. Como os nós RPC são componentes críticos de infraestrutura que conectam dApps e o blockchain subjacente, sua robustez é fundamental para uma operação contínua. Os designers de Blockchain sabem da importância de serviços RPC estáveis, por isso adotam linguagens seguras para memória, como Rust, para evitar vulnerabilidades comuns que podem destruir os nós RPC.
A adoção de uma linguagem segura para memória, como Rust, ajuda os nós RPC a evitar muitos ataques baseados em vulnerabilidades de corrupção de memória. No entanto, por meio de uma auditoria recente, descobrimos que mesmo as implementações Rust com segurança de memória, se não forem cuidadosamente projetadas e verificadas, podem ser vulneráveis a certas ameaças de segurança que podem interromper a disponibilidade dos serviços RPC.
Neste artigo, apresentaremos nossa descoberta de uma série de vulnerabilidades por meio de casos práticos.
Função do nó Blockchain RPC
O serviço de chamada de procedimento remoto (RPC) do blockchain é o principal componente de infraestrutura do blockchain da Camada 1. Ele fornece aos usuários um importante front-end de API e atua como um gateway para a rede blockchain de back-end. No entanto, o serviço RPC blockchain é diferente do serviço RPC tradicional, pois facilita a interação do usuário sem autenticação. A disponibilidade contínua do serviço é crítica e qualquer interrupção no serviço pode afetar gravemente a disponibilidade do blockchain subjacente.
Perspectiva de auditoria: servidor RPC tradicional VS servidor RPC blockchain
A auditoria de servidores RPC tradicionais concentra-se principalmente na verificação de entrada, autorização/autenticação, falsificação de solicitação entre sites/falsificação de solicitação do lado do servidor (CSRF/SSRF), vulnerabilidades de injeção (como injeção de SQL, injeção de comando) e vazamento de informações.
No entanto, a situação é diferente para servidores RPC blockchain. Desde que a transação seja assinada, não há necessidade de autenticar o cliente solicitante na camada RPC. Como front-end do blockchain, um dos principais objetivos do serviço RPC é garantir sua disponibilidade. Se falhar, os usuários não podem interagir com o blockchain, impedindo-os de consultar dados on-chain, enviar transações ou emitir funções de contrato.
Portanto, o aspecto mais vulnerável de um servidor RPC blockchain é a "disponibilidade". Se o servidor cair, os usuários perdem a capacidade de interagir com o blockchain. O mais grave é que alguns ataques se espalharão na cadeia, afetarão um grande número de nós e até levarão à paralisação de toda a rede.
Por que o novo blockchain usará RPC com segurança de memória
Alguns blockchains de Camada 1 conhecidos, como Aptos e Sui, usam a linguagem de programação segura para memória Rust para implementar seus serviços RPC. Graças à sua forte segurança e rigorosas verificações de tempo de compilação, o Rust torna os programas virtualmente imunes a vulnerabilidades de corrupção de memória, como estouros de pilha e vulnerabilidades de desreferência de ponteiro nulo e re-referência após liberação.
Para proteger ainda mais a base de código, os desenvolvedores seguem rigorosamente as práticas recomendadas, como não introduzir código inseguro. Use #![forbid(unsafe_code)] no código-fonte para garantir que o código não seguro seja bloqueado e filtrado.
Exemplos de desenvolvedores de blockchain implementando práticas de programação Rust
Para evitar o estouro de número inteiro, os desenvolvedores geralmente usam funções como check_add, check_sub, saturating_add, saturating_sub, etc. em vez de simples adição e subtração (+, -). Reduza o esgotamento de recursos definindo tempos limite apropriados, limites de tamanho de solicitação e limites de itens de solicitação.
Ameaças de RPC de segurança de memória no Blockchain de camada 1
Embora não sejam vulneráveis à insegurança de memória no sentido tradicional, os nós RPC são expostos a entradas facilmente manipuladas por invasores. Em uma implementação de RPC com segurança de memória, há várias situações que podem levar a uma negação de serviço. Por exemplo, a amplificação de memória pode esgotar a memória do serviço, enquanto problemas de lógica podem introduzir loops infinitos. Além disso, as condições de corrida podem representar uma ameaça em que as operações simultâneas podem ter uma sequência inesperada de eventos, deixando o sistema em um estado indefinido. Além disso, dependências gerenciadas incorretamente e bibliotecas de terceiros podem introduzir vulnerabilidades desconhecidas no sistema.
Neste post, nosso objetivo é chamar a atenção para maneiras mais imediatas pelas quais as proteções de tempo de execução do Rust podem ser acionadas, fazendo com que os serviços sejam interrompidos.
Explicit Rust Panic: Uma maneira de encerrar os serviços RPC diretamente
Os desenvolvedores podem introduzir código de pânico explícito, intencionalmente ou não. Esses códigos são usados principalmente para lidar com condições inesperadas ou excepcionais. Alguns exemplos comuns incluem:
assert!(): Use esta macro quando uma condição deve ser atendida. Se a condição declarada falhar, o programa entrará em pânico, indicando que há um erro grave no código.
panic!(): Esta função é chamada quando o programa encontra um erro do qual não pode se recuperar e não pode continuar.
unreachable!(): Use esta macro quando um pedaço de código não deve ser executado. Se esta macro for invocada, indica um erro lógico grave.
não implementado!() e todo!(): Essas macros são espaços reservados para funcionalidades não implementadas. Se esse valor for atingido, o programa falhará.
unwrap(): Este método é usado para os tipos Option ou Result.Quando uma variável Err ou None for encontrada, o programa falhará.
Vulnerabilidade 1: Acione a declaração no Verificador de Movimento!
A blockchain Aptos usa o verificador de bytecode Move para realizar análises de segurança de referência por meio de uma interpretação abstrata do bytecode. A função ute() faz parte da implementação do trait TransferFunctions e simula a execução de instruções de bytecode em blocos básicos.
A tarefa da função ute_inner() é interpretar a instrução de bytecode atual e atualizar o estado de acordo. Se tivermos executado até a última instrução no bloco básico, conforme indicado por index == last_index, a função chamará assert!(self.stack.is_empty()) para garantir que a pilha esteja vazia. A intenção por trás desse comportamento é garantir que todas as operações sejam balanceadas, o que também significa que cada push tenha um pop correspondente.
No fluxo normal de execução, a pilha é sempre balanceada durante a interpretação abstrata. Isso é garantido pelo Stack Balance Checker, que verifica o bytecode antes de interpretá-lo. No entanto, uma vez que ampliamos nossa perspectiva para o reino dos intérpretes abstratos, vemos que a suposição de equilíbrio da pilha nem sempre é válida.
Patch para a vulnerabilidade analise_função no AbstractInterpreter
Em sua essência, um interpretador abstrato emula bytecode no nível de bloco básico. Em sua implementação original, encontrar um erro durante ute_block solicitaria que o processo de análise registrasse o erro e continuasse a execução no próximo bloco no gráfico de fluxo de controle. Isso pode criar uma situação em que um erro em um bloco de execução pode fazer com que a pilha fique desbalanceada. Se a execução continuar neste caso, uma verificação assert! será feita se a pilha não estiver vazia, causando pânico.
Isso dá aos invasores uma oportunidade de explorar. Um invasor pode disparar um erro projetando um bytecode específico em ute_block() e, em seguida, ute() pode executar um assert se a pilha não estiver vazia, fazendo com que a verificação do assert falhe. Isso causará mais pânico e encerrará o serviço RPC, afetando sua disponibilidade.
Para evitar isso, a correção implementada garante que todo o processo de análise seja interrompido quando a função ute_block encontra um erro pela primeira vez, evitando assim o risco de falhas subsequentes que podem ocorrer ao continuar a análise devido ao desequilíbrio da pilha devido a erros. Essa modificação remove as condições que podem causar pânico e ajuda a melhorar a robustez e a segurança do interpretador abstrato.
** Vulnerabilidade 2: Acione o pânico no StarCoin! **
O blockchain Starcoin tem seu próprio fork da implementação do Move. Neste repositório Move, há um pânico no construtor do tipo Struct! Se o StructDefinition fornecido tiver informações de campo Native, o pânico será acionado explicitamente! .
Pânico explícito para estruturas inicializadas em rotinas de normalização
Esse risco potencial existe no processo de redistribuição dos módulos. Se o módulo publicado já existir no armazenamento de dados, a normalização do módulo será necessária para o módulo existente e o módulo de entrada controlado pelo invasor. Durante esse processo, a função "normalized::Module::new" constrói a estrutura do módulo a partir dos módulos de entrada controlados pelo invasor, disparando um "pânico!".
Pré-requisitos para a rotina de normalização
Esse pânico pode ser acionado enviando uma carga especialmente criada do cliente. Portanto, agentes mal-intencionados podem interromper a disponibilidade dos serviços RPC.
Patch de pânico de inicialização de estrutura
O patch Starcoin apresenta um novo comportamento para lidar com o caso Nativo. Agora, em vez de entrar em pânico, ele retorna um ec vazio. Isso reduz a possibilidade de os usuários enviarem dados causando pânico.
Pânico de ferrugem implícito: uma maneira facilmente negligenciada de encerrar serviços RPC
Os pânicos explícitos são facilmente identificáveis no código-fonte, enquanto os pânicos implícitos têm maior probabilidade de serem ignorados pelos desenvolvedores. Pânicos implícitos geralmente ocorrem ao usar APIs fornecidas por bibliotecas padrão ou de terceiros. Os desenvolvedores precisam ler e entender completamente a documentação da API, ou seus programas Rust podem parar inesperadamente.
Pânico implícito no BTreeMap
Vamos usar o BTreeMap do Rust STD como exemplo. BTreeMap é uma estrutura de dados comumente usada que organiza pares chave-valor em uma árvore binária classificada. O BTreeMap fornece dois métodos para recuperar valores por chave: get(&self, key: &Q) e index(&self, key: &Q).
O método get(&self, key: &Q) recupera o valor usando a chave e retorna uma Option. A opção pode ser Some(&V), se a chave existir, retorna a referência do valor, se a chave não for encontrada no BTreeMap, retorna None.
Por outro lado, index(&self, key: &Q) retorna diretamente uma referência ao valor correspondente à chave. No entanto, há um grande risco: acionará um pânico implícito se a chave não existir no BTreeMap. Se não for tratado adequadamente, o programa pode travar inesperadamente, tornando-se uma vulnerabilidade em potencial.
Na verdade, o método index(&self, key: &Q) é a implementação subjacente da característica std::ops::Index. Esta característica é uma operação de índice em um contexto imutável (ou seja, contêiner [index] ) fornece açúcar sintático conveniente. Os desenvolvedores podem usar diretamente btree_map [key] , chame o método index(&self, key: &Q). No entanto, eles podem ignorar o fato de que esse uso pode entrar em pânico se a chave não for encontrada, representando assim uma ameaça implícita à estabilidade do programa.
Vulnerabilidade 3: Acionar um pânico implícito no Sui RPC
A rotina de liberação do módulo Sui permite que os usuários enviem cargas úteis do módulo via RPC. O manipulador RPC usa a função SuiCommand::Publish para desmontar diretamente o módulo recebido antes de encaminhar a solicitação para a rede de verificação de back-end para verificação de bytecode.
Durante essa desmontagem, a seção code_unit do módulo enviado é usada para criar um VMControlFlowGraph. O processo de construção consiste na criação de blocos básicos, que são armazenados em um BTreeMap denominado "'blocks'". O processo inclui a criação e manipulação do Mapa, onde um pânico implícito é acionado sob certas condições.
Aqui está um código simplificado:
Pânico implícito ao criar VMControlFlowGraph
Nesse código, um novo VMControlFlowGraph é criado percorrendo o código e criando um novo bloco básico para cada unidade de código. Os blocos básicos são armazenados em um bloco nomeado BtreeMap.
O mapa de blocos é indexado usando block[&block] em um loop que itera sobre a pilha, que foi inicializada com ENTRY_BLOCK_ID. A suposição aqui é que há pelo menos um ENTRY_BLOCK_ID no mapa de blocos.
No entanto, essa suposição nem sempre é válida. Por exemplo, se o código confirmado estiver vazio, o "mapa de bloco" ainda estará vazio após o processo "criar bloco básico". Mais tarde, quando o código tentar percorrer o mapa de blocos usando for succ em &blocks[&block].successors , um pânico implícito poderá surgir se a chave não for encontrada. Isso ocorre porque a expressão blocks[&block] é essencialmente uma chamada para o método index(), que, conforme mencionado anteriormente, entrará em pânico se a chave não existir no BTreeMap.
Um invasor com acesso remoto pode explorar a vulnerabilidade nessa função enviando uma carga de módulo malformada com um campo code_unit vazio. Essa solicitação RPC simples trava todo o processo JSON-RPC. Se um invasor continuar enviando essas cargas malformadas com esforço mínimo, isso resultará em uma interrupção contínua do serviço. Em uma rede blockchain, isso significa que a rede pode não ser capaz de confirmar novas transações, resultando em uma situação de negação de serviço (DoS). A funcionalidade da rede e a confiança do usuário no sistema serão severamente afetadas.
Correção do Sui: remova a desmontagem da rotina de problemas de RPC
Vale a pena notar que o CodeUnitVerifier no Move Bytecode Verifier é responsável por garantir que a seção code_unit nunca esteja vazia. No entanto, a ordem das operações expõe os manipuladores RPC a possíveis vulnerabilidades. Isso ocorre porque o processo de validação ocorre no nó Validator, que é um estágio após o RPC processar os módulos de entrada.
Em resposta a esse problema, a Sui resolveu a vulnerabilidade removendo a função de desmontagem na rotina RPC de liberação do módulo. Essa é uma maneira eficaz de impedir que os serviços RPC processem bytecode potencialmente perigoso e não validado.
Além disso, vale a pena observar que outros métodos RPC relacionados a pesquisas de objetos também contêm recursos de desmontagem, mas não são vulneráveis ao uso de células de código vazias. Isso ocorre porque eles estão sempre consultando e desmontando os módulos publicados existentes. Os módulos publicados devem ter sido verificados, portanto, a suposição de células de código não vazias sempre é válida ao criar um VMControlFlowGraph.
Sugestões para desenvolvedores
Depois de entender as ameaças à estabilidade dos serviços RPC em blockchains de pânicos explícitos e implícitos, os desenvolvedores devem dominar estratégias para prevenir ou mitigar esses riscos. Essas estratégias podem reduzir a possibilidade de interrupções de serviço não planejadas e aumentar a resiliência do sistema. Portanto, a equipe de especialistas da CertiK apresenta as seguintes sugestões e as lista como melhores práticas para programação Rust.
Abstração do Rust Panic: Sempre que possível, considere o uso da função catch_unwind do Rust para detectar pânicos e convertê-los em mensagens de erro. Isso evita que todo o programa trave e permite que os desenvolvedores lide com os erros de maneira controlada.
Use APIs com cautela: pânicos implícitos geralmente ocorrem devido ao uso indevido de APIs fornecidas por bibliotecas padrão ou de terceiros. Portanto, é crucial entender totalmente a API e aprender a lidar com possíveis erros de maneira adequada. Os desenvolvedores devem sempre presumir que as APIs podem falhar e se preparar para tais situações.
Tratamento adequado de erros: use os tipos Resultado e Opção para tratamento de erros em vez de recorrer ao pânico. O primeiro fornece uma maneira mais controlada de lidar com erros e casos especiais.
Adicione documentação e comentários: certifique-se de que seu código esteja bem documentado e adicione comentários às seções críticas (incluindo aquelas em que podem ocorrer pânicos). Isso ajudará outros desenvolvedores a entender os riscos potenciais e lidar com eles de forma eficaz.
Resumir
Os nós RPC baseados em ferrugem desempenham um papel importante em sistemas blockchain como Aptos, StarCoin e Sui. Como eles são usados para conectar DApps e o blockchain subjacente, sua confiabilidade é fundamental para o bom funcionamento do sistema blockchain. Embora esses sistemas usem a linguagem segura de memória Rust, ainda existe o risco de um design ruim. A equipe de pesquisa da CertiK explorou esses riscos com exemplos do mundo real que demonstram a necessidade de um design cuidadoso e cuidadoso na programação segura para a memória.