在 Paradigm CTF 2022 中有着这样一道 DeFi 金融赛题:
// SPDX-License-Identifier: UNLICENSEDpragma solidity 0.8.16;contract Deployer { constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } }}contract Challenge { bool public solved = false; function safe(bytes memory code) private pure returns (bool) { uint i = 0; while (i < code.length) { uint8 op = uint8(code[i]); if (op >= 0x30 && op <= 0x48) { return false; } if ( op == 0x54 // SLOAD || op == 0x55 // SSTORE || op == 0xF0 // CREATE || op == 0xF1 // CALL || op == 0xF2 // CALLCODE || op == 0xF4 // DELEGATECALL || op == 0xF5 // CREATE2 || op == 0xFA // STATICCALL || op == 0xFF // SELFDESTRUCT ) return false; if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1; i++; } return true; } function solve(bytes memory code) external { require(code.length > 0); require(safe(code), "deploy/code-unsafe"); address target = address(new Deployer(code)); (bool ok, bytes memory result) = target.staticcall(""); require( ok && keccak256(code) == target.codehash && keccak256(result) == target.codehash ); solved = true; }}
题目总的要求:输入 bytes 类型的 code,通过检查逻辑,返回solved=true
其中,所需要通过的逻辑可以分为两个部分
Runtime code:合约运行时真正执行的 code,它的 keccak256 的结果即是 codehash
这里需要说明,我们只需要 runtime code,而不需要平常所见合约的 deploy code,因为要求了keccak256(code) == target.codehash && keccak256(result) == target.codehash
也就是说,code 本身必须是 runtime code
address target = address(new Deployer(code));(bool ok, bytes memory result) = target.staticcall("");require( ok && keccak256(code) == target.codehash && keccak256(result) == target.codehash);
能使用的主要 OPCODE 包括:POP MLOAD MSTORE MSTORE8 JUMP JUMPI PC MSIZE GAS JUMPDEST PUSH1~PUSH32 DUP1~DUP16 SWAP1~SWAP16 LOG0~LOG4
function safe(bytes memory code) private pure returns (bool) { uint i = 0; while (i < code.length) { uint8 op = uint8(code[i]); if (op >= 0x30 && op <= 0x48) { return false; } if ( op == 0x54 // SLOAD || op == 0x55 // SSTORE || op == 0xF0 // CREATE || op == 0xF1 // CALL || op == 0xF2 // CALLCODE || op == 0xF4 // DELEGATECALL || op == 0xF5 // CREATE2 || op == 0xFA // STATICCALL || op == 0xFF // SELFDESTRUCT ) return false; if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1; i++; } return true;}
通常来说,为了完成第一个部分的逻辑,只需要进行 codehash 的直接获取即可,也就是使用操作码 EXTCODEHASH 或者使用 EXTCODECOPY,但是这两个操作码并无法通过第二个部分的 safe 逻辑
因此,为了实现成功解题,回到最初的要求:
code 本身能够将 code 存入内存中并返回,在这个过程中,不能出现上述的操作码
为了解决这个要求,模拟出来一个合约的执行场景:
这里会发现一个问题,如果要使用整个的 code 作为参入压入栈,这种解法会不断的对 code 本身进行膨胀,因为随着 push code 作为参数,整个 code 会不断变长,递归修改下去
也就是说,code 本身作为参数是不行的,那么是否可以将 code 的真正的执行逻辑部分,称为 logic code,并作为参数 push 进去,另一部分执行这个 logic,并用来进行最后的 return 返回,也就是:
但这样就不是上面的膨胀了,而是减少了,也就是说返回的 logic code 和真正整个 code 本身相比,少了一份 logic code,也就是说本应该返回 logic code+logic code,现在只返回了 logic code*1
所以最关键的地方地方在这里:使用 DUP1,对 logic code 进行复制,即可返回 logic code*2
简单来说就是,将 logic code 作为参数 push 一次,再作为真正逻辑执行一次,所以代码中需要出现两次,最后返回的是 push 进去的 logic code 与通过 DUP1 指令复制出来的 logic code
基于以上分析,写出大概的操作码流程:
PUSHX(X 依赖于 logic code 的长度 ) logic code---- 以下为整个所有的 logic code----DUP1(此时栈中有两段相同的 logic code)PUSH logic code 要存的位置(初始,设置为 00)MSTORE(此时内存中有一段 logic code,栈中有一段 logic code)PUSH 第二段 logic code 内存地址 MSTOREPUSH logic code 长度 *2PUSH 内存地址(00)RETURN(返回)
也就是
60 ?8060 005260 code length5260 code length*260 00F3
写完逻辑后,从 80 开始即为 code length,长度为 12,即
6b(PUSH12) 8060005260125260246000F38060 005260 125260 60( 经过调试要长一些,因为存在着内存自动补 0)60 00F3 也就是:6b8060015260215260416000f38060015260215260606000f3
执行结果:
也就是,输入了两段,返回了两段,且没有用到禁止的操作码。
下面要解决的问题就是,如果处理当前返回值里这么多 0?
一个很简单的方案就是,既然我们能够去做到输入两份返回两份,那么这些 0 用相应的不会影响执行的操作码去填充就好了,也就是填充最终的 code,返回值里也有这些 code,对以下的答案进行修改,用 JUMPDEST 操作码填充,在这个过程需要明确内存地址新增后的 code 长度相应变化
7f(PUSH32,因为填充变长 ) 5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3 填充,有多少个 00 补多少 5b:5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80(DUP1)60 01( 将 code 存入,注意前面空出来,为后面的 7f 留地方 )52(MSTORE)60 21( 将 code length 长度流出来,存入 DUP 的结果 )52(MSTORE)60 7f ( 这里需要注意,因为最后要返回的 code 中也包含 PUSH32,也就是 7f,所以要再前面为 7f 也存进去 )60 00(7f 存入的位置)53(MSTORE8,之所以不用 MSTORE,是因为 MSTORE 存入了一个 uint256 的值,会留出大量 0)60 41 (整段代码长度)60 00 (返回值起始位置,从 00 开始)f3 (RETURN)
也就是:
7f5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f35b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3
在 remix 中:
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。