WTF-Solidity 合约安全实战:S13 未检查的低级调用(Unchecked Low-Level Call)漏洞解析与修复
WTF-Solidity 合约安全实战S13 未检查的低级调用Unchecked Low-Level Call漏洞解析与修复【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本篇是 WTF-Solidity 合约安全系列的第十三讲聚焦以太坊低级调用call()、delegatecall()、staticcall()、send()在失败时只返回布尔值false、却不会回滚交易这一特性并完整演示它在银行合约中的真实利用路径与三种修复方案。读完本篇你将掌握低级调用返回值检查的底层原理、可复现的攻击合约与 Remix 演练步骤以及基于require检查、call()加重入保护、OpenZeppelinAddress库三种可靠的防御写法。低级调用失败不抛异常只返回falseSolidity 中的常规函数调用在发生异常时会沿调用链向上传播导致整个交易回滚。但以太坊的低级调用low-level call行为完全不同call()、delegatecall()、staticcall()和send()出现异常时不会把异常传递给上层也不会导致交易整体回滚只会返回一个布尔值false来传递失败信息。因此如果上层函数没有检查低级调用的返回值那么无论低级调用成功与否上层代码都会继续执行。这正是未检查的低级调用漏洞的根源。调用方式默认 gas失败行为调用语法send()2300返回false不抛异常addr.send(amount)transfer()2300抛出异常交易回滚addr.transfer(amount)call()可自定义转发全部 gas返回(bool, bytes)失败为falseaddr.call{value: amount}()其中最容易出错的是send()它硬性限制转发 gas 不得超过 2300否则调用失败。当目标地址的回调函数receive()/fallback()逻辑较复杂时消耗的 gas 会超过 2300导致send()失败。如果上层函数此时没有检查返回值交易会继续执行从而引发意想不到的结果。历史上2016 年的链游 King of Ether 正是因该漏洞导致退款无法正常发送。关于低级调用的更多细节call、delegatecall、staticcall的完整语义可延伸阅读仓库中的 20_SendETH、22_Call 与 23_Delegatecall 教程。漏洞合约UncheckedBank银行合约下面的漏洞合约是在 S01 重入攻击 教程银行合约的基础上修改而来完整代码见 UncheckedCall.sol。它包含 1 个状态变量balanceOf记录所有用户的以太坊余额和 3 个函数deposit()存款函数将ETH存入银行合约并更新用户余额withdraw()提款函数流程为查询余额 → 清零余额 → 转账。注意它没有检查send()的返回值——提款失败但余额会被清零getBalance()获取银行合约中的ETH余额。// SPDX-License-Identifier: MIT // by 0xAA pragma solidity ^0.8.34; contract UncheckedBank { mapping (address uint256) public balanceOf; // 余额mapping // 存入ether并更新余额 function deposit() external payable { balanceOf[msg.sender] msg.value; } // 提取msg.sender的全部ether function withdraw() external { // 获取余额 uint256 balance balanceOf[msg.sender]; require(balance 0, Insufficient balance); balanceOf[msg.sender] 0; // Unchecked low-level call bool success payable(msg.sender).send(balance); } // 获取银行合约的余额 function getBalance() external view returns (uint256) { return address(this).balance; } }问题出在withdraw()的第 56 行send()的返回值被赋值给了局部变量success但从未被使用。由于send()失败不抛异常withdraw()会看起来成功地结束——但用户的余额已经被提前清零钱却没有真正转到用户手里。攻击合约一个取款失败却余额清零的倒霉储户攻击合约刻画了一个无法接收ETH的储户它的回调函数receive()中直接revert()因此任何向其转账ETH的操作包括send()都会失败但它仍然可以正常调用bank.withdraw()来清空自己在银行中的余额记录。contract Attack { UncheckedBank public bank; // Bank合约地址 // 初始化Bank合约地址 constructor(UncheckedBank _bank) { bank _bank; } // 回调函数转账ETH时会失败 receive() external payable { revert(); } // 存款函数调用时 msg.value 设为存款数量 function deposit() external payable { bank.deposit{value: msg.value}(); } // 取款函数虽然调用成功但实际上取款失败 function withdraw() external payable { bank.withdraw(); } // 获取本合约的余额 function getBalance() external view returns (uint256) { return address(this).balance; } }攻击链路是攻击合约先通过deposit()正常存款 1 ETH银行记录balanceOf[Attack] 1 ETH再调用withdraw()触发银行提款。银行在执行payable(msg.sender).send(balance)时会调用攻击合约的receive()而receive()中revert()使转账失败send()返回false但银行合约并未检查该返回值于是余额被清零、交易照常完成——攻击合约丢掉了银行账面上的 1 ETH 存款银行却永远收不到这笔提款对应的真实转账在真实业务场景中这通常表现为用户资金被锁定或项目方向用户退款失败。Remix 复现步骤按照以下 5 步即可在 Remix IDE 中完整复现该漏洞部署UncheckedBank合约。部署Attack合约构造函数填入UncheckedBank合约地址。调用Attack合约的deposit()存款函数存入1 ETH。调用Attack合约的withdraw()提款函数调用显示成功不报错。分别调用UncheckedBank合约的balanceOf()与Attack合约的getBalance()尽管上一步调用成功且储户的银行余额被清零但攻击合约地址上实际并未收到ETH——提款实际上失败了。预防办法针对未检查低级调用漏洞有以下三种主流修复方案。1. 检查低级调用的返回值最直接的修复方式是对send()的返回值做require断言一旦转账失败整个提款交易立即回滚余额也不会被错误清零。将银行合约的withdraw()修正如下function withdraw() external { uint256 balance balanceOf[msg.sender]; require(balance 0, Insufficient balance); balanceOf[msg.sender] 0; bool success payable(msg.sender).send(balance); require(success, Failed Sending ETH!); }2. 使用call()并做好重入保护合约间转账ETH时优先使用call()而不是send()/transfer()因为call()会转发全部可用 gas可显式指定不会因为 2300 gas 上限导致意外失败但call()会完整触发目标合约的receive()/fallback()因此必须配合重入保护使用。推荐结合检查-影响-交互checks-effects-interactions模式先更新状态再交互或使用nonReentrant重入锁。这两类防护的完整代码示例见 S01_ReentrancyAttack/ReentrancyAttack.sol其中的GoodBank与ProtectedBank分别是两种写法的可运行版本。3. 使用 OpenZeppelinAddress库OpenZeppelin 的Address库封装了检查返回值的低级调用开发者无需手写require(success)逻辑。仓库内置的 lib/openzeppelin-contracts/contracts/utils/Address.sol 中sendValue()的实现可以印证其防御思路function sendValue(address payable recipient, uint256 amount) internal { if (address(this).balance amount) { revert Errors.InsufficientBalance(address(this).balance, amount); } if (LowLevelCall.callNoReturn(recipient, amount, )) { // call successful, nothing to do return; } else if (LowLevelCall.returnDataSize() 0) { LowLevelCall.bubbleRevert(); } else { revert Errors.FailedCall(); } }从源码可以看到sendValue()先用callNoReturn即底层call转账并检查其返回值失败时若携带 revert 原因则向上冒泡bubbleRevert()否则统一以FailedCall错误回滚从而保证转账失败必然回滚从根本上杜绝未检查低级调用的问题。这正是对检查返回值这一原则的工程化封装。总结这一讲介绍了未检查低级调用的漏洞及其预防方法。核心要点以太坊低级调用call、delegatecall、staticcall、send失败时返回布尔值false但不会导致整个交易回滚开发者一旦漏掉对其返回值的检查上层代码就会在调用失败后继续执行产生资金冻结、账目错乱等严重后果。防御手段包括逐处检查返回值、改用call()并配合重入防护、或直接复用 OpenZeppelinAddress库的封装实现。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

软件行业技术趋势预判:AI 原生、云原生、轻量化架构

软件行业技术趋势预判:AI 原生、云原生、轻量化架构

站在2026年产业迭代的关键节点软件行业, 正告别局部优化单点迭代传统发展模式, 进入全栈范式建构、底层架构式革新、技术价值质变全新周期呀。过去十年, 云计算普及、微服务落地完成、实现了软件基础设施的初步现代化;而当下, 生成式AI深度渗透、企业数字化降本增效…

2026/11/3 15:03:32 阅读更多 →
NEWTON物理引擎关节系统详解与应用实践

NEWTON物理引擎关节系统详解与应用实践

1. NEWTON物理引擎中的关节系统概述在物理引擎的世界里,关节(Joint)就像人体骨骼系统中的连接点,它定义了刚体之间的约束关系和行为规则。NEWTON作为一款高性能物理引擎,其关节系统设计兼顾了计算效率与物理准确性。与…

2026/11/2 9:21:35 阅读更多 →
SSH人事管理系统源码解析:从环境搭建到迁移改造

SSH人事管理系统源码解析:从环境搭建到迁移改造

简介:这是一套面向企业人事管理场景的Java Web完整项目,基于Struts2、Spring、Hibernate和MySQL开发,适合Java Web学习者、毕业设计者及需要快速搭建人事系统的开发者参考。系统覆盖员工档案、部门信息、职位变动、工资信息、福利计算、培训管…

2026/10/25 19:36:29 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/3 1:09:25 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/3 10:57:27 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/3 11:35:49 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/3 2:27:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/3 10:47:27 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/3 10:57:33 阅读更多 →

日新闻

周新闻

月新闻