- 发布的
Ethernaut CTF的触发
- 作者
- 姓名
- Tony
- @sodexx7
目录
智能合约的几个方向
ethernaut puzzles 汇总
ethernaut puzzles 案例类型解析
- solidity 语言特性
- 与区块链本身的特性的相关点
- 合约之间的相互调用
- 第三方合约的使用
- gas 消耗攻击
- 业务逻辑的创新与新的可能性
- 合约代理更新机制
- Bytescode 级别
个人看法
其他资料
- Ethernaut 资料
- 相关链接
1 智能合约编程的几个方面
自从开始接触智能合约,有关智能合约编程的几个方面一直让我印象深刻,1 是安全,这可以从以太坊经典事件 DAO 攻击以及此起彼伏的链上黑客攻击体会安全性在整个行业的重要性;2 是 gas 优化,对于 gas 优化可能现在由于不是牛市,以及 layer2 层逐渐完善导致的 gas 费降低,对这方面的关注在变淡,但我认为 gas 是基础,某些情况下其重要性可能会凸显,我猜想可能类似于量化投资中对于速度的要求,如果未来随着用户量的指数级上升,将会有可能再次引起人们对于 gas 的关注,另外一方面,智能合约每一步的运行最终都会转化为 opcode,而每一个 opcode 都会消耗一定量的 gas,这是整个 EVM 的运行的基础,也是了解以太坊核心的基础。3 则是智能合约之间的复杂调用关系,很多情况下随着业务复杂度的上升其调用关系也变得复杂,甚至有点扑朔迷离。我这里面所指的是 CA 合约 CA 合约之间的调用关系。
CA(Contract account): 该类型合约由代码控制。
EOA(Externally-owned account):该类型合约由拥有私钥者控制。
我们在看到单个智能合约本身往往是静态的变量与方法逻辑阐述,而涉及到很多复杂合约之间的相互调用关系时,往往则比较复杂。比如如下的合约之间的相互调用。
智能合约之间的调用关系往往也是很多黑客事件主要的发生源,如经典的重入攻击。
4 则是基于智能合约基础之上可能展现的新的业务特性,我们知道智能合约本身就是建立在区块链本身的透明,不可篡改,永久持续运行的特点上,基于这些特点还有智能合约本身的特性将会使其与过往编程以及思考方式产生很多不同,比如基于开放式的基础设施上的开发,测试,以及以链上合约为基础单元的思考模式,比如如何更新链上合约的逻辑... 我认为这种新的环境或者方式需要逐渐去适应与揣摩,借鉴别人的思考去理解更多的可能性。
2. Ethernaut puzzles 汇总
基于如上的理解,我一直尝试让自己去适应以及感受这与传统编程方式有何不同?而 openzeppelin 部署的 ethernaut CTF,则是一个很好的测试自身以及增强自己对于此的理解。
如下是基于个人理解的对于 ethernaut CTF 的汇总
说明: 如下的 puzzles 案例类型介绍并不是针对每一个 puzzle 从头至尾的分析与代码演示,我认为已经有很多人写的很详细了。同时不少 puzzles 并不是那么容易解决的,其中不少 puzzles 我也是花了好几个小时才慢慢找到思路,甚至有些还需要借助网上别人提供的思路。我个人觉得如果是对初次接触智能合约的人来说,这可以算作一个提前性的参考,随着逐渐深入,如下提及的很多方面肯定都会遇到;如果是对智能合约有一定了解甚至是高阶的合约开发者,我觉得如下的内容可以算作是一个梳理性的操作,我认为其中的一些方面在未来还会继续衍化,某些方面我的描述可能过于压缩与不准确,欢迎讨论。
3. Ethernaut puzzles 案例解析
ethernaut CTF 主要以 puzzles 的形式去展示安全问题的各种情况,一方面可以据此去感受与实践在智能合约的编程中的潜在的安全问题; 二虽然呈现方式是安全问题,但据此可以在很多其他方面增强一定程度的理解,如 solidity 语言本身的特性,建立在区块链基础上的所衍生的问题,基于智能合约的业务设计;三则是建立一定的熟悉度,并且体会这种新架构下的与传统 web2 开发方式的不同。
这里仅仅就如下几个方面进行具体说明:
1.我个人在解决过程中碰到的难点问题
2.我觉得具有一定代表性的问题, 并且未来肯定会继续遇到相关的问题。如 solidity 语言特性。
3.生态发展肯定会涉及的一些事项,如代理更新机制。
3.1 solidity 语言特性
1.overflow/underflow check
如针对于 unit256 类型的加减运算,如果没有进行校验,将会导致溢出。 可参靠 Token puzzle 。 不过 solidity 在 0.8 之后已经默认加了 overFlow check。
2. fallback
如果调用一个智能合约,但是调用的方法没有匹配到智能合约的任何方法或者收到 ETH 时但是合约中没有定义 receive 方法而是存在 Fallback,则会默认调用 Fallback 方法。根据此特性,Fallback 在不少特殊场景下都会用到,如重入攻击时,攻击者地址在收到 eth 之后,可以继续在 Fallback 方法中继续调用被攻击地址的合约。又比如涉及到代理机制时,业务逻辑合约只负责逻辑,但是并不存储最终的业务数据,这时如何调用业务逻辑合约的方法,就是通过代理合约中的 Fallback 方法进行中转,使用 deleagteCall 调用业务逻辑合约的方法。
3.Customer error
Good Samaritan案例中涉及到在调用中合约中捕获异常,但是抛出的异常对于捕获异常的合约却是无法确定是谁抛出的。
4.selfdestruct 方法
该方法的作用是销毁一个智能合约,同时将对应的 eth 发送到指定的合约上。其设计初衷是为了鼓励节省 gas 费。但是该方法同时却造成了任何一个合约都无法保证自己有能力不接收 eth。如果将自己的合约逻辑建立在自身的 eth 余额上,如 address(this).balance == 0。 该漏洞将会可能被黑客利用。 最新的 solidity 版本中并不建议使用,https://eips.ethereum.org/EIPS/eip-6049,
3.2 与区块链本身的特性
1.透明性
在 solidity 中定义变量可见性时,有 public,private,internal. 但是将变量定义为 private 并不意味着该变量并不是不可见的,只是针对链上其他合约是不可见的,依然有方法可以获取其对应 slot 的值,如根据 ethers.js 提供的方法 provider.getStorageAt( addr , pos [ , blockTag = latest ] ) 。 可参考Vault ,Privacy 。
当然如果要实现链上存储加密信息可使用零知识证明,即可以证明自己知道该隐私参数,但是却不会泄露该隐私参数。
2.随机数
由于链上的数据, 即便是被定义为 private 的变量也是可见的,同时矿工可以控制一些数据如区块哈希,时间戳,是否包含某个交易,导致直接链上生成随机数是存在隐患及不安全的,这个时候可以通过引入链下随机数来实现,比如 chainlink。 对应案例可参考Coin Flip 当然有时智能合约需要的不仅仅是随机数,还需要更多业务数据,比如股票价格,这些也可以通过类似于 chanlink 的第三方来获取。
而自从 Ethereum merge 之后,新的 opcode:PREVRANDOA, 也会生成随机数并且比 blockhash 由更强的随机性。
3.地址生成
可以通过 keccak256(address, nonce)来生成地址。具体可参考https://swende.se/blog/Ethereum_quirks_and_vulns.html. 这样将导致给一个没有私钥的合约发送 eth,但是却可以用上述方法去获得这些 eth。可参考案例Recovery。
4.合约状态存储数据结构
合约状态存储数据结构如下。合约中有 2^256 个 slot,每个 slot 中存储的最大长度为 bytes32.
动态数组存储的结构如下所示
参考案例Alien Codex
Solidity 如何存储动态数组的值?,其对应的合约中的 slot 位置存储的是动态数组的长度,但是接下来存储动态数组的值并不是按照 slot 递增进行存储,而是根据该 slot 的值获取对应的 keccakHash 值,其对应的动态数组的第一个值存储的位置,动态数组后续的值则在此值后面依次排列。如图所示:
bytes32[] public codex 状态值存储在第三个 slot 值。此时对应的 slot-2 值存储的的是 codex 数组的长度。
数组中第一个值存储的位置则为 location0 = keccak256(abi.encode(2))。 依次类推第二个值存储的位置则为 location1 = location0 +1。然后根据 sload(location)获取到对应的 codex 中的每一个值。
案例 Alien Codex 解决该 puzzle 则是需要更改该合约的 owner 地址为我们自己的合约地址。
如上合约创立的时候,默认已经设定好 owner 地址, 如下所示第一个 slot 的值则为该合约的 owner 地址。
可见,只要将 slot0 的位置更改为自己的地址,即可成为该合约的所有者。
起初我的想法则是,要使得 keccakHashde(abi.encode(index))的值为 0,那么对应的 index 值应该是多少?但是找了半天没有找到。
此时对于动态数组如何存储的规律就可以派上用场了,既然 codex[0]值对应的 slot 位值已经知道了,同时整个合约存储的最大 slot 值为 2^256, 那么 slot[2^256- array[0]]则就会对应合约中最后一个 slot 存储的位置 2^256 的值。
假设此时 codex 动态数组长度为的最大值为 2^256。那么只要将 codex[ 2^256- array[0]+1]的值设置为我们的想要的合约地址,那么此时合约中的第一个 slot 对应的值则为我们想要的地址,此时即对该合约拥有了所有权。
至于如何将 codex 动态数组长度为的最大值为 2^256,不再此赘述,可参考如下方法。
3.3 合约之间的调用
- 合约与合约之间的基本调用
我将智能合约的调用主要实现功能分为两大类,一类是发送 ETH,一类是链上合约调用逻辑实现。当然两者可以结合起来,在实现链上智能合约逻辑时同时发送 ETH.
关于发送与接收 ETH,在 solidity 语言特性已经提到了 destroy,Fallback。关于 ETH 发送的方法的如何选择历史上也是经历了一番讨论。如 send,transfer,call 几个方法都可以发送 eth, 但是现在只是推荐使用 call,同时需要防止重入攻击。
使用 send,transfer 时,对应的 gas(2300)是固定的,由于实际区块消耗的 gas 费并不是一成不变的, 方法调用中也并不适合固定 gas 费.参考(https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/)
可参考案例 King可参考案例 King
合约与合约之间的调用底层对应的调用方法有 call,staticcall,delegateCall
call 可以直接调用被调用合约的方法,delegateCall 也是直接可调用被调用合约的方法,但与 call 明显的一点不同则是 delegateCall 会修改调用合约方的 state。由于 delegatecall 常用于代理更新机制方面,此处放在代理及更新机制进行说明。staticcall 则是指再调用其他合约方法时并不改变整个区块的的状态值。
2. 合约调用中的变量变化
tx.orgin 则是指一次合约调用中的初始方,经常则是 EOA 合约。
msg.sender:则是在整个合约调用链条中,被调用合约的调用方合约地址。
可参考案例 Telephone
合约调用可以是 EOA,也可以是 CA。而在 CA 中定义攻击逻辑也是常用的手段。
3. 业务逻辑依赖
涉及案例 Elevator,shop,Re-entrancy
业务逻辑依赖所导致的问题可能是最会频繁遇到的问题,无论是调用方还是被调用方的合约可能是 CA 合约,并且在 CA 合约中可以自定义任意逻辑,如果合约的相关业务逻辑依赖于外部,并且没有最好一定程度的校验,那么则是潜在的隐患。
案例 Elevator
其中 ! building.isLastFloor(_floor) 的实现依赖于外部调用方的合约逻辑,则攻击者则可以控制两次调用时的判断结果。
案例Shop
_buyer.price() 的逻辑,在攻击者合约中也可以进行控制。
而在实际的链上合约复杂的调用关系,这种依赖关系可能会更普遍也更复杂,有时想要发现其中的问题也不是一件容易的事情。
案例Re-entrancy 经典的重入攻击
当 msg.sender.call{value:_amount}(""); 调用方可以在自身的合约中再次调用该合约,这样会导致递归调用,
形如:合约(caller)B=》合约(called)A=》合约 B(caller)=》合约 A(called)... 从而提取所有合约 A 中的 eth。
如下是攻击合约提取完被攻击合约所有的 ETH 的示例。
攻击合约 A:0x.........d653666d
被攻击合约 B:0x.........90ac810
可以看到如下的递归调用关系: 合约 A => 合约 B =>合约 A=> 合约 B...
链接地址
3.4 第三方合约使用
第三方如 openzeppelin 提供的标准库有很多现成可用的合约可以拿来即用,但是如果缺乏一定的了解,有可能导致某些方面的隐患。
Naught Coin
可以看到针对 transfer 方法,其有 LockToken 进行限制,但是集成的 ERC20 而言,发送 ETH 不仅仅有 transfer 方法,还有 transferFrom()方法,如果攻击方直接调用该方法,则可以绕过 LockToken 的限制。
ERC20.sol(openzeppelin)
3.5 gas 消耗攻击
gas 消耗攻击也可以理解为合约逻辑依赖于外部逻辑,只不过这里指的是外部合约消耗完此次调用所有的 gas。
案例Denial
在 withdraw 中方法中,其中有调用 call 但是并没有指定使用多少 gas,如果攻击者合约此时消耗掉所有的 gas,那么该 withdraw 方法将永远不可能完成。
3.6 业务逻辑的创新与新的可能性
在我看来 defi 起初建立的公式基础则是 x*y=k。 其中 x 为 tokenA 的数量,y 为 tokenB 的数量,k 则是常数。tokenA 与 tokenB 的之间的价格则是基于这个公式进行变化推算出来,如 tokenA/tokenB 的价格则为:y/x。
但是如果仔细考虑一些情况,则有潜在的风险。
如案例Dex中可以看到如下代码:
该 puzzled 的要求则是只要可以完全提取 tokenA 或 tokenB 在其中任意一个在池子中的数量即可。由于池子中 tokenA 或者 tokenB 的数量我们可以去调节,从而影响其价格,只要价格达到可以完全提取完任意一种 token 的数量即可。
如下所示,可以通过 swap 来调节 tokenA 与 tokenB 池子中的数量,当我将池子中 tokenA/tokenB 数量比为 110/45 时,同时当前自己又拥有超过 45tokenB 时,此时则可以将全部 tokenA 通过 45 个 tokenB 全部置换出来。
如上图所示,我总计进行了 5 次 swap,通过第 5 次 swap,将池子中 tokenA 与 tokenB 的调整为 110/45. 此时我只要需 45 个 tokenB 即可兑换处所有的 tokenA。
由此可见,单纯依靠池子的数量来定义价格,可能会被第三方控制,这时可以借助第三方的价格信息如 chanlink 的提供的价格信息,以避免价格被控制。
又比如案例Dex2,其目的是将两个 token 都全部提取完,dex2 在 dex 的基础上只是省去了对于交易对 token 地址的校验。具体解决时感觉有些搞笑了。
此时我们可以构建自己新的交易对,比如构建 tokenC,然后存入 dex2 合约地址 10 个 tokenC,此时 TokenC 转换成 tokenA 的的比率则为 100/10,可以直接通过 10 个 tokenC 获得 100 个 tokenA。同理通过 20 个 tokenC 可以获得 100 个 TokenB.
3.7 合约代理更新机制
由于链上合约代码的不可更改特性,但是某些情况下需要升级代码功能,如何处理?此时代理更新机制便应运而生。代理更新机制简单讲就是是在不改变原先代码的情况下去升级代码本身。简单来讲,代理更新机制涉及两个合约,一个是代理合约(proxy contract),其用来存储合约运行的最终状态,另一个是业务逻辑实现合约(implemention contract),合约的业务调用逻辑都通过该合约来实现。
当前代理更新机制分为两类 Transparent 与 UUPS. 其主要区别是升级合约时前者是通过与代理合约(proxy)交互来实现,后者是通过与业务逻辑合约(implemention contract)交互来实现.
如案例Puzzle Walle,其是 Transparent 类型,代理合约权限修改及业务逻辑升级都是通过与代理合约交互来实现。该 puzzle 最终的实现是通过获取代理合约的所有者。
其代理合约为 PuzzleProxy, 业务逻辑合约 PuzzleWallet.
可以看到
代理合约 PuzzleProxy,slot0,与 slot1 的分别存储 pendingAdmin,admin.
业务逻辑合约 PuzzleWallet,slot0,与 slot1 的分别存储 owner,maxBalance
可见
业务逻辑合约(PuzzleWallet)-slot0:owner =>代理合约(PuzzleProxy)->slot0:pendingAdmin.
业务逻辑合约(PuzzleWallet)-slot1:maxBalance =>代理合约(PuzzleProxy)->slot1:admin.
此时只要将 maxBalance 设置为我们的的合约地址,即拥有了 PuzzleProxy 合约的所有权。接下来可以通过对 multicall 以及 deposit 方法的使用提取完代理合约中所有 eth,最后执行 setMaxBalance(uint256 _maxBalance)方法修改为我们想要的合约地址。
具体如何利用 multicall,deposit,setMaxBalance,网上现在已经有很多人写的很详细了,在此不再赘述。
这里所展示的是使用代理升级机制时一个不容忽视的注意事项,即代理合约负责存储最终状态,但是执行业务逻辑合约时需要注意业务合约的合约变量状态不能与代理合约的变量状态相违背。
https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies
案例 Motorbike
Motorbike 使用的则是 UUPS 类型. 这样合约更新可以直接通过业务逻辑合约来实现,同时也减少了原先部署代理合约的 gas 费。这里解决该 puzzle 是让其业务逻辑合约 Engine 不能正常使用。
代理合约为 Motorbike,业务逻辑实现合约为 Engine.并且可以通过调用 upgrateToAndCall 的方法来进行业务逻辑合约的更新或者升级。
但是这里的问题 Motorbike 初始化业务逻辑合约 Engine 时,业务逻辑合约 Engine 本身的 initialize()方法未执行,从而其对应的 horsePower 以及 upgrader 的状态也未更新。
从该点出发,无论是谁调用了 Engine 合约地址的 initialize()方法,即可成为 Engine 合约的所有者。那么也就意味着可以调用 Engine 合约的 upgradeToAndCall()方法,从而可以破坏 Engine 业务逻辑合约。
如下图所示,此时创建一个合约其包含销毁自身的方法(killMySelf), 如前所示我们当前可以直接调用 upgradeToAndCall()方法,让 Engine 合约直接调用该 Hacker address 的 KillMySelf,于是 Engine 最终会销毁自身。
回到开始的 motorbike,由于其对应的业务逻辑合约已经销魂,故该代理合约已经无法调用任务方法。
可见 UUPS 带来更好的灵活性同时,但使用时也要谨慎。
可参考如下讨论
ethernaut.openzeppelin
而针对https://ethernaut.openzeppelin.com/ 其部署合约时则用的 TransparentUpgradeableProxy 模式,业务逻辑的合约升级或者更新直接通过代理合约来实现。当我完成所有 puzzles 的时候,想要通过合约直接查询历史交互数据,一开始是直接通过业务逻辑合约的 abi 接口来访问业务逻辑合约的合约地址,大家知道实际业务数据是存在的代理合约中的,所以一开始怎么也查不到数据,当我意识到 ethernaut 是通过 TransparentUpgradeableProxy 来部署合约时,通过业务逻辑合约 abi 的接口直接访问代理合约的地址,于是数据就查出来了。如下
TransparentUpgradeableProxy
3.8 Bytescode 级别
链上存储的合约代码实际一串 16 进制字符串。实际运行时 EVM 根据预先设定好的操作规则,在堆栈中执行其对应的 opcode,同时修改 memory 或者 storage 中的数据状态,从而完成该笔 tx。
链上存储的合约代码
案例 MagicNumber
而针对于 MagicNumber puzzle,其涉及则是对于 EVM 如何处理 bytescode 级别的数据。对于合约的理解程度达到 bytescode 的级别是合约编程高阶的必然要求,很多情况下都会涉及这方面的操作,比如通过 Yui 或者 assembly 直接操作 memory,再很多头部协议的代码中如 uniswap 都可以看到。
具体可参考如下链接
4. 个人看法
该领域这两年获得不小的发展,如第三方工具包或者工具的完善,如大家常常用到的 openzeppelin,chainlink,还有开发环境工具 hardhat,foundary. 在我尝试完成上述 puzzles 的时候,ethnaut 经常会提供一些更多的参考资料从而可以了解更多、更深入的这个领域的信息。如关于合约代理更新机制,如下讨论的时间基本都是在两年之内。
比如DoubleEntryPoint 中提到了 forta,其由分布式网络的节点组成,扫描区块信息,并且可以监控对应的链上事件,并且当需要时,如发现潜在的风险事件可以同步告诉订阅者,并且触发订阅者一些自定义方法的执行。这一切都发生在链上。我的理解这是链上基础设施。与此类似基础设施如 thegraph,通过对链上合约方法或者相关数据建立索引,方便开发者快速查询。
又比如对于安全事故的解析
这些散落在不同社群中的讨论,以及不断出现的新的工具或者底层设计,或者是整个社群中不断有人总结梳理的最新有价值的分享或内容,是个人跟进这个领域发展的核心源头,也是提高自己相应技能栈不可或缺的养料。
5.其他资料
1. ethernaut 资料
ethernaut,github 地址
ethernaut 提供的解决办法
CTF 创建 puzzle 及提交 puzzle 流程图
挑战者每次创建对应的 puzzle 时,都会通过 ethernaut 合约调用对应的 level 工厂从而创建对应的实例。同时初始化对应的挑战者及挑战的 puzzle 数据。
每次提交时,都会通过 ethernaut 合约调用 level 工厂去检查对应的实例是否已经解决,如果解决同步相应统计数据。
业务逻辑合约代码 Statistics.sol(https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/contracts/metrics/Statistics.sol);对应的链上地址https://goerli.etherscan.io/address/0x7000e0f2f5a389df14b50c6f84686123f19b27f6#code
代理合约 ProxyAdmin.sol:(https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/contracts/proxy/ProxyStats.sol) ;对应的链上地址https://goerli.etherscan.io/address/0x7ae0655F0Ee1e7752D7C62493CEa1E69A810e2ed#code.
2.相关链接 1. https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7/ 2. https://blog.ethereum.org/2016/12/05/zksnarks-in-a-nutshell 3. https://ethernaut.openzeppelin.com/level/0x573eAaf1C1c2521e671534FAA525fAAf0894eCEb 4. https://medium.com/@dariusdev/how-to-read-ethereum-contract-storage-44252c8af925 5. https://ethernaut.openzeppelin.com/level/0xb4B157C7c4b0921065Dded675dFe10759EecaA6D 6. https://weka.medium.com/announcing-the-winners-of-the-first-underhanded-solidity-coding-contest-282563a87079 7. https://ethernaut.openzeppelin.com/level/0x9CB391dbcD447E645D6Cb55dE6ca23164130D008 8. https://ethernaut.openzeppelin.com/level/0x9CB391dbcD447E645D6Cb55dE6ca23164130D008 9. https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca 10. https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680 11. https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680 12. https://ethernaut.openzeppelin.com/level/0x9451961b7Aea1Df57bc20CC68D72f662241b5493 13. https://ethernaut.openzeppelin.com/level/0x9451961b7Aea1Df57bc20CC68D72f662241b5493
14. https://blog.openzeppelin.com/compound-tusd-integration-issue-retrospective/ 15. https://swende.se/blog/Ethereum_quirks_and_vulns.html 16. https://compound.finance/governance/proposals/76 17. https://blog.soliditylang.org/2021/04/21/custom-errors/ 18. https://medium.com/loom-network/ethereum-solidity-memory-vs-storage-how-to-initialize-an-array-inside-a-struct-184baf6aa2eb
19. https://samczsun.com/
20. https://docs.alchemy.com/reference/alchemy-simulateexecution