Blocksec 课程笔记
BlockSec 区块链安全课程学习笔记
还没写完, 最近没空搞, 先传一下
先说下我的backgroud哈, 我对web3的生态和基础设施都有一定了解, 在near生态参与开发过DeFi项目和数据索引项目, 因此可能课程中比较基础的知识会直接略过, 本文是在学习BlockSec
的一些学习笔记, 里面也会有一些课上内容的拓展研究, 多图预警!!!
第一节 账号与交易
原来用Hardhat
已经算是老古董了, 记得之前在Aurora
上开发空投项目的时候还是用的Hardhat
来着, 得试试新框架了
这个phalcon fork
还挺有意思的, 可以从主网某个区块高度fork出一条当测试链, 不知道效率咋样, 之前在near上开发的时候做测试确实很麻烦的, 创建各种账户地址之后, 需要去结点里取合约状态再patch进去, 尤其测试复杂应用的时候需要填好几个和合约, near的普通rpc结点拉取状态还有大小限制, 超过limit就得去archive结点取, archive结点资源又很紧张…不知道现在解决了没
搜索一下eth fork test, 可以发现刚刚提到的foundry的forge也有类似的功能, 不知道这个产品有什么特别的优势地方, 可能要深度使用才能发现
这章主要还是介绍的一些基础概念,没什么好记录的
第二节 智能合约
EOA地址和合约地址都可以部署智能合约, 并且会通过某种方式计算出下一个要部署的合约地址
第一种合约地址计算方式是CREATE
, 优点是可被提前预知, 缺点是无法再重复生成之前部署过的合约地址, 因为合约地址计算依赖账户的nonce, 部署合约本身是一笔交易会修改nonce, nonce变了就无法生成之前的地址了
最新的地址计算方式使用CREATE2
, 地址计算只涉及到合约代码的哈希,因此可以在不修改合约内容的情况下部署到同一个合约地址
可以看一下前一段时间使用create2碰撞地址钓鱼的example, 一般攻击者会使用vanity-eth 碰撞出
混用create2和create创建合约地址可能带来一定安全隐患,合约B如果代码不变的话想要创建出不同的合约地址C会不生效,同一个地址C可以在不同的时刻部署不同的合约代码,课程中提出的对应的攻击案例: https://learnblockchain.cn/article/5916 , 攻击者的交易: https://openchain.xyz/trace/ethereum/0x3274b6090685b842aca80b304a4dcee0f61ef8b6afee10b7c7533c32fb75486d ,大概的trick和课程中说的基本一致
写个最简单demo的话差不多就是下面这样
这个点之前不是很清楚,evm的memory中没有os来管理内存地址,因此需要一个指针指向当前可用的内存偏移,这个指针就是存在了0x40的地方
代理合约可用通过delegateCall实现,好处就是可以保证合约的可升级行,感觉目前大部分正规项目都是采用了代理合约的,也就是修改对用户透明(当然也带来了风险,可能被偷偷植入后门),安全与便携不可兼得
如果是多层合约调用,根据是否传递异常有两种调用方式,low-level call并不会对失败交易做revert,
第二个案例,由于实现合约更改了storage布局,从而导致精度计算错误被攻击,因此代理合约和实现合约的存储布局必须保证完全一致
第三个案例讲了UUVP攻击,由于实现合约的状态信息是没有意义的,因此constructor函数也没有意义,在实现合约中会被改成普通的init函数,然后使用delegate call去调用,因此往往调用init函数的账户会变成合约owner,如果项目方在部署实现合约后忘记调用init或者被抢先调用init,就会导致owner权限被窃取,虽说拿到的状态没有意义,但是可以通过调用updateAndCall函数自毁合约,使proxy合约被锁死彻底失效, 对应真实案例: wormhole未初始化攻击
最后一个案例,就是之前提到的tornado cash治理攻击, 攻击者通过先发起正常提案, 在DAO投票通过之后再利用上面的CREATE
和 CREATE2
混用技巧在不修改投票通过的指定合约地址的前提下更改合约内容, 本来社区中对这种create2创建出的合约地址应该保留高度警惕的,同时应该对合约中的自毁函数保持警惕
第三节课 合约与安全概念
课程上来先介绍了一些学习资源,mark
从一个high level的角度审视区块链安全
一些工具(介绍了phalcon explorer)
感觉block sec的学术背景确实很强啊,经常贴的是一些顶会论文的链接,确实很专业,下面是对区块链攻击的一些分类
终于开始讲案例了,还是从最最最经典的溢出开始,继续鞭尸beautiful token
编译器版本在0.8之后默认使用了safemath,溢出的攻击基本上很少了,但是还是需要小心为了节省gas把代码uncheck了
用了安全库就一定安全吗,不一定,以下图为例,safecast会在整数溢出时revert交易,这就造成了dos攻击,合约无法继续进行(场景:u32存时间、游戏参与人数等)
使用lowlevel call时输入校验不充分可能导致很多攻击,下面是今年的一些案例
delegate call使用不当导致的问题
一般来说通过账户的code length去判断账户是否为合约地址,但是在constructor函数里,codelength一直是0,从而导致这个防护失效了,黄皮书里还真是有很多trick, openzeppelin中实现了一个判断合约账户的函数,对合约的不同状态都做了详细的判断
Flurry Finance Hack攻击用了一个trick,在EOA地址和合约地址之间左右切换以实现最大收益,想深入学习这个案例发现知识空白很多,深刻认识到了和这种顶尖黑客的差距之大…
第四节课
好家伙上来就是顶会论文, 看看学术界怎么anti重入攻击的,首先介绍下可重入的概念, 没记错的话linux内核中分别提供了很多api的可重入和不可重入版本,重入和线程安全是不太一样的,很多人容易搞混, 很经典的就是malloc是线程安全的却是不可重入的,在我的理解中加入一个函数被中断打断,同时中断处理中又调用了这个函数,如果这个场景下能保证安全的话那就是可重入的,线程安全则是在多线程情况下不会出现一些资源竞争的情况
最简单的转账调用,如果转出的地址是合约账户的话则会触发fallback,在fallback中则可再次调用转账函数,这样就完成了一次最简单的重入攻击
为了在一定程度上避免重入, solidty在0.6之后提供了receive函数用于处理收到ETH之后的回调函数, 具体两者之间的关系可以看这里, 同时使用Transfer也在一定程度上避免重入攻击, 因为transfer有一定的gas限制, 重入攻击涉及到多次合约调用会很快消耗掉gas
随着安全的进步,攻击手段也被迫逐渐升级,出现了跨合约重入、跨函数重入已经只读重入,这些都需要自己去补充读一下
后面介绍了一些攻击的case,经典重入的攻击就不做笔记了,可以看一下新形态的read only的重入攻击,从下面简单的case来看,其实与经典重入没什么区别,都是利用了合约状态更新的延迟导致的状态不一致,只不过这里只是读取某个值用来操纵价格之类的,但是感觉这种情况基本上是不可避免的…业务场景里应该会经常出现
看看链接中说的出现readonly reentrany会出现的条件: 一个状态,一个会修改这个状态的外部调用,另一个依赖这个状态的外部调用
看一个更接近实战的例子,带入上面的三个条件, state就是每个地址的balance, 一个会修改状态的调用就是withdraw函数,totalSupply会在burn之后更新, balance会在最下面的transfer更新,假设更新没完成就有调用了getSharePrice函数,就会拿到不正确的价格(因为balance还没有更新),因此攻击者可以一边withdraw代币一边从使用了这个getSharePrice函数的dapp中获益
然后真实案例是对Sentiment这个借贷协议的攻击, 可以看一下下图,也是在调用BalanceChange之后再更新的Balance,符合上面所说的read only reentrancy的条件, 具体的攻击者交易可以用Phalcon Explorer
查看, 地址在这里
如何对抗重入
第五节 DEX与DEX安全
前几章节讲了一些比较general的漏洞, 后面的DEX安全章节则会根据不同DeFi协议去进行区别分析
DEX也就是去中心化交易所, 主要有两种实现方式, 订单簿与自动化做市商,
LOB
AMM
AMM之前的简单原理也介绍过, 下面还是看一下最经典的uniswap的乘积恒定原理
简单来说乘积很定原理的k值是有一开始添加LP时添加的两个代币的数量决定的, 后续的交易中只要不涉及LP流动性的添加与撤走k保持不变, 并且后续添加LP时也需要根据当前的价格来添加, 但是依旧会影响k值
至于上面的乘积公式中的997, 则是因为uniswap每笔交易需要百分之0.3的手续费
在课程中提到了所谓的课后作业– impermanent loss(无常损失), 首先你需要知道你添加流动性之后得到了一定比例的share, 这个share代表着你的资产占流动池中的总资产的比例, 假设池子中有2个eth和100个usdt且你的share份额占总份额的百分之10, 那么你在撤走流动性时会得到0.2个eth和10个usdt, 假设在此期间eth的价格产生巨大波动(在web3那就是家常便饭), 那么很可能你添加流动性的价值会小于你撤走流动性时的价值, 这部分损失就叫无常损失, 无常损失一般会通过给予流动性提供者一定的手续费奖励来对冲, 参考链接 点这里
spot价格是指在流动性池创建之初的币价,也就是 x/y, effective价格是指真正在swap时根据池子中比例计算出的价格,也就是 k/x, 而这两者之间的价格差距占比则称为滑点,流动性越高的池子滑点越低,因此大部分价格操纵的攻击事件都发生在流动性不足的池子里
另一种AMM模型则称为Balancer,
补充章节
这一章主要介绍一些web3无关的攻击手段, 更接近web2,下面这些内容会在Part2展示