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