一 工具&链接
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
原文始发于微信公众号(看雪学苑):区块链智能合约逆向-合约创建-调用执行流程分析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论