拓冰建站拓冰建站
首页 / 资讯中心 / 正文

重入攻击的起源:TheDAO事件如何改写智能合约安全史

1. TheDAO是什么为什么说它是区块链历史上绕不开的坐标点如果你刚接触以太坊可能对TheDAO这个名字有点陌生。但如果你翻过任何一本讲以太坊历史的书或者看过任何一届区块链安全峰会的KeynoteTheDAO几乎一定会被提及。它不是一个项目而是一整段历史的分水岭。1.1 一个大到必须被攻击的去中心化组织2016年4月底一个叫TheDAO的去中心化自治组织在以太坊主网上线。它的核心机制非常简单所有参与者往一个智能合约里存入ETH换取DAO Token然后用这些Token对项目提案进行投票决定资金投向。这个机制在今天看来很常见但在当时是真正意义上的代码自治——没有CEO没有董事会没有托管方一切资金由合约代码控制。让我把时间线拉出来看你就知道它当时有多火爆时间节点事件数据2016年4月30日TheDAO众筹启动28天窗口期2016年5月28日众筹结束锁定了约1150万ETH当时估值按ETH市价折算约1.5亿美元占比占当时ETH总流通量约14%注意最后一行的数据这是一个荒谬的比例。一个单一合约锁定了全网14%的以太坊总量。这意味着什么意味着任何针对这个合约的攻击都会直接冲击整个以太坊网络的价值体系。也意味着如果有漏洞攻击者的动机和经济激励都是空前的。后来的故事大家都知道了2016年6月17日TheDAO合约被利用重入漏洞持续抽取资金累计约360万ETH被分离到攻击者控制的子DAO中。这直接导致了以太坊在7月20日执行了一次颇具争议的硬分叉。这次分叉产生了两条链一条就是我们今天说的以太坊ETH另一条变成了Ethereum ClassicETC。1.2 代码即法律的理想在现实面前摔了一跤TheDAO背后的哲学很纯粹叫Code is Law——代码即法律。意思是既然规则写在智能合约里那么智能合约的执行就是最终的、不可推翻的。这种思想的源头可以追溯到加密朋克文化里对中心化权威的天然不信任。但是TheDAO事件用最残酷的方式证明了代码即法律这句话在实践中有个大前提——代码必须是对的。一旦代码本身有漏洞那么法律本身就成了攻击者的武器而受害者反而找不到任何申诉途径。我常说TheDAO是区块链行业第一次真正意义上的系统级安全事件。它不是某一个交易所被黑也不是某个钱包私钥泄露而是整个生态的基础设施层出了问题。当时圈内讨论最热烈的一个问题是如果一条链的核心合约都能被轻易掏空这条链还值得信任吗说实话我当年也是一个相信代码即法律的理想主义者但TheDAO事件之后我彻底改变了。代码确实是法律但法律也有bug而修bug的过程本身就充满了社会学博弈。2. 重入攻击的完整技术拆解splitDAO漏洞是如何被一步步引爆的现在进入正题。TheDAO事件最核心的技术元凶就是重入攻击。这个漏洞在今天已经被讲烂了但TheDAO是它的第一次大规模实战演示。要理解这个漏洞你得先理解DAO合约的资金流转模型。2.1 先看懂DAO合约的资金流转模型TheDAO不是一个单一的合约而是一套合约组合。最核心的是DAO.sol和Token.solDAOToken。Token.sol就是标准的ERC-20代币负责记账和转账DAO.sol则负责资金的募集、提案投票、奖励分配和拆分。整个资金流转逻辑可以简化成几步用户向DAO合约发送ETH合约记录该用户的余额用户获得DAO Token作为凭证当有人提交提案并通过投票后DAO合约向被资助方转出ETH用户可以选择拆分split把自己的资金从主DAO中分离出来创建新的子DAO。听起来很合理对吧问题出在第4步的拆分逻辑上。2.2 漏洞核心那个先发钱再记账的函数TheDAO的漏洞集中在splitDAO函数。我找到了当时被广泛分析的简化版本逻辑核心流程是这样的调用者调用splitDAO传入想要拆分的数额合约计算出调用者应得的拆分余额合约给调用者铸造对应的DAO Token作为新子DAO的凭证合约调用withdrawRewardFor函数把调用者之前积累的奖励ETH转账给调用者合约最后才更新调用者的余额状态。看到问题了吗步骤4和步骤5的顺序。合约先把ETH转出去了然后才更新余额。而以太坊的转账一旦触发接收方合约的receive回调攻击者就完全可以在回调里再次调用splitDAO因为此时合约认为自己还没处理完余额也还没扣减。我来给一个概念等价的重入漏洞代码示例用现代Solidity语法重写方便你理解当时的攻击逻辑// 简化版演示展示TheDAO splitDAO的重入利用逻辑 contract VulnerableDAO { mapping(address uint256) public balances; bool internal locked; function withdraw() external { // TheDAO的问题在于先转账后更新状态 uint256 amount balances[msg.sender]; (bool success, ) msg.sender.call{value: amount}(); require(success, Transfer failed); // 状态更新发生在转账之后 balances[msg.sender] 0; } // 攻击者合约 receive() external payable { if (address(this).balance target.balances(address(this))) { target.withdraw(); } } }这段代码是高度简化的但重入的链条跟TheDAO当时是一模一样的。攻击者先调用withdraw()合约把ETH转给攻击者此时触发攻击者的receive回调。在回调里攻击者再次调用withdraw()而合约的balances还没归零所以又能转出一笔。如此循环直到攻击者的递归终止条件被触发。TheDAO的实际情况比这个复杂因为它的漏洞散布在splitDAO、withdrawRewardFor、以及一个叫rewardToken的转账逻辑中。攻击者甚至做了多层的函数跳转来最大化资金的抽取效率。但底层的原理就是这种先转钱、后记账的时序缺陷。2.3 攻击者的聪明手法与gas机制扮演的角色这里有个特别有意思的细节值得多聊两句攻击者是怎么做到持续抽资而不被中途打断的以太坊上的每一笔交易都有gas上限。如果你在一个交易里无限递归迟早会把gas耗尽然后整个交易回滚。TheDAO的攻击者为什么能抽走那么多ETH答案是攻击者并不是在一笔交易里完成所有操作的。他是在一笔交易里通过重入调用splitDAO每次拆分都把自己的一部分ETH分离到子DAO里。而这些拆分操作本身是合理操作在合约设计逻辑里是允许的所以gas消耗被控制在了可接受的范围内。等到社区发现异常的时候攻击交易已经持续被矿工打包了。此时社区面临一个尴尬局面合约的代码还在按设计运行攻击者的操作在代码上没有违规唯一的违规是他在利用代码的逻辑漏洞套利。更麻烦的是攻击者还留了一个后手——他把一部分资金分散到了多个子DAO中增加了追踪和回收的难度。当时还有人调侃TheDAO攻击者的代码水平比TheDAO自己的开发团队还要高。关于gas机制我要多说一句。在TheDAO攻击中攻击者反复调用splitDAO每次调用都消耗gas但因为每笔交易都有独立的gas池所以只要单笔交易内的操作不超限就可以持续执行。如果你的攻击逻辑里有一个无限循环那就真的会把gas烧光。攻击者显然对这个约束非常清楚所以他的攻击代码设计得相当克制——每一步都是有明确产出和目的的函数调用而不是无脑的while循环。3. 从发现攻击到硬分叉那七天的博弈全过程2023年之后再看TheDAO事件人们往往只记住了发生了攻击和然后硬分叉了两个结论。但中间的七天是整个事件最精彩的博弈过程。这七天里的每一个决策都值得后来的从业者反复咀嚼。3.1 第一批发现攻击的人做了什么2016年6月17日TheDAO的攻击交易开始出现在链上。最先注意到异常的是几位在社区里做合约监控的开发者他们发现有一笔交易在反复调用splitDAO而且调用模式完全不符合正常人拆分的逻辑——每次拆分都是同一个地址间隔时间极短像是程序在自动执行。这里我要强调一个细节当时以太坊没有今天这么成熟的监控工具Blockscout、Tenderly之类的工具都不存在大家只能靠Etherscan当时还叫EtherScan手动查看交易历史。第一批发现攻击的人是肉眼在浏览器里一条一条交易翻出来的。发现之后社区的第一反应不是分叉而是试图联系矿工请求他们暂时停止处理攻击者的交易。但这在公链上是行不通的——矿工挖矿的动力是手续费攻击者给的gas费又不低你凭什么让矿工放弃合法收入还有一部分开发者尝试直接向合约发送交易抢在攻击者之前调用合约的函数试图通过挤出攻击交易来减缓资金流出。但公链的交易是并行的你发一笔交易矿工不一定会立刻打包攻击者也在持续发根本拦不住。最后真正起到作用的临门一脚是有人发现TheDAO合约里存在一个叫splitDAO的分离功能而攻击者的核心操作就是通过这个功能反复拆分。社区通过修改以太坊客户端的代码让所有包含攻击交易的区块被其他节点拒绝等于把攻击交易冻结在待处理池里不再被打包。这是当时能想到最快的止血方案也是软分叉的雏形。3.2 软分叉为什么失败一次计划外的降级测试具体来说软分叉的思路是客户端节点升级后任何区块只要包含攻击者的地址发起的交易该区块就会被认定为无效。这样攻击者即使连续发交易也无法被正式确认。听起来很完美对吧但现实很快就给了当头一棒。软分叉方案的争议在于它本质上是对交易历史的篡改违背了区块链不可篡改的基本原则。社区里有一批人坚决反对认为这种如果交易不符合我们的利益就不让它上链的做法会让区块链退化成中心化数据库。更大的技术问题出现在7月初的实施过程中。2016年7月初以太坊官方发布了软分叉客户端但版本中却引入了一个严重的bug这个bug会导致大量正常交易的处理出现问题。原本就高度紧张的网络在软分叉升级后出现了性能问题交易确认速度大幅下降。我当时看链上的情况真的有一种雪上加霜的感觉。攻击还没完全挡住结果修复工具自己又带了新bug整个网络变得不稳定社区情绪几乎到了崩溃边缘。3.3 硬分叉决策与代币回收的技术实现软分叉失败之后以太坊社区被迫面对一个更激进的方案硬分叉——从区块高度1920000开始把所有TheDAO合约里的ETH强制转移到一个新的恢复合约中所有参与过TheDAO众筹的用户可以从恢复合约中按比例退回自己的ETH。这个方案的技术实现其实不复杂区块链的共识就是节点共同认可的规则。只要大部分矿工和节点都运行新版本客户端那么从某个区块高度开始所有对旧规则的违反都视为不合法。具体到TheDAO硬分叉的目标不是回滚历史而是原历史保持不动但从分叉点开始资金规则被改写。执行层面做的事情简单说就是在区块1920000处强制修改DAO合约的资金余额记录将DAO合约中的ETH映射到一个新的退款合约地址原TheDAO参与者可以通过自己的DAO Token向退款合约索取对应ETH。这波操作在技术上让以太坊成功满血复活但代价是一部分社区成员拒绝接受这种主动干预他们坚持认为被攻击是命运的一部分智能合约就应该严格按照代码执行。于是他们继续跑在旧链上这个链就是后来的ETC。对于我个人来说硬分叉的技术实现让我见识到了区块链的治理弹性——只要社区达成共识代码甚至可以被覆盖。但ETC的出现也提醒我这种弹性的边界在哪里永远是一个社会学问题不是纯技术问题。4. 分叉之后ETH与ETC的分裂以及它给整个行业留下的安全遗产TheDAO事件没有随着硬分叉而终结。恰恰相反它的一整套余波在整个区块链行业里持续震荡了很多年直到今天你依然能在各种智能合约漏洞、各种治理辩论中看到TheDAO的影子。4.1 区块1920000一次不可逆的社区选择2016年7月20日零点30分左右以太坊在区块高度1920000执行硬分叉。分叉的那一刻链上数据被一分为二新链ETH上TheDAO合约里的ETH被全部转移到了恢复合约旧链ETC上攻击者依然控制着那笔资金。分叉之后的一段时间两条链上的币价都在波动。ETH凭借社区与开发团队的主流认可逐渐走高而ETC则被一部分代码即法律的原教旨主义者视为真正的以太坊。直到今天每当我们讨论区块链治理的时候ETH和ETC的这对双胞胎依然是用来解释共识分歧最直观的案例。我印象很深的一点是硬分叉后很多普通用户其实并不关心技术原理他们只关心我的ETH现在还安全吗。但对于开发者来说这次分叉暴露了一个问题——区块链的安全性并不仅仅取决于代码和密码学还取决于社区共识的稳定性。你写了一个自认为无懈可击的合约但链上规则可以被社区投票改写那你的合约其实并没有绝对的安全性。4.2 重入漏洞如何改变Solidity开发范式如果说TheDAO事件对区块链技术生态最直接的贡献那就是让Solidity开发者第一次认识到了重入攻击这个词汇并且开始研究防御手段。在2016年之前Solidity的文档里几乎没有针对重入攻击的安全指南OpenZeppelin也还没有推出ReentrancyGuard。TheDAO之后整个开发社区迅速形成了几条默认的安全准则安全准则核心思想解决什么问题Checks-Effects-Interactions先校验条件、再修改状态、最后才做外部调用避免状态更新被外部调用提前触发重入锁用一个bool变量标记函数是否正在执行物理上阻止重入调用转账方式优先使用transfer/send限制gas避免call压缩攻击者的回调空间状态显式化将关键状态变更放在外部调用之前让重入无利可图这些准则今天看起来非常基础但它们的提出全部源于TheDAO血的教训。我特别想强调Checks-Effects-Interactions这个模式。当时很多开发者的习惯是先把事做完最后更新记账这在传统中心化系统里没什么问题但在区块链上一条call执行就是一次穿越对方有代码执行权而你自己的状态还停留在旧世界。TheDAO事件告诉所有人在外部交互之前必须先把内部状态定下来。另外一个重要的技术演进是基于TheDAO事件出现的形式化验证工具。TheDAO的合约虽然经过了部分审计但审计依赖人工代码审查效率低且容易遗漏。事件之后像CertiK、OpenZeppelin等团队开始大力推动利用数学方法对合约行为进行系统性验证。到今天形式化验证已经成了安全基础设施的一部分。4.3 现代智能合约审计的诞生与演进2016年之前智能合约审计还不是一个正经职业。普通项目的合约开发者自己写完自己看一遍就敢上线了。TheDAO事件之后审计开始变成一个刚需甚至可以说现代智能合约审计行业的真正起点就是TheDAO被攻击。我在2023年跟几个做审计的朋友聊天时大家有一个共同的感受TheDAO事件对整个行业的最大贡献不是那笔钱被追回来了而是它让所有人意识到安全不是事后补救而是事前设计。一个成熟的审计流程到今天至少包含五个环节静态审查用Slither、Mythril等工具扫描已知漏洞模式人工代码阅读审计师逐行阅读合约逻辑理解业务语义环境模拟在测试网或者Fork主网环境下模拟攻击场景交互博弈尝试在复杂场景下组合多种漏洞进行利用报告交付输出漏洞清单、风险等级、修复建议。TheDAO事件之前这些环节一个都没有。很多项目就真的只是一个合约一个地址满怀着对代码的信任就上线了。TheDAO事件之后稍微像样一点的团队都会引入安全审计这个角色。但我也要说句实话即便有了审计也不能100%保证安全审计只能降低风险不能消除风险。今天依然有大量DeFi项目在被攻克原因往往不是审计没做而是审计覆盖不了项目上线之后的新增改动。最后说一点我个人在复盘TheDAO时最深的体会。这个事件最核心的教训不是不要写重入漏洞而是重入漏洞只是一个引子真正危险的是你对自己写的代码过于自信。TheDAO的开发者在当时已经是社区里技术最强的团队他们的代码也经过了多轮讨论和测试但依旧出了问题。这就意味着任何一份未经攻击检验的合约本质上都处于可能被攻破的状态。而在今天当我再看任何一份智能合约时我都会带着如果我是攻击者我会怎么打的视角去审视。这不是被害妄想而是TheDAO事件留给这个行业最真实、最持久的安全素质。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门