//Byte Double Cross
>TL;DR
- Pulled the Sepolia bytecode at
0x1d7E03675b15a6602A14Ff6321A2cc2ea16CF53Cvia public RPC. - Decompiled the contract to learn that selector
0xb8da5144verifies an array of eight hashed "chunks". - Read storage slot
0(length), slot1(owner) and the eightbytes32hashes from the dynamic array basekeccak256(0x00). - Noticed each chunk is validated as
keccak256(abi.encodePacked(chunk, owner)), so you only need to brute-force short strings whose hash matches the stored value. - Ran a short Python brute-forcer to recover every chunk and assemble the flag:
nite{Pr1v8_v4r$_4r3_n07_pRiv473}.
>1. Recon Phase
Network / Endpoints
- Chain: Sepolia
- RPC:
https://ethereum-sepolia-rpc.publicnode.com - Target contract:
0x1d7E03675b15a6602A14Ff6321A2cc2ea16CF53C
Bytecode + High-Level Structure
- Pulled bytecode with
eth_getCode. - Fed it to EtherVM to get a readable pseudo-Solidity view.
- Dispatch table showed three public selectors:
0x8da5cb5b→owner()0xb8da5144→ unknown puzzle entrypoint #10xc91d4ca6→ unknown puzzle entrypoint #2
- Selector
0xb8da5144decodes calldata as a dynamic array, then callsfunc_0079that:- Requires provided chunk count to equal
storage[0x00](length). - For each chunk, concatenates it with the owner address from
storage[0x01]and hashes the bytes. - Requires each hash to match the array stored at
keccak256(0x00) + i.
- Requires provided chunk count to equal
So the storage already contains all expected hashes—we just need to recover the cleartext chunks.
>2. Dumping Contract Storage
I wrote a tiny helper to read the slots we care about directly from the RPC. It uses eth_getStorageAt to retrieve:
- Slot
0: length (expected to be 8) - Slot
1: packed owner address - Slots
keccak256(0)…+7: each chunk hash
Tooling: dump_storage.py
#!/usr/bin/env python3
import json
import math
import requests
from Crypto.Hash import keccak
RPC_URL = "<https://ethereum-sepolia-rpc.publicnode.com>"
CONTRACT = "0x1d7E03675b15a6602A14Ff6321A2cc2ea16CF53C"
def rpc(method, params):
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
res = requests.post(RPC_URL, json=payload, timeout=30)
res.raise_for_status()
data = res.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]
slot0 = int(rpc("eth_getStorageAt", [CONTRACT, hex(0), "latest"]), 16)
slot1 = rpc("eth_getStorageAt", [CONTRACT, hex(1), "latest"])
owner = slot1[-40:]
base_slot = keccak.new(digest_bits=256)
base_slot.update((0).to_bytes(32, "big"))
start = int.from_bytes(base_slot.digest(), "big")
print(f"length={slot0}")
print(f"owner=0x{owner}")
print(f"base slot=0x{start:064x}")
for idx in range(slot0):
raw = rpc("eth_getStorageAt", [CONTRACT, hex(start + idx), "latest"])
print(f"chunk[{idx}] = {raw}")
Output
length=8
owner=0x1597126b98a9560ca91ad4b926d0def7e2c45603
chunk[0] = 0xf59964cd0c25442208c8d0135bf938cf10dee456234ac55bccafac25e7f16234
chunk[1] = 0xa12f9f56c9d0067235de6a2fd821977bacc4d5ed6a9d9f7e38d643143f855688
chunk[2] = 0x3486d083d2655b16f836dcf07114c4a738727c9481b620cdf2db59cd5acfe372
chunk[3] = 0x2dfb14ffa4d2fe750d6e28014c3013793b22e122190a335a308f0d330143da3d
chunk[4] = 0xd62d22652789151588d2d49bcd0d20a41e2ba09f319f6cf84bc712ea45a215ef
chunk[5] = 0x6cf18571f33a226462303a6ae09be5de3c725b724bf623b5691dcb60651ee136
chunk[6] = 0x2b86ca86c8cfc8aa383afc78aa91ab265b174071d300c720e178264d2f647a42
chunk[7] = 0xe9d5b7877c45245ca46dc5975dc6b577baa951b05f59a8e7b87468bfad4a956d
Great—we now know the owner address and the eight hash targets.
>3. Understanding the Hash Constraint
From func_0079 (decompiled):
- Each calldata chunk is ABI-decoded as
bytes32. - The function copies the chunk, concatenates the owner address (
storage[1]), and hashes usingkeccak256. - It compares the result to the stored hash at index
i.
That means each cleartext chunk is shorter than 32 bytes (likely ASCII) and padded with zeros. The hash is effectively:
target[i] = keccak256(chunk_i || owner_address)
Because the expected strings look flag-like, brute forcing short lengths is feasible.
>4. Brute-Forcing the Chunks
Tooling: solve.py
from __future__ import annotations
import itertools
from typing import Dict, List
from Crypto.Hash import keccak
OWNER = bytes.fromhex("1597126b98a9560ca91ad4b926d0def7e2c45603")
HASHES: List[str] = [
"f59964cd0c25442208c8d0135bf938cf10dee456234ac55bccafac25e7f16234",
"a12f9f56c9d0067235de6a2fd821977bacc4d5ed6a9d9f7e38d643143f855688",
"3486d083d2655b16f836dcf07114c4a738727c9481b620cdf2db59cd5acfe372",
"2dfb14ffa4d2fe750d6e28014c3013793b22e122190a335a308f0d330143da3d",
"d62d22652789151588d2d49bcd0d20a41e2ba09f319f6cf84bc712ea45a215ef",
"6cf18571f33a226462303a6ae09be5de3c725b724bf623b5691dcb60651ee136",
"2b86ca86c8cfc8aa383afc78aa91ab265b174071d300c720e178264d2f647a42",
"e9d5b7877c45245ca46dc5975dc6b577baa951b05f59a8e7b87468bfad4a956d",
]
TARGET_MAP: Dict[bytes, int] = {bytes.fromhex(h): idx for idx, h in enumerate(HASHES)}
ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-!@#$%^&*()[]<>?/\\\\|.,'\\"+="
def keccak_chunk(candidate: str) -> bytes:
chunk_bytes = candidate.encode()
padded = chunk_bytes + b"\\x00" * (32 - len(chunk_bytes))
sponge = keccak.new(digest_bits=256)
sponge.update(padded + OWNER)
return sponge.digest()
def search(max_len: int = 4) -> Dict[int, str]:
found: Dict[int, str] = {}
for length in range(1, max_len + 1):
for combo in itertools.product(ALPHABET, repeat=length):
candidate = "".join(combo)
digest = keccak_chunk(candidate)
index = TARGET_MAP.get(digest)
if index is None or index in found:
continue
found[index] = candidate
print(f"matched chunk #{index} -> {candidate!r}")
if len(found) == len(HASHES):
return found
return found
if __name__ == "__main__":
matches = search()
for idx in range(len(HASHES)):
value = matches.get(idx, "<missing>")
print(f"{idx}: {value}")
Running the Solver
$ . .venv/bin/activate && python solve.py
matched chunk #0 -> 'nite'
matched chunk #5 -> 'n07_'
matched chunk #6 -> 'pRiv'
matched chunk #2 -> 'v8_v'
matched chunk #4 -> '4r3_'
matched chunk #3 -> '4r$_'
matched chunk #7 -> '473}'
matched chunk #1 -> '{Pr1'
0: nite
1: {Pr1
2: v8_v
3: 4r$_
4: 4r3_
5: n07_
6: pRiv
7: 473}
All hashes resolved with strings of length ≤4 drawn from the printable alphabet defined above.
>5. Reconstructing the Flag
Concatenate the eight chunks in order (0 → 7):
nite {Pr1 v8_v 4r$_ 4r3_ n07_ pRiv 473}
Removing spaces (only for readability above) yields the final flag:
nite{Pr1v8_v4r$_4r3_n07_pRiv473}
>6. Optional: On-Chain Verification
If desired, craft calldata for selector 0xb8da5144 with the eight ABI-encoded bytes32 chunks you recovered. Invoking the function on Sepolia (with eth_call) should return true, confirming the solution without spending gas.