链上卫士:Paradigm CTF 2022 题目浅析—Sourcecode
2022-09-28 08:13
欧科云链
2022-09-28 08:13
订阅此专栏
收藏此文章

题目背景

在 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

其中,所需要通过的逻辑可以分为两个部分

  1. 保证部署 code 后的 codehash 与 code 本身的 keccak256 结果相同,且调用 code 的返回值与 code 本身相同

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);
  1. 保证 code 中不能出现 safe 中禁止的 OPCODE

能使用的主要 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 存入内存中并返回,在这个过程中,不能出现上述的操作码

为了解决这个要求,模拟出来一个合约的执行场景:

  1. 将 code 本身作为参数压入栈
  2. 将 code 存入 memory
  3. 将 code 的 memory 地址压入栈
  4. 使用 return 返回地址所对应的 code

这里会发现一个问题,如果要使用整个的 code 作为参入压入栈,这种解法会不断的对 code 本身进行膨胀,因为随着 push code 作为参数,整个 code 会不断变长,递归修改下去

也就是说,code 本身作为参数是不行的,那么是否可以将 code 的真正的执行逻辑部分,称为 logic code,并作为参数 push 进去,另一部分执行这个 logic,并用来进行最后的 return 返回,也就是:

  1. 将 logic code 作为参数压入栈
  2. 将 logic code 作为参数存入 memory
  3. 执行 logic code,包括:
    1. 将 logic code 的 memory 地址压入栈
    2. 使用 return 返回地址所对应的 logic code

但这样就不是上面的膨胀了,而是减少了,也就是说返回的 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 中:

相关Wiki

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

在 App 打开
特朗普
空投
rwa
稳定币
babylon
以太坊
wayfinder
wct
morph
香港
hyperliquid
wal