重入攻击
漏洞解析:合约转账交易使用call函数,当call函数被调用时会触发回调函数fallback(),若攻击者恶意在回调函数中调用合约的取款函数就会递归取款,直至合约余额清空,此漏洞便是重入攻击漏洞。
此合约修复方法如下:
修复方法1: 加入互斥锁,即第一次取款时,锁定本次交易过程,直至交易完成后解锁,这样攻击者就无法重入攻击,无法通过require(!locaked,”No re-entrancy”).
修复方法2:执行转账前先把该账户记录余额先清空,重入时无法通过require(bal>0).
访问控制
漏洞分析: 对于一个谁可以调用敏感函数(如提取以太币或改变所有权)缺少限制
漏洞1. 缺少require判断
修复:添加require(minters[msg.sender] == false,””)
漏洞2. "alreadyClaimed "永远不会被设置为真,所以申领者可以发出多次调用该函数
修复:require(!alreadyClaimed[msg.sender],””);
alreadyClaimed[msg.sender] == true;
不正确的输入验证
问题: 未在适当的地方设置require
- 只检查了提取的金额没有超过账户中的金额,但是没有阻止从一个任意的账户中提取
漏洞分析:此合约取款函数withdraw函数需要输入取款地址和金额,但是取款过程只做了提取金额不能超过账户余额的判断,没有对提取人做判断,导致任何人都可以在任意账户提取金额
修复:添加对提取人判断,只有账户所属者才能提取金额,require(msg.sender==from);
不适当的访问控制和不适当的输入验证之的区别
访问控制意味着msg.sender没有足够的限制。
不正确的输入验证意味着函数的参数没有得到充分的处理。
tx.origin(钓鱼)
前置内容:
- msg.sender:msg.sender 仅会读取上层调用者的地址。
- tx.origin:tx.origin 会读取启动交易的原始地址。
由上图可以看到,
Bob 通过 A 合约调用 B 合约,B 合约又调用 C 合约。对于 C 合约来说,tx.origin 为 Bob ,msg.sender 为 B 合约。对于 B 合约来说, tx.origin 也是 Bob , msg.sender 为 A 合约,对于 A 合约来说,tx.origin 与 msg.sender 均为 Bob 。这里我们可以得出一个结论:tx.origin 永远都是 EOA 地址,msg.sender 可以为 EOA 也可以为合约地址。
一般出现在require判断里面require(msg.sender == tx.origin, "no contracts");
问题: 如果tx.origin被用来识别交易发起人,那么可能会被中间人进行攻击,...即中间人让交易发起者调用一个恶意的合约,然后利用tx.origin的权限来进行破坏
漏洞例子:
漏洞分析:这是一个钱包合约,使用require(tx.orgin==owner,”Not onwer”),如上面所说tx.orgin会指向启动交易原始地址,攻击者就可以利用这个漏洞冒充其身份,盗取该账户钱包以太。
修复合约:只需将钱包合约中tx.orgin改为msg.sender即可。
攻击例子:
Alice 在注册时的签名并不是用于注册的,而是签名了调用 Attack.attack() 这笔交易。Attack.attack() 调用了 Wallet.transfer() 并传入 owner 也就是 Eve 的 EOA 地址,以及 Wallet 合约中的以太余额。因为签名这笔交易的地址为 Alice 的 EOA 地址,所以对于 Wallet 合约来说 tx.origin 就是 Alice 的 EOA 地址,所以 Eve 成功利用钓鱼伪造了 Alice 的身份,通过了权限检查并成功将 Wallet 合约中的以太转移到了自己的账户中。
Gas导致的拒绝服务(悲痛攻击)
问题: 1.智能合约可以通过进入一个无限循环,恶意地用完转发给它的所有Gas
注: 这个和以太之王的拒绝服务不一样
拒绝服务(以太之王)
对于 KingOfEther 合约, 我们可以看到,生成新王和退回旧王的逻辑是在同一函数内完成的,并且 claimThrone() 中还检查了退款的返回值 sent,此时如果调用者没有 fallback() 函数不能接收以太币的话,就会让别人成为新王的时候无法退回旧王,导致新王生成被回退
修复方法: 添加了 balances 映射,它记录了每个人向合约中打入以太的总数量相较于之前合约的优势是玩家失去王位后可以追加以太重新获得王位。修复版本的关键点是将退款逻辑作异步处理,需要玩家手动调用 withdraw() 来自助退款,就算遇到恶意玩家拒收以太也只能影响到自己,不会再造成之前的拒绝服务了。
不安全的随机数(区块链没有随机数)
问题: 目前不可能用区块链上的单一交易安全地产生随机数。区块链需要是完全确定的,否则分布式节点将无法达成关于状态的共识。因为它们是完全确定的,所以任何 "随机"的数字都可以被预测到。
建议: 使用 异步处理 或者 预言机 生成随机数
(最好用预言机)
如下是一个类似预言机的异步处理,修复随机数漏洞:
漏洞示例:
攻击示例:
修复建议:
deadline 参数将 guess 和 claim 做了异步处理,在部署合约后的 72 小时内可以调用 guess() 猜随机数,在 72 小时后 guess() 关闭 claim() 开启,玩家可以通过 claim() 来验证自己是否猜中. (但是这并不是最优解决方法,最优的解决办法还是接入知名预言机来获取随机数)
错误使用 Chainlink 随机数 Oracle
Chainlink是一个流行的解决方案,以获得安全的随机数。它分两步进行。首先,智能合约向预言机处发送一个随机数请求,然后在一些区块之后,预言机以一个随机数作为回应。
由于攻击者无法预测未来,所以他们无法预测随机数。
除非智能合约错误地使用预言机:
- 请求随机数的智能合约必须在随机数返回之前不做任何事情。否则,攻击者可以监视返回随机数的预言机的mempool,并在前面运行预言机,知道随机数会是什么。
- 随机数预言机本身可能会试图操纵你的应用程序。如果没有其他节点的共识,他们不能挑选随机数,但如果你的应用程序同时请求几个随机数,他们可以扣留和重新排序。
- 最终性在以太坊或大多数其他EVM链上不是即时的。仅仅因为某些区块是最新的,并不意味着它不一定会保持这种状态。这被称为 "链上重组"。事实上,链可以改变的不仅仅是最后一个区块。这就是所谓的 "深度重组"。Etherscan报告了各种链的re-orgs,例如以太坊重组和Polygon 重组。在Polygon上,重组的深度可以达到30个或更多的区块,所以等待更少的区块会使应用变得脆弱(当zk-evm成为Polygon上的标准共识时,这种情况可能会改变,因为最终性将与以太坊的一致,但这是未来的预测,而不是目前的事实)。
- 下面是其他 Chainlink 随机数的安全考虑。
从价格Oracle中获取陈旧的数据
Chainlink 没有SLA(服务水平协议)来保持它的价格预言机在一定时间范围内的更新。当链上的交易严重拥堵时,价格更新可能会被延迟。
使用价格预言机的智能合约必须明确地检查数据是否陈旧,即最近在某个阈值内被更新。否则,它不能对价格做出可靠的决策。
还有一个更复杂的问题,如果价格没有变化超过偏差阈值,预言机可能不会更新价格以节省Gas,所以这可能会影响到什么时间阈值被认为是 "陈旧"。
了解智能合约所依赖的 Oracle 的服务水平协议是很重要的。
只依赖一个预言机
无论一个预言机看起来多么安全,将来都可能发现攻击。对此的唯一防御措施是使用多个独立的预言机。
混合计算(用到自毁函数)
问题: 同时使用两种计算方法,导致合约可能会有不一致的行为
- 没有接收或回退函数,所以直接将以太传送给它就会回退。 可以通过自毁函数强行转入
此时 会出现 myBalanceIntrospect()
会比 myBalanceVariable()
大
把加密证明当作密码一样对待
问题: 对如何使用密码学来赋予地址特殊权限有普遍误解
这段代码是不安全的,原因有三:
- 任何知道被选中进行空投的地址的人都可以重新创建Merkle树并创造一个有效的证明。
- Leaf没有Hash。攻击者可以提交一个与Merkle根相同的Leaf,并绕过
require
语句。 - 即使上述两个问题被修复,一旦有人提交了有效的证明,他们就可以被抢跑。
加密证明(Merkle树、签名等)需要与msg.sender
绑定,攻击者在没有获得私钥的情况下无法操纵。
Solidity 不会向上转型 uint 大小
尽管product是一个uint256变量,但乘法结果不会大于255,否则代码将被回退。
这个问题可以通过向上转型每个变量来解决:
Solidity 截断不会回退
Solidity 并不检查将一个整数转换为一个较小的整数是否安全。除非某些业务逻辑能确保向下转型是安全的,否则应该使用 SafeCast 这样的库。
对存储指针的写入不会保存新数据
这段代码看起来像是把myArray[1]中的数据复制到了myArray[0]中,但其实不是。如果你把函数的最后一行注释掉,编译器会说这个函数应该变成一个视图函数。对foo的写入并没有写到底层存储。
删除包含动态数据类型的结构体并不会删除动态数据
如果一个映射(或动态数组)在一个结构体内,并且该结构被删除,那么映射或数组将不会被删除。
除了删除数组之外,删除关键字只能删除一个存储槽。如果该存储槽包含对其他存储槽的引用,这些存储槽不会被删除。
在Solidity中,map 永远不会是 "空"的。因此,如果有人访问一个已经被删除的项目,交易将不会回退,而是返回该数据类型的零值。
未检查的返回值
有两种方法来调用外部智能合约:1)用接口定义调用函数;2)使用.call方法
在合约 B 中,如果 _x 小于 10,setXV2 会默默地失败。当一个函数通过.call方法被调用时,被调用者可以回退,但父函数不会回退。必须检查返回成功的值,并且代码行为必须相应地分支。
所以在此次调用并未进行回退,但是却调用失败了
msg.value 在一个循环中
可能会让发起者 重复使用 msg.value
私有变量(区块链的一切都是可见的)
下面是读取已部署的智能合约的私有变量的javascript代码
不安全的代理调用(Delegatecall 和 call)
别名: 委托调用
Call: 合约A使用call调用合约B时, 上下文是在合约B,也就是使用合约B的环境
Delegatecall : 合约A使用Delegatecall 调用合约B时, 上下文是在合约A,也就是使用合约A的环境,这可能导致出现对自己环境变量的破坏
四舍五入的错误
Solidity 没有浮点,所以舍入错误是不可避免的。设计者必须意识到正确的做法是向上舍入还是向下舍入,以及舍入应该对谁有利。
除法应该总是最后进行。下面的代码在小数位数不同的稳定币之间进行了错误的转换。下面的兑换机制允许用户在兑换dai(精度为18)时免费获得少量的USDC(精度为6)。变量daiToTake将四舍五入为零,换取非零的usdcAmount时,用户拿不到任何东西。
抢跑(这是一个大类)
在 Etheruem(和类似的链)的背景下,Frontrunning 意味着观察一个待定的交易,并通过支付更高的 交易成本在它之前执行另一个交易。也就是说,攻击者已经 "跑到了 "交易的前面。如果该交易是一个有利可图的交易,那么除了支付更高的 交易成本,完全复制该交易是有意义的。
这种现象有时被称为MEV,意思是矿工可提取的价值,但有时在其他情况下是最大可提取的价值。区块生产者有无限的权力来重新排序交易和插入自己的交易,从历史上看,在以太坊进入股权证明之前,区块生产者就是矿工,因此而得名。
抢跑:不受限制的提款
从智能合约中提取以太币可以被认为是一种 "有利可图的交易"。你执行了一个零成本的交易(除了Gas),最终拥有的加密货币比你开始时更多。
如果你部署了这个合约并试图退出,一个先行者机器人会注意到你在mempool中对 "unsafeWithdraw "的调用,并复制它来先获得以太币。
注: 先行者机器人(Front-running bot)是指一类监视区块链交易池(mempool)的智能合约或机器人,它们的工作原理是:
- 监视交易池中新加入的交易,比如用户发起的提取资金或交易调用等。
- 一旦发现目标交易,先行者机器人会构建一个和目标交易完全相同的交易,但设置更高的gas费。
- 由于gas费更高,先行者机器人发起的交易会比目标交易更快进入下一个块并被打包。
- 这样一来,先行者机器人就抢占了目标交易的位置,实现了“先行”。比如提取合约资金或是参与IFO等。
- 等目标交易最终进入块链网络时,相关状态可能已经被先行者机器人改变,目标交易就会失败。
防范措施:
- 加强合约逻辑限制重复调用
- 使用时间锁或随机延迟来破坏先行者预测
- 设置更高的gas保证交易成功
抢跑:ERC4626 通膨攻击(抢跑和四舍五入错误的组合)
ERC4626 合约根据交易者贡献的 "资产 "的百分比来分配 "份额"代币
当池子里有20个资产时,他们贡献了200个资产,他们期望得到100 份额。但是,如果有人在交易中提前存入200个资产,那么公式将是200/220,四舍五入为零,导致受害者失去资产,获得零份额。
抢跑:ERC20授权
- 假设 Alice 授权了Eve的100个代币。
- Alice 改变了主意,发送了一个交易,将 Eve的授权改为50。
- 在将授权额度改为50的交易纳入区块之前,它位于Mempool中,Eve可以看到它。
- Eve 发送了一个交易,要求获得她的100个代币,这在将授权改为50之前。
- 对 50 的授权的交易通过了。
- Eve 又获取了 50 个代币。
现在 Eve 有150个代币,而不是100或50。解决这个问题的办法是,在处理不受信任的授权时,在增加或减少授权之前,将授权设置为零。
抢跑:三明治攻击
一项资产的价格会随着买卖压力的变化而变化。如果一个大订单在Mempool中,交易者有动力去复制这个订单,但要有更高的gas成本。这样一来,他们就会购买资产,让用户的大额订单使价格上涨,然后他们马上卖出。卖出订单有时被称为 "尾随"。卖出订单可以通过放置一个较低gas成本的卖出订单来完成,这样的序列看起来像这样的
- 抢跑买入
- 大额买入(用户)
- 卖出
对这种攻击的主要防御是提供一个 "滑点"参数。如果 "抢跑买入 "本身将价格推高到某个阈值以上,"大额买入"订单将回退,使抢跑者的交易失败。
这就是所谓的三明治攻击(sandwhich),因为大额买入被抢跑买入和尾随卖出夹在中间。这种攻击也适用于大额卖单,只是方向相反。
签名(和抢跑一样是个大类)
签名:ecrecover返回地址(0),当地址无效时,不会被回退
如果一个未初始化的变量与ecrecover的输出相比较,这可能导致漏洞。
签名重放
签名重放发生在合约没有跟踪签名是否先前被使用。在下面的代码中,我们修复了之前的问题,但它仍然不安全。
人们可以随心所欲地索取空投!
我们可以添加以下几行:
这段代码依旧不安全.
签名的可塑性
给定一个有效的签名,攻击者可以做一些快速的算术来推导出一个不同的签名。然后,攻击者可以 "重放"这个修改过的签名。但首先,让我们提供一些代码,证明我们可以从一个有效的签名开始,修改它,并显示新的签名仍然通过。
因此,我们的运行实例仍然是脆弱的。一旦有人提出一个有效的签名,就可以产生镜像签名并绕过使用的签名检查。