区块链智能合约(Soilidtiy)攻击--已知攻击类型

一.Reentrancy重入攻击

调用外部合约的主要危险之一是它们可以接管控制流,并对调用函数不期望的数据进行更改。此类错误可以有多种形式,导致 DAO 崩溃的两个主要错误都是此类错误

1.单个函数的可重入性

错误例子:

要注意的这个错误的第一个版本涉及在函数的第一次调用完成之前可以重复调用的函数。这可能会导致函数的不同调用以破坏性的方式进行交互。

// INSECUREmapping (address => uint) private userBalances;

function withdrawBalance() public {

   uint amountToWithdraw = userBalances[msg.sender];

    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call withdrawBalance again

    require(success);

    userBalances[msg.sender] = 0;}

由于直到函数结束时用户的余额才设置为 0,因此第二次(以及以后的)调用仍然会成功,并且会一遍又一遍地提取余额。 

更改后的例子:

解决办法:在给出的示例中,防止这种攻击的最佳方法是确保在完成所有需要执行的内部工作之前不要调用外部函数   

mapping (address => uint) private userBalances;

function withdrawBalance() public {

    uint amountToWithdraw = userBalances[msg.sender];

    userBalances[msg.sender] = 0;

    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // The user's balance is already 0, so future invocations won't withdraw anything

require(success);

}

请注意,如果您有另一个调用 的函数withdrawBalance(),它可能会受到相同的攻击,因此您必须将调用不受信任的合约的任何函数视为自身不受信任。有关潜在解决方案的进一步讨论,请参见下文。

 2.Cross-function Reentrancy跨功能重入

攻击者也可以使用共享相同状态的两个不同函数进行类似的攻击

// INSECURE

mapping (address => uint) private userBalances;

function transfer(address to, uint amount) {

    if (userBalances[msg.sender] >= amount) {

       userBalances[to] += amount;

       userBalances[msg.sender] -= amount;

    }}

function withdrawBalance() public {

    uint amountToWithdraw = userBalances[msg.sender];

    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call transfer()

    require(success);

    userBalances[msg.sender] = 0;}

在这种情况下,攻击者transfer()会在外部调用中执行其代码时调用withdrawBalance。由于他们的余额尚未设置为 0,因此即使他们已经收到提款,他们也可以转移代币。该漏洞也被用于DAO攻击。

相同的解决方案将起作用,但有相同的警告。还要注意,在这个例子中,两个函数都是同一个合约的一部分。但是,如果多个合约共享状态,则相同的错误可能会发生在多个合约中。

3.重入解决方案中的陷阱

由于重入可能发生在多个函数甚至多个合约中,任何旨在防止单个函数重入的解决方案都是不够的。

相反,我们建议先完成所有内部工作(即状态更改),然后再调用外部函数。如果仔细遵循此规则,则可以避免由于可重入而导致的漏洞。但是,不仅要避免过早调用外部函数,还要避免调用调用外部函数的函数。例如,以下是不安全的

// INSECURE

mapping (address => uint) private userBalances;

mapping (address => bool) private claimedBonus;

mapping (address => uint) private rewardsForA;

function withdrawReward(address recipient) public {

    uint amountToWithdraw = rewardsForA[recipient];

    rewardsForA[recipient] = 0;

    (bool success, ) = recipient.call.value(amountToWithdraw)("");

require(success);

}

function getFirstWithdrawalBonus(address recipient) public {

    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once

    rewardsForA[recipient] += 100;

    withdrawReward(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.

claimedBonus[recipient] = true;

}

即使getFirstWithdrawalBonus()不直接调用外部合约,调用withdrawReward()也足以使其容易受到重入的影响。因此,您需要将其withdrawReward()视为不受信任。

mapping (address => uint) private userBalances;

mapping (address => bool) private claimedBonus;

mapping (address => uint) private rewardsForA;

function untrustedWithdrawReward(address recipient) public {

    uint amountToWithdraw = rewardsForA[recipient];

    rewardsForA[recipient] = 0;

    (bool success, ) = recipient.call.value(amountToWithdraw)("");

    require(success);}

function untrustedGetFirstWithdrawalBonus(address recipient) public {

    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once



    claimedBonus[recipient] = true;

    rewardsForA[recipient] += 100;

    untrustedWithdrawReward(recipient); // claimedBonus has been set to true, so reentry is impossible}

除了无法重新进入的修复之外,不受信任的功能已被标记为。在各个层面同样的模式重复:因为untrustedGetFirstWithdrawalBonus() calls untrustedWithdrawReward(),要求外部合同,你也必须把untrustedGetFirstWithdrawalBonus()为不安全。

通常建议的另一个解决方案是互斥锁。这允许您“锁定”某些状态,因此它只能由锁的所有者更改。一个简单的例子可能如下所示:

// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state

mapping (address => uint) private balances;

bool private lockBalances;

function deposit() payable public returns (bool) {

    require(!lockBalances);

    lockBalances = true;

    balances[msg.sender] += msg.value;

    lockBalances = false;

    return true;}

function withdraw(uint amount) payable public returns (bool) {

    require(!lockBalances && amount > 0 && balances[msg.sender] >= amount);

    lockBalances = true;



    (bool success, ) = msg.sender.call(amount)("");



    if (success) { // Normally insecure, but the mutex saves it

      balances[msg.sender] -= amount;

    }



    lockBalances = false;

    return true;}

如果用户withdraw()在第一次调用结束前再次尝试调用,锁将阻止它产生任何影响。这可能是一种有效的模式,但是当您有多个需要合作的合约时,它会变得棘手。以下是不安全的

// INSECURE

contract StateHolder {

    uint private n;

    address private lockHolder;



    function getLock() {

        require(lockHolder == address(0));

        lockHolder = msg.sender;

    }



    function releaseLock() {

        require(msg.sender == lockHolder);

        lockHolder = address(0);

    }



    function set(uint newState) {

        require(msg.sender == lockHolder);

        n = newState;

    }}

攻击者可以调用getLock(),然后永远不会调用releaseLock()。如果他们这样做,那么合约将被永久锁定,并且无法进行进一步的更改。如果您使用互斥锁来防止重入,您将需要仔细确保没有任何方法可以声明锁并且永不释放。(在使用互斥锁编程时还有其他潜在的危险,例如死锁和活锁。如果您决定走这条路,您应该查阅已经写在互斥锁上的大量文献。)

以上是涉及攻击者在单个事务中执行恶意代码的可重入示例。以下是区块链固有的一种不同类型的攻击:交易本身(例如,在一个区块内)的顺序很容易受到操纵这一事实。

二.Front-Running(抢先交易攻击)

由于所有交易在执行之前都在内存池中短暂可见,因此网络的观察者可以在将其包含在块中之前看到并对其做出反应。如何利用这一点的一个例子是去中心化交易所,在那里可以看到购买订单交易,并且可以在包含第一笔交易之前广播和执行第二笔订单。防止这种情况很困难,因为这将归结为具体的合同本身

领先,最初是为传统金融市场创造的,是为了赢家的利益而整理混乱的竞赛。在金融市场中,信息流动催生了中介机构,它们可以通过第一个知道某些信息并对某些信息做出反应来获利。这些攻击主要发生在股票市场交易和早期域名注册中,例如 whois 网关

分类

通过定义分类法并将每个组与另一个组区分开来,我们可以更轻松地讨论问题并为每个组找到解决方案。

我们定义了以下类别的抢先攻击:

  1. Displacement移位

在第一种类型的攻击中,置换攻击,Alice(用户)的函数调用在 Mallory(对手)运行她的函数之后运行并不重要。爱丽丝的可以成为孤儿或运行没有任何有意义的影响。置换的例子包括: * Alice 试图注册一个域名,Mallory 首先注册它;* Alice 试图提交一个 bug 以获得赏金,而 Mallory 窃取它并首先提交它;* 爱丽丝试图在拍卖中出价,而马洛里抄袭。

这种攻击通常通过增加gasPrice高于网络平均值的倍数来执行,通常乘数为 10 或更多。

  1. Insertion插入

对于这种类型的攻击,原始函数调用在她的交易之后运行对对手来说很重要。在插入攻击中,Mallory 运行她的函数后,合约的状态发生了变化,她需要 Alice 的原始函数在这个修改后的状态上运行。例如,如果 Alice 以高于最佳报价的价格对区块链资产下订单,Mallory 将插入两笔交易:她将以最佳报价购买,然后以 Alice 稍高的购买价格出售相同的资产. 如果 Alice 的交易随后被执行,Mallory 将在无需持有资产的情况下从差价中获利。

与置换攻击一样,这通常是通过在 gas 价格拍卖中出价高于 Alice 的交易来完成的。

  1. Suppression抑制

在抑制攻击中,也就是Block Stuffing攻击,在 Mallory 运行她的函数后,她试图延迟 Alice 运行她的函数。

“Fomo3d”游戏的第一个获胜者和其他一些链上黑客就是这种情况。攻击者发送多个交易具有高gasPricegasLimit 定制智能合同是断言(或使用其他方式),以消耗所有的气体,并填补了块的gasLimit

这些攻击中的每一种都有两种变体,非对称和批量。

在某些情况下,Alice 和 Mallory 正在执行不同的操作。例如,Alice 试图取消一个要约,而 Mallory 则试图先完成它。我们称之为不对称位移。在其他情况下,Mallory 试图运行大量功能:例如 Alice 和其他人试图购买由一家公司在区块链上提供的一组有限的股票。我们称之为大容量位移。

三.时间戳依赖()

请注意,矿工可以操纵区块的时间戳,应考虑所有直接和间接使用时间戳的情况。

四.整数上溢和下溢

考虑一个简单的代币转移:

mapping (address => uint256) public balanceOf;

// INSECUREfunction transfer(address _to, uint256 _value) {

    /* Check if sender has balance */

    require(balanceOf[msg.sender] >= _value);

    /* Add and subtract new balances */

    balanceOf[msg.sender] -= _value;

    balanceOf[_to] += _value;}

// SECUREfunction transfer(address _to, uint256 _value) {

    /* Check if sender has balance and for overflows */

    require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);



    /* Add and subtract new balances */

    balanceOf[msg.sender] -= _value;

    balanceOf[_to] += _value;}

如果余额达到最大 uint 值 (2^256),它将循环回零以检查条件。这可能相关也可能不相关,具体取决于实现。想一想这个uint值有没有机会逼近这么大的数字。考虑uint变量如何更改状态,以及谁有权进行此类更改。如果任何用户都可以调用更新uint值的函数,则更容易受到攻击。如果只有管理员有权更改变量的状态,那么您可能是安全的。如果用户一次只能增加 1,你可能也是安全的,因为没有可行的方法来达到这个限制。

下溢也是如此。如果 uint 小于零,它将导致下溢并设置为其最大值。

小心较小的数据类型,如 uint8、uint16、uint24...等:它们更容易达到最大值。

猜你喜欢

转载自blog.csdn.net/qq_33842966/article/details/122137572
今日推荐