基础漏洞总结1
之前学的比较乱,现在复习整理一下。
重入re-entrancy
重入攻击是指在合约执行外部调用(通常是向外发送以太或调用另一个合约)时,接收方在该外部调用的执行流程中再次调用回原合约的某个函数,并借此在原合约未完成状态更新之前重复利用其尚未更新的状态,从而盗取资金或破坏状态一致性。
换句话说:合约 A 在执行到“对外调用”并等待返回时,被外部合约 B 回调回 A,利用 A 尚未完成的内部逻辑(比如余额未清零)重复触发受害逻辑。
示例
漏洞合约
1 | // SPDX-License-Identifier: MIT |
Attack
1 | // SPDX-License-Identifier: MIT |
正确的修复方法
(Checks — Effects — Interactions)
最根本的原则是 先检查(Checks)→更新合约内部状态(Effects)→再进行外部交互(Interactions),确保在进行任何外部调用之前把内部状态变安全。
交换上述合约的“更新状态”和“调用外部”部分
如何判断你的合约是否易被重入
- 是否在任何外部
call/callcode/delegatecall之前更新了关键状态?(CEI) - 是否有对不可信合约的外部调用?(包括发送 ETH、调用外部合约、转 ERC777/自定义 token)
- 是否为关键函数使用了互斥保护(例如 OpenZeppelin 的
nonReentrant)? - 是否有单元/集成测试用恶意合约模拟回退并进行重入?(必须做)
- 是否使用静态分析工具(Slither、Mythril 等)扫描重入相关告警?
ATN攻击
ERC223
ERC223 是对 ERC20 的一个改进提案,目标是解决把代币误发到不支持代币接收的合约时导致代币“丢失”的问题(ERC20 在向合约发送代币时只会改变余额,不会通知合约,使合约无法处理)。ERC223 在转账到合约时触发合约回调,以便合约能处理接收到的代币,从而提高安全性和可扩展性。
解决了ERC20调用两次的尴尬
为什么 ERC20 一次转账可能会被调用两次,而 ERC223 只需要一次?
🔹 ERC20 的缺陷
在 ERC20 中,如果你向 普通外部账户(EOA) 转账,只需要一次 transfer 即可。
但是如果你要向 合约地址 转账,由于合约本身无法自动接收代币,通常需要走以下两步:
| 步骤 | 操作 | 函数 | 谁调用? |
|---|---|---|---|
| 1️⃣ | 授权对方(合约)可以花你的代币 | approve(spender, amount) |
代币持有人 |
| 2️⃣ | 合约再去拉取授权的代币 | transferFrom(from, to, amount) |
接收方合约 |
所以对于 合约接收代币的情况,ERC20 至少需要调用两次函数。
🔹 ERC223 的改进
ERC223 在 transfer 时会检查接收方是否是合约:
- ✨ 如果接收方是 合约地址,则自动调用其
tokenFallback(address from, uint amount, bytes data)回调函数。 - ✨ 如果是普通外部账户,就跟 ERC20 一样直接转账。
这样可以 一次调用完成“转账 + 通知合约”操作,不需要 approve + transferFrom。
| 功能 | ERC20 | ERC223 |
|---|---|---|
| 向外部地址转账 | ✅ 一次即可 | ✅ 一次即可 |
| 向合约地址转账 | ❌ 需要 approve + transferFrom(两次调用) |
✅ 一次 transfer 自动调用回调 |
| 防止代币误转到黑洞合约 | ❌ 不会警告,代币直接卡死 | ✅ 合约必须实现 tokenFallback 才能接收 |
DS-Auth库
好的,下面把关键点说清楚——先定义术语和库的典型用法,再说明 为什么把 ERC223 的回调机制(tokenFallback/receive)和 ds-auth(DappSys 的权限/授权库)一起用时,会产生被滥用的攻击面(像你看到的 ATN 事件那样)。我会给出一个可理解的攻击路径示例并列出具体的防御措施。
1) DS-Auth 是什么
ds-auth 是 DappSys / DappHub 生态中常见的授权(authority)模块,用来实现可扩展的访问控制。典型接口类似:
1 | contract DSAuthority { |
在使用 ds-auth 的合约里,受保护函数会在入口处询问 authority:
1 | // pseudo |
也就是说:权限判断通常基于三要素 — 调用者地址(msg.sender)、目标合约地址(address(this))、和 函数选择子(msg.sig)。
2) msg.sig 是什么
msg.sig 是 calldata 的前 4 字节,也就是 ABI 的 函数选择子(function selector)。
它等同于 bytes4(msg.data[:4]) 或 bytes4(keccak256("functionName(type1,type2,...)"))。
msg.sig标识被调用的是哪一个函数(签名),用来在授权判断中识别 “哪个函数被请求执行”。msg.sig不包含调用者信息,它只是方法标识符。
ERC223 的回调机制为何带来风险
ERC223 在 transfer(to, value, data) 时:
- 如果
to是合约,会 自动触发接收合约的回调(例如常见的tokenFallback(address from, uint value, bytes data)),从而让接收合约能在收到代币时执行逻辑。 - 这就产生了“转账即回调”的行为链:代币合约 → 接收合约 的执行顺序中,会有外部调用与回调发生(可能带来重入/权限利用机会)。
攻击原理概述
关键点:ds-auth 用 msg.sender + msg.sig 判断权限,而 ERC223 的回调会使 msg.sender 变成“发起回调的合约地址(token 合约)或恶意合约地址”。攻击者可以利用这一点组合出可行攻击链,常见套路如下:
- 权限配置失误 / 过宽的授权
- 如果
authority被设置为允许某个合约(或一类合约)对任意(或若干)敏感函数调用canCall返回true,那么当该合约(或恶意实现)在回调期间发起对目标合约的调用时,就会被ds-auth视为合法调用。
- 如果
- ERC223 回调作为载体触发敏感函数
- 攻击者部署一个恶意 token 合约或操纵转账流程,使目标合约在 token 转账的过程中被回调。回调的上下文
msg.sender指向 token 合约(或恶意中间合约),而msg.sig仍然表明目标函数(比如setOwner(...)、mint(...))的 selector。 - 如果 authority 的策略允许该
msg.sender对该msg.sig进行调用(或者授权逻辑有缺陷),攻击者就能在回调中调用敏感接口,从而提权或铸币。
- 攻击者部署一个恶意 token 合约或操纵转账流程,使目标合约在 token 转账的过程中被回调。回调的上下文
- 掩盖来源 / 恶意恢复
- 攻击者可能在拿到权限后把
owner恢复为原地址,留下最小可追踪痕迹,而中间已经铸币/转走资产。
- 攻击者可能在拿到权限后把
换句话说:ERC223 提供了“在代币转账期间执行合约代码”的入口,而 ds-auth 的授权决策恰好基于“谁发起调用(msg.sender)与要调用哪个函数(msg.sig)”。当授权策略配置不严(或存在逻辑漏洞)时,攻击者可以通过构造回调调用把自己“伪装”为被授权的调用者,或利用被授权的合约作为跳板,去调用本不该自己调用的管理函数。
一个简单(抽象)示例流程
- 目标合约
Target使用ds-auth判断:DSAuthority.canCall(msg.sender, address(this), msg.sig)。 authority错误地允许SomeTokenContract对某些 selector(比如mint)返回true(例如因为某次配置失误或把“代币合约”列入白名单)。- 攻击者部署/控制
SomeTokenContract,并在其transfer内触发tokenFallback,在回调中 代表 token 合约 去调用Target.mint(attacker, amount)。 Target在权限检查看到msg.sender == SomeTokenContract,msg.sig == mint selector,而 authority 允许这种调用 → 因此执行mint,把代币铸给攻击者。
为什么这类攻击容易被忽视 / 成功率高
ds-auth的授权逻辑看起来“通用”:按src/dst/sig三元组授权非常灵活,但也因此若管理不慎(例如把某个合约列为可信、或批量授权某些 selector),会产生非常危险的后果。- ERC223 的回调看似方便(一次转账并通知接收方),但它把“通知”变成了“可以执行任意逻辑的入口”,给攻击者提供了时间窗口与上下文切换(
msg.sender会变)。
防御与实践建议
- 不要把代币合约或任意第三方合约作为广泛授权对象
authority的白名单应尽量精确到具体合约地址 + 具体 selector;不要把“代币合约”或“任何合约”广泛授权。
- 敏感函数优先使用严格的
msg.sender == owner或多签控制- 对极其敏感的管理操作(如
setOwner、mint、upgrade),尽量不依赖可扩展的三元组授权作为唯一门槛,采用owner-only或多签/治理合约。
- 对极其敏感的管理操作(如
- 对接收 token 的回调保持最小权限与最小行为
- 在
tokenFallback/receive中尽量只做最少的状态记录/事件,而不要在回调中直接触发高权限修改或管理操作。
- 在
- 审慎配置 authority,并对授权变更保留审计日志
- 对
authority的任何修改都要通过治理流程或多签,并记录变更,便于事后追查。
- 对
- 避免把授权决策仅仅依赖
msg.sig的“白名单”- 可以在
canCall内加入额外上下文检查(例如:只允许特定src在特定情况下对特定dst/sig调用),并对“合约调用”与“EOA 调用”做不同策略。
- 可以在
- 与 ERC223 集成时要小心:将接收回调只作为通知,不作为触发管理入口
- 如果必须在回调中执行动作,先把必要状态“锁定”或通过非回调的后续流程完成(避免在回调里完成关键授权变更)。
- 写攻击/回归测试并用静态分析工具扫描
- 用恶意 token 回调合约模拟攻击路径;用 Slither 等工具扫描
canCall的潜在配置问题与危险模式。
- 用恶意 token 回调合约模拟攻击路径;用 Slither 等工具扫描
- 使用互斥与 CEI 防止回调期间状态未一致
- 对易受重入的函数加
nonReentrant或采用 CEI(先更新状态再交互)。
- 对易受重入的函数加
总结
ds-auth 本身是灵活的授权框架,但它基于 msg.sender/msg.sig 的判断在 遇到 ERC223 的回调能力(或其它可执行回调的 token 标准/合约)时,会放大任何授权配置错误或设计缺陷的风险——攻击者可以用“在回调中发起调用”的技术把自己伪装为被授权的调用者,从而执行敏感操作(如擅自铸币、改 owner),这正是 ATN 类攻击背后的核心机制。
ATN 攻击流程
1. 利用 ERC223 方法漏洞提权(将自己设为 owner)
动作:攻击者利用合约中 ERC223 相关实现的漏洞,执行提权操作,把自己的地址设为了合约 owner。
目的:获取管理权限以便后续铸造或转移代币。
链上交易:
https://etherscan.io/tx/0x3b7bd618c49e693c92b2d6bfb3a5adeae498d9d170c15fcc79dd374166d28b7b
2. 发行/铸造大量 ATN 到攻击主地址
动作:拿到 owner 权限后,攻击者调用铸造/发行(mint)或类似的管理函数,把大量 ATN 发行到自己的主控地址。
描述:你记录为 “发行 1100W ATN”。(注:1100W 常被用来表示 1100 万,即 11,000,000 ATN;若表述有别,请按链上数量为准。)
链上交易:
https://etherscan.io/tx/0x9b559ffae76d4b75d2f21bd643d44d1b96ee013c79918511e3127664f8f7a910
3. 恢复 owner 为原来地址(企图掩盖痕迹)
动作:攻击者将 owner 设置回原先地址或其他地址,以掩盖刚才的提权痕迹,试图减少可追溯性/诱导审计误判。
链上交易:
https://etherscan.io/tx/0xfd5c2180f002539cd636132f1baae0e318d8f1162fb62fb5e3493788a034545a
4. 将偷得的代币分散到多个地址(洗分/分散风险)
动作:攻击者把获得的大额 ATN 分批转到 14 个地址,以分散和转移资产,增加追踪难度并为后续换币/出金做准备。
目的:规避监测、降低单一地址风控触发概率、准备跨交易所或通过去中心化交易所(DEX)套现。
(若需)链上交易示例:可在攻击主地址的 token 转账记录中查看相应多笔 transfer。
算术溢出(under flows)
算数溢出/下溢:当一个整数加法/减法超出其能表示的范围时(例如
uint8超过 255),旧版 Solidity 会wrap around(模 2^n),导致值变成看似“任意”的小/大数;攻击者可以利用这一点改写余额/计数,从而绕过检查或窃取资产。现代 Solidity(^0.8.0 及以上)默认检测溢出并 revert,所以真正的溢出攻击多见于使用旧版本或显式
unchecked的代码,或库错误地禁用了检查。
示例
1 | // SPDX-License-Identifier: MIT |
addCredits 是 public 并允许任意 amount,而且用 uint8 并不检查溢出 → 调用时可导致 credits[msg.sender] wrap(例如从 0 加 255 后是 255;从 200 加 100 会变成 44)。
如果攻击者能把 credits 的值设成一个非常大的(或恰好满足条件的)数,就能绕过逻辑或用大量积分兑换大量以太。
攻击合约
1 | // SPDX-License-Identifier: MIT |
修复方法
修复方式 A — 在旧 Solidity(<0.8)中使用 SafeMath(OpenZeppelin)
1 | import "@openzeppelin/contracts/math/SafeMath.sol"; |
修复方式 B — 在现代 Solidity (^0.8.0+) 使用内置检查(更简洁)
Solidity 0.8+ 默认对整型运算溢出做检测并
revert。推荐升级编译器并使用uint256。
漏洞实例:合约PolyAi(AI)
合约地址:https://cn.etherscan.com/address/0x5121e348e897daef1eef23959ab290e5557cf274#code
蜜罐合约
合约一
1 | function GetFreebie() |
https://github.com/thec00nsmart-contract-honeypots,blob/master/WVhaleGiveaway1.sol
为什么会攻击
向右滑看看…….

合约二
1 | function multiplicate(address payable adr) public payable { |
Github地址:
smart-contract-honeypots/MultiplicatorX3.sol smart-contract-honeypots/Multiplicator.sol
智能合约地址:
0x5aA88d2901C68fdA244f1D0584400368d2C8e739
关键点:
msg.value是调用者发送的以太数量。address(this).balance是函数执行时合约的余额 已经包含本次 msg.value。执行 transfer(如果条件成立)
如果条件成立(只有在合约原余额为 0 且 msg.value > 0 时),EVM 会执行:
1
adr.transfer(address(this).balance + msg.value);
注意此时:
address(this).balance已经包含 msg.value。- 这里又加上
msg.value→ 会试图转出 2倍的 msg.value,而合约实际余额只够msg.value→ 会 抛出异常,交易 revert。
交易结算
- 若 transfer 执行失败,整个交易 revert,合约余额和发送者余额回滚到交易前的状态。
- 已消耗的 gas 不会返还。
合约三
1 | contract Owned { |
owner:存储在 slot 0,用于记录合约的拥有者。
onlyOwner 修饰符:限制函数只能由拥有者调用,否则回滚。
msgSender:存储调用者地址(slot 1)。
ecode & evalue:用于紧急代码验证。
useEmergencyCode:
- 若输入
code与合约存储的ecode匹配,并且发送 ETH 等于evalue,则将owner设置为msg.sender。
withdraw:
- 仅限拥有者调用,可提现合约余额。
Github地址:
smart-contract-honeypots/TestBank.sol
智能合约地址:
0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9
1. Solidity 合约存储槽规则
- 每个状态变量在 EVM 存储中占一个或多个 slot(32 字节 = 256 位)。
- 变量存储顺序:
owner→ slot 0msgSender→ slot 1ecode→ slot 2evalue→ slot 3
注:这里假设没有打包优化,因为
uint256本身正好占 1 个 slot。
2. 获取存储槽的值
如果你有合约地址(比如 0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9),可以用 web3 或 ethers.js 读取 storage:
示例(ethers.js)
1 | const { ethers } = require("ethers"); |
getStorageAt会返回 slot 对应的 32 字节 hex。- 转成
BigNumber或整数即可得到 Solidity 的 uint256 原始值。
3. 理论推算
- 如果你 **知道合约部署时如何设置
ecode和evalue**(比如构造函数或管理员调用set函数),可以直接算出它们。 - 否则,合约内部没有公开 setter/getter 时,唯一可行的方式就是读取 链上存储槽(方法如上)。
4. 注意事项
- 读取 storage 并不花费 gas,只要使用
call或provider.getStorageAt。 - 即使变量是
private,链上存储仍可读取。 - 对蜜罐合约来说,这两个值通常就是 触发 owner 转移的条件。
- 一旦知道
ecode和evalue,可以通过useEmergencyCode(ecode)并发送evalueETH 来试图改变 owner。
- 一旦知道
读取脚本
1 | from web3 import Web3 |
使用说明
- 安装 Web3.py:
1 | pip install web3 |
- 替换
rpc_url为你的节点 URL(Infura / Alchemy / 自建节点都可以)。 - 运行脚本即可读取链上
ecode和evalue的原始 uint256 值。
同类项目 :KingOfTheHill
意外之财
依赖合约全局余额做为业务条件会产生“意外之财”风险,因为合约余额可被外部力量修改而不触发合约代码。稳健的做法是使用内部会计/凭证和更严格的资格检查,避免将关键权限或奖励直接绑定到 address(this).balance。
1. 合约示例
1 | // SPDX-License-Identifier: MIT |
2. 问题与风险(为什么会出现“意外之财”)
- 合约逻辑直接依赖
address(this).balance来判断是否到达目标(finalMilestone)。 - 合约余额可以在不执行合约任意代码的情况下被改变(例如
selfdestruct、矿工奖励或他人事先转账),导致claimReward条件被意外满足,任何人都能调用领取奖励(包括并非真正参与游戏的地址)。 - 因为
address(this).balance在进入函数时已经包含msg.value,所以在play()中错误地用address(this).balance + msg.value来判断会导致逻辑混淆(且通常是错误的)。
3. 示例攻击/触发场景
- 攻击者或第三方对合约执行
selfdestruct(targetContract),把一定数额的 ETH 强制发送到该合约,使address(this).balance恰好等于finalMilestone。 - 任何地址随后调用
claimReward(),因为address(this).balance == finalMilestone为真,合约把余额转给调用者 —— 原本未参与出资的人可直接领取。
4. 三种不会触发 EVM 合约代码执行但会影响合约余额的途径
- Mining reward:矿工/区块奖励(或内置奖励)分配到某些地址,会影响链上账户余额,但不触发目标合约的代码。
- **selfdestruct(target)**:
selfdestruct把以太直接写入目标合约的余额,不会调用目标合约的receive/fallback,因此不会触发合约逻辑。 - Pre-sent Ether(事先转账):任何地址直接向合约转账(
send/transfer/call),或者部署时即有初始余额,这些都可能改变合约余额而无须被目标合约的业务逻辑处理。
5. 防御与最佳实践
- **不要把关键业务逻辑直接依赖
address(this).balance**(尤其是决定谁可领取资产的逻辑)。 - 引入内部账本(内部记账):对每个参与者维护
mapping(address => uint256) deposits,并基于内部记账判断资格与分配奖励,而非全局合约余额。 - 对领奖逻辑增加资格校验:例如只有实际有贡献(
deposits[msg.sender] > 0)或在白名单内的参与者才可claimReward。 - 使用 Pull payments 模式:记录应得金额,受益人主动提取(
withdraw),避免一次性把合约全额发出给任意调用者。 - 对不可预见到的强制转账保持防御性设计:假定
address(this).balance可能随时被外部强制改变,业务逻辑应能容忍或检测这种情况(例如不根据全局余额做关键判断)。 - 日志与监控:对关键状态(如达到里程碑)做审计日志与链上告警,便于人工判断是否为异常触发。
Delegatecall
主要导致存储污染
存储污染指:一个合约的状态变量(storage slot)被意外或恶意修改,导致合约逻辑异常或关键数据被篡改。
典型场景:使用 delegatecall 调用外部库合约时,被调用合约的代码在调用合约上下文中执行,任何对 storage 的写入都会影响调用合约自身的状态。
1. Vulnerable: FibonacciBalance(存在 delegatecall 导致的存储污染问题)
1 | // SPDX-License-Identifier: MIT |
2. Intended library: FibonacciLib(正常实现,按自身 storage 布局写入)
1 | // SPDX-License-Identifier: MIT |
3. 恶意库:MaliciousLib(通过写入特定 storage slot 来篡改调用者合约的关键变量)
1 | // SPDX-License-Identifier: MIT |
4. 攻击合约(示例攻击流程)
1 | // SPDX-License-Identifier: MIT |
5. 漏洞要点
delegatecall会在调用者(caller)的上下文中执行被调合约(library)的代码:被调合约对 state 的写入会影响调用合约的 storage slots。- 若调用合约允许任意 calldata 被转发到
delegatecall(如fallback()直接delegatecall(msg.data)),攻击者可以构造 calldata 来执行恶意 library 的方法,从而修改调用合约的关键 storage(如fibonacciLibrary或calculatedFibNumber)。 - 由于 storage slot 的映射关系,库合约与调用合约必须严格对齐 storage 布局,否则将出现存储污染或被滥用的风险。
6. 修复建议
- 不要把不受信任的地址设为可 delegatecall 的目标;如果必须使用 library,确保其来源可信且 storage 布局一致且不可更改。
- 禁止将外部任意 calldata 直接转发给 library(不要实现
fallback()中无校验地delegatecall(msg.data))。 - 将 library 地址设为不可变(
immutable/constant)或只有 owner 可变,并对修改操作加严格权限和审计。 - 对关键 storage 的写入加入额外校验(例如检查写入前后值的合理性或使用 access control)。
- 尽量使用
delegatecall的替代方案(如library的 internal/linked library 或明确的 proxy 模式并做好初始化/检查)。
默认的可见性
这个很简单,就是可见性设置错了让黑客有可乘之机。



