签名延展性攻击

  • Q1ngying
  • 更新于 2024-06-02 18:15
  • 阅读 725

本文根据Owen21小时合约审计课程中第二部分合约升级指南部分中关于离线签名的拓展性问题展开讨论。原视频:https://www.youtube.com/watch?v=DRZogmD647U

例子

下面是一个易受攻击的代码:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.17;

contract signatureMealleable is Ownable {
    address token;
    mapping(bytes32 => bool) executed;

    function signedTransfer(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
        bytes32 msgHash = keccak256(abi.encode(msg.sender, to, amount));
        address signer = ecrecover(msgHash, v, r, s);
        require(signer == owner);

        bytes32 sigHash = keccak256(abi.encode(msgHash, v, r, s));
        require(!executed[sigHash]);
        executed[sigHash] = true;

        IERC(token).safeTransfer(to, amount);
    }
}

如何攻击?这里的签名具有可塑性

因为owner提供签名的vrs组件,但是攻击者可以观察到这些v, r, s的值来制作另一个签名,该签名也恢复到相同的 owner 地址。实际上,这是第二次执行owner的消息,使得这份合约双倍支出

但是为什么实际上是可能的?攻击者如何观察到这些值,来创建第二个有效的签名的呢?

首先,我们需要分解消息签名在以太坊中的工作原理,首先,我们要从椭圆曲线开始

椭圆曲线

首先是一些关于椭圆曲线的前置知识。以太坊的签名是根据$y^{2}=x^{3}+7(mod\ p)$这个椭圆曲线方程(SECP256K1)来计算公、私钥的。这里的

  • P是一个非常非常大的质数
  • N是这个群的阶(本质上是椭圆曲线上的点的数量)
  • G是该椭圆曲线的基点

有了这些,我们就可以讨论椭圆曲线数字签名算法:ECDSA,以及v, r, s值是如何参与的

首先,当我们有了私钥,在以太坊上签署一笔交易,e(私钥)只有我们知道,然后我们拥有一个任何人都知道的,由私钥生成的公钥(账户地址)。当我们共享公钥时,我们可以肯定,只有我们知道私钥是安全的。

但是当我们创建这个v, r, s签名时,我们基本上要在我们的信息中传递,我们将用我们的私钥来签名,所以实际上必须有一些混淆的随机值,这使得攻击者无法观察到我们的v, r, s和签名来还原得到我们的私钥。

ECDSA 签名生成过程

生成 ECDSA 签名v, r, s的过程:

  • 首先选取一个随机数k($1 \leq k \leq n-1$)
  • 随机数k乘以基点G得到公钥K(点)
  • 该点的横坐标x为作为离线签名中的r
  • 通过公式$s=k^{−1}⋅(h(m)+r⋅d) \ mod \ n$计算离线签名中的s
    • $h(m)$:交易m的哈希值
    • $d$:私钥(对应上面的e
    • $k^{-1}$:k 关于模 n 的逆元
  • v 值(恢复标志)在以太坊中,v值有两个可能值2728
    • v 的值是 recid + 27,其中 recid 是恢复参数,有两个可能的值:01
    • v需要使用借助 js 或者 py 来进行计算

签名延展性攻击

实际上,椭圆曲线上有两个有效点,这两个点具有相同的r值。这是真正产生签名可塑性的原因。事实证明:攻击者实际上可以非常容易地计算出相应的 s 值,这将用于推出相反的临时公钥,并提供第二个有效签名,这个签名也会恢复到同一个签名者

下面的代码,我们将验证这种签名的可塑性

const EC = require('elliptic').ec;
const ec = new EC('secp256k1');
const ecparams = ec.curve
const BN = require('bn.js');
const n = new BN(ecparams.n);
const privateKey = BigInt("0x3d81b4e2161512c578f9961ccf6860651901816272148053824fa5d91a791155");
const publicKey = ec.keyFromPrivate(privateKey).getPublic();
const msg = 1n;
const sig = ec.keyFromPrivate(privateKey).sign(msg);
sig
ec.verify(msg, sig, publicKey); // 验证签名
const s = new BN(sig.s); 
const sig2 = { r: sig.r, s: n.sub(s) }; 
ec.verify(msg, sig2, publicKey); // 验证伪造签名

经过验证,第二个签名对消息也有效。

在上面的示例合约中,在一开始我们就可以看到v, r, s的组合,允许攻击者观察所有者的签名。并且,在合约中确实出现了双花问题。

攻击者传入自己篡改后的v, r, s版本,他将会恢复到相同的所有者地址,使得 owner 发生了双花问题。

解决办法

最广泛的解决方法就是限制s值的右半段,这样大于n/2s值会变成非法值。所以我们可以进行限制,只允许大于或小于n/2s

下面是 OpenZeppelin ECDSA 安全库中的一个函数,他限制了s的值必须大于n/2

    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address, RecoverError, bytes32) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS, s);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature, bytes32(0));
        }

        return (signer, RecoverError.NoError, bytes32(0));
    }
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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