swiss hacking challenge 2024 - proof-of-work
Difficulty: hard
Categories: misc, web3
Author: sam.ninja
At General Management LLC, we’ve leaped into the future with a new system powered by blockchain, transforming how we measure work into an art form. Co-developed with a buzzword-loving consulting firm, this system introduces “Proof of Work” and “Proof of Wasted Time,” capturing the essence of effort and dedication in the digital age.
Employees now embark on a quest to immortalize their hard work in blockchain, where every hour spent is a badge of honor. The mantra, “Prove you’re a good employee, and be rewarded,” fuels a mix of curiosity and dread, with rewards as mysterious as the blockchain technology itself—ranging from prime parking spots to mythical extra vacation days.
Barbara from HR champions this tech odyssey with unmatched zeal, while Phil from IT battles to mesh this blockchain behemoth with our daily grind. This journey isn’t just about work; it’s about making our mark on the digital ledger, proving our worth one block at a time in the great blockchain in the sky. Welcome to the future at General Management LLC, where every keystroke is a step towards digital immortality.
Files
We get an http endpoint an the corresponding source:
proof-of-work
├── Dockerfile
├── foundry.toml
├── genesis.json
├── requirements.txt
├── run.sh
├── script
│ └── Deploy.s.sol
├── server.py
├── src
│ └── Challenge.sol
└── static
├── favicon.ico
├── index.html
└── js
└── script.js
Exploitation
Getting any $WORK
We can’t really do any transactions without initial funds to do any calls. However, we find an address and the corresponding private key in the files:
genesis.json
"alloc": {
"0xB05533cC1B2eB7E9B30882f66c6676A9743b0eb1": {
"balance": "0xd3c229af83a148640000"
}
}
run.sh
DEPLOYER_KEY=c42e935abd0c968b48bcaacdac0c2e12acee457123a957ee6b813aaf65b99be1
# Run Anvil, deploy the challenge and start the proxy
(./wait-for -t 60 127.0.0.1:8545 -- forge script script/Deploy.s.sol:Deploy --broadcast --rpc-url http://localhost:8545 --private-key $DEPLOYER_KEY && uvicorn server:app --host 0.0.0.0 --port 80 --log-level warning) &
The balance in the genesis.json
converted from Wei to Ether is 1000001 ETH
in theory. However, on start we also deploy the following contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import {Script, console2} from "forge-std/Script.sol";
import "../src/Challenge.sol";
contract Deploy is Script {
function run() public {
vm.startBroadcast();
IFactoryFactory factoryFactory = new FactoryFactory();
ICompanyFactory companyFactory = ICompanyFactory(factoryFactory.createFactory());
ICompany company = ICompany(companyFactory.createCompany{value: 1000000 ether}());
console2.log("Deployed Company at", address(company));
vm.stopBroadcast();
}
}
That means, we “only” have 1 Ether available for doing transactions.
Interacting with the RPC API
I used web3
in python to interact with the instance. I implemented all the functions required for the solutions already. The weird try/except case it due to creating signed transactions not returning status messages from the smart contract, unlike calling them. No clue why. Please tell me, if you know of any better solution.
from web3 import Web3
import solcx
def create_abi():
solcx.install_solc(version='0.8.19')
solcx.set_solc_version('0.8.19')
temp_file = solcx.compile_files('./src/Challenge.sol')
abi = temp_file['src/Challenge.sol:Company']['abi']
return abi
url = "https://your-instance.ctf.m0unt41n.ch:1337/rpc"
web3 = Web3(Web3.HTTPProvider(url))
if not web3.is_connected():
print("Not connected to the blockchain")
exit(1)
smart_contract_address = "0x18415Be06E7993F93aB74dD68b95E1fD3974de10"
smart_contract_abi = create_abi()
solve_method = web3.eth.contract(address=smart_contract_address, abi=smart_contract_abi)
# Print all functions
for func in solve_method.functions:
print(func)
# Print the latest block
print(web3.eth.get_block('latest'))
account_address = '0xB05533cC1B2eB7E9B30882f66c6676A9743b0eb1'
account_secret = 'c42e935abd0c968b48bcaacdac0c2e12acee457123a957ee6b813aaf65b99be1'
def transaction_data():
return {
'chainId': 31337,
'gas': 2000000,
'gasPrice': web3.eth.gas_price,
'nonce': web3.eth.get_transaction_count(account_address),
'from': account_address,
}
# Get the account balance
balance = web3.eth.get_balance(account_address)
# Convert the balance from Wei to Ether and print it
balance_in_ether = web3.from_wei(balance, 'ether')
print(f'Balance: {balance_in_ether} ETH')
def sign_work_contract():
t = solve_method.functions.signWorkContract().build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
web3.eth.send_raw_transaction(sig_t.rawTransaction)
def get_task():
t = solve_method.functions.getTask().build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
web3.eth.send_raw_transaction(sig_t.rawTransaction)
task = solve_method.functions.getTask().call({"from": account_address})
return task
def submit_work(pow: int):
t = solve_method.functions.submitWork(pow.to_bytes(32)).build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
t_hash = web3.eth.send_raw_transaction(sig_t.rawTransaction)
try:
web3.eth.wait_for_transaction_receipt(t_hash)
except Exception as e:
print(f"Error: {e}")
try:
ret = solve_method.functions.submitWork(pow.to_bytes(32)).call({"from": account_address})
except:
ret = ""
return ret
Solving the proof of work
The contract gives us the following requirements that we have to fulfill:
require(assignedTask[msg.sender] != 0, "No task assigned");
// Check Proof of Work
require(uint256(keccak256(abi.encode(proofOfWork, assignedTask[msg.sender]))) % 0xffff == 0, "Invalid proof of work");
// Check Proof of Wasted Time
require(block.timestamp - lastSubmission[msg.sender] > 60, "You thought you could leave when you've finished your work? That's not how serious companies work!");
require(block.timestamp % 60 == 0, "You must hand in your work EXACTLY on time, not a second earlier or later!");
require(uint256(proofOfWork) >> 128 == block.timestamp, "You've missed the deadline, don't try to submit your old Proof of Waster Time!");
We can calculate the second and last requirement like this:
task = get_task()
ts = web3.eth.get_block('latest')['timestamp']
base = ts << 128
print(f"Base: {hex(base)}")
rem = 1
while rem != 0:
base += 1
hash = web3.solidity_keccak(['bytes32', 'bytes32'], [base.to_bytes(32), task])
rem = int(hash.hex(), 16) % 0xffff
print(f"Proof of Work: {hex(base)}")
print(f"Hash: {hash.hex()}")
Fixing timing issues
As we have to submit the proof of work when timestamp % 60 == 0
and the forge
server increments the timestamp by 1 on every new block, we can just get new tasks until we’re one block away from the correct timestamp:
# wait for timestamp+1 % 60 == 0
task = get_task()
ts = web3.eth.get_block('latest')['timestamp']
while (ts+1) % 60 != 0:
print(ts)
task = get_task()
ts = web3.eth.get_block('latest')['timestamp']
base = (ts+1) << 128
print(f"Base: {hex(base)}")
Full solve script
Finally, we can get enough salary using the following, then claim it in the web interface and buy the flag:
from web3 import Web3
import solcx
def create_abi():
solcx.install_solc(version='0.8.19')
solcx.set_solc_version('0.8.19')
temp_file = solcx.compile_files('./src/Challenge.sol')
abi = temp_file['src/Challenge.sol:Company']['abi']
return abi
url = "https://your-instance.ctf.m0unt41n.ch:1337/rpc"
web3 = Web3(Web3.HTTPProvider(url))
if not web3.is_connected():
print("Not connected to the blockchain")
exit(1)
smart_contract_address = "0x18415Be06E7993F93aB74dD68b95E1fD3974de10"
smart_contract_abi = create_abi()
solve_method = web3.eth.contract(address=smart_contract_address, abi=smart_contract_abi)
# should be false if the contract has not already been solved
# print all functions
for func in solve_method.functions:
print(func)
#task = solve_method.functions.getTask().call()
print(web3.eth.get_block('latest'))
account_address = '0xB05533cC1B2eB7E9B30882f66c6676A9743b0eb1'
account_secret = 'c42e935abd0c968b48bcaacdac0c2e12acee457123a957ee6b813aaf65b99be1'
def transaction_data():
return {
'chainId': 31337,
'gas': 2000000,
'gasPrice': web3.eth.gas_price,
'nonce': web3.eth.get_transaction_count(account_address),
'from': account_address,
}
# Get the account balance
balance = web3.eth.get_balance(account_address)
# Convert the balance from Wei to Ether and print it
balance_in_ether = web3.from_wei(balance, 'ether')
print(f'Balance: {balance_in_ether} ETH')
def sign_work_contract():
t = solve_method.functions.signWorkContract().build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
web3.eth.send_raw_transaction(sig_t.rawTransaction)
def get_task():
t = solve_method.functions.getTask().build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
web3.eth.send_raw_transaction(sig_t.rawTransaction)
task = solve_method.functions.getTask().call({"from": account_address})
#print(f"Task: {hex(int(task.hex(), 16))}")
return task
def submit_work(pow: int):
t = solve_method.functions.submitWork(pow.to_bytes(32)).build_transaction(transaction_data())
sig_t = web3.eth.account.sign_transaction(t, account_secret)
t_hash = web3.eth.send_raw_transaction(sig_t.rawTransaction)
try:
web3.eth.wait_for_transaction_receipt(t_hash)
except Exception as e:
print(f"Error: {e}")
try:
ret = solve_method.functions.submitWork(pow.to_bytes(32)).call({"from": account_address})
except:
ret = ""
return ret
sign_work_contract()
sal=0
while sal < 3342:
# wait for timestamp+1 % 60 == 0
task = get_task()
ts = web3.eth.get_block('latest')['timestamp']
while (ts+1) % 60 != 0:
print(ts)
task = get_task()
ts = web3.eth.get_block('latest')['timestamp']
base = (ts+1) << 128
print(f"Base: {hex(base)}")
rem = 1
while rem != 0:
base += 1
hash = web3.solidity_keccak(['bytes32', 'bytes32'], [base.to_bytes(32), task])
rem = int(hash.hex(), 16) % 0xffff
print(f"Proof of Work: {hex(base)}")
print(f"Hash: {hash.hex()}")
res = submit_work(base)
print(f"Transaction: {res}")
sal = web3.from_wei(solve_method.functions.earnedSalary(account_address).call(), 'ether')
print(f"Salary: {sal}")
Flag
Conclusion
This was a great introduction to web3 challenges! I never attempted any of the ones on HTB before because there aren’t really any “how to get started with web3 ctf” guides out there.