diff --git a/README.md b/README.md index 4bd7ce3..c529228 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,23 @@ [![](https://img.shields.io/david/MrLuit/selfdestruct-detect.svg?style=flat-square)](https://david-dm.org/MrLuit/selfdestruct-detect) [![](https://img.shields.io/github/license/MrLuit/selfdestruct-detect.svg?style=flat-square)](https://github.com/MrLuit/selfdestruct-detect/blob/master/LICENSE) -Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the [Ethereum Virtual Machine](https://medium.com/@jeff.ethereum/optimising-the-ethereum-virtual-machine-58457e61ca15) contains a possibly reachable `SELFDESTRUCT` instruction. +Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the [Ethereum Virtual Machine](https://medium.com/@jeff.ethereum/optimising-the-ethereum-virtual-machine-58457e61ca15) contains a (possibly) reachable `SELFDESTRUCT` instruction. ## Usage > npm i selfdestruct-detect -## Features -- Unreachable code (like the [metadata hash](https://solidity.readthedocs.io/en/latest/metadata.html)) will not result in a false positive - ## How does it work? -First of all, the application breaks down bytecode into its opcodes. Secondly, it loops over the opcodes, and skips over push data (if any). If it comes across a halting opcode (`STOP`, `RETURN`, `REVERT`, `INVALID`, `SELFDESTRUCT`), it will correctly assume all following opcodes are **unreachable**, until it can find a valid jump destination (`JUMPDEST`). While this method does prevent false positives caused by the metadata hash included by the Solidity compiler, it will not detect whether code is unreachable due to exceptional halting, excluding halting due to the `INVALID` (`0xfe`) opcode. Please note that his tool is also not able to determine whether a contract will *actually* self destruct at any given time, it only detects whether it *might* be possible. +First of all, the application breaks down bytecode into its opcodes. Secondly, it loops over the opcodes, and skips over push data (if any). If it comes across a halting opcode (`STOP`, `RETURN`, `REVERT`, `INVALID`, `SELFDESTRUCT`), it will correctly assume all following opcodes are **unreachable**, until it can find a valid jump destination (`JUMPDEST`). While this method does prevent some false positives (like the [metadata hash](https://solidity.readthedocs.io/en/latest/metadata.html), included by the Solidity compiler), it will not detect whether code is unreachable due to exceptional halting, excluding halting due to the `INVALID` (`0xfe`) opcode. + +**Please note that this tool is unable to determine whether a contract will *actually* self destruct at any given time, it only detects whether it *might* be possible.** + +## Why is this useful? +Ethereum's Constantinople fork introduces a new opcode called [CREATE2](https://eips.ethereum.org/EIPS/eip-1014), which allows contracts to deploy other contracts at address `keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]` (instead of the usual `keccak256(rlp([sender, nonce]))`). Unfortunetaly, including `keccak256(init_code)` in the address formula does not prevent contracts from redeploying different code at the same address. For example, `init_code` could essentially just be *"call contract x for runtime bytecode"* which would allow for `keccak256(initCode)` to stay the same, even if contract x decides to return different runtime bytecode. + +While there is no way to deploy code at an address which already exists in the state, contracts can remove themselves from the state by self-destructing (using the `SELFDESTRUCT` opcode), which would allow different code to be redeployed. Since (currently) there's no way of finding out whether a contract was deployed using `CREATE2`, a possible detection method for this attack vector would be to check whether the runtime bytecode contains a `SELFDESTRUCT` opcode. Interfaces which allow users to interact with Ethereum smart contracts can (and should!) implement this check to warn users when interacting with unstable smart contracts. + +Read more: [Potential security implications of CREATE2? (EIP-1014)](https://ethereum-magicians.org/t/potential-security-implications-of-create2-eip-1014/2614) ## Example @@ -24,18 +30,33 @@ First of all, the application breaks down bytecode into its opcodes. Secondly, i const { mightSelfdestruct } = require("selfdestruct-detect"); const Web3 = require('web3'); const web3 = new Web3(new Web3.providers.HttpProvider("https://api.mycryptoapi.com/eth")); - -web3.eth.getCode("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d").then(code => { /* CryptoKitties contract */ - console.log(mightSelfdestruct(code)); +const CryptoKitties = "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d"; + +web3.eth.getCode(CryptoKitties).then(code => { + if(mightSelfdestruct(code)) { + console.log("Warning: CryptoKitties contract possibly contains a self-destruct method!"); + } else { + console.log("Success: CryptoKitties contract does not contain a reachable self-destruct instruction."); + } }); ``` #### Browser ```javascript -const { mightSelfdestruct } = window.SelfdestructDetector; -const web3 = new Web3(window.web3.currentProvider); -web3.eth.getCode("0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", function(err,code) { /* DAI contract */ +const { mightSelfdestruct } = window.SelfdestructDetect; +const web3 = new Web3(new Web3.providers.HttpProvider("https://api.mycryptoapi.com/eth")); +const DAI = "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359"; + +web3.eth.getCode(DAI, function(err, code) { if(err) throw err; - console.log(mightSelfdestruct(code)); + if(mightSelfdestruct(code)) { + console.log("Warning: DAI contract possibly contains a self-destruct method!"); + } else { + console.log("Success: DAI contract does not contain a reachable self-destruct instruction."); + } }); -``` \ No newline at end of file +``` + +## References +- [EIP 1014: Skinny CREATE2](https://eips.ethereum.org/EIPS/eip-1014) +- [Potential security implications of CREATE2? (EIP-1014)](https://ethereum-magicians.org/t/potential-security-implications-of-create2-eip-1014/2614) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b807377..64f695c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "evm-selfdestruct-detect", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ac23455..45763de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "selfdestruct-detect", - "version": "1.0.0", + "version": "1.1.0", "description": "Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the Ethereum Virtual Machine contains a possibly reachable SELFDESTRUCT instruction", "main": "lib/SelfdestructDetect.node.js", "module": "lib/SelfdestructDetect.js", diff --git a/tests/contracts/metadata.sol b/tests/contracts/metadata.sol index e6f834c..55f2007 100644 --- a/tests/contracts/metadata.sol +++ b/tests/contracts/metadata.sol @@ -1,5 +1,5 @@ pragma solidity 0.5.4; contract Contract { - string public data = "xyz"; + bytes32 constant data = "[randomData]"; } \ No newline at end of file diff --git a/tests/index.ts b/tests/index.ts index c00e76e..227992b 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,11 +1,13 @@ import 'mocha'; import { expect } from 'chai'; import { mightSelfdestruct } from '../src'; +import generateFFMetadataContract from './utils/generateFFMetadata'; import Contract from './utils/contract.class'; describe('Contracts', () => { describe('hello_world.sol', () => { - const contract = new Contract("hello_world.sol"); + const contract = new Contract(); + contract.loadFile("hello_world.sol"); it('should compile without errors', () => { expect(contract.valid(), contract.errors().join("\n")).to.be.true; @@ -17,7 +19,8 @@ describe('Contracts', () => { }); describe('selfdestruct.sol', () => { - const contract = new Contract("selfdestruct.sol"); + const contract = new Contract(); + contract.loadFile("selfdestruct.sol"); it('should compile without errors', () => { expect(contract.valid(), contract.errors().join("\n")).to.be.true; @@ -29,7 +32,7 @@ describe('Contracts', () => { }); describe('metadata.sol', () => { - const contract = new Contract("metadata.sol"); + const contract = generateFFMetadataContract(); it('should compile without errors', () => { expect(contract.valid(), contract.errors().join("\n")).to.be.true; diff --git a/tests/utils/contract.class.ts b/tests/utils/contract.class.ts index 673e258..6928b52 100644 --- a/tests/utils/contract.class.ts +++ b/tests/utils/contract.class.ts @@ -4,7 +4,7 @@ const solc = require("solc"); export default class Contract { private output: any; - constructor(filename: string) { + loadFile(filename: string) { const source = fs.readFileSync("./tests/contracts/" + filename, "utf8"); const input = { language: 'Solidity', @@ -24,6 +24,25 @@ export default class Contract { this.output = JSON.parse(solc.compile(JSON.stringify(input))); } + load(name: string, content: string) { + const input = { + language: 'Solidity', + sources: { + [name]: { + content: content + } + }, + settings: { + outputSelection: { + '*': { + '*': [ '*' ] + } + } + } + } + this.output = JSON.parse(solc.compile(JSON.stringify(input))); + } + valid() { return 'contracts' in this.output && (!('errors' in this.output) || this.output.errors.length === 0); } @@ -41,13 +60,13 @@ export default class Contract { return bytecode; } - hash(): string | false { + hash(): string { const regex = /a165627a7a72305820([a-f0-9]{64})0029$/; const match = this.bytecode().match(regex); if (match && match[1]) { - return 'bzzr://' + match[1]; + return match[1]; } else { - return false; + return ''; } } } \ No newline at end of file diff --git a/tests/utils/generateFFMetadata.ts b/tests/utils/generateFFMetadata.ts new file mode 100644 index 0000000..b3ea47c --- /dev/null +++ b/tests/utils/generateFFMetadata.ts @@ -0,0 +1,16 @@ +import Contract from './contract.class'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; + +const metadata = fs.readFileSync("./tests/contracts/metadata.sol", "utf8"); + +export default () => { + while(true) { + const contract = new Contract(); + const randomData = crypto.randomBytes(16).toString("hex"); + contract.load("metadata.sol", metadata.replace("[randomData]", randomData)); + if(contract.hash().includes('ff')) { + return contract; + } + } +} \ No newline at end of file