对合约进行逆向工程

您所在的位置:网站首页 以太坊合约调用失败 对合约进行逆向工程

对合约进行逆向工程

2024-07-13 23:13| 来源: 网络整理| 查看: 265

对合约进行逆向工程以太坊虚拟机操作码逆向工程反编译器高级Ori Pomerantz 2021年12月30日38 分钟阅读 minute read在本页面简介准备可执行代码入口点 (0x00)0x5E 处的处理程序(用于非应用程序二进制接口数据调用)0x7C 的处理程序DELEGATECALL 失败应用程序二进制接口调用splitter()E4 代码currentWindow()DA 代码merkleRoot()0x81e580d30x1f135823方法摘要构造函数代理合约scaleAmountByPercentageclaim1e7df9d3总结简介

区块链上没有秘密,发生的一切都是持续的、可验证的、公开的。 理想情况下,应将智能合约的源代码发布到 Etherscan 上进行验证(opens in a new tab)。 然而,情况并非总是如此(opens in a new tab)。 在本文中,你将研究一份没有源代码的合约 0x2510c039cc3b061d79e564b38836da87e31b342f(opens in a new tab),从而学习如何对合约进行逆向工程。

有一些反编译器,但它们不一定能提供有用的结果(opens in a new tab)。 在本文中,你将从操作码(opens in a new tab)入手,学习如何对合约手动进行逆向工程并理解合约,以及如何解读反编译器生成的结果。

为了能够理解本文,你应当已经了解以太坊虚拟机基础知识,并至少对以太坊虚拟机汇编器有几分熟悉。 点击此处了解这些主题(opens in a new tab)。

准备可执行代码

你可以在 Etherscan 上获得合约的操作码,操作如下:点击 Contract 选项卡,然后切换至 Opcodes 视图。 你将看到每行有一条操作码。

但是,为了能够理解跳转,你需要知道每条操作码在代码中的位置。 为此,一种方式是打开 Google Spreadsheet 并把操作码粘贴到 C 列。你可以创建这个已制作好的电子表格的副本,从而跳过以下步骤(opens in a new tab)。

下一步是获得正确的操作码位置,以便我们能够理解跳转。 我们将操作码大小放入 B 列,操作码位置(十六进制形式)放入 A 列。在单元格 B1 中输入下面的函数,然后复制并粘贴到 B 列其余单元格中,直到代码结束。 完成后,你就可以隐藏 B 列。

1=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)

首先该函数给操作码增加一个字节,然后查找 PUSH 操作码。 Push 操作码比较特殊,因为它们需要额外的字节表示压入的值。 如果操作码是 PUSH,我们提取该字节的数值并在函数中增加相应的值。

在 A1 单元格中输入第一个偏移量 0。 然后在 A2 单元格中,输入下面的函数,并再次将它复制粘贴到 A 列其余他单元格中:

1=dec2hex(hex2dec(A1)+B1)

我们需要此函数提供十六进制值,因为跳转(JUMP 和 JUMPI)之前压入的值也是十六进制的。

入口点 (0x00)

智能合约总会从第一个字节开始执行。 下面是代码的开始部分:

偏移量操作码堆栈(在操作码之后)0PUSH1 0x800x802PUSH1 0x400x40, 0x804MSTORE空5PUSH1 0x040x047CALLDATASIZECALLDATASIZE 0x048LTCALLDATASIZE[6] CALLVALUE 0 6 CALLVALUE[6] 中的值。 我们还不知道这个值是什么,但我们可以查找合约收到的没有调用数据的交易。 仅转账以太币而没有任何调用数据(因此没有方法)的交易在 Etherscan 中具有方法 * CALLVALUE 0 6 CALLVALUE* 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE*2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE** CALLVALUE 0x75 0 6 CALLVALUE** CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE* CALLVALUE 0x75 0 6 CALLVALUE*+CALLVALUE 0x75 0 6 CALLVALUE*+CALLVALUE 0 6 CALLVALUE*+CALLVALUE 0 6 CALLVALUE*+CALLVALUE 6 CALLVALUE*+CALLVALUE 0 CALLVALUE[3] 0x9D 0x00[3] 0x9D 0x00[3]-as-address 0x9D 0x00[3] 读取的值截断为 160 位,即以太坊地址的长度。[3]-as-address 0x00[3]-as-address 0x00[3]-as-address 0x00[3]-as-address[3]-as-address[3]-as-address[0x40] Storage[3]-as-address[0x40] 设置为 0x80。 如果我们在后面部分查找 0x40,会发现我们没有更改它 - 所以我们可以假设它是 0x80。[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address[3] 中的地址来完成真实的工作。 [3] 做为地址[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address[3]-as-address ,运行 EQ 检查是否相等,然后如果方法签名匹配,JUMPI。 以下是方法签名、它们的地址以及相应的方法定义(opens in a new tab)(如果已知):

方法方法签名跳转到的偏移量splitter()(opens in a new tab)0x3cd8045e0x0103???0x81e580d30x0138currentWindow()(opens in a new tab)0xba0bafb40x0158???0x1f1358230x00C4merkleRoot()(opens in a new tab)0x2eb4a7ab0x00ED

如果没有找到匹配项,代码跳转到 0x7C 处的代理处理程序,指望我们作为代理的合约有匹配项。

splitter()偏移量操作码堆栈103JUMPDEST104CALLVALUECALLVALUE105DUP1CALLVALUE CALLVALUE106ISZEROCALLVALUE 0 CALLVALUE107PUSH2 0x01df0x010F CALLVALUE==0 CALLVALUE10AJUMPICALLVALUE10BPUSH1 0x000x00 CALLVALUE10DDUP10x00 0x00 CALLValUE10EREVERT

此函数首先检查调用没有发送任何以太币。 此函数不是 payable(opens in a new tab)。 如果有人向我们发送了以太币,而那肯定是误发,我们希望 REVERT 以避免将此以太币放入他们无法取回的位置。

偏移量操作码堆栈10FJUMPDEST110POP111PUSH1 0x030x03113SLOAD(((Storage[3] a.k.a the contract for which we are a proxy)))114PUSH1 0x400x40 (((Storage[3] a.k.a the contract for which we are a proxy)))116MLOAD0x80 (((Storage[3] a.k.a the contract for which we are a proxy)))117PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xFF...FF 0x80 (((Storage[3] a.k.a the contract for which we are a proxy)))12CSWAP10x80 0xFF...FF (((Storage[3] a.k.a the contract for which we are a proxy)))12DSWAP2(((Storage[3] a.k.a the contract for which we are a proxy))) 0xFF...FF 0x8012EANDProxyAddr 0x8012FDUP20x80 ProxyAddr 0x80130MSTORE0x80

0x80 现在包含代理地址

偏移量操作码堆栈131PUSH1 0x200x20 0x80133ADD0xA0134PUSH2 0x00e40xE4 0xA0137JUMP0xA0E4 代码

这是我们第一次看到这些行,但它们与其他方法是共享的(见下文)。 所以我们将调用堆栈 X 中的值,记住在 splitter() 中此 X 的值是 0xA0。

偏移量操作码堆栈E4JUMPDESTXE5PUSH1 0x400x40 XE7MLOAD0x80 XE8DUP10x80 0x80 XE9SWAP2X 0x80 0x80EASUBX-0x80 0x80EBSWAP10x80 X-0x80ECRETURN

因此,此代码接收堆栈 (X) 中一个内存指针,并导致合约 RETURN 缓冲区 0x80 - X。

对于 splitter() 方法,将返回我们作为代理的地址。 RETURN 返回 0x80-0x9F 之间的缓冲区,这是我们写入此数据的位置(上面的偏移量 0x130)。

currentWindow()

偏移量 0x158-0x163 中的代码与我们在 splitter() 中看到的 0x103-0x10E 中的代码相同(除 JUMPI 目标地址外),因此我们知道 currentWindow () 也不是 payable。

偏移量操作码堆栈164JUMPDEST165POP166PUSH2 0x00da0xDA169PUSH1 0x010x01 0xDA16BSLOADStorage[1] 0xDA16CDUP20xDA Storage[1] 0xDA16DJUMPStorage[1] 0xDADA 代码

此代码也与其他方法共享。 所以我们将调用堆栈 Y 中的值,并且记住在 currentWindow() 中这个 Y 的值是 Storage[1]。

偏移量操作码堆栈DAJUMPDESTY 0xDADBPUSH1 0x400x40 Y 0xDADDMLOAD0x80 Y 0xDADESWAP1Y 0x80 0xDADFDUP20x80 Y 0x80 0xDAE0MSTORE0x80 Y 0xDA

将 Y 写入 0x80-0x9F。

偏移量操作码堆栈E1PUSH1 0x200x20 0x80 0xDAE3ADD0xA0 0xDA

其余部分已在上面解释过。 所以跳转到 0xDA,将栈顶 (Y) 的值写入 0x80-0x9F,并返回该值。 对于 currentWindow() 方法,返回 Storage[1]。

merkleRoot()

偏移量 0xED-0xF8 中的代码与我们在 splitter() 中看到的 0x103-0x10E 中的代码相同(除 JUMPI 目标地址外),因此我们知道 merkleRoot () 也不是 payable。

偏移量操作码堆栈F9JUMPDESTFAPOPFBPUSH2 0x00da0xDAFEPUSH1 0x000x00 0xDA100SLOADStorage[0] 0xDA101DUP20xDA Storage[0] 0xDA102JUMPStorage[0] 0xDA

我们已经弄清楚了跳转后会发生什么。 嗯,merkleRoot() 返回 Storage[0]。

0x81e580d3

偏移量 0x138-0x143 中的代码与我们在 splitter() 中看到的 0x103-0x10E 中的代码相同(除 JUMPI 目标地址外),因此我们知道此函数也不是 payable。

偏移量操作码堆栈144JUMPDEST145POP146PUSH2 0x00da0xDA149PUSH2 0x01530x0153 0xDA14CCALLDATASIZECALLDATASIZE 0x0153 0xDA14DPUSH1 0x040x04 CALLDATASIZE 0x0153 0xDA14FPUSH2 0x018f0x018F 0x04 CALLDATASIZE 0x0153 0xDA152JUMP0x04 CALLDATASIZE 0x0153 0xDA18FJUMPDEST0x04 CALLDATASIZE 0x0153 0xDA190PUSH1 0x000x00 0x04 CALLDATASIZE 0x0153 0xDA192PUSH1 0x200x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA194DUP30x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA195DUP5CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA196SUBCALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA197SLTCALLDATASIZE-4=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA199PUSH2 0x01a00x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA19CJUMPI0x00 0x04 CALLDATASIZE 0x0153 0xDA

看起来此函数至少使用调用数据的 32 个字节(一个字)。

偏移量操作码堆栈19DDUP10x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA19EDUP20x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA19FREVERT

如果此函数没有获得调用数据,则交易将回滚且不会任何返回数据。

我们来看看,如果此函数确实获得了它需要的调用数据,会出现什么情况。

偏移量操作码堆栈1A0JUMPDEST0x00 0x04 CALLDATASIZE 0x0153 0xD1A1POP0x04 CALLDATASIZE 0x0153 0xDA1A2CALLDATALOADcalldataload(4) CALLDATASIZE 0x0153 0xDA

calldataload(4) 是在方法签名之后调用数据的第一个字

偏移量操作码堆栈1A3SWAP20x0153 CALLDATASIZE calldataload(4) 0xDA1A4SWAP1CALLDATASIZE 0x0153 calldataload(4) 0xDA1A5POP0x0153 calldataload(4) 0xDA1A6JUMPcalldataload(4) 0xDA153JUMPDESTcalldataload(4) 0xDA154PUSH2 0x016e0x016E calldataload(4) 0xDA157JUMPcalldataload(4) 0xDA16EJUMPDESTcalldataload(4) 0xDA16FPUSH1 0x040x04 calldataload(4) 0xDA171DUP2calldataload(4) 0x04 calldataload(4) 0xDA172DUP20x04 calldataload(4) 0x04 calldataload(4) 0xDA173SLOADStorage[4] calldataload(4) 0x04 calldataload(4) 0xDA174DUP2calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA175LTcalldataload(4)[4] calldataload(4) 0x04 calldataload(4) 0xDA[4] calldataload(4) 0x04 calldataload(4) 0xDA[4],则函数失败。 此函数回滚且没有任何返回值:[4],我们得到以下代码:[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA[4] 位置的值)提供一个数据项。[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA* 0xDA* 0xDA* 0xDA[3],代理地址[1][0][4][6], a.k.a. 值*[3] 中的合约还提供了其他功能。 也许如果我们知道该合约是什么,它就会给我们一条线索。 值得庆幸的是,这是区块链,一切都是已知的,至少理论上是这样。 我们没有看到任何设置 Storage[3] 的方法,所以它一定是由构造函数设置的。[3] 包含 -1 / _param1:4 revert with 0, 175 return (_param1 * _param2 / 100 * 10^6) 复制

第一个 require 测试调用数据除了有函数签名的 4 个字节外,至少还要有 64 个字节,方可容纳两个参数。 如果不是,那么显然有问题。

if 语句似乎检查 _param1 不为零,并且 _param1 * _param2 不是负数。 这可能是为了防止发生回绕的情况。

最后,此函数返回一个调整后的值。

claim

反编译器创建的代码很复杂,并不是所有代码都与我们相关。 这里将跳过其中一些内容,专注于我认为提供有用信息的行

1def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:2 ...3 require _param2 == addr(_param2)4 ...5 if currentWindow 复制复制[5] 是一个窗口和地址数组,并知道该地址是否申领了此窗口内奖励。复制复制复制[2] 也是我们调用的合约。 如果我们复制 mem[(32 * idx) + 128]:10 ...11 s = sha3(mem[_58 + 32 len mem[_58]])12 continue13 mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])14 ...15 if unknown2eb4a7ab != s:16 revert with 0, 'Invalid proof'17 ...18 call addr(_param1) with:19 value s wei20 gas 30000 wei21 if not return_data.size:22 if not ext_call.success:23 require ext_code.size(stor2)24 call stor2.deposit() with:25 value s wei26 gas gas_remaining wei27 ...28 log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)显示全部 复制

主要区别在于第一个参数,即要提款的窗口,不存在。 相反,所有可以声明的窗口都有一个循环。

1 idx = 02 s = 03 while idx = unknown81e580d3.length:14 revert with 0, 5015 mem[0] = 416 if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:17 revert with 0, 1718 if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):19 revert with 0, 1720 if idx == -1:21 revert with 0, 1722 idx = idx + 123 s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)24 continue显示全部 复制

所以它看起来像一个声明所有窗口的 claim 变体。

总结

现在,你应该知道如何通过操作码或反编译器(当它有效时)理解没有源代码的智能合约。 从本文的篇幅明显可以看出,对合约进行逆向工程并非易事,但在安全性至关重要的系统中,能够验证合约是否按承诺运作是一项重要技能。

w

上次修改时间: @wackerow(opens in a new tab), 2024年4月2日

本教程对你有帮助吗?


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3