Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

登链-第二课

constructor()构造函数的 bytecode 是不会部署到区块链上的,因为constructor()的逻辑只在部署合约时调用,所以不用部署到区块链上,而在部署一个合约时,传入deployBytecode的时候是需要传递constructor()中函数逻辑的bytecode的。

合约地址预测

python 脚本实现:

1
2
3
4
5
6
7
8
import rlp from eth_utils
import keccak, to_checksum_address, to_bytes
def mk_contract_address(sender: str, nonce: int) -> str :
sender_bytes = to_bytes(hexstr=sender)
raw = rip.encode([sender_bytes, nonce])
h = keccak(raw)
address_bytes = h[:12]
return to_checksum_address(address_bytes)

CREATE 联合 CREATE2 攻击

关于合约自销毁的小知识:

部署合约时,如果计算出的地址存在 code 那么将会部署失败。

合约销毁时(selfdestruct()),该地址对应的code,storage 也会跟着销毁。

CREATE 创建合约:

CREATE 操作码创建出来的合约地址只与两个值有关:**sendernonce**

1
new_address = keccak256(sender, nonce)
  • sender:部署这个合约的地址
  • nonce:当前账号发起的交易数量

nonce 的值随着每一笔交易的发起不断增加,就算合约销毁后,使用 CREATE 操作创建合约,也不能在原地址部署新的合约

CREATE2 创建合约

CREATE2 操作码创建出来的合约地址与这三个值有关:senderbytescodesalt 与 nonce 无关

1
new_address = keccak256(0xff, sender, salt, bytescode)
  • sender:部署这个合约的地址
  • bytescode:要部署的合约的字节码
  • salt:盐值

CREATE2 的用处就是确保:在要部署的合约 bytescode 不变,salt 不变的情况下,生成的合约地址不变

CREATE 和 CREATE2 联合使用攻击

该攻击的目的:在不改变合约地址的情况下,来修改合约的代码

create 出的地址是由 sender 和 nonce 决定

而 create2 出的地址是由 bytecode,sender,bytecode 决定,这就出现了漏洞:

我使用合约 A CREATE2 出来一个 B1 合约,B1 合约使用 Create 出来一个 C1 合约,这个时候销毁 B 合约和 C 合约,在重新部署一个 B2 合约(B2 合约和B1 合约完全一致,salt 和 bytescode不变,新生成的 B2 合约地址和第一次合约 B1 地址相同),这时我使用 B2 再创建 C2 合约(此时 C2 合约可以与 C1 合约不同,因为 C2 合约是由 B2 合约使用 CREATE 创建出来,CREATE只和B2 的地址和 nonce 有关,B2 合约地址由于 B2 合约代码没有发生改变,盐值不变,所以 B2 地址和 B1 地址相同,nonce 因为 B2 重新部署发生重置,所以新生成的 C2 合约尽管和 C1 合约代码不同,但是地址是相同的)

eea2c8e1708acba786658af3957e8aa

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
// contract A
contract ContractFactoryContract {
event CreatorFactoryDeploy(address addrodc);

address public _createContractaddr;

function deployCreator(uint _salt) external {
CreatorContract _creatorContract = new CreatorContract{salt: bytes32(_salt)}();
emit CreatorFactoryDeploy(address(_creatorcontract));
_createContractaddr = address(_creatorcontract);
}
}

// contract B
contract FactoryContract {
event CreatorDeploy (address addr);
address public _targetaddr;

function deployTarget() external {
Target _contract = new Target();
emit CreatorDepoly(address(_contract));
_tragetaddr = address(_contract);
}

function destory() public {
selfdestruct(payable(msg.sender));
}
}

// contract C
contract C {
address public owner;
constructor() {
owner = msg.sender;
}

function destory() public {
selfdestruct(payable(msg.sender));
}
}

EVM 虚拟机

EVM 虚拟机是一个基于栈的虚拟机,每一条指令都会有操作数和操作符

  • opcode 表示这条指令是干什么的

因为有些攻击合约不是开源的,就需要能看懂 Solidity 汇编代码,看懂机器指令

内存模型(Memory Model)

在 EVM 中分成两大类 Memory:

  • non-volatile:非易逝性——持久化存储
  • volatile:易逝——非持久化存储

持久化存储:智能合约执行完后,数据依然存在的

非持久化存储:智能合约代码执行完毕后,数据消失不见

non-volatile——持久化存储:

持久化存储包括以下内容:

  • code :代码
  • storage:在智能合约的 Account 里来保存合约状态。(智能合约也是一个 Account)

volatile——非持久化存储:

非持久化存储包括:

  • stack 栈
  • args 参数
  • memory 临时变量

在 EVM 机器指令中,有专门的指令来操作 Memory 和 Storage 的。

storage

Storage 实际上是一个 key-value 的映射关系

Storage 在一个智能合约中很大,都有其 persistent storage(持久存储),一个 key-value 的映射

存在:key^256^ -> 32 bytes

Storage 中的 key 是由 slot(插槽)计算而来的

操作 Storage

  • SSTORE:从堆栈中加载 key 和 value ,然后将32字节值保存到 key 位置
  • SLOAD:从堆栈加载 key ,从存储中获取值,然后压入堆栈

Memory

Memory:``mstore(p ,v) -> mem[p..(p+32)) := v`

在 Memory 区域的前一部分字节是作为保留的,不可操作

规定:在0x40的 4 个字节始终保存的是下一个可用的 Memory 地址

所以在 Solidity 内联汇编(assembly代码块)中出现mload(0x40)的含义其实就是获取下一个可用操作 Memory opset 是多少

Call & Delegatecall

  • Call:调用另一个智能合约中的函数
  • Delegatecall: 调用另一个智能合约内的函数,同时使用自己的存储和合约(这里涉及到一个细节:使用 Delegatecall 修改合约 A 的状态变量时,是根据插槽位置,而并非变量名修改的)

image-20231106162949359

Proxy & Logic(Implementation)Contract (代理和逻辑(实施)合约)

image-20231106163428536

代理合约安全学习网址:https://proxies.yacademy.dev/pages/security-guide/

Low-level Call

  • Contract Call:contract.function()——自动传递异常 contract call 底层也是通过 low-level call 的方式调用函数的,但是编译器已经帮我们做出了判断
  • Solidity 提供的通过address.call()的 low-level call (低级调用)形式:low-level call 本身不传递异常
    • low-level call 有两个返回值一个是调用是否成功,一个是调用的返回值。low-level call 调用失败会在第一个返回值返回 false,但是不会导致交易回滚,所以要对 low-level call 的返回值进行检验,才能判断是否成功转移 Token。