# Blockchain 解题入门
写在前面:近年来 Blockchain 在 CTF 比赛中的出现频率越来越高,并且相比于其他方向,Blockchain 在解题入门方面的教程还比较少,对于新手来说可能连题目都看不懂,本文以 Chainflag 平台为例,简单介绍了 Blockchain 方向题目的解题步骤。
智能合约语言目前以 Solidity 为主,除此以外,还有 Vyper、Mandala 和 Obsidian 等在不同方向改善智能合约的语言。
# 一些工具
这里主要写一下解题时需要用到的一些工具。
# IDE
Solidity 网站上有编译器的安装教程:https://docs.soliditylang.org/en/develop/installing-solidity.html
推荐使用 REMIX:http://remix.ethereum.org/,可以在浏览器中快速部署测试智能合约,不需要在本地安装任何程序,也是以太坊官方推荐的智能合约开发 IDE。
# REMIX 主界面
# 左侧 REMIX logo 下方第一个选项是 workspaces,可以建立多个工作区,在工作区内可以新建合约。
# logo 下方第二个选项是编译,可以选择语言及版本,合约中的语法错误也会显示在这里。
# 第三个选项是部署和执行,在代码编译完成后,可以在这里部署合约,也可以和已部署的合约进行交互。
ENVIRONMENT:默认使用 JS 虚拟机本地模拟一条测试链,如果想要连接测试网,选择 Injected Provider。
ACCOUNT:当前使用的账户,如果是默认的 JS 虚拟机会分配数个有大量测试币的测试账户,如果环境是 Injected Provider,可以在 MetaMask 中切换账户。
GAS LIMIT:合约的 gas 上限。
VALUE:转入多少 ETH。
CONTARCT:一个文件中可能包含多个合约,这里选择具体部署哪一个合约。
At Address:如果合约已经部署,可以在这里输入该合约的地址进行交互。
# 钱包
推荐使用 MetaMask,chrome 浏览器上的一个插件。
记住要保存好 MetaMask 的解锁密码。
# 新建账户
MateMask 上创建账户非常简单,点击右上角的用户栏,选择新建账户,随意输入一个名称即可。
# 如何获取以太币
在新建一个账户后,我们的账户中是没有 ETH 的,需要从水龙头中获取 ETH。
https://goerlifaucet.com/
每天可以获得 0.25ETH
# 网络切换
本次比赛题目部署在 Goerli 测试网络上,如果在其他测试网络上,可以在 MetaMask 的右上角进行网络切换。
# 反编译器
合约其实也跟程序差不多,可以反编译,也可以进行调试,这里推荐一个在线反编译合约的网站。
https://ethervm.io/decompile
# 可以得到合约反编译出来的代码,以及合约中的函数或变量。
# 区块链浏览器
区块链浏览器是专门用来查看链上的各种信息的工具,可以检索的信息包括但不限于:账户地址、合约地址、交易 Hash 等。
查询账户地址可以看到当前余额,近期交易等信息。
# 解题流程
以 chainflag 平台为例,题目会给出题目名称、题目描述、题目 Docker 等信息。
在连接上 docker 后,给出了四个选项以及题目要求:
题目要求:让 isSolved () 函数返回 true
四个选项:
- 创建一个用于该题目的账户。
- 用步骤 1 的账户部署题目。
- 达成目标后获取 flag。
- 显示合约源代码。
# 1. 创建题目账户
这里我们先选择 1,创建一个账户:
创建完成后,返回给了我们:
账户地址,用于部署题目合约。
token,用于选项 2 部署合约以及选项 3 获取 flag。
同时提示我们需要向该账户转一定量的 ETH,转入的 ETH 会用于部署合约,需要注意的是,这里的数值是当前部署合约的预估价,根据实时 Gas 价格不同可能会有浮动(不够就多转点)。
# 2. 部署题目合约
由于 docker 执行完 1 后不会自动断开连接,所以手动断开 docker 并重新 nc。
接着我们选择 2,部署题目合约:
这时会要求我们输入 token,也就是选项 1 所给的 token。
在部署完成后,返回给了我们:
合约地址,即部署的题目合约。
部署合约的交易 hash。
当执行完选项 2 后,题目就算部署完毕了,接下来就需要在 REMIX 上编写攻击合约对题目进行攻击,以实现题目要求,获取 flag。
# 3. 编写攻击合约
在我们编写攻击合约时,需要调用题目合约中的函数,比如 isSolved()
。
和传统语言一样,我们在调用某个函数时,需要先有该函数的定义。因此,我们需要得到题目合约的源码,一般情况下题目都会给出源码,也就是上面的选项 4:
而对于那些没有给出源码的题目,我们就需要通过反编译工具来逆出题目合约的逻辑,来复写代码。
pragma solidity 0.8.7; | |
contract Greeter { | |
string greeting; | |
constructor(string memory _greeting) public { | |
greeting = _greeting; | |
} | |
function greet() public view returns (string memory) { | |
return greeting; | |
} | |
//setGreeting 可以接受一个 string 类型的参数,并把这个参数的值赋给 greeting | |
function setGreeting(string memory _greeting) public { | |
greeting = _greeting; | |
} | |
function isSolved() public view returns (bool) { | |
string memory expected = "HelloChainFlag"; | |
return keccak256(abi.encodePacked(expected)) == keccak256(abi.encodePacked(greeting)); | |
} | |
} |
得到题目合约的源代码后,分析一下函数,有 greet()
、 setGreeting()
、 isSolved()
三个函数。
题目要求让 isSolved()
返回 true,就需要让 keccak256(abi.encodePacked(expected)) == keccak256(abi.encodePacked(greeting))
,也就是 expected==greeting
,而题目合约中使 expected = "HelloChainFlag"
,那么我们只要修改 greeting
为 "HelloChainFlag"
,就可以实现题目要求。
而题目合约中刚好存在可以修改 greeting
的函数 —— setGreeting()
。那么我们只需要调用 setGreeting()
并修改 greeting
,然后再调用 isSolved()
,就算完成题目要求。
那么下面开始编写攻击合约:
pragma solidity 0.8.7; | |
// 因为我们的攻击合约需要用到题目合约的函数,所以这里直接照搬题目合约的代码,用来提供定义。 | |
contract Greeter { | |
string greeting; | |
constructor(string memory _greeting) public { | |
greeting = _greeting; | |
} | |
function greet() public view returns (string memory) { | |
return greeting; | |
} | |
function setGreeting(string memory _greeting) public { | |
greeting = _greeting; | |
} | |
function isSolved() public view returns (bool) { | |
string memory expected = "HelloChainFlag"; | |
return keccak256(abi.encodePacked(expected)) == keccak256(abi.encodePacked(greeting)); | |
} | |
} | |
contract exp { | |
// 首先要确定需要攻击的合约,因此这里填上之前选项 2 部署的合约的地址 | |
address instance_address = 0x6bed92ec6DD3363ED64E27A669C24880a79A56c3 ; | |
// 这里把题目合约地址和模块简化一下,后面调用时直接用 target 就可以了 | |
Greeter target = Greeter(instance_address); | |
constructor()payable{} | |
// 在攻击合约里定义一个 hack 函数,public 说明该函数可供外部、子合约、合约内部访问。后面的 returns 则是定义该函数的返回值类型 | |
function hack() public returns (bool){ | |
bool answer = false; | |
// 定义一个 string 类型的变量 greeting 并赋值为 "HelloChainFlag",memory 表示数据储存在内存中,并不会储存在链上,可以节省 gas。 | |
string memory greeting = "HelloChainFlag"; | |
// 调用题目合约的 setGreeting 函数,并将 greeting 作为参数传输过去 | |
target.setGreeting(greeting); | |
// 调用题目合约的 isSolved 函数,在执行完上一条语句后,题目合约中的 expected 就会等于 greeting,因此 isSolved 会返回 true,达成题目要求。 | |
answer = target.isSolved(); | |
return answer; | |
} | |
} |
# 4. 部署攻击合约并进行攻击
编写好攻击合约后,我们要先编译攻击合约,在编译时要选择好对应的 Solidity 版本。
前面提到一个 sol 文件中可能会存在多个合约,因此这里选择攻击合约(exp)。
在部署完成后,左边的 Deployed Contracts
下会出现我们的攻击合约,我们将他展开:
名为 hack 的黄色按钮就是我们攻击合约中的 hack 函数,点击按钮就会执行一次 hack 函数,当然,因为它和已经上链的题目合约产生了交互,所以需要花费 gas。
我们点击 hack,MateMask 会弹出一个支付窗口用于核对交易金额及手续费。
确认支付后,可以在下方看到执行状态,出现绿勾说明函数执行成功,点击绿勾可以查看详细信息,同时我们也可以在区块链浏览器中查到我们这一笔交易的信息。
# 5. 获取 flag
因为我们成功使 isSolved 函数返回 true,完成了题目的要求,所以我们重新连接 docker,选择选项 3,还是输入之前的 token,在验证完成后就会给我们回显 flag。