OKLink:2023年7月安全事件盘点
链上卫士:Paradigm CTF 2022 题目浅析—Sourcecode
题目背景
在Paradigm CTF 2022中有着这样一道DeFi金融赛题:
// SPDX-License-Identifier: UNLICENSED
pragma 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
其中,所需要通过的逻辑可以分为两个部分
- 保证部署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
);
- 保证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存入内存中并返回,在这个过程中,不能出现上述的操作码
为了解决这个要求,模拟出来一个合约的执行场景:
- 将code本身作为参数压入栈
- 将code存入memory
- 将code的memory地址压入栈
- 使用return返回地址所对应的code
这里会发现一个问题,如果要使用整个的code作为参入压入栈,这种解法会不断的对code本身进行膨胀,因为随着push code作为参数,整个code会不断变长,递归修改下去
也就是说,code本身作为参数是不行的,那么是否可以将code的真正的执行逻辑部分,称为logic code,并作为参数push进去,另一部分执行这个logic,并用来进行最后的return返回,也就是:
- 将logic code作为参数压入栈
- 将logic code作为参数存入memory
- 执行logic code,包括:
- 将logic code的memory地址压入栈
- 使用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内存地址
MSTORE
PUSH logic code长度*2
PUSH 内存地址(00)
RETURN(返回)
也就是
60 ?
80
60 00
52
60 code length
52
60 code length*2
60 00
F3
写完逻辑后,从80开始即为code length,长度为12,即
6b(PUSH12) 8060005260125260246000F3
80
60 00
52
60 12
52
60 60(经过调试要长一些,因为存在着内存自动补0)
60 00
F3
也就是:6b8060015260215260416000f38060015260215260606000f3
执行结果:
也就是,输入了两段,返回了两段,且没有用到禁止的操作码。
解决一些小问题
下面要解决的问题就是,如果处理当前返回值里这么多0?
一个很简单的方案就是,既然我们能够去做到输入两份返回两份,那么这些0用相应的不会影响执行的操作码去填充就好了,也就是填充最终的code,返回值里也有这些code,对以下的答案进行修改,用JUMPDEST操作码填充,在这个过程需要明确内存地址新增后的code长度相应变化
7f(PUSH32,因为填充变长)
5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3
填充,有多少个00补多少5b:
5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b
80(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中: