智能合约安全前传-基础知识入门

2018-07-16 10,018

本文综述

本文将简述以太坊、智能合约的基础知识,以及如何搭建私链和编写、部署一个有漏洞的智能合约。

 

以太坊简介

以太坊是一个开源的区块链平台,以太坊的模块结构和比特币类似,和比特币最大区别就在于,用户可以在以太坊平台上随意的开发属于自己的去中心化智能合约。 
以太坊常用的几款工具: 

  1. go-ethereum:官方的Go语言客户端,客户端文件是geth。这是使用最广泛的客户端,类似于比特币的中本聪核心客户端,可用于挖矿,组建私有链、管理账号、部署合约等 

  2. cpp-ethereum:与上面实现功能一样,不过是c++实现 

  3. Mist 客户端 :mist目前是钱包客户端,未来定义为一个DAPP市场交易客户端,类似于苹果市场

  4. Solidity:智能合约编程语言,下一章节会详细介绍 

  5. Remix:智能合约的开发IDE,在线地址 https://remix.ethereum.org

以太坊里还有个不同于比特币的概念Gas。EVM (Ethereum Virtual Machine, 以太坊虚拟机)在执行合约代码时,每一步执行都会消耗一定 Gas,Gas 可以被看作是能量,一段代码逻辑可以假设为一套 “组合技”,而外部调用者在调用该合约的某一函数时会提供数量一定的 Gas,如果这些 Gas 大于这一套 “组合技” 所需的能量,则会成功执行,否则会由于 Gas 不足而发生 out of gas 的异常,合约状态回滚。那为什么需要加入gas这个概念呢?因为智能合约是一个图灵完备的语言,加入Gas,可以避免无限循环和拒绝服务攻击 。

 

智能合约简介

智能合约是一种以信息化方式传播、验证或者执行合约的计算机协议,能够允许在没有第三方的情况下进行可信的交易,并且这些交易是无法被追踪、同时也是不可逆的。 
Solidity 是一门面向合约并且图灵完备的编程语言(还有其他几种语言可以编写智能合约,但是目前使用最广泛的就是 Solidity 语言)。Solidity是个编译型语言,需要编译后运行在EVM(Ethereum Virtual Machine, 以太坊虚拟机)上。 
每一个合约账户中的代码都是一个 Contract,它与面向对象编程中类的概念非常类似,无论是合约还是类都可以有变量和函数,但是类是可以实例化的,而合约并没有实例化这一功能,它的变量和函数可以直接在合约本身上访问或者调用。

以太坊中有两类账户,它们共用同一个地址空间。 

  1. 外部账户,该类账户被公钥-私钥对控制(人类);外部账户的地址是由公钥决定的。 

  2. 合约账户,该类账户被存储在账户中的代码控制;合约账户的地址是在创建改合约时确定的(这个地址由合约创建者的地址和该地址发出过的交易数量计算得到,地址发出过的交易数量也被称作"nonce") 

 

Solidity语法总结

基本结构

  • 合同(contract)声明:合同类似于面向对象语言中的类(Class)

contract HelloWord { }
  • 状态变量(State variable)声明:状态变量是永久存储在合同存储中的值。

contract HelloWord { uint storedData; // State variable }
  • 函数(function)声明:函数是合约内代码的可执行单元。

contract HelloWord {     function get () {         // todo something     } }

fallback函数

单独拎出来讲,是因为这个函数很重要,智能合约的安全漏洞,有很大一部分都与合约实例的回退函数有关。下面就是定义的一个fallback函数,可以看到这个函数没有名字,也没有返回值。每一个合约有且仅有一个没有名字的函数。一个没有定义回退函数的合约,如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。那什么时候执行 fallback 函数呢?

  1. 当外部账户或其他合约向该合约地址发送 ether 时;

  2. 当外部账户或其他合约调用了该合约一个不存在的函数时;

  3. 当外部账户或其他合约调用该合约确没有传入任何数据时;

类型

  • bool:false / true

  • 操作符:! , && , || , == , !=

  • uinit/int:无符整型、有符整型 (默认是256bit)

  • 操作符:

    • 比较:<= , < , == , >= , >

    • 位计算:& , | , ^ , ~

    • 计算:+ , - , * , / , % , **

  • address:用于表示以太坊地址,20字节,160bit。

    成员:

    • address.banlance (uint256):地址余额,单位 Wei

    • address.transfer(uint256 value) :向地址类型发送数量为 amount 的 Wei,失败时抛出异常,并回滚转态;当地址类型是合约地址时,调用合约地址的fallback函数,并且最多发送 2300 gas 的矿工费,不可调节。

    • address.send(value) returns (bool):向地址类型发送数量为 amount 的 Wei,失败时返回false;当地址类型是合约地址时,调用合约地址的fallback函数,并且最多发送 2300 gas 的矿工费,不可调节。

    • address.call.value()() 当发送失败时会返回 false 布尔值;传递所有可用 Gas 进行调用;

    • address.call(...) returns (bool):call 的外部调用上下文是外部合约

    • address.delegatecall(...) returns (bool) 的外部调用上下是调用合约上下文

    • address.callcode(...) returns (bool) delegatecall之前的一个版本,不鼓励使用,未来会废除

全局变量

block:块

    • block.blockhash(uint blockNumber) returns (bytes32): 传入 blockNumber,返回块的哈希值

    • block.coinbase (address): 挖到当前块矿工的地址

    • block.difficulty (uint): 当前块的难度

    • block.gaslimit (uint): 当前块最多的 gas

    • block.number (uint): 当前块是第几个

    • block.timestamp (uint): 当前块创建的时间戳

    • now (uint): block.timestamp 的别名

msg: 当执行某一个函数的时候,函数想要知道调用函数的数据信息

    • msg.data (bytes): 包括函数名字等等,一些没有经过加工的信息。

    • msg.gas (uint): 函数调用方携带的 gas

    • msg.sender (address): 函数调用方的地址

    • msg.sig (bytes4): 整个 msg.data 的前 4 个 byte

    • msg.value (uint): 函数调用方携带的 gas,以 wei 为单位计价。

关键词:

    • constant 用于变量: 表明当前变量不可修改。如果修改,编辑器会报错。

    • constant 用于函数: 表明当前函数中,不应该修改状态。但要十分小心,因为即便修改了,编译器也不会报错。

    • view : 和 constant 用于函数时功能一样。

    • payable: 表明调用函数可以接受以太币。

    • this: 指向的是当前合同的 address。

    • revert: 函数执行失败,需要通过调用 revert() 抛异常告诉函数调用方。调用后恢复合同状态,并将剩余 gas 返还。throw 已被废弃。

    • require: 用于检查条件,并在不满足条件的时候抛出异常,更偏向代码逻辑健壮性检查

    • assert:用于检查条件,并在不满足条件的时候抛出异常,更偏向用于确认一些本不该出现的情况异常发生的时候

 

搭建私链

以太坊属于公有链,官方不但提供了主链,也提供了测试链。以太坊公链的运行节点遍布全球,即使是使用测试链,运行速度也是无法达到实验级的要求的,而且不方便去控制网络中的每一个节点。所有我们有必要搭建一个测试链,由于这个测试链运行在用户自己的局域网中,一般情况下并不会开放到公网中,因此这个测试链也称私有链。 那如何快速搭建测试环境呢?TestRPC和Truffle这两款工具能帮助我们快速部署环境。TestRPC是在本地使用内存模拟的一个以太坊环境,可以用于搭建测试环境,基于Nodejs开发。Truffle是针对以太坊智能合约应用的一套开发框架。

安装工具的命令: 

1.安装 TestRPC 

sudo npm install -g ethereumjs-testrpc

2.安装 Truffle 

sudo npm install -g truffle

3.安装 solc 

sudo npm install -g solc


一般学某种语言,教程都是从 hello world 说起。那我们也以 hello world为例,来部署一个合约。

测试环境

Truffle v4.1.13 EthereumJS TestRPC v6.0.3 (ganache-core: 2.0.2) 
0.4.24+commit.e67f0147.Emscripten.clang

项目创建

terminal终端,创建一个新目录,并truffle项目初始化。 

> mkdir HelloWorld 
> cd HelloWorld 
> truffle init

初始化好之后的目录结构如下: 

HelloWorld/├── build
│   └── contracts
│       └── Migrations.json
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
├── truffle-config.js
└── truffle.js

撰写HelloWorld合约

pragma solidity ^0.4.24;

contract HelloWorld{ 
  address creator; 
  string message;

  constructor() {
    creator = msg.sender;
  }

  function say() constant returns (string) {
    return message;
  }

  function setMessage(string _newMsg) {
      message = _newMsg;
  }

   /**********
   Standard kill() function to recover funds 
   **********/

  function kill() { 
        if (msg.sender == creator)
            selfdestruct(creator);  // kills this contract and sends remaining funds back to creator
  }
}

把代码保存到contracts目录下的HelloWorld.sol


编译

在HelloWorld目录下: 

> truffle compile

compile只会编译更新过的合约文件,如果有多个文件,且想全部编译,可以使用 truffle compile-all

运行测试

1. 启动testrpc

通过testrpc可以很方便的进行测试,打开一个新的terminal终端执行命令: 

> testrpc

默认会在localhost:8545进行合约部署的监听。 

2. 修改合约配置 

因为合约是要发给testrpc做运行,需要再HelloWorld/truffle.js中配置testrpc的地址信息,如下:

module.exports = { 
    networks: { 
        development: { 
            host: "localhost", 
            port: 8545, 
            network_id: "*" // Match any network id         } 
    } 
};


3. 添加迁移信息(migrate) 

需要配置告诉truffle迁移哪些合约到testrpc,添加一个文件HelloWorld/migrations/2_deploy_contracts.js

var HelloWorld = artifacts.require("./HelloWorld.sol");

module.exports = function(deployer) { 
deployer.deploy(HelloWorld); 
};


4. 运行迁移命令,部署合约到testrpc

truffle migrate

同样的,这个命令只会迁移修改过的合约,如果有异常错误或者需要手动全部重新迁移,可以运行 truffle migrate --reset 。迁移成功后在testrpc窗口也会有响应的提示信息,包括函数调用和事务执行信息等。

 

5. 命令行测试合约 

通过console可以方便的测试合约的开发接口是否访问正常,运行命令:

truffle console

运行成功后进入到truffle的命令行程序中,可以通过以下命令来测试合约接口,设置信息:

HelloWorld.deployed().then(i=>i.setMessage("Hello world!"));

 

remix基本用法

使用在线的remix,如果要调试本地搭建的私有网络,Environment选择Web3 Provider

这边注意的是,编译要对相应的版本号,版本号在Settings中设置

写个有溢出漏洞的代码

pragma solidity ^0.4.24;


contract MyToken {
    mapping (address => uint) balances;

    // 查看余额
    function balanceOf(address _user) returns (uint256) { 
        return balances[_user]; 
    }
    
    // 添加余额
    function deposit() payable { 
        balances[msg.sender] += msg.value; 
    }

    // 转账操作
    function transfer(address _to, uint256 _value) payable { 
        require(balances[msg.sender] - _value> 0);	//存在溢出
        msg.sender.transfer(_value);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
    }
}

使用remix 直接运行代码,过程:(仔细想了下,感觉这个例子不是太好,不过可以先不要管,因为主要目是为了让大家学会remix的基本用法)

1. 账户 a 点击 deposit 转入 50 ether

2. 账户 a 转出  115792089237316195423570985008687907853269984665640564039457584007913129639984 到账户b

 

3. 查看账户 b 的余额

131.gif

 

使用 truffle console 调用合约

1. 关于私有链的智能合约部署上文已经说到,根据上面教程搭建MyToken智能合约

2. truffle console 进入交互,命令可以参考这里 

几点说明:

  1. web3.eth.accounts 是testrpc测试账户的数组

  2. deployed() 函数是获取合约实例

  3. call 它完全是一个本地调用,不会向区块链网络广播任何东西,它的返回值完全取决于函数方法的代码,不会消耗Gas

 

总结

智能合约漏洞的原理和平常的安全漏洞原理并没有什么不同,但是很多人因为不懂编写和调试从而望而却步。本文就是基础的入门文章,并没有讲到漏洞原理,而是帮助大家了解一些基本的概念和从搭建环境开始,后续有漏洞可以懂得自己调试,这样才能取得事半功倍的效果。

 

参考: 
https://www.cnblogs.com/bugmaking/p/9211225.html

https://www.anquanke.com/post/id/146322

https://my.oschina.net/u/2349981/blog/863731

https://truffleframework.com/docs/getting_started/contracts

https://bitshuo.com/topic/587e03c44dea36e72c1b381b


本文作者:he1m4n6a@163.com

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/73557.html

Tags:
评论  (0)
快来写下你的想法吧!

he1m4n6a@163.com

文章数:7 积分: 102

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号