Da última vez, aprendemos sobre a função self-destruction (autodestruição) no Solidity (espero que todos tenham sido capazes de entender). Agora, aprenderemos sobre como acessar dados privados.
Panorama geral
Vamos ver os três métodos de armazenamento de dados em Solidity:
1. Armazenamento
- Os dados em armazenamento são armazenados permanentemente. São armazenados no slot como um par chave-valor.
- Os dados em armazenamento são escritos na blockchain (de forma que eles mudam de estado) e é por isso que o uso do armazenamento é muito caro.
- O custo do gas para ocupar um slot de 256 bits é de 20.000.
- A taxa de gas para modificação do valor do armazenamento custará 5,000.
- Uma certa quantidade de gas é reembolsada quando um slot de armazenamento é limpo (ou seja, quando bytes não nulos são definidos como zero).
- O armazenamento tem um total de 2²⁵⁶ slots, 32 bytes de dados em cada slot são armazenados sequencialmente na ordem de declaração. Os dados serão armazenados a partir do lado direito de cada slot. Se as variáveis adjacentes couberem em um único slot de 32 bytes, então elas serão armazenadas em pacotes no mesmo slot. Caso contrário, um novo slot será habilitado para armazenamento.
- Os métodos para armazenar arrays no armazenamento são exclusivos. Arrays em solidity são divididas em dois tipos.
a. Array de comprimento fixo (Fixed-length)
Cada elemento em um array de comprimento fixo terá um slot separado para armazenamento. Tomando como exemplo um array de comprimento fixo com três elementos uint64, na figura a seguir, você pode ver claramente seu método de armazenamento:
b. Array de comprimento variável (Variable-length - O comprimento varia de acordo com o número de elementos)
O método de armazenamento de arrays de comprimento variável é muito estranho. Ao encontrar arrays de comprimento variável, um novo slot, slotA, será habilitado para armazenar o comprimento do array, e seus dados serão armazenados em outro slotV numerado. O slotA representa a posição declarada do array de comprimento variável; length representa o comprimento do array de comprimento variável, o slotV representa o local de armazenamento dos dados do array de comprimento variável, value representa o valor de um determinado dado no array de comprimento variável, e index representa o índice subscrito correspondente ao valor. Contudo, length = sload(slotA), slotV = keccak256(slotA) + index e value = sload(slotV).
Os arrays de comprimento variável não podem saber o comprimento da matriz durante a compilação e não há como reservar espaço de armazenamento com antecedência, então o Solidity usa o slotA para armazenar o comprimento do array de comprimento variável.
Vamos escrever um exemplo para verificar como o array de comprimento variável descrito acima é armazenado:
Após a implantação do contrato, chame a função addUser e passe o parâmetro a = 998. Uma vez feita a depuração, você pode ver como o array de comprimento variável é armazenado.
O primeiro slot aqui é onde o comprimento do array de comprimento variável é armazenado:
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
O valor é igual a:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)
key = 0 é o número do slot atual.
value = 1, isto significa que há apenas uma parte dos dados no array de comprimento variável user[], o que significa que o comprimento do array é 1.
O segundo slot aqui é onde os dados do array de comprimento variável são armazenados:
0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9
Esse valor é igual a:
sha3(“0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563”)
Os números dos slots são:
key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
Esse valor é igual a:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)+0
Os dados armazenados no slot são:
value=0x0000000000000000000000000000000000000000000000000000000000003e6
Esse é 998 em hexadecimal, que foi o valor que passamos.
Para uma verificação mais precisa, chamamos novamente a função addUser e passamos a=999 para obter o seguinte resultado.
Aqui podemos ver que o novo slot é:
0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a.
Esse valor é igual a:
sha3(“0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564”)
O número do slot é: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
Esse valor é igual a:
sha3(“0x000000000000000000000000000000000000000000000000000000000000000”)+1
Os dados armazenados no slot são:
value=0x0000000000000000000000000000000000000000000000000000000000003e7
Este valor é 999 em hexadecimal, que é o valor passado pela função addUser que acabamos de chamar.
Os exemplos acima devem lhe dar uma compreensão geral de como os arrays de comprimento variável são armazenados.
2. Memory (Memória)
- Memory é um array de bytes com um tamanho de slot de 256 bits (32 bytes). Os dados só são armazenados durante a execução da função e são apagados após a execução. Eles não são salvos na blockchain.
- Ler ou escrever um byte (256 bits) requer 3 de gas.
- A fim de evitar muito trabalho para os mineradores, o custo começará a subir após 22 operações de leitura e escrita.
3. Calldata (chamar dados)
- calldata é uma área não-persistente e inalterável utilizada para armazenar parâmetros de funções e se comporta basicamente como a memória.
- Calldata é necessária para argumentos de chamadas a funções externas e também pode ser usada para outras variáveis.
- Ela evita a duplicação e assegura que os dados não possam ser modificados.
- Arrays e structs com localização de dados calldata também podem ser retornados de funções, mas nenhuma alocação a este tipo é possível.
Agora que entendemos os três métodos de armazenamento em solidity, vejamos as quatro palavras-chave de visibilidade no contrato: Em solidity, há quatro palavras-chave de visibilidade: external (externa), public (pública), internal (interna) e private (privada). A visibilidade da função é pública por padrão. Para variáveis de estado, exceto a external, que não pode ser usada para definir variáveis, as outras três podem ser usadas para definir variáveis. A visibilidade padrão das variáveis de estado é interna.
1. Palavra-chave External
As funções externas, definidas como "external", podem ser chamadas por outros contratos. A função() externa marcada como “external” não pode ser chamada diretamente como uma função interna, ou seja, o método de chamada da função() deve usar this.function().
2. Palavra-chave Public
Funções definidas como públicas podem ser chamadas usando funções internas ou mensagens externas. Para variáveis de estado definidas como públicas, o sistema irá gerar automaticamente uma função getter.
3. Palavra-chave Internal
As funções e variáveis de estado definidas internamente só podem ser acessadas dentro do contrato atual ou contratos derivados do contrato atual.
4. Palavra-chave Private
As funções e variáveis de estado definidas como privadas são visíveis apenas para o contrato que as define. Nenhum dos contratos derivados do contrato pode chamar, acessar funções e variáveis de estado.
Em resumo, as palavras-chave que modificam o armazenamento da variável no contrato limitam apenas o escopo de sua chamada, mas não estabelecem se ela é legível ou não. Portanto, hoje mostraremos a você como ler todos os dados do contrato.
Exemplo de vulnerabilidade
Desta vez nosso contrato alvo é um contrato implantado na testnet Ropsten.
Endereço do contrato:
0x3505a02BCDFbb225988161a95528bfDb279faD6b
Link:
https://ropsten.etherscan.io/address/0x3505a02BCDFbb225988161a95528bfDb279faD6b#code
Código fonte do contrato
Análise de Vulnerabilidade
Pelo código do contrato acima, podemos ver que o contrato Vault registra dados confidenciais tais como o nome de usuário e a senha no contrato. Pelo conhecimento dos pré-requisitos, podemos entender que as palavras-chave que modificam as variáveis no contrato limitam apenas seu escopo de chamada. Isto prova indiretamente que os dados no contrato são públicos e podem ser lidos arbitrariamente, fazendo com que não seja seguro registrar dados confidenciais no contrato.
Dados de Leitura
Em seguida, examinaremos os dados deste contrato. Primeiro, vamos examinar os dados no slot0: pode-se ver pelo contrato que apenas um tipo de dado é armazenado no slot0.
Vou usar o Web3.py para interagir com os dados aqui
Após a execução
Vamos convertê-lo usando um conversor de base
Aqui chegamos com sucesso à contagem de variáveis do tipo uint=123 armazenadas no primeiro slot, o slot0, do contrato.
Vamos continuar:
Três variáveis são armazenadas no slot1: u16, isTrue, owner
Da direita para a esquerda, são:
owner = f36467c4e023c355026066b8dc51456e7b791d99
isTrue = 01 = true
u16 = 1f = 31
A senha da variável privada é armazenada no slot2.
Vamos analisar isto:
Os slots 3, 4, 5 armazenam três elementos no array de comprimento variável.
O slot6 armazena o comprimento do array de comprimento variável.
Podemos ver pelo código do contrato que a identificação e a senha do usuário são armazenadas sob a forma de pares chave-valor. Vamos examinar o id e a senha dos dois usuários.
user1 (usuário1)
user2 (usuário2)
Lemos com sucesso todos os dados do contrato. Provamos que os dados privados no contrato também podem ser lidos.
Técnicas de Prevenção
(1) Como desenvolvedor
Não armazenar nenhum dado confidencial no contrato, pois qualquer dado do contrato pode ser lido.
(2) Como auditor
Durante o processo de auditoria, deve-se prestar atenção se há dados confidenciais no contrato, tais como chaves secretas, senhas etc.
Referências
Favor consultar os seguintes artigos para obter informações adicionais.
- https://solidity-by-example.org/hacks/accessing-private-data/
- “Aprenda Solidity comigo: Armazenamento de Variáveis” https://learnblockchain.cn/article/1759
- “Início Rápido — web3.py” https://web3py.readthedocs.io/
- “Permissões para variáveis de estado e funções” https://blog.csdn.net/liyuechun520/article/details/78408608
Esse artigo foi escrito por SlowMist e traduzido por Fátima Lima. O original pode ser lido aqui.
Top comments (0)