以太坊区块链基础

区块链的用途与设计

实现去中心化的“电子账本”

  • 任何人都可以查看、验证、添加账本
  • 无需信任任何中心化的机构,依靠密码学算法来保证安全。

将一系列交易打包形成一个区块,广播到网络中

  • 通过共识机制保证所有人都认可这个区块是合法的
  • 区块保证了交易的顺序、完整性、不可篡改性

智能合约 → 去中心化应用

  • 在区块链上运行,所有人都可以查看、验证、调用

以太坊模型全览 - 区块与世界状态

每个区块包含三颗 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

公开链:真实的交易

私链:自建搭建的链,模拟真实的链(一般使用 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 在客户端之间进行交互

交易

  • 一条交易包含以下内容:

    • 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

  • 以太坊官方的编写智能合约的语言

  • IDE:https://remix.ethereum.org/

  • 通过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_// SPDX-License-Identifier: MIT_
pragma solidity ^0.6.0;

contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;

constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

解题步骤

分析合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// SPDX-License-Identifier: MIT  
pragma solidity ^0.6.0;

contract Token {
// 定义一个映射(类似字典),记录每个地址的代币余额
mapping(address => uint256) balances;

// 记录代币总供应量,public表示会自动生成一个查询函数
uint256 public totalSupply;

/* 构造函数:在合约部署时执行一次
* 参数 _initialSupply:初始代币供应量
*/
constructor(uint256 _initialSupply) public {
// 将初始供应量分配给合约部署者(msg.sender)
balances[msg.sender] = totalSupply = _initialSupply;
// 等同于:
// totalSupply = _initialSupply;
// balances[msg.sender] = _initialSupply;
}

/* 转账函数:从调用者地址转出_value个代币到_to地址
* 参数 _to:接收地址
* 参数 _value:转账数量
*/
function transfer(address _to, uint256 _value) public returns (bool) {
// 漏洞点1:使用减法检查余额是否足够
// 问题:如果 balances[msg.sender] < _value,减法会下溢(得到极大值),但require检查会通过!
require(balances[msg.sender] - _value >= 0);

// 漏洞点2:直接扣除余额,未检查是否足够
// 如果 _value > balances[msg.sender],这里会导致下溢(余额变为极大值)
balances[msg.sender] -= _value;

// 给接收地址增加代币
balances[_to] += _value;

return true; // 返回成功
}

/* 查询余额函数:返回指定地址的代币余额
* 参数 _owner:要查询的地址
*/
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner]; // 直接从映射中读取余额
}
}

balances 字典是 uint256 类型,无符号减法有溢出风险

我们给任意地址转账,原余额 20 Token,转 21 个, balances 会变的巨大,使其通过 require 的检查,成功转账。

使用 balanceOf 函数查看 player 用户的余额为 20 Token

我们给任意地址转账 21Token

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

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