学习文章
应用二进制接口说明
官方文档中的应用二进制接口部分内容。前面部分摘录自官方文档,主要是对后面参数编码进行学习和理解。
函数选择器
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序) (译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
类型
参考官方文章:
uint<M>
:M
位的无符号整数,0 < M <= 256
、M % 8 == 0
。例如:uint32
,uint8
,uint256
。int<M>
:以 2 的补码作为符号的M
位整数,0 < M <= 256
、M % 8 == 0
。address
:除了字面上的意思和语言类型的区别以外,等价于uint160
。在计算和 函数选择器Function Selector 中,通常使用address
。uint
、int
:uint256
、int256
各自的同义词。在计算和 函数选择器Function Selector 中,通常使用uint256
和int256
。bool
:等价于uint8
,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用bool
。fixed<M>x<N>
:M
位的有符号的固定小数位的十进制数字8 <= M <= 256
、M % 8 == 0
、且0 < N <= 80
。其值v
即是v / (10 ** N)
。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)ufixed<M>x<N>
:无符号的fixed<M>x<N>
。fixed
、ufixed
:fixed128x18
、ufixed128x18
各自的同义词。在计算和 函数选择器Function Selector 中,通常使用fixed128x18
和ufixed128x18
。bytes<M>
:M
字节的二进制类型,0 < M <= 32
。function
:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于bytes24
。
定长数组类型:
<type>[M]
:有 M 个元素的定长数组,M >= 0,数组元素为给定类型。
非定长类型:
bytes
:动态大小的字节序列。string
:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。<type>[]
:元素为给定类型的变长数组。
编码的形式化说明
注意区分一下动态和静态类型,下面会用到:
bytes
string
- 任意类型 T 的变长数组
T[]
- 任意动态类型 T 的定长数组
T[k]
(k >= 0
) - 由动态的
Ti
(1 <= i <= k
)构成的 元组tuple(T1,...,Tk)
所有其他类型都被称为“静态”。
定义: len(a)
是一个二进制字符串 a
的字节长度。len(a)
的类型被呈现为 uint256
。
我们把实际的编码 enc
定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X
的类型是动态的,len(enc(X))
(即 X
经编码后的实际长度,译者注)才会依赖于 X
的值。
定义: 对任意ABI值 X
,我们根据 X
的实际类型递归地定义 enc(X)
。
-
(T1,...,Tk)
对于k >= 0
且任意类型T1
,…,Tk
enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
这里,
X = (X(1), ..., X(k))
,并且 当Ti
为静态类型时,head
和tail
被定义为head(X(i)) = enc(X(i))
andtail(X(i)) = ""
(空字符串)否则,比如
Ti
是动态类型时,它们被定义为head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1))))
tail(X(i)) = enc(X(i))
注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以
head(X(i))
是定义明确的。它的值是从enc(X)
的开头算起的,tail(X(i))
的起始位在enc(X)
中的偏移量。 -
T[k]
对于任意T
和k
:enc(X) = enc((X[0], ..., X[k-1]))
即是说,它就像是个由相同类型的
k
个元素组成的 元组tuple 那样被编码的。 -
T[]
当X
有k
个元素(k
被呈现为类型uint256
):enc(X) = enc(k) enc([X[1], ..., X[k]])
即是说,它就像是个由静态大小
k
的数组那样被编码的,且由元素的个数作为前缀。 -
具有
k
(呈现为类型uint256
)长度的bytes
:enc(X) = enc(k) pad_right(X)
,即是说,字节数被编码为uint256
,紧跟着实际的X
的字节码序列,再在高位(左侧)补上可以使len(enc(X))
成为 32 的倍数的最少数量的 0 值字节数据。 -
string
:enc(X) = enc(enc_utf8(X))
,即是说,X
被 utf-8 编码,且在后续编码中将这个值解释为bytes
类型。注意,在随后的编码中使用的长度是其 utf-8 编码的字符串的字节数,而不是其字符数。 -
uint<M>
:enc(X)
是在X
的大端序编码的高位(左侧)补充若干 0 值字节以使其长度成为 32 字节。 -
address
:与uint160
的情况相同。 -
int<M>
:enc(X)
是在X
的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为0xff
(即 8 位全为 1,译者注)的字节数据,对于非负数,添加 0 值(即 8 位全为 0,译者注)字节数据。 -
bool
:与uint8
的情况相同,1
用来表示true
,0
表示false
。 -
fixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为int256
。 -
fixed
:与fixed128x18
的情况相同。 -
ufixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为uint256
。 -
ufixed
:与ufixed128x18
的情况相同。 -
bytes<M>
:enc(X)
就是X
的字节序列加上为使长度成为 32 字节而添加的若干 0 值字节。
注意,对于任意的 X
,len(enc(X))
都是 32 的倍数。
上面的部分需要细细看一下。
例子
pragma solidity ^0.4.16;
contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}
baz函数传 69和true
baz函数的方法ID是0xcdcd77c0
。接下来就是参数的编码。
首先应用定义:
有uint32和bool,二者都是静态,因此tail都是0,直接递归算head。
head(uint32)=enc(uint32)
,69应用定义:
是0x0000000000000000000000000000000000000000000000000000000000000045
同理,true应用定义:
是0x0000000000000000000000000000000000000000000000000000000000000001
所以最终得到:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
看懂了第一个,接下来就是慢慢递归应用那些定义了。
bar函数传 [“abc”, “def”]
byte3[2]是静态类型,因此tail为空,直接求tail。
数组满足定义,然后递归求元组:
对于这个元组2个元素"abc"和"def"都是bytes3类型,都是静态,tail为空,直接求head,满足这个:
需要注意,这里是右填充。
因此"abc"编码为:0x6162630000000000000000000000000000000000000000000000000000000000
"def"编码为:0x6465660000000000000000000000000000000000000000000000000000000000
,加上前面的函数ID,得到:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
sam函数传 “dave”、true和[1,2,3]
可以看到有2个动态的,bytes和uint[]。在有动态的情况下再按照官方的那种思考感觉很迷,按照ctfwiki总结的来应用就比较简单了:
动态类型的数据,比如动态数组,结构体,变长字节,其编码后存储其 offset
、length
、data
- 先把参数顺序存储:如果是定长数据类型,直接存储其
data
,如果是变长数据类型,先存储其offset
- 顺序遍历变长数据:先存储
offset
,对于第一个变长数据,先存储其offset = 0x20 * number
(number
是函数参数的个数 );对于下一个变长数据,其offset = offset_of_prev + 0x20 + 0x20 * number
(第一个0x20
是存储前一个变长数据的长度占用的大小,number
是前一个变长数据的元素个数) - 顺序遍历变长数据:存储完
offset
,接着就是遍历每个变长数据,分别存储其length
和data
- (
ps:
对于结构体这样的类型,存储的时候可把结构体内元素看成是一个新函数的参数,这样的话,对于结构体中的第一个变长数据,其offset = 0x20 * num
,num
是结构体元素的个数 )
bytes的offset,0x20*3
,即:
0x0000000000000000000000000000000000000000000000000000000000000060
然后是bool的data:0x0000000000000000000000000000000000000000000000000000000000000001
,
然后是uint[]的offset是offset_of_prev + 0x20 + 0x20 * number
,即之前的96+32+32*1
,即160:
0x00000000000000000000000000000000000000000000000000000000000000a0
然后是bytes
内容的编码:
先是enc(4):0x0000000000000000000000000000000000000000000000000000000000000004
,
然后是pad_right(x):0x6461766500000000000000000000000000000000000000000000000000000000
。
然后就是uint[]内容的编码:
enc(3)是0x0000000000000000000000000000000000000000000000000000000000000003
然后enc(1,2,3)按照前面那样,就分别是:
0x0000000000000000000000000000000000000000000000000000000000000001
,
0x0000000000000000000000000000000000000000000000000000000000000002
,
0x0000000000000000000000000000000000000000000000000000000000000003
。
再把所有这些拼接起来:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
动态类型的使用
前面其实已经涉及到了动态类型,但是官方并未对着做过多的解释,在这一块官方的解释就非常的详细了。
只看这个例子:
让我们使用相同的原理来对一个签名为 g(uint[][],string[])
,参数值为 ([[1, 2], [3]], ["one", "two", "three"])
的函数来进行编码。
函数的签名就不说了,参数是两个动态:uint[][]和string[]
,需要分别计算二者的偏移量和数据部分。
再来看,计算[1, 2], [3]
,也就是数据部分,还得再嵌套一层,算[1,2]
的偏移量和数据部分,还有[3]
的偏移量和数据部分。
先看数据部分,[1,2]
编码后是:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个数组中的元素数量 2;元素本身是1
和2
)0x0000000000000000000000000000000000000000000000000000000000000001
(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000002
(第二个元素)
[3]的编码是:
0x0000000000000000000000000000000000000000000000000000000000000001
(第二个数组中的元素数量 1;元素数据是3
)0x0000000000000000000000000000000000000000000000000000000000000003
(第一个元素)
然后需要知道这个动态数组[[1, 2], [3]]
的偏移量,我一开始没太懂这个偏移量到底是什么,看了官方的这个才终于懂了:
0 - a - [1, 2] 的偏移量
1 - b - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
偏移量 a
指向数组 [1, 2]
内容的开始位置,即第 2 行的开始(64 字节);所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 b
指向数组 [3]
内容的开始位置,即第 5 行的开始(160 字节);所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
这就懂了,同理再看["one", "two", "three"]
,对第二个根数组的嵌入字符串进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(单词"one"
中的字符个数)0x6f6e650000000000000000000000000000000000000000000000000000000000
(单词"one"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000003
(单词"two"
中的字符个数)0x74776f0000000000000000000000000000000000000000000000000000000000
(单词"two"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000005
(单词"three"
中的字符个数)0x7468726565000000000000000000000000000000000000000000000000000000
(单词"three"
的 utf8 编码)
再看偏移量:
0 - c - "one" 的偏移量
1 - d - "two" 的偏移量
2 - e - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 c
指向字符串 "one"
内容的开始位置,即第 3 行的开始(96 字节);所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060
。
偏移量 d
指向字符串 "two"
内容的开始位置,即第 5 行的开始(160 字节);所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
偏移量 e
指向字符串 "three"
内容的开始位置,即第 7 行的开始(224 字节);所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0
。
然后我们对第一个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个根数组的元素数量 2;这些元素本身是[1, 2]
和[3]
)
而后我们对第二个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(第二个根数组的元素数量 3;这些字符串本身是"one"
、"two"
和"three"
)
然后就要开始计算这两个大部分的偏移量了:
0x2289b18c - 函数签名
0 - f - [[1, 2], [3]] 的偏移量
1 - g - ["one", "two", "three"] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数
3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"] 的元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - "one" 的偏移量
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - "two" 的偏移量
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - "three" 的偏移量
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 f
指向数组 [[1, 2], [3]]
内容的开始位置,即第 2 行的开始(64 字节);所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 g
指向数组 ["one", "two", "three"]
内容的开始位置,即第 10 行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140
。
接下来就是拼接的事情了。
上述的大部分都是摘录是官方文档,一开始确实看的很懵,但是最后看到这些例子,就感觉很清楚了。学到了学到了。
此外再看一下ctfwiki上的一个例子:
pragma solidity >=0.4.16 <0.9.0;
pragma experimental ABIEncoderV2;
contract Demo {
struct Test {
string name;
string policies;
uint num;
}
uint public x;
function test5(uint, Test memory test) public { x = 1; }
}
test5(0x123,[“ cxy”,“ pika”,123])
首先是0x123的:
0x0000000000000000000000000000000000000000000000000000000000000123
然后就是第二个参数,这个结构体的偏移量。
按照ctfwiki上的总结,或者按照官方文档的思考方式都可。
偏移量,即可以直接算:offset = 0x20 * number ( number 是函数参数的个数 )
,是64,也可以:
0x4ca373dc // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - a // offset of second parameter,偏移量
2 - xxxx//uint是静态已经结束,因此这里一定是结构体关于data的编码了,因此偏移量指向第二行的开始(64字节)
都可以知道是0x0000000000000000000000000000000000000000000000000000000000000040
。
接下来就是进入这个结构体,这个结构体是string,string,uint,结构体内元素可当成函数参数拆分。因此接下来就是又一个嵌套,先是第一个string的偏移量,然后第二个string的偏移量,然后是uint的编码,然后就是第一个string的data编码和第二个string的data编码。
最终应该是这样:
0x4ca373dc // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000040 //结构体的偏移量
2 - 0x0000000000000000000000000000000000000000000000000000000000000060 //第一个string的偏移量
3 - 0x00000000000000000000000000000000000000000000000000000000000000a0 //第二个string的偏移量
4 - 0x000000000000000000000000000000000000000000000000000000000000007b //uint的编码
5 - 0x0000000000000000000000000000000000000000000000000000000000000003 //第一个string的length
6 - 0x6378790000000000000000000000000000000000000000000000000000000000 //第一个string的data
7 - 0x0000000000000000000000000000000000000000000000000000000000000004 //第二个string的length
8 - 0x70696b6100000000000000000000000000000000000000000000000000000000 //第二个string的data
学到了学到了。