06.sourcecode
2023-09-18 11:09:44 # 14.Paradigm CTF 2022

sourcecode

本题需要用到:https://www.evm.codes/playground?fork=shanghaihttps://www.evm.codes/

分析

1.任务

我们的任务是调用isSolved()返回true,其实也就是让challeng合约的solve()返回true。

2.分析

2.1寻找突破点

来好好分析一下solve()方法:

  • require(code.length > 0);:输入的形参长度要大于0
  • require(safe(code), "deploy/code-unsafe");:调用safe()的返回结果为true
  • address target = address(new Deployer(code));:用形参部署一个新的合约Deployer
  • (bool ok, bytes memory result) = target.staticcall("");:调用新合约的方法,输入啥也没有,换句话说,就是输入任何内容返回结果都一样
  • require
    • ok:调用一定要成功
    • keccak256(code) == target.codehash:要为true,意思是solve()输入的形参内容的哈希值必须要和新合约的runtimecode哈希值一样,也就是说solve()输入的形参内容就是新合约的runtimecode
    • keccak256(result) == target.codehash:调用新合约返回来结果的哈希值必须要和新合约的runtimecode哈希值一样,也就是说新合约返回来结果就是新合约的runtimecode

总结一下:输入的内容 = 新合约的runtimecode = 任何调用新合约的返回值

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

要做到【输入的内容 = 新合约的runtimecode = 任何调用新合约的返回值】其实并不难,也就是执行一段bytecode,执行完返回的结果还是bytecode本身。但是有个限制:safe()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}
// 除了0x30~0x48的一些操作码,下面这些操作码也不可以用
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;
// 如果操作码是0x60~0x7f,那么判断的位置i就可以向前推进
// 似乎可以跳过一些判断?意思是bytecode某些位置可以包含这些黑名单操作码?
// 只要这些黑名单操作码位于操作码0x60~0x7f后面的适当位置
if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1;

i++;
}

return true;
}

可以看得出来,safe()对输入的形参做出了一些限制:bytecode不能包含一些字符,也就是说不得包含一些特定的操作码,然而这些操作码是可以很容易做到【输入的内容 = 新合约的runtimecode = 任何调用新合约的返回值】。那么题目的意思就是让我们只能用其他操作码来做到这件事(我不考虑0x60~0x7f,这样问题更加复杂了,我们就单纯用黑名单之外的操作码做题),刁难我们!

2.2构造bytecode

我们来看看我们可以用哪些操作码:如下。也就是说,我们要利用下面这些操作码来实现题目的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
算数:加减乘除
STOP ADD MUL SUB DIV SDIV MOD SMOD ADDMOD MULMOD EXP SIGNEXTEND

比较
LT GT SLT SGT EQ ISZERO

转换、移位
AND OR XOR NOT BYTE SHL SHR SAR

内存
MLOAD MSTORE MSTORE8 MSIZE PUSH? DUP? SWAP?

跳转
JUMP JUMPI JUMPDEST

其他
SHA3 POP PC GAS LOG? RETURN REVERT

这么一来,对我们来说,本题能做点事情的操作码也就这么几个了:DUP?、MSTORE、RETURN、PUSH?

那么构造操作码的思路是怎么样呢?有种想法是将整个code作为参数压入栈中,然后放到memory,最后用return从memory返回。但是这么做,除了参数还有压入、放到、返回这一系列操作,会让code递归膨胀修改,不可行。

可行的思路:我们压入栈的参数只是我们执行的操作码,我们称他为logic code,那么我们logic code就有两份(一份是作为参数压栈,一份是实际执行的),然后返回的是两份logic code。大概流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PUSH?(?是logic code的长度) logic code

//////// 以下为整个所有的logic code //////

DUP1(复制logic code)

PUSH 01 (01是logic code要存位置的offset,后面32字节内容全是logic code。push 01的原因是要留给PUSH?)
MSTORE(将logic code存进memory,并且栈中也有一段logic code)

PUSH offset(第二段logic code内存地址的offset)
MSTORE (把栈中的第二段logic code也存进内存)

PUSH PUSH? (这里需要注意,因为最后要返回的code中也包含PUSH?)
PUSH 00(PUSH?存入的位置)
MSTORE8(MSTORE8,之所以不用MSTORE,是因为MSTORE存入了一个uint256的值,会留出大量0,而mstore只一个字节一个字节的操作,刚好符合PUSH?)

PUSH 41 (也就是1+32+32,也就是PUSH? + logic code + logic code的长度)
PUSH 00 (从0位置开始复制)
RETURN

但是,我们知道,绝大部分是每32字节来处理的,不足位置补0,并且由于我们存入的是数值因此会把数据从低位开始存,但是如果执行0就是执行stop,因此我们要用一些执行了但是任何效果都没有的操作码来代替0,选择5b[ JUMPDEST ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
7f( PUSH32 ) 5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3

//////// 以下为整个所有的logic code //////
填充,需要填充的个数 = 32 - logic code的长度,这里啥也不干,只是为了对应上面的补5b操作
5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b

80(DUP1)

60 01(01是logic code要存位置的offset,后面32字节内容全是logic code。push 01的原因是要留给PUSH32)
52(MSTORE)

60 21(也就是33字节,拼接的offset)
52(MSTORE,写进内存)

60 7f (这里需要注意,因为最后要返回的code中也包含PUSH32,也就是7f,所以要再前面为7f也存进去)
60 00(7f存入的位置)
53(MSTORE8,之所以不用MSTORE,是因为MSTORE存入了一个uint256的值,会留出大量0,而mstore只一个字节一个字节的操作,刚好符合7f)

60 41 (整段代码长度)
60 00 (返回值起始位置,从00开始)
f3 (RETURN)

也就是:

1
2
3
7f    5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3

5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3

即:7f5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f35b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity 0.8.16;

import "../../src/06.sourcecode/Setup.sol";
import "../../src/06.sourcecode/Challenge.sol";
import "forge-std/Test.sol";

contract sourcecodeTest is Test{

Setup level;
Challenge challenge;

function setUp() public {
level = new Setup();
challenge = level.challenge();
}

function test_isSolved() public {
IChallenge(address(challenge)).solve(
abi.encodePacked(
hex"7f5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f35b5b5b5b5b5b5b5b5b5b5b5b5b5b5b80600152602152607f60005360416000f3")
);
assertEq(level.isSolved(),true);
}

}

interface IChallenge{
function solve(bytes memory ) external;
}