智能合约攻击和漏洞百科全书 Attacks----Reentrancy

        重入(Reentrancy)是一种攻击,当合约函数中的错误允许函数交互在本应被禁止的情况下进行多次时,就会发生这种攻击。 如果恶意使用,这可用于从智能合约中抽取资金。 事实上,重入是 DAO 黑客攻击中使用的攻击向量。

Single-function reentrancy

当易受攻击的函数与攻击者试图递归调用的函数相同时,就会发生单函数重入攻击。

// INSECURE
function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(msg.sender.call.value(amount)());
    balances[msg.sender] = 0;
}

代码的功能是允许用户从其账户余额中提取以太币。具体执行过程如下:

  1. 代码中的第一行声明了一个名为 amount 的变量,其值等于调用者(msg.sender)在合约中的余额(balances[msg.sender])。
  2. 接下来的一行代码使用了一个 require 语句,用于判断以下条件是否成立:msg.sender.call.value(amount)()call.value(amount)() 这一表达式的作用是尝试向 msg.sender 发送 amount 数量的以太币,并返回一个布尔值表示是否发送成功。
  3. 如果 require 语句的条件不满足(即无法成功发送以太币),则会抛出异常并中止函数执行。
  4. 如果 require 语句的条件满足(即成功发送以太币),则继续执行下一行代码。
  5. 下一行代码将调用者的余额(balances[msg.sender])设置为0,即清空调用者的余额。

需要注意的是,这段代码存在安全问题。问题在于使用了 call.value(amount)() 这种直接发送以太币的方式。这种方式可能会导致以下安全风险:

        调用者可能是一个恶意合约,通过利用递归攻击(Reentrancy attack)来反复调用 withdraw 函数,从而重复提取以太币,造成经济损失。

        如果 call.value(amount)() 失败(例如,调用者没有足够的以太币来接收),则会导致整个函数的执行被中止,无法回滚之前对 balances 的更改,从而可能导致资金损失。

        在这里,我们可以看到余额仅在资金转移后才被修改。 这可以让黑客在余额设置为 0 之前多次调用该函数,从而有效地耗尽智能合约。

Cross-function reentrancy

跨功能重入攻击(Cross-function reentrancy)是同一进程的更复杂版本。 当易受攻击的函数与攻击者可以利用的函数共享状态时,就会发生跨函数重入。

在下图代码中:

// INSECURE
function transfer(address to, uint amount) external {
  if (balances[msg.sender] >= amount) {
    balances[to] += amount;
    balances[msg.sender] -= amount;
  }
}

function withdraw() external {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}

        这段代码存在某些安全问题:

  1. 没有权限控制:任何人都可以调用transfer函数和withdraw函数,这可能导致未经授权的转账和提取操作。
  2. 缺乏检查和断言:在transfer函数中,没有对地址参数进行有效性检查,也没有对代币数量进行检查。这可能导致无效地址的转账和转账超过余额的情况。
  3. 使用call函数:在withdraw函数中,使用了call函数来发送以太币。这种方式可能存在安全风险,因为调用者的合约可能有不可预测的行为。

        要提高合约的安全性,应该考虑添加权限控制、进行有效性检查和断言,并使用更安全的方式处理以太币的发送。下面是对代码的一定改进,仅供参考:

address owner;

constructor() {
    owner = msg.sender;
}

modifier onlyOwner() {
    require(msg.sender == owner, "Only the contract owner can call this function.");
    _;
}

function transfer(address to, uint amount) external onlyOwner {
    require(to != address(0), "Invalid address.");
    require(amount > 0, "Invalid amount.");

    if (balances[msg.sender] >= amount) {
        balances[to] += amount;
        balances[msg.sender] -= amount;
    }
}

function withdraw() external onlyOwner {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "Insufficient balance.");

    balances[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}

        在此示例1中,黑客可以通过在 withdraw() 函数中将余额设置为 0 之前调用 transfer() 的回退函数来转移花费的资金来利用此合约。但是在示例2中,可以明显改善这种问题。

Reentrancy prevention

        在智能合约中转移资金时,请使用 send 或 transfer 而不是 call。 使用 call 的问题与其他函数不同,它没有 2300 的 gas 限制。这意味着 call 可以用于外部函数调用,可用于执行重入攻击。

        另一种可靠的预防方法是标记不受信任的功能。如下图所示:

function untrustedWithdraw() public {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}

        此外,为了获得最佳安全性,请使用checks-effects-interactions模式。 这是订购智能合约功能的简单经验法则。该函数应以检查开始——例如,require 和 assert 语句。接下来,应该执行合约的效果——例如,状态修改。最后,我们可以与其他智能合约进行交互——例如,外部函数调用。

        这种结构可以有效防止重入,因为合约的修改状态将防止不良行为者执行恶意交互。

        由于在执行任何交互之前余额设置为 0,如果递归调用合约,则在第一笔交易之后没有任何内容可发送。如下示例代码所示:

        

function withdraw() external {
  uint256 amount = balances[msg.sender];
  balances[msg.sender] = 0;
  require(msg.sender.call.value(amount)());
}

猜你喜欢

转载自blog.csdn.net/ljh1528207303/article/details/130923530