智能合约安全前传-基础知识入门
本文首发自https://www.secpulse.com/archives/73557.html,转载请注明出处。
本文综述
本文将简述以太坊、智能合约的基础知识,以及如何搭建私链和编写、部署一个有漏洞的智能合约。
以太坊简介
以太坊是一个开源的区块链平台,以太坊的模块结构和比特币类似,和比特币最大区别就在于,用户可以在以太坊平台上随意的开发属于自己的去中心化智能合约。
以太坊常用的几款工具:
-
go-ethereum:官方的Go语言客户端,客户端文件是geth。这是使用最广泛的客户端,类似于比特币的中本聪核心客户端,可用于挖矿,组建私有链、管理账号、部署合约等
-
cpp-ethereum:与上面实现功能一样,不过是c++实现
-
Mist 客户端 :mist目前是钱包客户端,未来定义为一个DAPP市场交易客户端,类似于苹果市场
-
Solidity:智能合约编程语言,下一章节会详细介绍
-
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,它与面向对象编程中类的概念非常类似,无论是合约还是类都可以有变量和函数,但是类是可以实例化的,而合约并没有实例化这一功能,它的变量和函数可以直接在合约本身上访问或者调用。
以太坊中有两类账户,它们共用同一个地址空间。
-
外部账户,该类账户被公钥-私钥对控制(人类);外部账户的地址是由公钥决定的。
-
合约账户,该类账户被存储在账户中的代码控制;合约账户的地址是在创建改合约时确定的(这个地址由合约创建者的地址和该地址发出过的交易数量计算得到,地址发出过的交易数量也被称作"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 函数呢?
-
当外部账户或其他合约向该合约地址发送 ether 时;
-
当外部账户或其他合约调用了该合约一个不存在的函数时;
-
当外部账户或其他合约调用该合约确没有传入任何数据时;
类型
-
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 的余额
使用 truffle console 调用合约
1. 关于私有链的智能合约部署上文已经说到,根据上面教程搭建MyToken智能合约
2. truffle console 进入交互,命令可以参考这里
几点说明:
-
web3.eth.accounts 是testrpc测试账户的数组
-
deployed() 函数是获取合约实例
-
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