以太坊区块链基础

区块链的用途与设计

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

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

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

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

智能合约 → 去中心化应用

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

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

每个区块包含三颗 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 用户的余额,发现已经变成一个极大的数

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