前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >逆向 EVM - 解析原始Calldata数据

逆向 EVM - 解析原始Calldata数据

作者头像
Tiny熊
发布2023-01-09 17:54:10
1.3K0
发布2023-01-09 17:54:10
举报

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

你可能想知道如何破译和读取 evm 的 calldata,然后试图读取以太坊智能合约的交易 calldata,EVM(和其他 L1 分叉)以特定的方式对静态和动态类型的 calldata 进行编码和解码,在某种程度上让数据变得很困惑,起码最初是这样的。

在这篇文章中,我们将深入研究 calldata 的编码顺序,以便你能理解任何经过验证或未经验证的智能合约交易,并理解这些字节。通过这样做,我希望能让你有能力创建自己的原始 calldata。

什么是 Calldata?

Calldata 是我们发送给函数的编码参数,在这里是发送给以太坊虚拟机(EVM)上的智能合约。每块 calldata 有 32 个字节长(或 64 个字符)。有两种类型的 calldata:静态和动态。

静态变量是相当简单易懂的。另一方面,动态变量则要复杂得多,这可能是你难以直观地阅读原始 calldata 的原因。然而,一旦我们了解了动态变量是如何工作的,你就能轻松地阅读原始 calldata 了。

首先,让我们了解一下 calldata 是如何编码和解码的,以便为这一切的工作建立一个基础。

编码 Calldata

要对类型进行编码,你可以将它们传入abi.encode(parameters)方法,以生成原始 calldata。

如果你想为一个特定的接口函数编码 Calldata,你可以使用 abi.encodeWithSelector(selector, parameters)。这将与直接传入函数和它的参数一样。

比如说:

代码语言:javascript
复制
interface A {
  function transfer(uint256[] memory ids, address to) virtual external;
}

contract B {
  function a(uint256[] memory ids, address to) external pure returns(bytes memory) {
    return abi.encodeWithSelector(A.transfer.selector, ids, to);
  }
}

方法.selector产生了 4 个字节(称为:函数选择器),在接口上代表该方法。我们用它来告诉 EVM,我们正在向该函数发送我们的 calldata。这就是 UniswapV2 如何实现闪电兑换。

还有abi.encodePacked(...),它可以有效地将所有动态变量放在一起,去掉 0 的填充。它的问题是,它不能防止碰撞,只有在你确定了参数的类型和长度时才可以使用。

解码 calldata

那么你有了 calldata,你如何解码它呢?

如果 calldata 是用abi.encode(...)创建的,那么我们可以用abi.decode(...)对参数进行解码,只要传入我们想把 calldata 解码成的参数。

例如:

代码语言:javascript
复制
(uint256 a, uint256 b) = abi.decode(data, (uint256, uint256))

其中data代表被传入的 calldata。

现在我们了解了如何对参数进行编码和解码,我们可以继续讨论不同的变量类型以及它们如何反映在 calldata 输出中。

静态变量

静态变量是以下类型的简单编码表示,uint , int, address, bool, bytes1 to bytes32 (包括函数选择器), 和tuple (然而它们可以有动态变量)。

例如,假设我们正在与以下合约进行交互:

代码语言:javascript
复制
pragma solidity 0.8.17;
contract Example {
    function transfer(uint256 amount, address to) external;
}

带有输入参数:

代码语言:javascript
复制
amount: 1300655506
address: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45

我们将生成 calldata:0x000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45

但是......我们怎么读这个呢?

好吧,让我们把它分成可读的部分,首先去掉前缀0x,然后把每一行分成 64 个字符(或 32 字节)的部分

代码语言:javascript
复制
0x
// uint256
000000000000000000000000000000000000000000000000000000004d866d92
// address
00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45

酷,现在我们知道前 32 字节是 "uint256 amount "变量,后 32 字节是 "address to"。

函数

但是如果我们想直接调用transfer函数呢?

我们需要知道参数类型的顺序,并使用一种叫做 "keccak256 "的 Hash 算法,将输入的数据变成一个 32 字节的 hash 值:

在此案例中,要获取函数哈希:

代码语言:javascript
复制
function transfer(uint256 amount, address to) external;

我们会这样做:

代码语言:javascript
复制
keccak256("transfer(uint256,address)");

这将返回以下 32 字节的哈希值:

代码语言:javascript
复制
0xb7760c8fd605b6ef5a068e1720c115665f9699a5c439e3c0ee9709290ff8a3bb

为了得到函数签名,我们只需要前 4 字节(或 8 个字符,不包括0x前缀):b7760c8f

这个 4 字节的签名,b7760c8f,是告诉 EVM 我们正在与该函数进行交互,下面的 calldata 被作为参数传入:

例如,如果我们要调用transfer,参数与之前的静态变量相同,其 calldata 为:

代码语言:javascript
复制
0x000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45

并在前 32 个字节的前 4 个字节的开头加上b7760c8f

代码语言:javascript
复制
0xb7760c8f000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45

代码语言:javascript
复制
0x
b7760c8f
000000000000000000000000000000000000000000000000000000004d866d92
00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45

你可能想知道,calldata 参数究竟是如何被输入到带有签名的函数中的?

答案是,合约的字节码通过匹配目标函数b7760c8f来读取它,然后用00000000替换它,然后传入参数。

动态变量

动态变量是非固定大小的类型,包括bytesstring和动态数组<T>[],以及固定数组<T>[N]

动态类型的结构总是以偏移量开始,偏移量是动态类型开始位置的十六进制表示。例如,十六进制的 "20 "代表 "32 字节"。一旦我们到达偏移量,就会有一个更小的数字代表该类型的长度。

简而言之:第一个 32 字节=偏移量,第二个 32 字节=长度,其余的是元素。

对于数组,这个长度代表数组中包含的元素数量。对于字节和字符串类型,它代表该类型的长度。例如,字符串 "Hello World!"是 12 字节的长度,每个字符是 1 字节。请记住,这些类型从 calldata 的左边开始,而不是像其他东西一样从右边开始。

例如,这里是对string “Hello World!” 的编码:

代码语言:javascript
复制
0x
0000000000000000000000000000000000000000000000000000000000000020
000000000000000000000000000000000000000000000000000000000000000c
48656c6c6f20576f726c64210000000000000000000000000000000000000000

观察一下前 32 个字节是如何代表十六进制的偏移量20的,也就是十进制的32。所以我们从000000000000000000000000000000000000000000000020开始跳过 32 字节,把我们带到下一行,十六进制为0c,十进制为12,代表我们的字符串的字节长度。现在,当我们把48656c6c6f20576f726c6421转换为字符串类型时,会返回我们的原始值。

祝贺你! 现在你知道如何读取动态类型了。

解读静态和动态参数

假设我们正在与下面的合约进行交互:

代码语言:javascript
复制
pragma solidity 0.8.17;
contract Example {
    function transfer(uint256[] memory ids, address to) external;
}

有了下面的 "transfer"的参数:

代码语言:javascript
复制
ids: ["1234", "4567", "8910"]
to: 0xf8e81D47203A594245E36C48e151709F0C19fBe8

我们将生成 calldata:0x8229ffb60000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000011d700000000000000000000000000000000000000000000000000000000000022ce

我们可以把它切成一个更可读的形式:

代码语言:javascript
复制
// 前缀,不管
0x
// 函数选择器 (`transfer(uint[], address)`)
8229ffb6
// `uint256[] ids` 参数数组偏移 (64-bytes below from start of this line)
0000000000000000000000000000000000000000000000000000000000000040
// `address to` param
000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8
// `ids` 数组长度:3
0000000000000000000000000000000000000000000000000000000000000003
// 第一个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000004d2
// 第二个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000011d7
// 第三个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000022ce

请注意,数组参数是由一个偏移量来代表数组的开始位置。然后我们转到第二个参数,地址类型,然后完成数组类型。

现在我们知道了如何读取静态参数和动态参数,让我们来剖析一个更复杂的例子!

解码一个 Multicall 的 Calldata

我们将从这个[4]交易中得到一个 UniswapV3 multicall 的输入 calldata,在这里,用户从 multicall 函数中调用 3 个不同的函数:

Etherscan 很好地给了我们一个简单的解码版本:

代码语言:javascript
复制
MethodID: 0xac9650d8
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000120
00000000000000000000000000000000000000000000000000000000000002c0
0000000000000000000000000000000000000000000000000000000000000084
13ead56200000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c
6e28c531000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead908
3c756cc200000000000000000000000000000000000000000000000000000000
00002710000000000000000000000000000000000000000000831162ce86bc88
052f80fd00000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000164
8831645600000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c
6e28c531000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead908
3c756cc200000000000000000000000000000000000000000000000000000000
00002710ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffaf17800000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000002e3bdc2534919
6582d720000000000000000000000000000000000000000000000000c249fdd3
2778000000000000000000000000000000000000000000000002e1e525c2ef9d
cec50c53000000000000000000000000000000000000000000000000c1cd7c9a
dfb0d9dc000000000000000000000000ed6c2cb9bf89a2d290e59025837454bf
1f144c5000000000000000000000000000000000000000000000000000000000
635ce8bf00000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000004
12210e8a00000000000000000000000000000000000000000000000000000000

我们将对其进行一些修改,并在此基础上逐行展开,使其更具有可读性。请记住,每个值都是十六进制格式,"20 个十六进制==32 字节",以便快速参考。

代码语言:javascript
复制
MethodID: 0xac9650d8
// 数组_1 的偏移 (starting next line)
0000000000000000000000000000000000000000000000000000000000000020
// 数组_1 的长度  (how many elements in array)
0000000000000000000000000000000000000000000000000000000000000003
// 数组_1中 第一个元素 数组_1A 的偏移  (96-bytes / 32 = 3)
0000000000000000000000000000000000000000000000000000000000000060
// 数组_1中 第二个元素 数组_1B 的偏移 (288-bytes / 32 = 9)
0000000000000000000000000000000000000000000000000000000000000120
// 数组_1中 第三个元素 数组_1C 的偏移 (704-bytes / 32 = 22)
00000000000000000000000000000000000000000000000000000000000002c0

// 数组_1A 的长度  (132-bytes (inc. selector))
000000000000000000000000000000000000000000000000000000000000008

// 读接下来的 132 个字节
// 函数选择器; 4 of 132
13ead562
// 1st param; 36 of 132
00000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c6e28c531
// 2nd param; 68 of 132
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
// 3rd param; 100 of 132
0000000000000000000000000000000000000000000000000000000000002710
// 4th param; 132 of 132
// this marks the end of array_1A
000000000000000000000000000000000000000000831162ce86bc88052f80fd

// 32-bytes of `0` indicating next elemet
0000000000000000000000000000000000000000000000000000000000000000
// length 2nd element of array_1, array_1B (356-bytes (inc. selector))
// we have 4-bytes missing due to the embedded fn selector, 13ead562
// the next fn selector, 88316456, will be inserted here
00000000000000000000000000000000000000000000000000000164

// 读接下来的 356 个字节
// 函数选择器; 4 of 356
88316456
// 1st param; 36 of 356
00000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c6e28c531
// 2nd param; 68 of 356
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
// 3rd param; 100 of 356
0000000000000000000000000000000000000000000000000000000000002710
// 4th param; 132 of 356
// notice how all the `0`s are `f`s. this indicates a `int` type!
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffaf178
// 5th param; 164 of 356
// we have 32-bytes of `0`, but since we're still reading the bytes
// we know this is a paramter, representing 0 of a type
0000000000000000000000000000000000000000000000000000000000000000
// 6th param; 196 of 356
00000000000000000000000000000000000000000002e3bdc25349196582d720
// 7th param; 228 of 356
000000000000000000000000000000000000000000000000c249fdd327780000
// 8th param; 260 of 356
00000000000000000000000000000000000000000002e1e525c2ef9dcec50c53
// 9th param; 292 of 356
000000000000000000000000000000000000000000000000c1cd7c9adfb0d9dc
// 10th param; 324 of 356
000000000000000000000000ed6c2cb9bf89a2d290e59025837454bf1f144c50
// 11th param; 356 of 356
// this marks the end of array_1B
00000000000000000000000000000000000000000000000000000000635ce8bf

// 32-bytes of `0` indicating next elemet
0000000000000000000000000000000000000000000000000000000000000000
// this is the same thing as before, the length!
// we can see there's only 32-bytes left so we can conclude
// that it's going to be a fn with no inputs
00000000000000000000000000000000000000000000000000000004

// a call to the fn selector 12210e8a; 4 of 4
12210e8a00000000000000000000000000000000000000000000000000000000

现在你已经能够读取原始的嵌入式动态类型了!

最后

我希望这些信息能够帮助你理解 calldata 是如何编码、解码和读取的。为了学习,我花了一些时间来研究和试验这一切,但这是值得的。从这里开始的下一步是学习如何读取字节码,以便在最底层了解 EVM(然后一切都变得开源了>:D)。

原文链接:https://degatchi.com/articles/reading-raw-evm-calldata

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

这个: https://etherscan.io/tx/0x31a45e8893f0cc7de009da5546539f703ed725d076ccdf73d307df5caa8c72b3

Twitter : https://twitter.com/NUpchain Discord : https://discord.gg/pZxy3CU8mh

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 Calldata?
  • 编码 Calldata
  • 解码 calldata
  • 静态变量
  • 函数
  • 动态变量
  • 解读静态和动态参数
  • 解码一个 Multicall 的 Calldata
  • 最后
    • 参考资料
    相关产品与服务
    区块链
    云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档