一
工具&链接
https://ethereum.org/zh/developers/docs/evm/opcodes/
https://ethervm.io/decompile#google_vignette
二
Thought
三
调试分析&静态分析
Deletegation
合约部署的交易Hash丢到调试器里,先从合约创建开始分析。3.1函数创建分析
3.1.1 Non Payable Check
000 PUSH1 80
002 PUSH1 40
004 MSTORE ;MSTORE(40h,80h)
04
执行前,调试器中显示的Memory为No data available,但EVM实际执行过程中在此处是否真为空暂时无从得知。关于
MSTORE
在以太坊官网的虚拟机操作码说明书中有如下定义:Stack:
No data available
Memory:
0x0:
00000000000000000000000000000000
0x10:
00000000000000000000000000000000
0x20:
00000000000000000000000000000000
0x30:
00000000000000000000000000000000
0x40:
00000000000000000000000000000000
0x50:
00000000000000000000000000000080
(ost=40h,val=80h)
,而80h
最终被写到了50h
的位置,这与说明书中的操作不符,我暂时不知道是调试器的错误还是我的理解错误。使用官方的Remix-ide
进行同样的指令调试,最终看到的Memory
和推测的一样,80h
应该存储在了0x40
处才正确。005 CALLVALUE ;将msg.value压栈
006 DUP1 ;拷贝一份栈顶
007 ISZERO ;弹出当前栈顶,判断是否为0,结果再次压栈
008 PUSH2 0010 ;10h压栈
011 JUMPI ;if(栈顶==1){jmp 10h}
012 PUSH1 00
014 DUP1
015 REVERT
msg.value
是否为0,若为0则正常跳转,反之撤销本次交易,以下是构造函数定义。constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
payable
关键字修饰,再进行编译,后查看此处汇编指令,此时发现,与预想中的不同,并非将ISZERO
进行NOT
取反,而是直接去除了该判断跳转,于是我进行了一次不转账的合约部署,发现成功了,我之前并不知道合约的构造函数加了payable
修饰是不强制转账的,现在知道了。3.1.2 Arguments Copy
016 JUMPDEST ;跳转点
017 POP ;弹出上一操作中的遗留数据(不理解为何上一步中专门DUP拷贝一份,但实际上只用一次,此处还需要弹出)
018 PUSH1 40
020 MLOAD ;加载Memory+0x40所存数据并压栈,既上一步中写入的80h
021 PUSH2 033e
024 CODESIZE ;获得执行合约代码的长度,并压栈
025 SUB ;弹值后计算得到 codesize-33eh
026 DUP1 ;计算结果拷贝
027 PUSH2 033e
030 DUP4 ;Memory+0x40值拷贝
031 CODECOPY ;指令拷贝到Memory->CODECOPY(0x80,33eh,subResult)
032 DUP2 ;拷贝Memory+0x40值
033 DUP2 ;拷贝SUB长度计算结果
034 ADD ;两值相加
035 PUSH1 40
037 MSTORE ;结果存回Memory+0x40
038 DUP2 ;原Memory+0x40值拷贝【80h】
039 ADD ;再与Sub长度计算结果相加
040 SWAP1
041 PUSH2 0032
044 SWAP2
045 SWAP1
046 PUSH2 00ce
049 JUMP ;调用了某个函数
CODECOPY(80h,33eh,20h)
,操作完成后,最终栈剩余数据80h、80h+20h(拷贝数据长度)、32h(push)
,随后便进入了下一个函数中。3.1.3 Arguments Check
284 JUMPDEST
285 PUSH1 00
287 PUSH1 20
289 DUP3 ;push 80h
290 DUP5 ;push a0h
291 SUB ;pop->pop->push a0h-80h
292 SLT ;pop->pop->push stack[0] < stack[1](有符号)
293 ISZERO ;TRUE
294 PUSH2 0132
297 JUMPI
306 JUMPDEST
307 PUSH1 00
309 PUSH2 0140
312 DUP5 ;push a0h
313 DUP3 ;push 0
314 DUP6 ;push 80h
315 ADD ;pop->pop->push 80h+0
316 PUSH2 0107
319 JUMP
263 JUMPDEST
264 PUSH1 00
266 DUP2 ;push 80h
267 MLOAD ;push memory[80h]
268 SWAP1
269 POP
270 PUSH2 0116
273 DUP2 ;push memory[80h]
274 PUSH2 00f0
277 JUMP
240 JUMPDEST
241 PUSH2 00f9
244 DUP2 ;push memory[80h]
245 PUSH2 00de
248 JUMP
222 JUMPDEST
223 PUSH1 00
225 PUSH2 00e9
228 DUP3 ;push memory[80h]
229 PUSH2 00be
232 JUMP
190 JUMPDEST
191 PUSH1 00
193 PUSH20 ffffffffffffffffffffffffffffffffffffffff
214 DUP3 ;push memory[80h]
215 AND ;将memory[80h]的值保留20字节
216 SWAP1
217 POP
218 SWAP2
219 SWAP1
220 POP
221 JUMP
221-JUMP
指令前,我们先看一下此时的栈情况。0:
0x00000000000000000000000000000000000000000000000000000000000000e9
1:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
2:
0x0000000000000000000000000000000000000000000000000000000000000000
3:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
4:
0x00000000000000000000000000000000000000000000000000000000000000f9
5:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
6:
0x0000000000000000000000000000000000000000000000000000000000000116
7:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
8:
0x0000000000000000000000000000000000000000000000000000000000000080
9:
0x00000000000000000000000000000000000000000000000000000000000000a0
10:
0x0000000000000000000000000000000000000000000000000000000000000140
11:
0x0000000000000000000000000000000000000000000000000000000000000000
12:
0x0000000000000000000000000000000000000000000000000000000000000000
13:
0x0000000000000000000000000000000000000000000000000000000000000080
14:
0x00000000000000000000000000000000000000000000000000000000000000a0
15:
0x0000000000000000000000000000000000000000000000000000000000000032
233 JUMPDEST
234 SWAP1
235 POP
236 SWAP2
237 SWAP1
238 POP
239 JUMP
0:
0x00000000000000000000000000000000000000000000000000000000000000f9
1:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
2:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
3:
0x0000000000000000000000000000000000000000000000000000000000000116
4:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
5:
0x0000000000000000000000000000000000000000000000000000000000000080
6:
0x00000000000000000000000000000000000000000000000000000000000000a0
7:
0x0000000000000000000000000000000000000000000000000000000000000140
8:
0x0000000000000000000000000000000000000000000000000000000000000000
9:
0x0000000000000000000000000000000000000000000000000000000000000000
10:
0x0000000000000000000000000000000000000000000000000000000000000080
11:
0x00000000000000000000000000000000000000000000000000000000000000a0
12:
0x0000000000000000000000000000000000000000000000000000000000000032
249 JUMPDEST
250 DUP2 ;push Memory[80h]
251 EQ
252 PUSH2 0104
255 JUMPI
256 PUSH1 00
258 DUP1
259 REVERT
260 JUMPDEST
261 POP
262 JUMP
278 JUMPDEST
279 SWAP3
280 SWAP2
281 POP
282 POP
283 JUMP
320 JUMPDEST
321 SWAP2
322 POP
323 POP
324 SWAP3
325 SWAP2
326 POP
327 POP
328 JUMP
328-JUMP
的栈状态如下:0:
0x0000000000000000000000000000000000000000000000000000000000000032
1:
0x000000000000000000000000d9145cce52d386f254917e481eb44e9943f39138
Delegation
合约的构造函数参数进行了检查,最终stack
中仅剩下参数值。Delegation
合约的构造函数仅将一个地址参数赋值给成员,但依旧对其作了&0xFF(x20)
保留20bytes
的操作,注意,是所有针对地址的操作。&
前后结果作对比,也就是不论结果如何,并不会因此而撤销交易。AND -> EQ
,随后就进行多次栈交换,清理栈空间最终进入构造函数。3.2 DelegateCall攻击分析
Non Payable Check
,因为没有参数,所以没有Arguments Copy/Check
,注意,Arguments Copy/Check
是合约创建时的操作,发起交易时的参数不会通过CODECOPY
来加载,而是通过CallData
进行传递和加载。3.2.1 CallData Prepared&Compared()
016 JUMPDEST
017 POP
018 PUSH1 04
020 CALLDATASIZE ;push len(msg.data)
021 LT ;push len(msg.data) < 4
022 PUSH2 002f
025 JUMPI ;if()jmp 2fh
026 PUSH1 00
028 CALLDATALOAD ;push calldata[0]
029 PUSH1 e0
031 SHR ;stack[0] = stack[0] >> (256-32),位移后获函数签名
032 DUP1 ;push stack[0]
033 PUSH4 8da5cb5b
038 EQ
039 PUSH2 00c3
042 JUMPI
043 PUSH2 0030
046 JUMP
由于当前合约除
fallback
之外,对外public
的成员仅有一个owner
,所以上述指令段较短,但当合约中存在多个public
成员时,该指令段便会一一对应存在多个签名匹配。形如
SwitchCase
,但却没有SwitchCase
的效率,是纯if-elseif-else
25-JUMPI
和46-JUMP
,此两处跳转分别对应两个处理函数。receive&fallback
CallData
为空时,就意味着本次交易连最基本的目标函数都没有被指定,那么进入后面的签名匹配流程也就毫无意义,熟悉合约开发的话就知道,在合约中存在receive
和fallback
两个特殊的函数,我认为将其叫做回调函数并不太恰当,因为它们的每一次调用实际上如其它函数一般,都是“指名道姓”的,只是它们并不存在签名,而作为上述指令段中的头
和尾
存在。041 PUSH1
043 DUP1
044 REVERT
041 PUSH1
043 DUP1
044 REVERT
fallback
或者receive
的话,交易是会被撤销的,原因在此。receive
是当calldata
不存在的时候被调用的,fallback
则是殿后的那位,所以当receive
不存在时,匹配签名前的第一个跳转将直接跳转到fallback
的入口点前,从而进入fallback。
calldata
存在且无法匹配到函数签名,最终进入fallback
,下面瞅指令。049 PUSH1 00
051 PUSH1 01
053 PUSH1 00
055 SWAP1
056 SLOAD ;push storage[0]
057 SWAP1
058 PUSH2 0100
057 SWAP1
058 PUSH2 0100
061 EXP
062 SWAP1
063 DIV
064 PUSH20 ffffffffffffffffffffffffffffffffffffffff
085 AND
086 PUSH20 ffffffffffffffffffffffffffffffffffffffff
107 AND ;对Delegate合约地址进行了两次保留20字节的与操作,意义不明
108 PUSH1 00
110 CALLDATASIZE
111 PUSH1 40
113 MLOAD ;加载预留指针80h
114 PUSH2 007c
117 SWAP3
118 SWAP2
119 SWAP1
120 PUSH2 0139
123 JUMP
0:
0x0000000000000000000000000000000000000000000000000000000000000080
1:
0x0000000000000000000000000000000000000000000000000000000000000004
2:
0x0000000000000000000000000000000000000000000000000000000000000000
3:
0x000000000000000000000000000000000000000000000000000000000000007c
4:
0x000000000000000000000000358aa13c52544eccef6b0add0f801012adad5ee3
5:
0x0000000000000000000000000000000000000000000000000000000000000000
6:
0x00000000000000000000000000000000000000000000000000000000dd365b8b
313 JUMPDEST
314 PUSH1 00
316 PUSH2 0146
319 DUP3
320 DUP5
321 DUP7
322 PUSH2 0114
325 JUMP
276 JUMPDEST
277 PUSH1 00
279 PUSH2 0120
282 DUP4
283 DUP6
284 PUSH2 016d
287 JUMP
365 JUMPDEST
366 PUSH1 00
368 DUP2
369 SWAP1
370 POP
371 SWAP3
372 SWAP2
373 POP
374 POP
375 JUMP
288 JUMPDEST
289 SWAP4
290 POP
291 PUSH2 012d
294 DUP4
295 DUP6
296 DUP5
297 PUSH2 01aa
300 JUMP
0:
0x0000000000000000000000000000000000000000000000000000000000000000
1:
0x0000000000000000000000000000000000000000000000000000000000000080
2:
0x0000000000000000000000000000000000000000000000000000000000000004
3:
0x000000000000000000000000000000000000000000000000000000000000012d
4:
0x0000000000000000000000000000000000000000000000000000000000000000
5:
0x0000000000000000000000000000000000000000000000000000000000000000
6:
0x0000000000000000000000000000000000000000000000000000000000000004
7:
0x0000000000000000000000000000000000000000000000000000000000000080
8:
0x0000000000000000000000000000000000000000000000000000000000000146
9:
0x0000000000000000000000000000000000000000000000000000000000000000
10:
0x0000000000000000000000000000000000000000000000000000000000000080
11:
0x0000000000000000000000000000000000000000000000000000000000000004
12:
0x0000000000000000000000000000000000000000000000000000000000000000
13:
0x000000000000000000000000000000000000000000000000000000000000007c
14:
0x000000000000000000000000358aa13c52544eccef6b0add0f801012adad5ee3
15:
0x0000000000000000000000000000000000000000000000000000000000000000
16:
0x00000000000000000000000000000000000000000000000000000000dd365b8b
426 JUMPDEST
427 DUP3
428 DUP2
429 DUP4
430 CALLDATACOPY ;CALLDATACOPY(80, 0, 4)
431 PUSH1 00
433 DUP4
434 DUP4
435 ADD ;80h+4
436 MSTORE ;MSTORE(84h,0)
437 POP
438 POP
439 POP
440 JUMP
301 JUMPDEST
302 DUP3
303 DUP5
304 ADD
305 SWAP1
306 POP
307 SWAP4
308 SWAP3
309 POP
310 POP
311 POP
312 JUMP
326 JUMPDEST
327 SWAP2
328 POP
329 DUP2
330 SWAP1
331 POP
332 SWAP4
333 SWAP3
334 POP
335 POP
336 POP
337 JUMP
calldata
的数据根据长度拷贝至了Memory
中,随后对栈进行了清理,至此,栈终于干净了。0:
0x0000000000000000000000000000000000000000000000000000000000000084
1:
0x000000000000000000000000358aa13c52544eccef6b0add0f801012adad5ee3
2:
0x0000000000000000000000000000000000000000000000000000000000000000
3:
0x00000000000000000000000000000000000000000000000000000000dd365b8b
3.2.2 DelegateCall
124 JUMPDEST
125 PUSH1 00
127 PUSH1 40
129 MLOAD ;MLOAD(40h)加载预留指针
130 DUP1 ;push 80h
131 DUP4 ;push 84h
132 SUB ;计算calldata长度
133 DUP2 ;push 80h
134 DUP6 ;push Delegate.address
135 GAS ;push gas
136 DELEGATECALL ;DelegateCall(gas,addr,argOst,argLen,retOst,retLen)
Non Payable Check
,接着直接来到了Calldata Prepared
& Compared
,最后根据我们传入的Calldata
找到pwn()
函数。135 JUMPDEST LINE 12
136 CALLER ;push msg.sender
137 PUSH1 00
139 DUP1
140 PUSH2 0100
143 EXP
144 DUP2
145 SLOAD ;SLOAD(0)加载Delegate的Owner成员
146 DUP2
147 PUSH20 ffffffffffffffffffffffffffffffffffffffff
168 MUL
169 NOT
170 AND
171 SWAP1
172 DUP4
173 PUSH20 ffffffffffffffffffffffffffffffffffffffff
194 AND
195 MUL
196 OR
197 SWAP1
198 SSTORE ;owner = msg.sender
199 POP
200 JUMP
097 JUMPDEST
098 STOP
STOP
后便回到了DelegateCall
下一条指令处。137 SWAP2
138 POP
139 POP
140 RETURNDATASIZE
141 DUP1
142 PUSH1 00
144 DUP2
145 EQ
146 PUSH2 00b7
149 JUMPI ;返回值判断,若返回值长度为0,跳转
194 JUMPDEST
195 PUSH1 60
197 SWAP2
198 POP
199 JUMPDEST
200 POP
201 POP
202 SWAP1
203 POP
204 POP
205 STOP ;结束
DelegateCall
之后跟合约源代码一样,就没有其它的操作,直接进入结束流程了,但是结果是,Storage[0]
的位置,被写入了此刻的msg.sender。
DelegateCall
,虽然叫做委托调用,但实际上只是引用了外部的指令,并不开辟或引用新内存,在此情况下,若被调用函数的签名可被随意操控,也就意味着攻击者可以编写任意的shellcode在你的合约中执行,篡改你的内存数据。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
}
}
四
写在最后
看雪ID:LeaMov
https://bbs.kanxue.com/user-home-952954.htm
# 往期推荐
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):区块链智能合约逆向-合约创建-调用执行流程分析