区块链智能合约逆向-合约创建-调用执行流程分析

admin 2023年12月23日01:15:28评论7 views字数 11882阅读39分36秒阅读模式
工具&链接
http://remix.zhiguxingtu.com/
https://ethereum.org/zh/developers/docs/evm/opcodes/
https://ethervm.io/decompile#google_vignette


Thought
先把合约编译部署了一下,然后看了一下remix上的调试器,和我想的一样,所谓合约调试并不是真正的断点调试,而是一次模拟,EVM并没有提供类似系统异常中断的功能,本身并不支持调试,所有的交易都在一瞬间完成。

而调试器的原理(猜测)是根据交易Hash所查询到的数据,例如发起者,调用参数,调用函数等信息,再找到对应的合约bin,归结所有程序流程,然后从opcode开始模拟一次执行流程。


调试分析&静态分析
编译部署完,然后拿着Deletegation合约部署的交易Hash丢到调试器里,先从合约创建开始分析。


区块链智能合约逆向-合约创建-调用执行流程分析
可以看到调试器直接跳过了我第一次做智能合约EVM流程实现分析的那部分,[[1.合约安全&漏洞审计#3.2 Delegation静态分析]],而是直接来到了用户代码处。

在 编译器->编译详情 中可以看到 函数签名、汇编 等详细信息,功能还是很全面的。

补充:关于上面直接跳到用户代码处,可以往回拖动,直接回到0开始,那就从0到构造函数结束进行一次分析吧。

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

在EVM中,函数并不像其它语言中存在一个既定的内存地址,用户发起交易并不是靠一个随机的地址来跳转,而是靠一个唯一的签名来判断,该签名为函数声明的哈希值。

由于当前合约除
fallback之外,对外public的成员仅有一个owner,所以上述指令段较短,但当合约中存在多个public成员时,该指令段便会一一对应存在多个签名匹配。

形如
SwitchCase,但却没有SwitchCase的效率,是纯if-elseif-else
请注意25-JUMPI46-JUMP,此两处跳转分别对应两个处理函数。

receive&fallback


CallData为空时,就意味着本次交易连最基本的目标函数都没有被指定,那么进入后面的签名匹配流程也就毫无意义,熟悉合约开发的话就知道,在合约中存在receivefallback两个特殊的函数,我认为将其叫做回调函数并不太恰当,因为它们的每一次调用实际上如其它函数一般,都是“指名道姓”的,只是它们并不存在签名,而作为上述指令段中的存在。

即便我们没有为这两个函数声明,在上述指令段中依旧存在第一处跳转,而跳转目的指令是这样的:

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

我们已知的数据分别有预留指针、calldataSize、函数签名。

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)

至此就正式进入了委托调用流程,参数分别如下:
◆剩余Gas:2497
◆合约地址:Delegate.address
◆参数存储位置偏移:80h
◆参数长度:4
◆返回值存储位置偏移:0
◆返回值长度:84h
随后我们继续步入,首先进入到了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); } }
合约来源于:https://ethernaut.openzeppelin.com/


写在最后
区块链萌新,关于本文中所列指令内容,我也依然有很多不理解的地方,如有错误,欢迎指出,感谢。


区块链智能合约逆向-合约创建-调用执行流程分析

看雪ID:LeaMov

https://bbs.kanxue.com/user-home-952954.htm

*本文为看雪论坛优秀文章,由 LeaMov 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):区块链智能合约逆向-合约创建-调用执行流程分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月23日01:15:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   区块链智能合约逆向-合约创建-调用执行流程分析https://cn-sec.com/archives/2325536.html

发表评论

匿名网友 填写信息