Entendendo o código genético dos criptocóticos

... e aprenda a trabalhar com as ferramentas de desenvolvedor Ethereum usando um exemplo da vida real.

Parte zero: o objeto apareceu


Acabei de terminar minhas palestras sobre o curso completo de desenvolvimento de aplicativos descentralizados baseados no Ethereum em Solidity em chinês. Eu dei isso no meu tempo livre para aumentar o nível de conhecimento sobre blockchain e contratos inteligentes entre a comunidade chinesa de desenvolvedores. Durante meu trabalho, fiz amizade com alguns alunos.

E no final do curso, de repente nos encontramos cercados por essas criaturas:



Imagem de cryptokitties.co

Como a maioria das pessoas que encontrou esse fenômeno, também não conseguimos resistir a essas criações fofas de criptografia e rapidamente nos tornamos viciadas no jogo. Gostávamos de criar novos gatos e até substituímos o método de patinho pelo método de cripto gato . Acredito que o vício em jogos seja ruim, mas não neste caso, porque a paixão por criar gatinhos rapidamente nos levou à pergunta:

Como certos gatos criptografados obtêm seu conjunto de genes?


Decidimos dedicar o sábado à noite para encontrar a resposta e achamos que conseguimos fazer algum progresso no desenvolvimento de software que nos permita determinar a mutação genética de gatinhos cripto recém-nascidos antes de nascerem. Em outras palavras, este programa pode ajudá-lo a verificar e determinar o tempo apropriado para a fertilização da mãe-gato e, assim, obter a mutação mais interessante possível.

Publicamos este material na esperança de que sirva a todos como um artigo introdutório para se familiarizar com as ferramentas de desenvolvimento Ethereum muito úteis, assim como os próprios gatinhos de criptografia permitiram que muitas pessoas não familiarizadas com o blockchain se unissem às fileiras de usuários de criptomoedas.

Parte um: a lógica de alto nível da geração de gatinhos


Para começar, nos perguntamos: como é o nascimento de gatinhos cripto?

Para responder a essa pergunta, usamos o excelente condutor de blockchain Etherscan, que nos permite fazer muito mais do que apenas "estudar os parâmetros e o conteúdo dos blocos". Então descobrimos o código fonte do contrato CryptoKittiesCore:



https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code

Observe que o contrato expandido é realmente um pouco diferente do usado no programa de recompensas. De acordo com este código, um filhote é formado em duas etapas: 1) a mãe gata é fertilizada pelo gato; 2) um pouco mais tarde, quando o período de maturação do feto termina, a função giveBirth é chamada. Essa função geralmente é chamada por um certo processo-demônio, mas, como você verá mais adiante, para obter mutações interessantes, será necessário selecionar corretamente o bloco em que seu gatinho nasceu.

function giveBirth(uint256 _matronId) external whenNotPaused returns(uint256) { Kitty storage matron = kitties[_matronId]; // ,  - . require(matron.birthTime != 0); // ,       ! require(_isReadyToGiveBirth(matron)); //      -. uint256 sireId = matron.siringWithId; Kitty storage sire = kitties[sireId]; // ,         uint16 parentGen = matron.generation; if (sire.generation > matron.generation) { parentGen = sire.generation; } //     . uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1); 

No código acima, você pode ver claramente que os genes de um gatinho recém-nascido são determinados no momento do nascimento, chamando a função mixGenes do contrato inteligente externo de geneScience. Essa função usa três parâmetros: o gene mãe, o gene pai e o número do bloco em que o gato estará pronto para dar à luz.

Você provavelmente terá uma pergunta lógica: por que os genes não são determinados no momento da concepção, como é o caso no mundo real? Como você verá no decorrer da narrativa subsequente, isso permite que você se defenda de maneira bastante elegante das tentativas de prever e decifrar genes. Essa abordagem elimina a possibilidade de previsão 100% precisa dos genes dos gatinhos antes que o fato da gravidez entre mães e gatos seja registrado na blockchain. E mesmo se você pudesse descobrir o código exato responsável pela mistura dos genes, isso não lhe daria nenhuma vantagem.

Seja como for, no começo ainda não sabíamos disso, então vamos continuar. Agora precisamos descobrir o endereço do contrato da geneScience. Para fazer isso, use MyEtherWallet:



Endereço do contrato GeneScience

É assim que o bytecode do contrato se parece:

 0x60606040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416630d9f5aed81146100715780631597ee441461009f57806354c15b82146100ee57806361a769001461011557806377a74a201461017e575b600080fd5b341561007c57600080fd5b61008d6004356024356044356101cd565b604051908152602001604051809........ 

Pela aparência dele, você não pode dizer que, como resultado, algo tão fofo quanto um gatinho aparece em tudo, mas temos muita sorte de que este seja um endereço público e não precisamos procurá-lo no repositório). De fato, acreditamos que não deve ser tão facilmente acessível. Se os desenvolvedores realmente quiserem garantir que o endereço do contrato esteja correto, eles devem usar a função checkScienceAddress, mas não seremos incomodados.

Parte dois: o colapso de uma hipótese simples


Então, o que queremos alcançar no final? Deve-se entender que não estabelecemos o objetivo de compilar completamente o bytecode, transformando-o em um código de solidez legível por humanos. Precisamos de um método barato (sem a necessidade de pagar por transações no blockchain de combate) para determinar os genes dos gatinhos, desde que saibamos quem são seus pais. É isso que faremos.

Para começar, vamos usar a ferramenta opcode Etherscan para uma análise rápida. É assim:



Muito mais claro

Seguimos a regra de ouro para decodificar o código do assembler: começamos com uma hipótese simples e ousada sobre o comportamento do programa e, em vez de tentar entender seu trabalho como um todo, nos concentramos em confirmar a suposição feita. Analisaremos o bytecode para responder a algumas perguntas:

  1. Ele usa carimbos de data e hora? Não, porque o código de operação TIMESTAMP está ausente. Se houver algum acidente simples, sua fonte é definitivamente outro código de operação.
  2. Um hash de bloco é usado? Sim, o BLOCKHASH ocorre duas vezes. Portanto, a aleatoriedade, se houver, pode surgir de seus códigos de operação, mas ainda não temos certeza disso.
  3. Algum hashes é usado? Sim, existe o SHA3. Não está claro, no entanto, o que ele está fazendo.
  4. O msg.sender é usado? Não, porque o código de operação CALLER está ausente. Portanto, nenhum controle de acesso é aplicado ao contrato.
  5. Está sendo usado algum contrato externo? Não, não há código de chamada CALL.
  6. O COINBASE é usado? Não, e, portanto, excluímos outra possível fonte de aleatoriedade.

Tendo recebido a resposta para essas perguntas, apresentamos e pretendemos testar uma hipótese simples: o resultado do mixGene é determinado por três e apenas três parâmetros de entrada dessa função. Se assim for, poderíamos simplesmente implantar esse contrato localmente, continuar chamando essa função com os parâmetros de interesse para nós e, talvez, obtermos um kit de genes de gatinhos antes mesmo da fertilização da mãe-gato.

Para verificar essa suposição, chamamos a função mixGene na rede principal com três parâmetros aleatórios: 1111115, 80, 40 e obtemos algum resultado X. Em seguida, implante esse bytecode usando trufa e testrpc . Portanto, nossa preguiça levou a uma maneira um pouco fora do padrão de usar trufas.

 contract GeneScienceSkeleton { function mixGenes(uint256 genes1, uint256 genes2, uint256 targetBlock) public returns (uint256) {} } 

Começamos com o esqueleto do contrato, colocamos na estrutura de pastas da estrutura de trufas e executamos a compilação de trufas. No entanto, em vez de migrar diretamente esse contrato vazio para o testrpc, substituímos o bytecode do contrato na pasta de construção pelo bytecode expandido real e pelo bytecode do contrato geneScience. Essa é uma maneira atípica, mas rápida, se você deseja implantar um contrato com apenas bytecode e alguma interface aberta limitada para testes locais. Depois disso, chamamos diretamente Mixgenes com os parâmetros 1111115, 80, 40 e, infelizmente, obtemos um erro com a resposta reverter em resposta. Ok, olhe mais fundo. Como sabemos, a assinatura das funções mixGene é 0x0d9f5aed; portanto, pegamos uma caneta e um papel e rastreamos a execução do bytecode, começando no ponto de entrada dessa função para explicar as alterações na pilha e no armazenamento. Após alguns JUMPs, nos encontramos aqui:

 [497] DUP1 [498] NUMBER [499] DUP14 [500] SWAP1 [501] GT [504] PUSH2 0x01fe [505] JUMPI [507] PUSH1 0x00 [508] DUP1 [509] 'fd'(Unknown Opcode) 

A julgar pelo conteúdo dessas linhas, se o número do bloco atual for menor que o terceiro parâmetro, revert () será chamado. Bem, esse é um comportamento bastante razoável: chamar uma função real em um jogo com um número de bloco do futuro é impossível e isso é lógico.

É fácil contornar essa verificação de entrada: nós apenas extraímos alguns blocos no testrpc e chamamos a função novamente. Desta vez, a função retorna Y com êxito.

Mas infelizmente X! = Y

Que pena. Isso significa que o resultado da execução da função depende não apenas dos parâmetros de entrada, mas também do estado do blockchain da rede principal, que, é claro, difere do estado do falso blockchain testrpc.

Parte Três: arregaçar as mangas e cavar a pilha


Ok. Então é hora de arregaçar as mangas. O papel não é mais adequado para rastrear o status da pilha. Portanto, para um trabalho mais sério, lançaremos um desmontador EVM muito útil chamado evmdis .

Comparado ao papel e caneta, este é um passo tangível à frente. Vamos continuar com o que paramos no último capítulo. A seguir, uma conclusão encorajadora com o evmdis:

 ............. :label22 # Stack: [@0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1EB PUSH(0x0) 0x1ED DUP1 0x1EE DUP1 0x1EF DUP1 0x1F0 DUP1 0x1F1 DUP1 0x1F3 DUP13 0x1F9 JUMPI(:label23, NUMBER() > POP()) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1FA PUSH(0x0) 0x1FC DUP1 0x1FD REVERT() :label23 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1FF DUP13 0x200 PUSH(BLOCKHASH(POP())) 0x201 SWAP11 0x202 POP() 0x203 DUP11 0x209 JUMPI(:label25, !!POP()) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x20C DUP13 0x213 PUSH((NUMBER() & ~0xFF) + (POP() & 0xFF)) 0x214 SWAP13 0x215 POP() 0x217 DUP13 0x21E JUMPI(:label24, !!(POP() < NUMBER())) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 @0x213 @0x85 @0x82 :label3 @0x34] 0x222 DUP13 0x223 PUSH(POP() - 0x100) 0x224 SWAP13 0x225 POP() :label24 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 [@0x223 | @0x213] @0x85 @0x82 :label3 @0x34] 0x227 DUP13 0x228 PUSH(BLOCKHASH(POP())) 0x229 SWAP11 0x22A POP() :label25 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 [@0x200 | @0x228] 0x0 [@0x88 | @0x223 | @0x213] @0x85 @0x82 :label3 @0x34] 0x22C DUP11 0x22D DUP16 0x22E DUP16 ........... 

O que evmdis realmente serve é a sua utilidade para analisar o JUMPDEST nos rótulos corretos, que não podem ser superestimados.

Então, depois de passarmos o requisito inicial, nos encontramos no rótulo 23. Vemos o DUP13 e lembramos do capítulo anterior que o número 13 na pilha é nosso terceiro parâmetro. Então, estamos tentando obter o BLOCKHASH do nosso terceiro parâmetro. No entanto, a ação do BLOCKHASH é limitada a 256 blocos. É por isso que é seguido por JUMPI (este é um construto if). Se traduzirmos a lógica dos opcodes no idioma do pseudo-código, obteremos algo assim:

 func blockhash(p) { if (currentBlockNumber - p < 256) return hash(p); return 0; } var bhash = blockhash(thrid); if (bhash == 0) { thirdProjection = (currentBlockNumber & ~0xff) + (thridParam & 0xff); if (thirdProjection > currentBlockNumber) { thirdProjection -= 256; } thirdParam = thirdProjection; bhash = blockhash(thirdProjection); } label 25 and beyond ..... some more stuff related to thirdParam and bhash 

mais algumas coisas relacionadas ao thirdParam e ao bhash - outro código relacionado ao thirdParam e ao hash do bloco

Agora acreditamos ter encontrado uma razão pela qual nossos resultados diferem daqueles que observamos na rede principal. Mais importante, aparentemente conseguimos descobrir a fonte do acaso. A saber: o hash do bloco é calculado com base no terceiro parâmetro ou na previsão do terceiro parâmetro. É importante observar que na pilha o terceiro parâmetro também é substituído por esse número de bloco previsto.

Obviamente, durante a execução local fora da rede principal, não temos uma opção simples para impor um retorno BLOCKHASH que corresponda aos valores da rede principal. Seja como for, como sabemos todos os três parâmetros, podemos monitorar facilmente a rede principal e obter o hash do bloco H para o terceiro parâmetro, bem como o hash do bloco previsto.

Em seguida, podemos inserir esse hash diretamente no código de bytes em nosso ambiente de teste local e, se tudo correr conforme o planejado, finalmente obteremos o conjunto correto de genes.

Mas existe um problema: DUP13 e BLOCKHASH são apenas 2 bytes no código e, se os substituirmos por PUSH32 0x * hash * de 33 bytes, o contador do programa mudará completamente e precisaremos corrigir cada JUMP e JUMPI. Ou teremos que fazer JUMP no final do código e substituir as instruções para o código implantado, e assim por diante.

Bem, desde que chegamos até aqui, cheiraremos um pouco mais. Como inserimos o hash diferente de zero de 32 bytes na ramificação if, a condição sempre será verdadeira e, portanto, tudo escrito na parte else pode ser simplesmente descartado para dar espaço ao hash de 32 bytes. Bem, em geral, foi o que fizemos:



O ponto principal é que, uma vez que abandonamos a outra parte da condição, precisamos substituir o terceiro parâmetro de entrada da função mixGene pela previsão do terceiro parâmetro antes de chamá-lo.

Isso é o ponto que, se você está tentando obter o resultado de uma operação
mixGene (X, Y, Z), onde currentBlockNumber é Z <256, você só precisa substituir o hash PUSH32 pelo hash do bloco Z.
No entanto, se você pretende fazer o seguinte
mixGene (X, Y, Z), onde currentBlockNumber é Z ≥ 256, será necessário substituir o hash PUSH32 pelo hash do bloco proj_Z, em que proj_Z é definido da seguinte forma:

 proj_Z = (currentBlockNumber & ~0xff) + (Z & 0xff); if (proj_Z > currentBlockNumber) { proj_Z -= 256; } <b>    Z  proj_Z   ,   mixGene(X, Y, proj_Z).</b> 

Observe que o proj_Z permanecerá inalterado em um determinado intervalo de blocos. Por exemplo, se Z & 0xff = 128, o proj_Z muda apenas em cada bloco zero e 128º.

Para confirmar esta hipótese e verificar se existem armadilhas à frente, alteramos o bytecode e usamos outro utilitário interessante chamado hevm .



Se você nunca usou o hevm, recomendo que experimente. A ferramenta está disponível junto com sua própria estrutura, mas acima de tudo em seu conjunto, deve-se notar uma coisa indispensável como um depurador de pilha interativo.

 Usage: hevm exec --code TEXT [--calldata TEXT] [--address ADDR] [--caller ADDR] [--origin ADDR] [--coinbase ADDR] [--value W256] [--gas W256] [--number W256] [--timestamp W256] [--gaslimit W256] [--gasprice W256] [--difficulty W256] [--debug] [--state STRING] Available options: -h,--help 

Acima estão as opções de lançamento. O utilitário permite que você especifique uma variedade de parâmetros. Entre eles está o --debug, que oferece a capacidade de depurar interativamente.

Então, aqui fizemos várias chamadas para o contrato de geneScience implantado na blockchain da rede principal e registramos os resultados. Em seguida, usamos o hevm para executar nosso bytecode quebrado com dados preparados levando em conta as regras descritas acima e ...

Os resultados são os mesmos!

O último capítulo: conclusão e continuação do trabalho (?)


Então, o que conseguimos alcançar?

Usando nosso software de hackers, você pode ter 100% de chance de prever um gene de 256 bits para um gatinho recém-nascido se ele nascer na faixa de blocos [coolDownEndBlock (quando o bebê estiver pronto para aparecer), o bloco atual será + 256 (aproximadamente)]. Você pode argumentar sobre isso da seguinte maneira: quando o bebê está no útero da mãe, seus genes sofrem mutações ao longo do tempo, devido à fonte de entropia na forma de um hash do bloco coolDownEndBlock previsto, que também muda com o tempo. Portanto, você pode usar este programa para verificar a aparência do gene do bebê se ele nasceu agora. E se você não gosta deste gene, pode esperar cerca de 256 blocos a mais (em média) e verificar o novo gene.

Alguém pode dizer que isso não é suficiente, já que apenas 100% de precisão da previsão pode ser considerada um hacking ideal antes mesmo da gravidez de uma mãe gata. No entanto, isso não é possível, já que o gene do gatinho é determinado não apenas pelos genes de seus pais, mas também pelo hash previsto do bloco como um fator de mutação, que simplesmente não pode ser conhecido antes da fertilização.

O que pode ser melhorado e quais são as nuances aqui?

Examinamos rapidamente as alterações que ocorrem na pilha na parte lógica real do contrato inteligente (etiqueta 25 e tudo depois) e acreditamos que essa parte previsível do código mixGene deve ser analisada e estudada. Esperamos que o hash do bloco como fator de mutação também tenha algum significado físico, ajudando, por exemplo, a determinar qual gene deve ser mutado. Se conseguirmos descobrir isso, obteremos o gene original, sem mutações. Isso é útil porque, se você não possui um bom gene fonte, mesmo a melhor mutação pode não ser suficiente.

Também não medimos a correlação entre o gene de 256 bits e as características do gatinho (cor dos olhos, tipo de cauda, ​​etc.), mas acreditamos que isso é bem possível com a ajuda de um bot de alto desempenho e um classificador simples.

E, em geral, entendemos perfeitamente a intenção da equipe de desenvolvimento do CryptoKitties de estabilizar a mutação por um curto período de tempo. Mas o outro lado dessa abordagem é a capacidade de realizar uma análise como fizemos.

Também gostaríamos de agradecer à maravilhosa comunidade ethereum por desenvolver ferramentas como Etherscan, hevm, evmdis, trufa, testrpc, myetherwallet e Solidity. Esta é uma comunidade muito legal e estamos felizes em fazer parte dela.

E, finalmente, o código modificado https://github.com/modong/GeneScienceCracked/

Lembre-se de alterar $ CONSTBLOCKHASH $ para o hash do bloco previsto.

imagem

Source: https://habr.com/ru/post/pt409345/


All Articles