# 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 等信息。

image-20220120205753648

在连接上 docker 后,给出了四个选项以及题目要求:

题目要求:让 isSolved () 函数返回 true

四个选项:

  1. 创建一个用于该题目的账户。
  2. 用步骤 1 的账户部署题目。
  3. 达成目标后获取 flag。
  4. 显示合约源代码。

# 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。