- Published on
QuinEVM Write-up (LakeCTF)
- Authors
- Name
- Fabrisme
This was a chall of the LakeCTF.
The objectif was to deploy a quine smart-contract with a bytecode less than 7 octets that return his own bytecode.
A quine is a computer program which takes no input and produces a copy of its own source code as its only output. The standard terms for these programs in the computability theory and computer science literature are "self-replicating programs", "self-reproducing programs", and "self-copying programs". Thanks Wikipedia
For examples in different languages I suggest you RosetaCode.
To create the contract we're going to first checks how to write a smart-contract bytecode that return a storage value then a deployment bytecode that write a smart-contract bytecode copy in storage then the smart-contract bytecode.
First I'm going to install ethers
mkdir QuinEVM
cd QuinEVM
npm init -y
npm i ethers
To "compile" the bytecode I'm going tu use this python snippet in a file name compile.py:
with open('out', 'w') as f:
for line in open('quine.etk', 'r').readlines():
f.write(line.split('//')[0].strip())
f.flush()
And to deploy that going to be this one in a script called deploy.js:
const ethers = require('ethers')
const { readFileSync } = require('fs')
const ABI = JSON.parse(readFileSync('./quine.abi', { encoding: 'utf-8' }))
const BIN = readFileSync('./out', { encoding: 'utf-8' })
const provider = new ethers.providers.JsonRpcProvider('YOUR_PROVIDER_URL')
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider)
;(async function () {
const factory = new ethers.ContractFactory(ABI, BIN, wallet)
const contract = await factory.deploy()
console.log(contract.address)
console.log(contract.deployTransaction.hash)
await contract.deployed()
console.log(await provider.getCode(contract.address))
console.log(await provider.getStorageAt(contract.address, '0x00'))
// Wil throw an error but it an encoding one so the output is visible
console.log(await contract.connect(wallet).quinevm())
})()
The quine.abi file contains this JSON:
[
{
"inputs": [],
"name": "quinevm",
"outputs": [{ "internalType": "raw", "name": "", "type": "raw" }],
"stateMutability": "view",
"type": "function"
}
]
After reading the EVM opcodes I've decided to deploy this bytecode for the smart-contract:
// Bytecode
34 // callvalue (push 0x00 on the stack)
54 // sload (load storage at 0x00)
34 // callvalue (push 0x00 on the stack)
52 // mstore (load stack in memory)
38 // codesize (push bytecode on the stack)
34 // callvalue (push 0x00 on the stack)
f3 // ret
So the deployment bytecode is the following:
// Deployment
7f345434523834f3000000345434523834f300345434523834f3345434523834f3 // push32 garbage then code
6000 // push1 0x00
55 // sstore (storage it to memory)
6007 // push1 0x07 (bytecode size)
6030 // push1 0x15 (current position of runtime opcodes)
6000 // push1 0x00 (destination memory index 0)
39 // codecopy
6007 // push1 0x0a (runtime opcode length)
6000 // push1 0x00 (access memory index 0)
f3 // ret
And quine.etk is:
// Deployment
7f345434523834f3000000345434523834f300345434523834f3345434523834f3 // push32 garbage then code
6000 // push1 0x00
55 // sstore (storage it to memory)
6007 // push1 0x07 (bytecode size)
6030 // push1 0x30 (current position of runtime opcodes)
6000 // push1 0x00 (destination memory index 0)
39 // codecopy
6007 // push1 0x07 (runtime opcode length)
6000 // push1 0x00 (access memory index 0)
f3 // ret
// Bytecode
34 // callvalue (push 0x00 on the stack)
54 // sload (load storage at 0x00)
34 // callvalue (push 0x00 on the stack)
52 // mstore (load stack in memory)
38 // codesize (push bytecode on the stack)
34 // callvalue (push 0x00 on the stack)
f3 // ret
I can finally deploy the contract and get the flag with:
python compile.py
node deploy.js