SourceCode
“Fixed point EVM bytecode”
After downloading sourceCode.zip, we could see that there are only two solidity files here, Setup and Challenge.sol. And we get that lt’s a evm bytecode challenge from description.
First, let’s take a look at setup contract. It’s simple to read that It creates a new challenge contract and check if challenge.solved === true for passing challenge.
Then, we need to deep into solv() function, and there are three conditions.
- code.length > 0
- safe(code) returns true
- keccak256(code) == target.code && keccak256(result) == target.codehash, which means code equals to result
In this case, It’s clear that we need to draft a contract that contract code equals to its static call return value.
So, what’s the “staticcall” funtion?
The Byzantium network upgrade scheduled add a STATICCALL opcode that enforces read-only calls at runtime. Byzantium hardfork, EIP-214: calls a method in another contract with state changes such as contract creation, event emission, storage modification and contract destruction disallowed.
STATICCALL
functions equivalently to a CALL
, except it takes only 6 arguments (the "value" argument is not included and taken to be zero), and calls the child with the STATIC
flag set to true
for the execution of the child. Once this call returns, the flag is reset to its value before the call.
As for CALL option, you should be familiar with it if you’ve viewed some code of complex contracts. In short, ethereum create a new EVM instance and returns after completing logic operation.
BTW, most of contracts bytecode have two parts, creation bytecode(aka bytecode) and run-time bytecode. The difference between them is that creation bytecode contains constructor info and initialization code which is needed when deploy contract, and we just need run-time bytecode after deployed.
If you send a transaction to contract with data, EVM creates a instance and doing operation according to contract bytecode(It need to load calldata by calldataload opcode for most cases).
Since the parameter “code” we provided is constructor parameter, we need to know what happened while creating new Deployer contract.
We could see that “code” is a type of bytes, and the layout of calldata about bytes is that bytes lenth + bytes data. For example, if code is 0x1234, the encoded data should be like 0x0000000000000000000000000000000000000000000000000000000000000020
0x0000000000000000000000000000000000000000000000000000000000000002
0x1234000000000000000000000000000000000000000000000000000000000000
The first 0x20 is the offset for dynamic types, and 0x02 is the number of elements (or lenth ). Maybe you’re confused about the length of whole encoded data is 96bytes, but why that it just add 0x20 in assembly code? The answer is simple, contract code will handle your calldata and init the pointer of Variables(just in memory) to the start position. As we know, dynamic type(such as buyes) in memory consists of its length and actual data. So, you just need to add more 0x20 for mloading your data from real position.In this case, the assembly code will return (the offset of code, the length of code).
Finally, we completed analysis of challenge code. It’s time to kick off.
Draft a contract which code equals to its return
It’s sure that we can not use solidity to draft this contract , because It’s hard to control compilation details. We need to use op code and convert it to bytes.
My think is that If we have a input code(in memory), we need to create contract by it. But there is another condition that this code is your operate code also, it’s used as your op code.
let’s assume “code” is bytes32 at first. If the bytes of opcode is called “X”, we need to push32 an input “X” and returns “0x7fXX”. (0x7f is uint8 of PUSH32). You could find that we make a process which code equals to return.
Now it’s time to find out X. We could just use “safe” opcode such as PUSH.
Since we get the process of generating return value. And we just need convert opcode to hex(from DUP1 to end) as X. You can convert it yourself or depend on some open source tools.
python3 -m pyevmasm -a < test.bc
0x80607f60005360015260215260416000f3
We get X is 0x80607f60005360015260215260416000f3, since we used X as bytes32, so it need to be padded right with zero as 0x80607f60005360015260215260416000f3000000000000000000000000000000.
Also, we could convert it to byes as following.
python3 -m pyevmasm -a < test.bc
0x7f80607f60005360015260215260416000f300000000000000000000000000000080607f60005360015260215260416000f3
Here is the exp (too short to explain).
Also, you could use X as bytes16, and some change shoud be made to opcode. I’ll give another version of bytes16 soon.
You could get full code and repo in https://github.com/SilasZhr/paradigm-ctf-2022-solution, I’ll keep updating.