任意存储写入漏洞+任意目的地址跳转漏洞+gas耗尽拒绝服务漏洞
本文介绍了三类智能合约安全漏洞:任意存储写入漏洞、任意目的地址跳转漏洞、gas 耗尽拒绝服务漏洞并对他们进行分析
参考论文:基于符号执行的智能合约自动化安全审计_杨坤
任意存储写入漏洞
漏洞原理
若一个合约开发者在合约中使用了动态数据结构,如映射或动态数组,却未对该动态数据结构的索引值进行正确的限制,那么就很有可能会产生动态数据结构索引越界错误,可能引发的后果有:
-
利用该动态数据结构索引越界错误,向合约存储区域的任意位置写入数据,那么攻击者就可以通过修改 storage 存储区域的状态变量值,来规避合约中所有借助于该状态变量值进行权限校验的检测,从而达到权限提升的目的
-
利用此漏洞破坏合约存储结构,进行任意变量覆盖操作,如覆盖存储合约所有者地址的状态变量的值,这就有可能造成合约功能执行异常、资金冻结等危害
我觉得这里是属于缓冲区溢出漏洞的
智能合约存储机制
说到这里,我们要对智能合约的存储机制进行简要的学习一下
参考:https://xz.aliyun.com/t/9837
文中简要的描述了重点,具体详细的内容请看原文章
- 存储机制
每个在以太坊虚拟机(EVM)中运行的智能合约的状态都在链上永久地存储着。这些值存储在一个巨大的数组中,数组的长度为2^256,下标从零开始且每一个数组能够储存32字节(256个比特)长度的值。并且存储是稀疏的,并没有那么密集。
- 简单值类型的存储
指的是布尔类型,整型,地址类型等
- 大端序
- 存储顺序从后往前
- 存储优化原则
- byte.length == bytes1.length == 8bits
- 定长数组
pragma solidity ^0.4.25;
contract TEST{
bytes8[5] a = [byte(0x6a),0x68,0x79,0x75];
bool b=true;
}

可以看的我虽然规定了了长度为5,但是实际上只用了4个,所以就只是用了四个bytes8的空间。
如果再加一个,编译器就会报错

- 变长数组
pragma solidity ^0.4.25;
contract TEST{
uint[] a=[0x77,0x88,0x99];
function add(){
a.push(0x66);
}
}


0x0000000000000000000000000000000000000000000000000000000000000003 slot0
//存储的是数组a的长度3
0x0000000000000000000000000000000000000000000000000000000000000077 slotx
//a[0]
0x0000000000000000000000000000000000000000000000000000000000000088 slot(x+1)
//a[1]
0x0000000000000000000000000000000000000000000000000000000000000099 slot(x+2)
//a[2]
数组的大小是存储在第一位,且是按照定义的顺序存储在对应的槽里,后面动态数组具体的值的Storage Address的由来是根据x=keccak_256(slot), slot是指数组长度存储的位置,此处对应的就是0,也就是0x0000000000000000000000000000000000000000000000000000000000000000
根据x=keccak_256(slot)计算出第一位值存储的位置后,address值依次递增。
在调用a.push(0x66)后,可以看到:

第一步改变了数组a的长度;第二步在a[2]后面的一个插槽写入0x66
漏洞检测方案
检测流程说明:检测合约执行过程中是否存在“SSTORE”指令,若存在“SSTORE”指令,则检测“SSTORE”指令参数值是否可以为任意值,若发现合约“SSTORE”指令参数可以为任意值,则报告相应漏洞
任意目的地址跳转漏洞
漏洞原理
当合约开发者错误地使用了函数类型变量,致使用户具有任意更改函数类型变量并因此执行代码指令的能力时,就会产生任意目的地址跳转漏洞。
虽然由于 solidity 不支持指针运算而无法将函数类型变量更改为任意值,一旦合约开发者在合约中使用了汇编指令,如 mstore 或assign 运算符,那么攻击者有可能可以将函数类型变量指向任意代码指令,从而可以绕过合约的验证和所需要的状态更改,进一步可破坏合约执行流程,造成合约资金流失等危害。
函数类型
函数类型即是函数这种特殊的类型。
- 可以将一个函数赋值给一个变量,一个函数类型的变量。
- 还可以将一个函数作为参数进行传递。
- 也可以在函数调用中返回一个函数。
函数类型有两类;可分为internal和external函数。
内部函数(internal)
因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。
外部函数(External)
外部函数由地址和函数方法签名两部分组成。可作为外部函数调用的参数,或者由外部函数调用返回。函数的定义
完整的函数的定义如下:
function (<parameter types>) {internal(默认)|external} [constant] [payable] [returns (<return types>)]
若不写类型,默认的函数类型是internal的。如果函数没有返回结果,则必须省略returns关键字。
存储位置指定
在 Solidity 中,有两个地方可以存储变量 —— storage以及memory。
- Storage 变量是指永久存储在区块链中的变量。
- Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。
状态变量(在函数之外声明的变量)默认为“storage”形式,并永久写入区块链;而在函数内部声明的变量默认是“memory”型的,它们函数调用结束后消失;然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的结构体和数组时
原文链接:https://blog.csdn.net/qq_33829547/article/details/80459480
对于Solidity编码过程来讲,变量的存储位置(memory和storage存储区)的正确指定非常重要,因为不同数据位置变量赋值产生的结果也不同
不同数据类型的变量会有各自默认的存储位置:
-
状态变量总是会存储在storage 中
-
函数参数和返回值默认存放在 内存memory 中
-
结构、数组或映射类型的局部变量,默认会放在 存储storage 中
-
除结构、数组及映射类型之外的简单局部值变量,会储存在栈中
在memory和storage之间,以及它们和状态变量(即便从另一个状态变量)中相互赋值,总是会创建一个完全不相关的数据拷贝。
将一个storage的状态变量,赋值给一个storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。另一方面,将一个memory的引用类型赋值给另一个memory的引用,不会创建另一个新的拷贝。
原文链接:https://blog.csdn.net/yjhzxhyzq123/article/details/80598220
漏洞检测方案
检测合约执行过程中是否存在“JUMP”或“JUMPI”指令,若存在“JUMP”或“JUMPI”指令,则检测指令参数值是否可以被修改为任意值,若发现合约 “JUMP” 或 “JUMPI” 指令参数值可以被修改为任意值,则报告相应漏洞
gas 耗尽拒绝服务漏洞
漏洞原理
以太坊网络限定了每个区块的最大 gas 总量值,区块中所有交易的 gas 总和不能超过此区块最大 gas 总量值。一旦合约中的某个操作将大量消耗 gas 以至于消耗的 gas 值达到了区块最大 gas 总量值,此操作将不会被成功执行,所有依赖此操作的验证步骤也将失效,合约会因此无法正常完成其余功能,从而造成一种拒绝服务状态。
通常当一个合约开发者未考虑到区块 gasLimit 而在合约中引入了修改随时间增加大小会改变的数组等动态数据结构变量操作时,会发生此种拒绝服务攻击。
攻击者可以在一个区块被开采出来后,马上以较高的 gas 价格发出多个交易,然后利用合约上述操作消耗整个区块 gas 限额,使该区块在特定的时间之前不包含其他任何交易,以阻止其他用户正常使用合约的功能,从而达到赢取合约奖励的目标。
漏洞检测方案
在合约执行过程中,判断是否存在循环次数较大的循环操作,若存在,继续判断该循环操作中是否会执行“SSTORE”指令,若循环操作中会执行“SSTORE”指令,则报告存在相应漏洞。

浙公网安备 33010602011771号