Solidity 基础漏洞

整数溢出

由于solidity本身底层代码的原因,导致solidity的数据类型会产生整数溢出,当数据达到最小值或最大值时,对这个数据进行-1 or +1操作时会导致数据变成最大值或最小值,这被称为整数下溢或上溢。

Delegatecall滥用问题

1.delegatecall是很危险的,该函数会将其所调用的合约中的函数在本合约的环境中运行,会导致修改一些变量比如owner从而达成攻击。

**2.**当用delegatecall()调用其他合约时,如果未处理各自合约变量存储位置时,用了默认的卡槽,则可能会产生变量覆盖问题。

**3.**通过delegatecall()实现代理模式时,通过利用代理充当存储层时,也一定要初始化实施的合约,否则可能就会造成漏洞。

自毁合约攻击

在solidity中,如果一个合约要接收ether,一定要有fallbackreceive并用修饰符payable进行修饰。但是如果一个合约通过利用函数selfdestruct()来自毁合约向目标合约发送ether是允许的,所以不要在合约的判断逻辑上用address(this).balance == 0上,否则就会产生漏洞被攻击。

拒绝服务攻击

通过拒绝目标服务的发款,从而阻止目标合约的拥有者得到钱,或者让这个合约无法实现其原本的功能,从而可以要挟合约拥有者获利。

如果未指定固定数量的gas,对未知合约的外部调用仍会造成拒绝服务攻击。

重入攻击

转账时transfersend不再被推荐使用,因为他们在Istanbul硬分叉之后可能会破坏合约(只是单纯知道这个点但还没有学不太了解),而使用call去转账时,他只会返回false而不会中断执行流,假设其转账的目标是个合约而不是钱包地址时,而这个合约没有接收函数,则会调用这个合约的fallback()函数,而在fallback()中继续调用转账合约的转账函数,则会不断重复这个过程。

变量覆盖

数组上溢

在以太坊的设计思路中,所有的storage变量共有一片大小为2^256*32字节的存储空间,所以当变成数组长度很大时即可修改任意变量的值。

可以通过使动态数组长度达到最大值,然后放进一个所想放入的值,使得数组长度算术下溢,扩大数组到整个2^256的存储区域,就可以修改目标合约的storage

动态数组刚开始会先往插槽中放入动态数组的长度,而存入动态数组中的值会根据下标值存入keccak256(slot(动态数组长度的插槽))+x

例如

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

bool public contact;
bytes32[] public codex;

modifier contacted() {
assert(contact);
_;
}

function makeContact() public {
contact = true;
}

function record(bytes32 _content) contacted public {
codex.push(_content);
}

function retract() contacted public {
codex.length--;
}

function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}

例如这道题中,可以利用retract()这个函数使得数组长度达到最大值,然后发现这个数组长度时存储在slot1中的,因此该动态数组的变量值存放在keccak256(1)+index,而该题要修改的存储值在slot0中,因此我们要修改的时在2^256,由2^256=keccak256(1)+index可计算出,index=2^256-keccak256(1),在通过使用revise()函数,输入计算出的index以及想要修改的值,即可覆盖slot0

数组未初始化

当一个合约定义了一个动态结构体数组时,如果这个数组未初始化,那么定义的这个结构体数组名就会作为一个storage指针指向其他的变量区域,从而覆盖其他变量的值。

杂项

命名错误

由于程序员的马虎,使得合约中函数命名错误,导致没有起到限制作用,进而产生漏洞。

伪随机数

目前没有一个很自然的方法可以通过solidity产生随机数,因为在智能合约上的东西都是公开可见的,同时要知道合约之间的调用是在同一个区块的,因此hacker可以利用在自身写的攻击合约上控制随机数产生,符合要求再调用相关合约。

混淆tx.origin和msg.sender

由于混淆tx.originmsg.sender导致没有达成限制目的,进而产生漏洞。

未理解私有的概念

private会将变量设置成私有,只能保证不让别的合约去访问它,但是其值在链上是公开的,如果想要数据加密,要在上链前加密。

未限制接口

对接口函数没有进行限制,使得攻击合约可以利用接口使得目标合约调用函数时输入相同的结果,但返回结果是不同的。

gasleft()

gasleft()是检测到执行完这一步函数时还剩多少gas,可以通过调试计算出到这时还剩多少gas.

检测合约代码长度

通过汇编中的extcodesize()来检测合约的代码长度以限制攻击合约,但是在solidity中,如果将函数放在构造函数中时,检测到的代码长度为0。

计算合约地址

如果创建的合约地址丢失了,是可以通过计算找回的,合约地址是由keccak256(address,nonce)计算的,其中address是指创建合约的以太坊地址,而nonce是指该合约发起的交易数量。

限制合约字节长度

当目标合约限制攻击合约字节长度时,可以通过将合约代码转换为字节码,然后去除一些无用的字节码达到目标合约要求字节长度。

滑点

对一般去中心化交易所来说,都会有滑点的概念,随着交易额的增长,理论汇率和实际汇率之间差值会越来越大,而没有滑点的概念的话,就会产生漏洞,可能使得合约中余额被一人通过多次交易操作全部掏空。

抛出错误

solidity中可以自定义抛出的错误,当我们在写合约时要确保自定义错误的唯一性,同时在利用抛出错误时,要检测错误的来源,否则就会造成漏洞。

Calldata编写

动态calldata编写 前三十二个字节用于存放偏移量在最末尾,接下来的三十二个字节存放的是长度,再接下来的字节存放值,可以通过自己编写calldata来绕过一些硬编码的calldata检测,从而达成攻击目的。

区块

block.blockhash只能获得最新的256个块内的哈希值,超过这256哥区块的只会返回零,同时block.blockhash(block.number)返回值为0,因为block.number返回的是当前区块号,而当前区块号是属于未来区块,所以无法获得区块哈希。

create2

不同于原来的create操作码,在合约地址的计算方法上create2是这样计算合约地址的,keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:](solidity4.0版本不支持该函数),其中address是创建合约者的地址。

该函数使用方法是:

1
2
3
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

其中bytecode是所想部署的合约的字节码,addr就是创建之后的合约地址,而salt则是创建合约者自己设置的特定值,因此我们就可以通过计算keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:],来自己设置salt,来控制得到自己想要的合约地址。