区块链安全

EVM 字节码编程基础

有过编程或逆向经验的同学应该都懂字节码是什么意思,参考 https://ethervm.io/,我们知道 00 代表 STOP,表示 STOP() 函数,功能为停止执行合同。

如果一个合约里面,我们就想执行 STOP() 一个函数,那么这个合约的字节码应该就是 0x00。但是如何把这个合约部署到链上呢?

  • rule1:部署的字节码需要包含 return(你想要部署代码) 的这段代码。

也就是 return(内存地址,代码长度)。但是由于栈是FILO的结构,所以压栈时候应该是:

push length
push offset

内存和堆栈

上面提到了堆栈,EVM中内存和堆栈是什么概念呢?

堆栈可以理解为寄存器,主要作用是给函数传参以及暂存返回值。内存是一串位置,用来存储数据

f3/return 就是用来返回存储在内存中的东西。要想让 f3 返回东西,你需要提供「偏移」和「长度」这两个参数。这里就提到参数了,可以通过将参数压入栈来实现传入实参。

压入栈主要使用 push/60。push 是 evm 中唯一一个带操作码的指令。

要从内存中 return 00 这个数据,我们先得存入内存。存入内存使用的函数是 mstore/52。非常好理解 mstore = memory store。

  • 可以看到:mstore 对栈中的两个参数进行处理,分别是偏移和值。也就是位于某处偏移的某个值存入内存(因为已经明确值了,没必要再传入长度)。

所以目前的逻辑是:

  • 为了部署 00 合约代码,我们需要返回 00 合约代码。
  • 为了返回 00 合约代码,需要将 00 合约代码存入内存。
  • mstore 需要两个参数,也就是偏移和值。需要先把这两个值压入栈。

按照写代码顺序是这样:

  1. 压 mstore 的两个参数入栈
  2. 调用 mstore
  3. 压 return 的两个参数入栈
  4. 调用 return
push value -- 栈filo,所以顺序是反的
push offset
mstore
push length
push offset
return

换成hex就是:

60 00
60 00   -- 从00的内存位置开始存入数据
52
60 01  --00 是一个字节,因为1 bytes=8bits,一个hex用4bit表示,所以两个hex字符是1字节。   
60 00
f3

也就是:

0x600060005260016000f3

这就是一个可被部署的合约啦。

总结一下:部署的合约= 存合约代码入内存的代码 + return合约代码的代码

字节码写 hello world

先写出来 hello world 的合约代码,很简单,也就是 hello world 转为 ascii 码。

0x68656c6c6f20776f726c64000000000000000000000000000000000000000000

在 Solidity 中,字符串是动态大小的数组,要定义一个字符串,需要指定位置,然后是长度,最后是实际的字符串:

0x0000000000000000000000000000000000000000000000000000000000000020
0x000000000000000000000000000000000000000000000000000000000000000b
0x68656c6c6f20776f726c64000000000000000000000000000000000000000000
  • 第一个参数是位置。字符串从内存位置32处开始存放
  • 第二个是字符串的长度。带空格11个字节
  • 第三个是字符串的内容

所以最终字符串是:

0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20776f726c64000000000000000000000000000000000000000000

然后开始写存入字符串进内存的代码,之前我们用的是 push 和 mstore,先通过 push 将数据压入栈。但是 push 系列最多一次压入32字节的数据:

所以我们分三次压入栈。

对于这个字符串,我们应该压入3个32字节的内存槽,即为 0x40 长度的内存槽:

push [offset value]
  • 第一次压入第一个内存槽
push1 20
push1 00
52
  • 第二次压入第二个内存槽
push1 0b
push1 20
52
  • 第三次压入第三个内存槽
push11 0x68656c6c6f20776f726c64
push1 40
52 

因为这是一个有返回值的函数,所以还要写从内存中返回字符串(动态数组)的字节码:

此图片的alt属性为空;文件名为image-1024x39.png
  • 压入参数
push 60  -- 内存中的长度是3个32字节的内存条
push 00  -- 从内存中的 00 位置开始存的
return

所以截至目前,字节码为:

60 20
60 00
52
60 0b
60 20
52
6a 68656c6c6f20776f726c64
60 40
52 
60 60 
60 00 
f3

以上就是合约代码。总结一下:这个合约代码就是将 hello world 字符串存入内存,然后返回。

但为了部署,还需要写存合约代码入内存的代码和返回合约代码的代码。在上面我们写入 00 时候,那只有一个字节,所以我们先 push 入栈,但是这段代码的字节码很长,有30个字节。如何存入内存呢?

可以使用这个 CODECOPY 指令。顾名思义,是用来复制代码入内存的。

[memoryPosition codePosition length]
push 1e -- 30个字节
push ??
push 00
39

memoryPosition 是在内存中的偏移,是从0开始的。至于 codePosition 是指,真正的合约代码的位置。真正的合约代码是在这一段代码块之后的,也就是指明存入的代码内容,所以计算一下这段代码有多少个字节然后替换 ?? 就行了。

但是我们还需要一段代码,就是返回合约代码的代码:

push 1e  --长度
push 00  -- 位置
f3

在这种多字节的合约代码部署中,存合约代码进内容和返回合约代码的这两段代码写在一起。因为反正要通过 ?? 寻找合约代码的位置。

60 1e 
60 0c  --这一段代码一共12个字节,替换此处
60 00
39
60 1e 
60 00 
f3

所以输出hello world 的最终完整字节码是:

60 1e 
60 0c  
60 00
39
60 1e 
60 00 
f3
60 20
60 00
52
60 0b
60 20
52
6a 0x68656c6c6f20776f726c64
60 40
52 
60 60 
60 00 
f3

也就是:

601e600c600039601e6000f36020600052600b6020526a68656c6c6f20776f726c6460405260606000f3

总结一下:

  • 存入内存:mstore 52
  • 压入栈:push 60
  • 将代码存入内存:codecopy 39
  • 返回:return f2