[HV22.23] Code but no code

4 minute read

Santa loves puzzles and hopes you do too ;) Find the secret inputs which fulfil the requirements and gives you the flag.

Solution

Another web3! Nice!

So, we got a website again, giving us the source code of two contracts (Setup.sol and Challenge.sol), the address where Setup is deployed and a private key, as well as a function to sign something with santas signature and to check if we have solved it.

website

In the beginning, we need to be able to interact with both contracts. To do this, we use web3.py again and do some boilerplate:

#!/usr/bin/env python3
from web3 import Web3
import solcx
import requests
import re
import hexbytes
import sha3

fqdn = "f46ba93c-eb12-4cae-9d8b-5dae8826d1a8.rdocker.vuln.land"

url = f"http://{fqdn}:8080"
page = requests.get(url).text
addresses = re.findall(r'0x[0-9A-Za-z]+', page)
w3 = Web3(Web3.HTTPProvider(f"http://{fqdn}:8545"))
setupAddress = addresses[0]
privateKey = addresses[1]


def findSetupContract():
    with open('./Setup.sol', 'r') as file:
        setupSource = file.read()
        setupCompiled = solcx.compile_standard({
            "language": "Solidity",
            "sources": {
                "Setup.sol": {
                    "content": setupSource
                }},
            "settings": {
                "outputSelection": {
                    "*": {
                        "*":
                        ["abi", "evm.bytecode"]}
                }
            }
        }, solc_version="0.8.16")
    setupAbi = setupCompiled["contracts"]["Setup.sol"]["Setup"]["abi"]
    return w3.eth.contract(address=setupAddress, abi=setupAbi)


def findChallengeContract():
    with open('./Challenge.sol', 'r') as file:
        challengeSource = file.read()
        challengeCompiled = solcx.compile_standard({
            "language": "Solidity",
            "sources": {
                "Challenge.sol": {
                    "content": challengeSource
                }},
            "settings": {
                "outputSelection": {
                    "*": {
                        "*":
                        ["abi", "evm.bytecode"]}
                }
            }
        }, solc_version="0.8.16")
    challengeAbi = challengeCompiled["contracts"]["Challenge.sol"]["Challenge"]["abi"]
    challengeAddress = setupContract.functions.challenge().call()
    return w3.eth.contract(address=challengeAddress, abi=challengeAbi)

# get account from address, since this one is checked from website
account = w3.eth.account.from_key(privateKey).address

# find existing contracts
setupContract = findSetupContract()
challengeContract = findChallengeContract()

This gives me the opportunity to interact with both contracts, although I don’t need the setup contract.

I knew I had to call solve() on the challenge contract, give it a helper address with 19 of the 20 bytes be 0, a valid signature and a message, which has not been hashed (this is done in the challenge contract), where the signer has to be the same account which deployed this contract, so most probably santa.

After some long research, I found precompiled contracts, since there weren’t any other contracts deployed on this chain and it seemed odd that it has to have so many 0 in it. 0x01 also is erecover, which sounds like what I need so the last verification in the challenge contract checks out.

So, we had our first component, the helper address is 0x0000000000000000000000000000000000000001.

For the signature, I can simply use the website. I entered a and got back the signature 0xccc9faf415f06e491a6d867ccd225116baea71ffafd5bdce6912531b289cb7805b1f9bb06a07ab3de3386722eba9e47fbcc56e81c18e11daa2a7e07ec369ddb81b.

Having found this article about ECDSA: Public Key Recovery from Signature I knew I needed to find the parameters r, s and v.

r and s are quite easy to find (simply the first and second 32 bytes of the signature), but v is an issue. v is 0x1b in the signature from santa, but 28 / 0x1c in the challenge contract. Ugh.

Searching for three letters is hard, but eventually I found this stackexchange question asking for exactly that and also a cheat sheet for what this stands for (answer #2 at the moment).

Futhermore, I stumbled over this proposal to ethereum which is already implemented and notes exactly what we need:

flipping of v

So, to do that, we need to find secp256k1n and make s = secp256k1n - s to flip our v. This is what it looks like in python:

# signature from website
sig = '0xccc9faf415f06e491a6d867ccd225116baea71ffafd5bdce6912531b289cb7805b1f9bb06a07ab3de3386722eba9e47fbcc56e81c18e11daa2a7e07ec369ddb81b'
secp256k1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
r = sig[2:66]
s = sig[66:130]
s = int(s, 16)
s = secp256k1n - s
s = hex(s)[2:]
sig = '0x' + r + s + '1c'

So, we got our helper address, signature, now only the message is missing.

The (signed) message format needs to look like this: How is the signature computed?

This translates to python:

message = "a"
message = f"\x19Ethereum Signed Message:\n{len(message)}{message}".encode()

Packing all of this in a transaction and sending it:

nonce = w3.eth.getTransactionCount(account)
tx = challengeContract.functions.solve('0x0000000000000000000000000000000000000001', sig, message).buildTransaction({
    "from": account,
    "nonce": nonce,
    "gasPrice": w3.eth.gas_price,
    "gas": 210000
})
signed_tx = w3.eth.account.sign_transaction(tx, privateKey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
w3.eth.wait_for_transaction_receipt(tx_hash)

This sets solved to true in the challenge contract and now the website gives us the flag HV22{H1dd3N_1n_V4n1Ty}

flag

Full code:

#!/usr/bin/env python3
from web3 import Web3
import solcx
import requests
import re

fqdn = "f1f2718f-2027-4574-b1e1-f92c4036b018.rdocker.vuln.land"

url = f"http://{fqdn}:8080"
page = requests.get(url).text
addresses = re.findall(r'0x[0-9A-Za-z]+', page)
w3 = Web3(Web3.HTTPProvider(f"http://{fqdn}:8545"))
setupAddress = addresses[0]
privateKey = addresses[1]


def findSetupContract():
    with open('./Setup.sol', 'r') as file:
        setupSource = file.read()
        setupCompiled = solcx.compile_standard({
            "language": "Solidity",
            "sources": {
                "Setup.sol": {
                    "content": setupSource
                }},
            "settings": {
                "outputSelection": {
                    "*": {
                        "*":
                        ["abi", "evm.bytecode"]}
                }
            }
        }, solc_version="0.8.16")
    setupAbi = setupCompiled["contracts"]["Setup.sol"]["Setup"]["abi"]
    return w3.eth.contract(address=setupAddress, abi=setupAbi)


def findChallengeContract():
    with open('./Challenge.sol', 'r') as file:
        challengeSource = file.read()
        challengeCompiled = solcx.compile_standard({
            "language": "Solidity",
            "sources": {
                "Challenge.sol": {
                    "content": challengeSource
                }},
            "settings": {
                "outputSelection": {
                    "*": {
                        "*":
                        ["abi", "evm.bytecode"]}
                }
            }
        }, solc_version="0.8.16")
    challengeAbi = challengeCompiled["contracts"]["Challenge.sol"]["Challenge"]["abi"]
    challengeAddress = setupContract.functions.challenge().call()
    return w3.eth.contract(address=challengeAddress, abi=challengeAbi)


# get account from address, since this one is checked from website
account = w3.eth.account.from_key(privateKey).address

# find existing contracts
setupContract = findSetupContract()
challengeContract = findChallengeContract()

# signature from website
sig = '0x1aa390a8699431789672e5b3655a2aed422e2afd508e908268f85a8d0bf8d01b57a1e468f0f395a292eb86fd1072dd5d410910d6bb1b3a9238d258bfdf37599a1b'
secp256k1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
r = sig[2:66]
s = sig[66:130]
s = int(s, 16)
s = secp256k1n - s
s = hex(s)[2:]
sig = '0x' + r + s + '1c'

# call eresolve with a signature
message = "a"
message = f"\x19Ethereum Signed Message:\n{len(message)}{message}".encode()
nonce = w3.eth.getTransactionCount(account)
tx = challengeContract.functions.solve('0x0000000000000000000000000000000000000001', sig, message).buildTransaction({
    "from": account,
    "nonce": nonce,
    "gasPrice": w3.eth.gas_price,
    "gas": 210000
})
signed_tx = w3.eth.account.sign_transaction(tx, privateKey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
w3.eth.wait_for_transaction_receipt(tx_hash)


challengeContract.functions.solved().call()

Updated: