基础漏洞总结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)
并发送evalue
ETH 来试图改变 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 模式并做好初始化/检查)。
默认的可见性
这个很简单,就是可见性设置错了让黑客有可乘之机。