tx.origin安全问题

115 阅读3分钟

tx.origin 安全问题

在合约代码中,最常用的是使用 msg.sender 来检查授权,但有时由于有些程序员不熟悉tx.origin和 msg.sender 的区别,如果使用了 tx.origin 可能导致合约的安全问题.黑客最典型的攻击场景是利用tx.origin的代码问题常与钓鱼攻击相结合的方式进行攻击。

图解msg.sender和msg.sender 的区别

tx.origin:表示最初的调用者,通常取得的是 EOA 的地址。

msg.sender:表示最近的调用者,通常取得是的上级调用者的地址,可以是 EOA 地址,也可以是合约地址。

如果 EOA 用户 A 调用合约 B,合约 B 调用合约 C。那么

  • 在 C 合约中,msg.sender 就是 B 合约的地址,tx.origin 为 A 地址。
  • 在 B 合约中,msg.sender 是 A 地址,tx.origin 也为 A 地址。

通过判断tx.origin==msg.sender来确定调用者是合约还是 EOA 账户。

image.png

思考 :可不可以通过判断一个账户的是否包含执行代码来区分这个账户是 EOA 还是 SCA?

答案:不可以。因为一个合约地址的 CODESIZE是大于零的,但当地址的 CODESIZE等于零时,并不能保证其为非合约,因为合约在构造阶段 CODESIZE也为零。

代码演示

下面的漏洞合约代码,在 transfer 方法中做了检查,本意是只有 owner 可以进行 transfer 操作。在这里使用的是tx.origin==owner进行检查。我们假设该 Wallet 合约的部署者是 Alice.

contract Wallet {
    address public owner;

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

    function transfer(address payable _to, uint _amount) public {
        require(tx.origin == owner, "Not owner");

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

假设 Eve 为黑客,写了一个攻击合约对钱包合约进行攻击:

  1. 黑客编写一个 Attack 的合约,并进行部署。
  2. 黑客通过钓鱼等手段诱导 Wallet 合约的部署者调用 Attack 合约的 attack 方法。
  3. 黑客就窃取到了 Wallet 合约的 ETH。

代码如下:

contract Attack {
    address payable public owner;
    Wallet wallet;

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

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }
}

实施过程: Alice 调用了 Attack 合约的 attack 方法,attack 方法调用了 wallet 合约的 transfer 方法,在 transfer 方法中 tx.origin 是 alice(在 transfer 方法中 tx.sender 是 attack 合约),因为 alice 就是 Wallet 合约的 owner,因此通过检测,将 ETH 转给了黑客 Eve。

思考

黑客如何诱骗Alice去调用攻击合约?

  1. 嵌套合约 将攻击合约嵌套在NFT、ERC20合约中,对Alice进行空头,让其对合约的奖励进行兑现或者进行转账行为。如:合约 A 调用合约 B,合约 B 调用合约 C,合约 C 调用合约 D,…………,最后合约中调用 wallet.transfer。
  2. 黑客可以将漏洞利用隐藏在receive函数中,通过诱导用户向指定的合约转账内触发漏洞利用。如假装与用户进行换币,给客户很大的折扣诱导等。

安全建议

使用msg.sender 代替 tx.origin。确保调用者就 owner。

function transfer(address payable _to, uint256 _amount) public {
  require(msg.sender == owner, "Not owner");

  (bool sent, ) = _to.call{value: _amount}("");
  require(sent, "Failed to send Ether");
}

参考

原文:zhuanlan.zhihu.com/p/564540141