CTF学习笔记 - 区块链
以太坊区块链基础
区块链的用途与设计
实现去中心化的“电子账本”
- 任何人都可以查看、验证、添加账本
- 无需信任任何中心化的机构,依靠密码学算法来保证安全。
将一系列交易打包形成一个区块,广播到网络中
- 通过共识机制保证所有人都认可这个区块是合法的
- 区块保证了交易的顺序、完整性、不可篡改性
智能合约 → 去中心化应用
- 在区块链上运行,所有人都可以查看、验证、调用
以太坊模型全览 - 区块与世界状态
每个区块包含三颗 Merkle 树根节点
- stateRoot 即世界状态树根节点,状态是一组用户状态的组合
区块由“矿工”或“验证者”将交易打包形成,后广播到网络中
- 每条交易会引发世界状态的转变,消耗一定 gas
以太坊模型全览 - 交易与世界状态转变
- 每条交易都会引起状态的转变
- 多个交易打包到一起,最终状态就是新区块存储的状态
- 交易信息中包含 hash/v/r/s 为交易签名,用于验证交易的合法性
- 合约在 EVM 上执行,执行过程中也有各种漏洞
账户
外部账户
有一对公私钥,用于部署交易
- 私钥是随机生成的 256 位数(32 字节)
- 公钥由私钥经过 ECDSA 算法计算而来,是一个 64 字节的数
- 地址由公钥经过 Keccak-256 哈希后取前 20 字节得到
合约账户
- 由 EOA 通过交易创建的账户,其中包含合约代码
- 合约可以存储,拥有以太币
- 向合约账户发送交易 → 调用合约中的函数
- 合约本身不能主动发起交易,但可以在被调用时向外发送交易
共识机制
时刻牢记区块链是去中心化的:
- 所有人都可以广播区块的产生(包含了一系列交易)
- 那么如何保证所有人都认可这个区块是合法的呢?
- 比如篡改旧区块/伪造新区块等,如何防止这些行为发生?
- 再比如由于网络延迟,同时产生了两个区块,该保留哪一个?
一个很简单的共识:
- 即然所有区块都是按照特定顺序并计算 hash 连接的
- 那么我们只要认可最长的一条链即可(如果等长,则继续等)
这就产生了女巫攻击:伪造大量用户,迫使某条链最长,取得信任
工作量证明(Pow)
防止女巫攻击,要限制用户产生区块的能力,一种是工作量证明:
只有完成一定的工作量(比如计算 hash),才能够产生区块
- 同时可以获得区块奖励以及交易手续费
思考以下此时如何伪造区块?
- 计算新的 hash,并维护新的链最长,争取信任
- 注意此时所有的区块都需要工作量
- 所以攻击者需要拥有超过全网 % 的算力(51% 攻击)
Pow 的缺点:计算大量耗电,出块时间不定,51% 算力并非不可能达到
权益证明(Pos)
2022.9.15,以太坊主链完成 The Merge(和信标链合并)
- 完全抛弃了 Pow,转向 Pos 权益证明
Pos 方式:
质押 32ETH(约 78w 人民币)成为验证者
每 12 秒(1 slot)进行:
- 随机选择一个验证者为区块提议者,负责打包
- 随机选择验证者委员会进行投票验证合法性
每 32 区块(1 epoch)进行:
- 对 32 区块范围内进行检查,如果大于 2/3 投票通过,则合法
- 旧的检查的标记为“确定”,新的检查点标记为“合理”
链与交互
关于链与 faucet
公开链:真实的交易
主网(mainnet):真正的金钱交易,很少使用
测试链:Sepolia / Holesky / Ephemery
- 可以使用 faucet 获取免费代币
- https://sepolia-faucet.pk910.de
- Ethernaut 等大型公开合约 CTF 平台会使用
私链:自建搭建的链,模拟真实的链(一般使用 PoA)
- 一般 CTF 题目都使用私链部署,且限制公开性
- 可以通过 geth 等工具部署私链
部署私链
使用 geth 可以搭建私链:
一般使用 Clique 共识(PoA),定期出块,只由授权的签名者出块
利用 geth 创建账户
- geth account new –datadir data
- geth account import –datadir data privatekey-file
配置创世区块 genesis.json
- chainId 随意设置,需要 extradata 提供 PoA 配置
- geth init –datadir data genesis.json
geth –datadir data –rpc 启动私链
geth attach http://localhost:8545 连接本地私链
geth 交互
geth attach http://... 连接 rpc 接口
直接键入 eth 查看所有函数
- eth.chainId(), eth.blockNumber(), eth.getBalance(addr)
geth 通过 JSON RPC 在客户端之间进行交互
- JSON RPC: ethereum docs > JSON RPC
- attach 后的 console 提供了交互功能,来方便发送 rpc
- 可以进行从已认证账户出发的交易的发送等
交易
一条交易包含以下内容:
- from:交易发送者地址
- to:交易接收者地址,如果为空则表示是在创建智能合约
- value:交易金额,即发送方要给接收方转移的以太币数量(wei 为单位)
- data:交易数据,如果是创建智能合约则是智能合约代码,如果是调用智能合约则是调用的函数名和参数
- gasPrice:交易的 gas 价格,即每单位 gas 的价格(wei 为单位)
- gasLimit:交易的 gas 上限,即交易允许执行的最大 gas 数量
- nonce:交易的序号,即发送者已经发送的交易数量
除此之外发送的交易数据包还需要包含:
- hash:交易的哈希值,由前面的内容和 chainId 计算得到
- v、r、s:交易签名的三个部分,由发送者私钥对交易哈希值进行签名得到
以太币单位:https://converter.murkin.me/
公开链上的交易
比如主链、测试链上的交易等,可查看ChainList 通过 rpc 连接,除此之外:
安装 MetaMask 浏览器插件作为钱包
- 可以通过 MetaMask 创建账户(可查看私钥)、导入账户、发送交易等
- 左上角可以切换所在的网络(也可以添加自定义网络,稍后演示)
合约相关复杂交易可以直接利用 Remix IDE 进行交互
- 有 Remix VM 可以作为虚拟环境的测试
- 可以连接到 MetaMask 当前的网络并进行真实的交互
- 编译好合约后可以直接部署以及调用
一般的公开链都有区块浏览器可以查看区块、账户、交易等信息
关于合约
合约的创建和调用都通过交易来进行
合约调用:
- data 字段为编码后的函数名(selector)和参数,称为 calldata
- selector 是函数签名 keccak256 的前四个字节
- 不存在对应 selector 则会调用 fallback 函数,还不存在则 revert
合约存储:全公开存储,都在链上,可以 getStorageAt 查看
revert:回滚,所有当前调用中的状态改变全都复原
合约编译后得到字节码在 EVM 上运行:
智能合约安全基础
Solidity 语言
官方文档:https://docs.soliditylang.org/en/latest/index.html
以太坊官方的编写智能合约的语言
通过 contract 关键字声明一个合约
通过 function 定义一个可以调用的函数
- public、internal、external、private
- 属性(状态)会自动创建 getter 函数
- 通过 view、pure 关键字定义一个不改变状态的函数
通过 payable 关键字定义一个可以接收以太币的函数
特殊函数:constructor、fallback、receive
理论结束,开始做题
Hello Ethernaut
部署题目
点击 Deploy Level
将题目部署到链上

点击获取一个新的实例

解题步骤
在控制台输入 await ``contract.info``()
来调用合约的 info
方法

根据提示调用 info1
方法

根据提示,调用 info2
方法,并将 hello
作为参数

根据提示调用 infoNum
方法来获取该方法中的数字

根据提示,调用 theMethodName
方法

提示了下一个需要调用的方法为 method7123949

继续调用 method7123949
方法,提示需要找到 password 来通过 authenticate()
方法提交

我们在合约中到了了一个名为 password
的方法,进行调用,返回了需要的 password
值

将获得的密码通过 authenticate()
函数提交。

在平台进行提交,显示该题目已通关。

Token - 整型溢出
题目描述
这一关的目标是攻破下面这个基础 token 合约
你最开始有 20 个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好
这可能有帮助:
- 什么是 odometer?
1 | _// SPDX-License-Identifier: MIT_ |
解题步骤
分析合约代码
1 | // SPDX-License-Identifier: MIT |
balances 字典是 uint256 类型,无符号减法有溢出风险
我们给任意地址转账,原余额 20 Token,转 21 个, balances 会变的巨大,使其通过 require 的检查,成功转账。
使用 balanceOf 函数查看 player 用户的余额为 20 Token

我们给任意地址转账 21Token

再次查看 player 用户的余额,发现已经变成一个极大的数

此时已经完成题目要求,直接提交该实例,即可完成题目
