EVM 是一个轻量级的虚拟机,其设计初衷就是提供一种可以忽略硬件、操作系统等兼容性的虚拟的执行环境供以太坊网络运行智能合约。
简单来说 EVM 是一个完全独立的沙盒,在 EVM 中运行的代码是无法访问网络、文件系统和其他进程的,以此来避免错误的代码能让智能合约毁灭或者影响外部环境。
在此基础上,知道创宇区块链安全实验室带大家一起深入理解 EVM 的存储机制和安全问题。
可以看到 EVM 存储数据分为两类:
存储在 code 和 storage 里的数据是 non-volatile (不容易丢失的)
存储在 stack,args,memory 里数据是volatile(容易丢失的)
Code
code 部署合约时储存 data 字段也就是合约内容的空间,即专门存储智能合约的二进制源码的空间
Storage
Storage 是一个可以读写修改的持久存储的空间,也是每个合约持久化存储数据的地方。Storage 是一个巨大的 map,一共 2^256 个插槽 (slot),每个插糟有 32byte,合约中的“状态变量”会根据其具体类型分别保存到这些插槽中。
Stack
stack 即所谓的“运行栈",用来保存 EVM 指令的输入和输出数据。可以免费使用,没有 gas 消耗,用来保存函数的局部变量,数量被限制在 16 个。stack 的最大深度为 1024 ,其中每个单元是 32 byte。
Args
args 也叫 calldata,是一段只读的可寻址的保存函数调用参数的空间,与栈不同的地方的是,如果要使用 calldata 里面的数据,必须手动指定偏移量和读取的字节数。
KuCoin在ERTHA元宇宙推出沉浸式办公室:8月2日消息,加密交易平台Kucoin宣布与元宇宙ERTHA达成合作,在ERTHA NFT Land虚拟地块构建KuCoin Office。据悉,Ertha的世界是一个复杂的游戏空间,玩家可以在其中创建新的政府、经济体,并与其他玩家建立不稳定的联盟。(AMBCrypto)[2022/8/2 2:54:06]
Memory
Memory 一个简单的字节数组,主要是在运行期间存储数据,将参数传递给内部函数。基于 32byte 进行寻址和扩展。
前面已经说过 Storage 是每个合约持久化存储数据的地方其储存数据的方式是通过插槽来实现的,现在就具体介绍它是怎么实现的:
1.对于大小在 32 字节以内的变量(常量),以其定义的顺序作为它的索引值来存储。即第一个变量的索引为 key(0),第二个变量的索引为 key(1)...
2.对于连续较小的值,可能被优化存储在同一个位置,比如:合约中前四个状态变量都是 uint64 类型的,则四个状态变量的值会被打包成一个 32 字节的值存储在 0 位置。
未优化:
pragma solidity ^0.4.11;contract C { uint256 a = 12; uint256 c = 12; uint256 b = 12; uint256 d = 12; function m() view public returns(uint256,uint256,uint256,uint256){ return (a,b,c,d); }}
优化后:
pragma solidity ^0.4.11;contract C { uint64 a = 12; uint64 c = 12; uint64 b = 12; uint64 d = 12; function m() view public returns(uint64,uint64,uint64,uint64){ return (a,b,c,d); }}
OKT跌破160美元关口 日内跌幅为20.37%:欧易OKEx数据显示,OKT短线下跌,跌破160美元关口,现报159.75美元,日内跌幅达到20.37%,行情波动较大,请做好风险控制。[2021/4/20 20:39:53]
对于大小在 32 字节以内的结构体同样也是顺序存储,例如结构体变量索引定义在位置 0,结构体内部有两个成员,则这两个成员的依序为 0 和 1。
pragma solidity ^0.4.11;contract C {struct Info { uint256 a ; uint256 b ;} function m() external returns(uint256,uint256){ Info storage info; info.a = 12 ; info.b = 24 ; return(info.a,info.b); }}
map 存储位置是通过 keccak256 (bytes32(key) + bytes32(position) ) 计算得到的,position 表示 key 对应 storage 类型变量存储的位置。
pragma solidity ^0.4.11;contract Test { mapping(uint256 => uint256) knownsec; function go() public { knownsec[0x60] = 0x40; }}
同上,只要在 32 字节以内也是顺序存储,不过在编译时编译器会进行边界检查防止越界。
24小时合约市场爆仓超11.63亿美元 BTC合约爆仓3.53亿美元:据合约帝行情统计报告显示:过去24小时合约市场全网总计爆仓11.63亿美元,爆仓人数218497人。其中,Huobi爆仓1.72亿美元,OKEx爆仓1.17亿美元,BitMEX爆仓2074万美元,Binance爆仓6.14亿美元,Bybit爆仓2.38亿美元。爆仓金额前三的币种是BTC3.53亿美元,ETH1.44亿美元,XRP1.32亿美元。[2021/4/16 20:28:01]
pragma solidity ^0.4.11;contract C { uint256 a = [12,24,48] ; function m() public view returns(uint256,uint256,uint256){ return (a,a,a); } }
由于可变长度数组长度不定,一般在编译可变长度数组时会提前预留存储空间,所以就会使用状态变量的位置存储可变长度数组的长度。
而具体的数据地址会通过计算 keccak256 (bytes32(position)) 算得数组首地址,再加数组长度偏移量获得具体的元素。
pragma solidity ^0.4.11;contract C { uint256[] a = [12,24,48] ; function m() public view returns(uint256,uint256,uint256){ return (a,a,a); } }
如果长度小于等于31字节 :
1.对于定长字节数组则是同定长数组一样;
2.对于可变字节数组和字符串,会在存储值位置补0一直到32字节,并用补0的最后一个字节存储字符串的编码长度。
pragma solidity ^0.4.4;contract A{ string public name0 = "knownsec"; bytes8 public name=0x6b6e6f776e736563; bytes public g ; function test() public { g.push(0xAA); g.push(0xBB); g.push(0xCC); } function go() public view returns(bytes){ return g; }}
当节数组和字符串长度大于31字节时
1.变量位置存储编码长度,并且编码长度公式更换为编码长度 = 字符数 * 2 + 1
2.真实存储值第一个位置通过公式 keccak256(bytes32(position)) 获取,剩余值在获取到的位置顺序存储,同样在最后存储位置补0到32字节。
string public name = "knownsecooooooooooooooooooooooooo";
前面已经讲到EVM的存储结构及存储机制,现在我们再来探讨其安全问题。
漏洞原理:
在官方手册中提到结构体,数组和映射的局部变量默认是放在 storage 中的,而 solidity 语言中函数中设置的局部变量的默认类型取决于它们本身的类型。
因此如果在函数内部设置以上 storage 类型变量却没有进行初始化,他们就相当于存储指针指向合约中的其他变量,当我们对其进行改变时改变的就是其指向的变量。漏洞合约,目的修改 owner 为自己地址:
pragma solidity ^0.4.0;contract testContract{ bool public unlocked = false; address public owner = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;struct Person { bytes32 name; address mappedAddress;} function test(bytes32 _name , address _mappedAddress) public{ Person person; person.name = _name; person.mappedAddress = _mappedAddress; require(unlocked); }}
漏洞合约分析:
可以看到该合约在函数部分创建新的结构体时没有进行初始化,由此我们可以利用该函数进行对owner的修改。不过使用该函数我们还要通过require验证,不过这也不难因为状态变量unlocked也同样在我们可控的范围内。
具体操作:
调用test函数分别传入向_name 传入:0x0000000000000000000000000000000000000000000000000000000000000001(真值)
_mappedAddress 传入:0xfB89eCb0188cb83c220aADDa1468C1635208e821(个人地址)
传参前:
传参后:
可以看到已经成功更改了地址。
可以看到 EVM 的存储器就是一个 key=>value 的健值数据库,存储的数据可以通过校验和来确保一致。但是其也是和智能合约语言进行交互的,当其中一些规则发生冲突很可能就被别有用心的人用来作恶,所以规范的使用智能合约语言是避开漏洞的必要条件。
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。