Visão geral
Entre vários problemas de segurança em contratos inteligentes, a vulnerabilidade de reentrância sempre foi a mais prejudicial e ocorre com frequência. A vulnerabilidade é causada pelo contrato que não gerencia adequadamente a ordem das alterações nas variáveis de estado ao processar chamadas de função. A exploração de reentrância mais famosa foi o ataque The DAO de 2016, que levou a uma bifurcação da Ethereum. Atualmente, devido à melhoria da conscientização de segurança dos desenvolvedores, a maioria dos projetos usam bloqueios de reentrância para evitar vulnerabilidades de reentrância e reduzir os riscos de segurança. No entanto, no campo DeFi, embora o contrato use bloqueios de reentrância em algumas funções importantes, ainda é possível ser atacado por vulnerabilidades de reentrância read-only.
Introdução à vulnerabilidade de reentrância read-only
A vulnerabilidade de reentrância read-only é um caso especial da vulnerabilidade de reentrância. O local onde a vulnerabilidade ocorre é a função de visualização no contrato inteligente. Como esse tipo de função não modifica as variáveis de estado do contrato, geralmente esse tipo de função não será decorada com bloqueios de reentrância. Quando o contrato da vítima chama a função de visualização do contrato vulnerável, como as variáveis de estado usadas na função de visualização não foram atualizadas neste momento, os dados obtidos pelo contrato da vítima através da função de visualização também não são atualizados. Se o contrato da vítima depende da função de visualização para retornar o valor, pode haver situações anormais, como cálculo anormal de preços colaterais e cálculo incorreto de recompensas.
Análises de casos
A seguir, uma análise das vulnerabilidades de reentrância read-only em dois protocolos DeFi famosos, Curve e Balancer. O protocolo Curve se concentra na negociação de stablecoins (moedas estáveis) entre criptomoedas, como USDC, DAI, USDT, etc. O protocolo Balancer foi desenvolvido para permitir que os usuários criem seus portfólios de ativos e possam ser usados como pools de liquidez para que outros usuários negociem. As vulnerabilidades de reentrância nesses dois protocolos foram descobertas pela ChainSecurity, e nós admiramos muito suas contribuições para a segurança da Web3.
Curve
Análise de vulnerabilidade na Curve
A Curve é uma exchange descentralizada. Os usuários podem fornecer ativos de liquidez ao pool de liquidez para obter uma parcela correspondente de tokens LP, que representam a parcela no pool de liquidez. Quando o usuário retirar a liquidez, o contrato queimará tokens LP e enviará os ativos de liquidez correspondentes ao usuário. Ao mesmo tempo, o contrato fornece uma função get_virtual_price
para calcular o valor virtual dos tokens LP. A fórmula de cálculo é dividir D pela oferta total de tokens LP. D pode ser simplesmente entendido como o valor total dos tokens de liquidez. A função é implementada da seguinte forma:
Os usuários podem remover a liquidez chamando a função remove_liquidity
do contrato após adicionar liquidez:
A função primeiro obtém o fornecimento total de tokens LP, calcula quantos ativos de liquidez o usuário pode retirar, subtrai o valor correspondente do saldo de ativos do pool de liquidez e, em seguida, envia para o usuário. Se for ETH, chama a função call
de baixo nível para enviar, se for um token ERC20, usa sua função de transferência para enviar e, finalmente, grava o token LP.
Aqui, quando o ativo líquido for ETH, ele usará call
para enviar ETH, e entrará na lógica da função fallback
de msg.sender. Parece haver pontos que podem ser explorados, mas as funções-chave no contrato, como add_liquidity
, exchange
, remove_liquidity
e assim por diante, usaram o bloqueio de reentrância, não podem ser reentrantes. Voltando à função remove_liquidity
, como a atualização do estado não é concluída de forma síncrona, ao entrar na função fallback
de msg.sender, os tokens LP ainda não foram queimados, mas o saldo no pool de liquidez foi deduzido, portanto, o saldo ficará menor enquanto TotalSupply não mudou. Observando as funções que usam essas duas variáveis de estado no contrato, pode-se descobrir a função get_virtual_price
é afetada e, essa função é uma função do tipo de visualização que não usa o bloqueio de reentrância. Se esta função for chamada durante a reentrância, o resultado será menor que o valor normal, ou seja, o preço dos tokens LP será reduzido.
Se o contrato externo depender do resultado retornado pela função get_virtual_price
na Curve para processamento lógico, ele poderá ser afetado por essa vulnerabilidade de reentrância read-only.
Análise de casos de ataques
Em 9 de fevereiro de 2023, o protocolo DeFi dForcenet sofreu uma invasão, e a causa principal do ataque foi o uso do valor de retorno da função get_virtual_price
do pool de liquidez da Curve por seu oráculo.
A transação de ataque é: (https://arbiscan.io/tx/0x5db5c2400ab56db697b3cc9aa02a05deab658e1438ce2f8692ca009cc45171dd)
Análise do processo de ataque:
Primeiro, uma grande quantidade de WETH foi emprestada por meio de um empréstimo flash e, em seguida, a liquidez foi adicionada ao pool ETH/wstETH na Curve usando a função add_liquidity
, obtendo wstETHCRV:
Então, parte do wstETHCRV foi transferida para outro contrato de ataque, e wstETHCRV-gauge e USX foram emprestados, depositando wstETHCRV-gauge no contrato wstETHCRV-gauge:
Em seguida, a função remove_liquidity
do pool ETH/wstETH na Curve foi chamada para retirar a liquidez. Ao retirar a liquidez, a lógica entrou na função fallback
do contrato de ataque. Devido ao oráculo dForcenet usar a função get_virtual_price
do pool ETH/wstETH na Curve, no estado reentrante atual, uma vez que o saldo de ETH no pool diminuiu, mas a oferta total de wstETHCRV não mudou, o preço virtual obtido será menor. Portanto, o atacante liquidou outro contrato de ataque e um empréstimo de usuário na função fallback
:
Por fim, o invasor trocou o medidor wstETHCRV-gauge por WETH por meio de uma série de operações, devolveu o empréstimo instantâneo e obteve um lucro de 1.236 ETH e 710.000 tokens USX.
A implementação do oráculo no dForcenet:
Pode se ver que a função getPrice
chama a função get_virtual_price
do pool de liquidez da Curve e executa o processamento de multiplicação.
Balancer
Análise de vulnerabilidade na Balancer
Os usuários podem criar pools de liquidez para uma variedade de ativos por meio do protocolo Balancer e ganhar taxas facilitando negociações de outros usuários. Os usuários podem adicionar liquidez ao pool de liquidez criado por meio da função joinPool
do contrato Balancer: Vault ou retirar liquidez por meio da função exitPool
. Ambas as funções são implementadas chamando a função interna _joinOrExit
, mas o tipo de operação transmitida é diferente. A função _joinOrExit
é implementada da seguinte forma:
A função primeiro verifica os parâmetros de entrada e, em seguida, obtém os saldos dos tokens no pool de liquidez correspondente. Em seguida, a função _callPoolBalanceChange
será chamada para calcular o saldo, transferir ativos e pagar taxas:
A função chamará a função onJoinPool
ou onExitPool
do pool de liquidez com base no tipo de operação e, em seguida, realizará as transferências de ativos. Ao remover a liquidez, a função chamará _processExitPoolTransfers
para transferir ativos para o usuário. A implementação é a seguinte:
A função chama _sendAsset
para transferir ativos. Se o token for WETH, ele será convertido em ETH e enviado usando sendValue
. Como sendValue
usa call
para enviar ETH, se o destinatário for um contrato e tiver implementado a função fallback
, a lógica continuará para a função fallback
.
Após executar a função _callPoolBalanceChange
, a função modificará o saldo de acordo com o tipo de pool chamando a função correspondente:
A lógica da função está aqui. Percebe-se que existe um problema onde a transferência é realizada primeiro, e depois o saldo é atualizado. Se houver ETH nos ativos, ele acionará a função fallback
do receptor. Neste ponto, a transferência já foi concluída, mas o saldo interno do contrato ainda não foi atualizado. Portanto, estamos procurando possíveis vulnerabilidades no contrato e descobrindo que todas as principais funções usam bloqueio de reentrância para evitar tais ataques.
Embora nenhuma vulnerabilidade tenha sido encontrada na função de gravação, descobrimos que a função getPoolTokens
, que é do tipo de visualização, usa o saldo interno do contrato e não é protegida por um bloqueio de reentrância porque não modifica nenhuma variável de estado.
Como resultado, se um contrato externo chamar a função getPoolTokens
do contrato Balancer: Vault e usar seu valor de retorno para processamento posterior, ele poderá ficar vulnerável a um ataque de reentrância read-only.
Análise do caso de ataque
O projeto Sentiment na Arbitrum sofreu um ataque, resultando em uma perda de $ 1 milhão. A causa principal desse ataque foi que o oráculo do Sentiment ter usado os dados retornados pela função getPoolTokens
no contrato Balancer: Vault, que possui uma vulnerabilidade conhecida de reentrância read-only.
A transação do ataque é: (https://arbiscan.io/tx/0xa9ff2b587e2741575daf893864710a5cbb44bb64ccdc487a100fa20741e0f74d)
Análise do processo de ataque:
O invasor emprestou 606 WBTC, 10.050 WETH e 18.000.000 USDC por meio de um empréstimo flash, criou uma conta por meio do Sentiment e depositou 50 WETH nela. Usando esta conta, eles depositaram os 50 WETH no Balancer: Vault:
Depois disso, o invasor forneceu liquidez ao pool chamando a função joinPool
do contrato Balancer: Vault por meio do contrato de ataque, depositando 606 WBTC, 10.000 WETH e 18.000.000 USDC. Em seguida, ele removeu a liquidez chamando a função exitPool
e acionou a função fallback
do contrato de ataque ao enviar o ETH:
Na função fallback
, o invasor estava chamando a função de empréstimo para emprestar fundos. Essa função estava usando RiskEngine
.isAccountHealthy
para verificar a integridade da conta e a função getPrice
do oráculo foi obtida por WeightedBalancerLPOracle
.getPrice
. Esta função também estava usando os dados retornados pela função getPoolTokens
do contrato Balancer: Vault. Nesse ponto, como a lógica ainda estava na função fallback
do contrato de ataque, as quantidades de WBTC, WETH e USDC no pool de liquidez não haviam sido atualizadas. Portanto, o preço do oráculo era 16 vezes maior do que antes, permitindo que o invasor tomasse mais ativos emprestados usando os 50 WETH:
O invasor chamou repetidamente as funções borrow
e exec
para emprestar outros ativos e, após pagar o empréstimo flash, obteve um lucro de 0,5 WBTC, 30 WETH, 538.399 USDC e 360.000 USDT:
Confira a função getPrice
do contrato WeightedBalancerLPOracle:
É possível observar que, depois que a função obtém o saldo de tokens do pool de liquidez correspondente por meio da função getPoolTokens
do contrato Balancer.vault, o saldo do token será multiplicado na lógica subsequente. O saldo de token não atualizado fará com que aqui os resultados sejam ampliados.
Resumo
Este artigo apresenta brevemente o conceito de vulnerabilidades de reentrância read-only e analisa as vulnerabilidades de reentrância read-only em dois protocolos DeFi principais, bem como os casos de ataque reais correspondentes. Ao desenvolver um projeto DeFi, a possível integração de outros projetos deve ser considerada e várias medidas de segurança devem ser tomadas para reduzir os danos causados por vulnerabilidades de reentrância. Se necessário, as funções do tipo visualização também podem ser protegidas por bloqueios de reentrância para aumentar a segurança.
Sobre nós
Na Eocene Research, fornecemos informações sobre intenções e segurança por trás de tudo o que você conhece ou não sobre blockchain e capacitamos cada indivíduo e organização a responder a perguntas complexas com as quais nem sonhávamos naquela época.
Saiba mais: Site | Medium | Twitter
Referência
Curve LP Oracle Manipulation: Post Mortem
Reentrancy Vulnerability Scope Expanded
Artigo escrito por Eocene | Security. Traduzido por Panegali.
Top comments (0)