Gemastik 2022 - Fast Miner

#ctf-writeup#editorial#web#crypto

Ide awal dari soal ini sederhana sebenernya buat aware orang tentang Bitcoin block hash. Bitcoin block hash ini punya satu karakteristik dimana hasil double hash (SHA-256) dari block header harus memiliki beberapa bit nol di belakangnya sebagai bentuk Proof-of-Work. Ide ini sebenernya lebih cocok untuk jadi soal misc-cryptography, but here we are :p

Ide penggunaan bitcoin block hash ini sudah ada di beberapa CTF sebelumnya, i.e.

Ringkasan Soal

source code bisa diakses di https://gist.github.com/circleous/a97be308540c9163592a540750456589#file-main-py

Skema penyimpanan notes menggunakan plain dictionary dimana untuk key-nya menggunakan 8 trailing bytes hasil dari SHA256(UID + title). Jadi untuk dapetin flagnya cukup straightforward dengan cukup cari hash dengan 8 trailing zero bytes karena sudah ada flag pada key 00…000 dimana Bitcoin block hash sudah lebih dari mencukupi untuk kebutuhan tersebut.

Solve

Bitcoin chain data itu besar, jadi ga perlu download seluruh datanya. Cukup pakai block explorer api dan simpan beberapa hash dari block data yang bisa digunakan untuk tahap selanjutnya,

def get_block_data(num=25) -> Dict[bytes, bytes]:
    latest_block = requests.get("https://blockchain.info/q/latesthash").text

    cur_block = latest_block
    res = {}

    for _ in range(num):
        resp = requests.get(f"https://blockchain.info/rawblock/{cur_block}",
                            params={"format": "hex"})
        block_data = resp.text
        header = bytes.fromhex(block_data[:160])

        h = sha256(header)
        hash = h.digest()

		# store data
        res[hash[:2]] = hash[2:]

        # reverse the byte order
        prev_block = header[35:3:-1].hex()

        # change cur_block to prev_block for the next iteration
        cur_block = prev_block

    return res

def main():
    block = get_block_data(12)
...

karena block data yang di dapat tidak banyak perlu sedikit brute untuk mendapatkan UID yang cocok lalu baru bisa dapat flagnya.

def get_uid() -> Tuple[str, bytes]:
    resp = requests.get("http://localhost:8000")
    uid = bytes.fromhex(resp.text.split()[-1][:4])

    return resp.cookies.get("sessionId"), uid

def main():
	...
    while True:
        sessionId, uid = get_uid()
        title = block.get(uid)
        if title is not None:
            break

    assert sha256(uid + title).hexdigest().endswith("0000000000000000")

    resp = requests.get("http://localhost:8000/notes",
                        cookies={"sessionId": sessionId},
                        params={"title": title})
    print(resp.text) # flag

The Pitfall and Unreleased Part of the Challenge

notes: Dict[str, str] = {
    "00000000": os.environ.get("FLAG", "FLAG{test_flag}"),
}

Jadi 8 trailing bytes ini bukan tanpa sebab karena eksekusi awal dari ide soal ini memakai dotnet core dimana UID tidak diberikan dan perlu di overwrite terlebih dahulu dengan menggunakan UAF di stack allocation, tapi karena sampai beberapa jam sebelum lomba dimulai remote exploit belum bisa dan gagal terus, akhirnya saya speedrun untuk rewrite ke python saja dan lupa menaikkan difficulty dari PoW xD.


solver dan soal bisa diakses di https://gist.github.com/circleous/a97be308540c9163592a540750456589