本文深入探讨了智能合约中十大最关键的漏洞,包括重入攻击、整数溢出/下溢、不当访问控制、抢跑攻击、拒绝服务(DoS)攻击、弱随机性、易受攻击的外部调用、逻辑错误、Oracle操纵和闪电贷攻击。文章详细解释了每种漏洞的成因、常见的攻击方式,并提供了相应的缓解措施和代码示例,强调了安全审计、使用安全库以及遵循最佳实践的重要性。
智能合约可以自动化和保护区块链交易,而无需中介,这使得它们对于 Web3 和去中心化应用至关重要。然而,它们容易受到漏洞的影响。在 2024 年第一季度,智能合约漏洞导致了几乎 $45 million 的损失,共发生了 16 起事件,平均每次漏洞利用造成 280 万美元的损失。本文探讨了 10 个最关键的智能合约漏洞以及如何缓解它们。
智能合约漏洞可以分为几种类型,每种类型都会对区块链应用程序构成重大风险。 了解这些漏洞对于开发强大且安全的智能合约至关重要。
| 漏洞 | 描述 |
| 重入 (Reentrancy) | 利用合约的外部调用功能,允许在初始函数完成之前重复调用。 |
| 整数溢出/下溢 (Integer Overflow/Underflow) | 由算术运算超过数据类型的存储容量引起。 |
| 不正确的访问控制 (Improper Access Control) | 由于访问限制不足,允许未经授权的用户访问或修改合约数据或函数(例如,未受保护的提款)。 |
| 抢跑交易 (Front-Running) | 利用交易广播和将其包含在区块链中的时间差。 |
| 拒绝服务 (DoS) | 通过消耗所有可用 gas 或导致交易持续失败,使合约不可用或无响应。 |
| 弱随机性 (Weak Randomness) | 使用不安全的与区块相关的方法来生成随机数,这些随机数可以被操纵。 |
| 易受攻击的外部调用 (Vulnerable External Calls) | 与在没有适当验证的情况下进行外部调用相关的风险。 |
| 逻辑错误 (Logic Errors) | 包括合约逻辑中的缺陷,导致意外行为。 |
| 预言机操纵 (Oracle Manipulation) | 扭曲预言机价格或其他链下数据以窃取资产。 |
| 闪电贷攻击 (Flashloan Attacks) | 使用无抵押贷款来操纵市场或利用合约漏洞。 |
智能合约漏洞通常由常见的编码错误或逻辑错误引起。 未经检查的外部调用、不正确的验证和算术错误等问题可能导致漏洞。 让我们深入研究具体的漏洞以及它们的缓解措施。
当合约在更新其状态之前对另一个合约进行外部调用时,就会发生重入攻击。 然后,被调用的合约可以回调到原始合约,从而导致意外行为。
有几种类型的重入攻击:
contract Deposit {
mapping(address => uint) userBalance;
function deposit() external payable {
userBalance[msg.sender] = msg.value;
}
// 这个函数容易受到重入攻击
function withdraw() external {
require(userBalance[msg.sender] >= 0);
(bool sent,) = payable(msg.sender).call{value: userBalance[msg.sender]}("");
require(sent, "Failed to send Ether");
userBalance[msg.sender] = 0;
}
}
interface IDeposit {
function deposit() external payable;
function withdraw() external;
}
contract AttackDeposit {
IDeposit private depositContract;
constructor(address _target) {
depositContract = IDeposit(_target);
}
function attack() external payable {
require(msg.value == 1 ether, "Invalid attack amount");
depositContract.deposit{value: msg.value}();
depositContract.withdraw();
}
receive() external payable {
if(address(depositContract).balance >= 1 ether) {
depositContract.withdraw();
}
}
}
当我们向用户发送他们请求数量的以太币时,就会出现漏洞。 在这种情况下,攻击者会利用 withdraw() 函数。 因为他们的余额尚未重置为 0,所以他们可以转账 Token,即使已经收到了一些 Token。 该攻击涉及调用受害者合约中的 withdraw 函数。 收到 Token 后,receive 函数会无意中再次触发 withdraw 函数。 由于检查通过,合约会将 Token 发送给攻击者,随后激活 receive 函数。
Rari Capital 黑客事件(8000 万美元)
2022 年 4 月 30 日,去中心化借贷平台 Rari Capital 由于其从 Compound 借用的代码中的缺陷而遭到黑客攻击。 borrow 函数缺少适当的检查-效果-交互模式。 攻击者通过以下方式利用了这一点:
accountBorrows 映射之前转移了借入金额。 在没有重入保护的情况下,攻击者在映射更新之前重复调用 borrow 函数,耗尽了 8000 万美元。function borrow() external {
…
doTransferOut(borrower, borrowAmount);
// doTransferOut: function doTransferOut(borrower, amount) {
(bool success, ) = to.call.value(amount)("");
require(success, "doTransferOut failed");
}
// !!状态更新在转移后进行
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
…
}
黑客使用闪电贷借入资产,并在循环中运行 doTransferOut 函数五次。 在偿还闪电贷后,他们拿走了剩余的资金,并带着 8000 万美元消失了。 交易:0xab486012
Orion 协议(300 万美元)
2023 年 2 月 2 日,Orion 协议由于其核心合约之一中的重入漏洞而遭到黑客攻击,导致 $3 million loss。 攻击者利用 ExchangeWithOrionPool 合约的 depositAsset() 方法,该方法缺乏重入保护。 他们创建了一个具有自毁功能的虚假 Token (ATK),从而导致 transfer() 函数。
使用检查-效果-交互模式,以确保状态更改发生在外部调用之前。
存在漏洞的实现
mapping (address => uint) public balances;
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}
推荐的实现
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
}
一般来说,为了防止重入攻击,Web3 项目可以:
当算术运算超过数据类型的存储容量时,就会发生整数溢出和下溢,从而导致意外结果。
当值减小到零以下时,会发生下溢,而当值超过其最大值时,会发生溢出。 这些漏洞会导致智能合约中出现意外行为,可能导致财务损失或系统故障。
考虑一个 uint8 变量,它可以容纳最多 8 位。 这意味着它可以存储的最高数字在二进制中表示为 11111111(或在十进制中表示为 2^8 - 1 = 255)。 如果发生下溢,从设置为 0 的 uint8 中减去 1 将导致其值回绕到 255。相反,当尝试将 1 添加到设置为 255 的 uint8 时,会发生溢出,从而导致该值重置回 0。
contract MyContract {
uint256 public a = type(uint256).min; // uint256 的最小值,即 0
uint256 public b = type(uint256).max; // uint256 的最大值,即 2^256 - 1
function add() external {
// b == 115792089237316195423570985008687907853269984665640564039457584007913129639935
b = b + 1; // 这会导致整数溢出
// 递增后,b 回绕到 0
// b == 0
}
function substract() external {
// a == 0
a = a - 1; // 这会导致整数下溢
// 递减后,a 回绕到 uint256 的最大值
// a == 115792089237316195423570985008687907853269984665640564039457584007913129639935
}
}
Poolz Finance 黑客事件(39 万美元)
2023 年 3 月 15 日,由于未经审计的 LockedControl 智能合约中的整数溢出漏洞,Poolz Finance 合约遭到黑客攻击,导致至少 $390K 的损失,涉及 BSC 和 Polygon。 攻击者通过操纵 GetArraySum() 方法来利用溢出,该方法使总和超过其最大限制,从而允许他们将过多的 Token 提取到他们的钱包中。
PoWHC 黑客事件 (80 万美元)
弱手币 (PoWHC) 的证明(本身就是一个庞氏骗局)由于整数下溢漏洞而被利用,导致黑客窃取了 866 ETH。 ERC-20 实现的 "approve" 函数中的漏洞导致第二个帐户的余额被错误地调整,从而导致余额膨胀。 通过操纵 transferFrom() 和 transferTokens() 函数,攻击者导致第二个帐户的余额下溢到 2²⁵⁶-1,从而实现了盗窃。
随着 Solidity 0.8 的发布,这个问题已得到解决。 编译器现在会自动验证每个算术运算是否存在溢出和下溢,如果检测到,它会抛出一个错误。 这减轻了开发人员的负担,因为他们不再需要手动处理这些问题。
对于低于 0.8 版本的 Solidity,请使用 SafeMath 库来防止溢出和下溢。 该库提供了防止溢出和下溢漏洞的函数,从而确保智能合约中算术运算的完整性。
存在漏洞的实现
function transfer(address _to, uint256 _value) public {
balances[msg.sender] -= _value;
balances[_to] += _value;
}
推荐的实现
using SafeMath for uint256;
function transfer(address _to, uint256 _value) public {
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
}
访问控制漏洞允许未经授权的用户访问或修改合约的数据或函数。 当代码未能根据权限限制访问时,就会出现这些漏洞。 在智能合约中,访问控制问题会影响治理和关键功能,例如铸造 Token、投票、提取资金、暂停/升级合约以及更改所有权。
function mint(address account, uint256 amount) public {
// mint 函数未实现正确的访问控制
_mint(account, amount);
}
HospoWise 黑客事件
HospoWise 由于公共销毁函数而被黑客入侵,从而允许任何人销毁 Token。 该代码的销毁函数缺乏访问控制,从而使攻击者可以销毁 UniSwap 上的所有 Hospo Token,从而导致通货膨胀并耗尽 ETH 池。
预防措施:正确的访问控制,例如 onlyOwner,或使该函数成为 internal 类型。
Rubixy 黑客事件
由于构造函数命名错误,Rubixy 被利用。 函数 Fal1out 应该是构造函数,但任何人都可以调用它,从而允许攻击者声明所有权并耗尽资金。
预防措施:使用正确的构造函数语法和仔细的合约命名。
// 此代码尚未经过专业审核。 使用风险自负。
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControl {
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
address public oracle;
address public treasury;
constructor(address minter) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MANAGER_ROLE, minter);
}
function setOracleAddress(address _oracle) external onlyRole(MANAGER_ROLE) {
require(_oracle != address(0), "Invalid oracle address!");
oracle = _oracle;
}
function setTreasuryAddress(address _treasury) external onlyRole(ADMIN_ROLE) {
require(_treasury != address(0), "Invalid treasury address!");
treasury = _treasury;
}
}
在上面的代码中,RBAC 根据预定义的角色分配用户权限,从而提供精细的控制。 它支持多个角色,例如 onlyAdminRole 和 onlyModeratorRole,从而通过限制对必要功能的访问来增强安全性。 OpenZeppelin 的 AccessControl 合约简化了智能合约中 RBAC 的实现。
当恶意行为者通过提交具有更高 gas 费的类似交易来抢占交易时,就会发生抢跑交易。 自 2020 年 6 月以来,运营机器人的最大可提取价值交易者(又名 MEV 机器人)已在 Ethereum、BSC 和 Solana 上获得了超过 $1 billion 的利润,通常会损害散户投资者。
在竞标期结束后,使用提交-揭示方案来隐藏竞标细节。
存在漏洞的实现
function placeBid(uint256 _bid) public {
require(_bid > highestBid);
highestBid = _bid;
}
推荐的实现
function placeBid(bytes32 _sealedBid) public {
sealedBids[msg.sender] = _sealedBid;
}
function revealBid(uint256 _bid, bytes32 _secret) public {
require(sealedBids[msg.sender] == keccak256(abi.encodePacked(_bid, _secret)));
require(_bid > highestBid);
highestBid = _bid;
}
为了保护交换应用程序,请根据网络费用和交换规模在 0.1% 到 5% 之间实施滑点限制。 这会最大限度地减少滑点,从而防御利用更高利率的抢跑者,从而保护你的交易并降低掠夺性风险。
有关抢跑交易攻击的详细指南,并了解其他缓解策略,请参阅 区块链中的抢跑交易:真实案例和预防。
DoS 攻击可能会通过利用恢复、外部调用失败和 gas 限制问题来破坏合约功能,从而使合法用户无法使用。
当合约操作失败时,它会恢复更改。 EVM 使用 REVERT (0xFD) 和 INVALID (0xFE) 操作码来处理这些错误,其中 REVERT 将剩余的 gas 返回给调用者,而 INVALID 不返回任何 gas。
意外恢复导致的 DoS 示例
当合约尝试使用 call 方法发送 1 以太币时,会发生意外恢复。 如果接收方是在收到以太币时恢复的合约,则交易失败,从而阻止了受益人标志被重置。 如果相同的地址重复尝试提取,这可能会导致 DoS 状况,从而阻止进一步的提取。
function withdraw() public {
require(beneficiaries[msg.sender]);
beneficiaries[msg.sender] = false;
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success);
}
意外恢复导致的 DoS 缓解
使用 pull 而不是 push 付款模式来防止 DoS。 pull 模式将提取资金的责任转移到接收方,从而防止合约因转移失败而被锁定。 完全缓解的代码将存储待处理的提取,并允许用户声明它们。
function withdraw() public {
require(beneficiaries[msg.sender]);
beneficiaries[msg.sender] = false;
payable(msg.sender).transfer(1 ether);
}
外部调用可能会意外或故意失败,从而导致 DoS 状况。
外部调用失败缓解
让用户提取资金,而不是自动将资金 push 给他们。
大型数组或循环可能会超过区块 gas 限制,从而导致 DoS 状况。
Gas 限制漏洞缓解
由于以太坊的确定性性质,在其上生成随机数具有挑战性。 Solidity 依赖于伪随机因素,并且复杂的计算在 gas 方面成本高昂。
智能合约开发人员通常使用不安全的与区块相关的方法来生成随机数,例如当前区块时间戳、难度、编号、当前矿工的地址或给定区块的哈希。 但是,这些方法可能不安全,因为矿工可以操纵它们,从而影响合约的逻辑。
function guess(uint256 _guess) public {
uint256 answer = uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender)));
}
在没有适当验证的情况下进行外部调用(例如,未经检查的调用或对任意地址的调用)可能会导致意外行为或安全风险。
该函数不会验证调用的成功或失败。 即使外部调用失败,交易也会继续进行,这可能会导致意外行为或漏洞。
function externalCall(address _to) public {
(bool success, ) = _to.call("");
require(success);
}
经过检查的外部调用
调用已验证,并且已处理失败。
function externalCall(address _to) public {
require(isValidAddress(_to));
(bool success, ) = _to.call("");
require(success);
}
function isValidAddress(address _addr) internal pure returns (bool) {
return _addr != address(0);
}
对任意地址的调用是指与未预先确定或信任的外部地址的交互。 攻击者可以利用此缺陷来运行未经授权的代码、提取资产或破坏合约的功能。
Dexible 漏洞(200 万美元)
2023 年 2 月 20 日,Dexible DEX 聚合器和执行管理系统的自我交换功能因其外部调用漏洞而被利用,该漏洞允许用户定义路由合约。
contract Dexible {
function selfSwap(address tokenIn, address tokenOut, uint112 uint112 amount, address router, address spender, TokenAmount routeAmount, bytes routerData) external {
IERC20(routeAmount.token).safeApprove(spender, routeAmount.amount);
// 此处对路由器进行外部调用
(bool s, ) = router.call(routerData);
}
}
黑客没有使用安全有效的 DEX,而是制作了一个合约来调用恶意的 ERC20 合约,并耗尽了价值 200 万美元的 Token。 最关键的部分是合约没有经过审计。
contract maliciousRouter {
...
//黑客没有实现经过验证的路由合约,而是实现了这个棘手的函数,并使 Dexible 合约将其资产转移到这个恶意的合约。
function transfer() external {
IERC20(USDC).transferFrom(msg.sender, address(this), IERC20(USDC).balanceOf(msg.sender));
}
}
transfer() 和 send() 准确转发 2,300 gas,由于 gas 成本不断变化,这可能不足以满足接收方的需求。 请改用 .call() 并检查返回值。delegatecall 可能会导致状态更改和合约余额的潜在损失。智能合约中的逻辑错误会导致意外行为,从而危及安全性和功能。
该函数盲目地将提供的金额添加到发送者的余额中,而没有进行验证,从而导致潜在的问题,例如溢出和无效输入。
function updateBalance(int256 _amount) public {
balances[msg.sender] += _amount;
}
总体思路是实施彻底的测试和代码审查,以检测和修复逻辑错误。
例如,经过缓解的代码添加了一个验证检查,以确保在更新余额之前金额为正数,从而防止溢出和无效输入问题。
function updateBalance(int256 _amount) public {
require(_amount != 0, "Invalid amount");
balances[msg.sender] = balances[msg.sender].add(_amount);
}
预言机是区块链通往现实世界的门户。 它们将智能合约连接到链下数据(现实世界事件、价格摘要、随机数生成)。 但是,预言机操纵会通过欺骗、拉升、熊市袭击、跨市场操纵、虚假交易和抢跑交易等方法来显着扭曲市场价格。
Inverse Finance (1560 万美元)
攻击者使用 SushiSwap 的 TWAP 预言机操纵了 INV Token 的价格,通过存入膨胀的 INV Token 作为抵押品,借入了 1560 万美元。 依赖单一预言机是一个主要问题。
Lodestar (650 万美元)
一个不良行为者操纵了 plvGLP 抵押品的价格预言机,使他们能够耗尽贷款池并获利约 650 万美元。 核心漏洞在于 GLPOracle 如何计算 plsGLP 的价格。 攻击者通过捐赠函数增加总资产来操纵价格,这推高了价格,并允许攻击者借用比其抵押品的真实价值更多的资金。
BonqDAO (180 万美元)
由于智能合约代码错误,Polygon DeFi 协议 BonqDAO 成为了价格预言机攻击的受害者。 攻击者窃取了 1 亿个 $BEUR 稳定币和 1.2 亿个 $WALBT。 漏洞是由智能合约内部的价格摘要漏洞启用的,该智能合约使用 Tellor 预言机的 ALBT 价格向 Bonq 协议提供价格。
AaveV3(已阻止)
后备预言机中的漏洞允许攻击者设置任意资产价格,从而构成重大的安全风险。2024年5月16日,由于 Compound V2 forks 中一个已知的漏洞,Sonne Finance 被攻击,损失了 2000 万美元。尽管之前的事件发出了警告,但该协议未能实施全面的安全措施,使得攻击者能够操纵治理权限,并使用闪电贷耗尽资金。
了解更多关于 闪电贷攻击 & 预防 的信息。
安全模式对于开发健壮的智能合约至关重要。关键模式包括:
通过遵循这些最佳实践,开发人员可以构建一个强大的防御系统,从而加强其 智能合约的安全性:
01. **重用经过审计的库**:使用像 OpenZeppelin 这样成熟的库,并彻底阅读文档以理解和正确使用代码。
02. **测试你的代码**:目标是 100% 的分支覆盖率,使用多个用户测试,并使用测试 [模糊测试](https://hacken.io/discover/fuzzing-for-blockchain/)(例如,Foundry 框架)来检测错误。
03. **申请审计**:考虑专业的审计,例如 Hacken 的审计,以识别潜在的漏洞。
04. **使用 SafeERC20 库**:在处理 ERC20 代币时,使用 SafeERC20 库来处理可能失败的操作。
05. **使用 MultiSig 钱包**:MultiSig 钱包可用于合约所有权,以提高安全性并减少单点故障。
06. **使用锁定的 Pragma 版本**:锁定 Solidity 版本以避免已知的错误并确保稳定性(例如,使用 0.8.10 而不是 ^0.8.10)。
07. **小心地与第三方合约交互**:验证第三方合约是否存在错误、审计和正确的使用,以最大限度地减少外部调用。
08. **谨慎使用 Delegatecall**:理解 `delegatecall` 在调用合约的上下文中执行代码,从而保留 `msg.sender` 和 `msg.value`。
09. **使用 msg.sender 进行身份验证**:首选 `msg.sender` 而不是 `tx.origin`,以提高安全性并防止授权漏洞。
10. **避免算术溢出和下溢**:使用 0.8 以上的 Solidity 版本,或者为更早的版本使用 SafeMath 库。
11. **使用静态代码分析器**:使用像 Slither 这样的工具进行静态代码分析,以检测漏洞并提高代码质量。
12. **验证函数参数**:确保验证所有用户提供的参数,以防止恶意输入。
13. **注意整数除法舍入**:使用乘数来提高精度,或者单独存储分子和分母。
14. **显式标记可见性**:为了避免意外访问,请为函数和存储变量定义可见性。
15. **避免使用 extcodesize 检查 EOA。**`extcodesize` 在合约构建期间返回零,这可能会导致潜在的问题。
16. **限制对关键函数的访问**:实施访问控制以限制重要函数。
17. **使用紧急停止模式**:实施 Pausable 合约以在紧急情况下停止操作。
18. **首选拉取而不是推送模式**:使用拉取模式来安全地处理付款。
鉴于概述的广泛的潜在漏洞——从高亮显示的特定示例到我们的智能合约审计清单中确定的 30 多个额外问题——区块链领域的威胁形势是复杂且多方面的。智能合约开发人员在创建尖端应用程序和建立安全措施以保护这些合约免受不断演变的威胁方面起着关键作用。他们面临的挑战性任务是领先于这些风险,确保其应用程序的安全性、完整性和可信赖性。
Web3 项目必须优先考虑其区块链应用程序的安全性。通过遵守准则并主动解决漏洞,开发人员可以建立用户信任、保护资产,并为区块链生态系统的稳定和增长做出贡献。优先考虑安全保障个人项目并加强整个去中心化金融领域。让我们共同努力,为每个人创建一个更安全、更可靠的区块链环境。
- 原文链接: hacken.io/discover/smart...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!