diff --git a/README.md b/README.md index fed67e5..0b3af49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# DCTF +# DCTF Beta -Decentralized CTF engine, used by CCTF \ No newline at end of file +Decentralized CTF engine, used by CCTF + +Testnet deployment on Moonbase Alpha: https://moonbase.moonscan.io/address/0x919f68cc35ce5d49a45c94dc44e7bf444f9a7531 + +Contributors: six, Silur, Anonz team, Metafaka team, BMEta team \ No newline at end of file diff --git a/contracts/CCTF_2022.sol b/contracts/CCTF_2022.sol new file mode 100644 index 0000000..018dca2 --- /dev/null +++ b/contracts/CCTF_2022.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: six and Silur +pragma solidity ^0.8.16; + +contract CCTF9 { + address public admin; + uint256 public volStart; + uint256 public volMaxPoints; + uint256 public powDiff; + bool public started; + + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint256 points; + } + + modifier onlyAdmin { + require(msg.sender == admin, "Not admin"); + _; + } + + modifier onlyActive { + require(started == true, "CCTF not started."); + _; + } + + struct Flag { + address signer; + bool onlyFirstSolver; + uint256 points; + string skill_name; + } + + mapping (address => Player) public players; + mapping (uint256 => Flag) public flags; + + event CCTFStarted(uint256 timestamp); + event FlagAdded(uint256 indexed flagId, address flagSigner); + event FlagRemoved(uint256 indexed flagId); + event FlagSolved(uint256 indexed flagId, address indexed solver); + + constructor(uint256 _volMaxPoints, uint256 _powDiff) { + admin = msg.sender; + volMaxPoints = _volMaxPoints; + powDiff = _powDiff; + started = false; + } + + function setAdmin(address _admin) external onlyAdmin { + require(_admin != address(0)); + admin = _admin; + } + + function setCCTFStatus(bool _started) external onlyAdmin { + started = _started; + } + + function setFlag(uint256 _flagId, address _flagSigner, bool _onlyFirstSolver, uint256 _points, string memory _skill) external onlyAdmin{ + flags[_flagId] = Flag(_flagSigner, _onlyFirstSolver, _points, _skill); + emit FlagAdded(_flagId, _flagSigner); + } + + function setPowDiff(uint256 _powDiff) external onlyAdmin { + powDiff = _powDiff; + } + + + function register(string memory _RTFM) external { + require(players[msg.sender].status == PlayerStatus.Unverified, 'Already registered or banned'); + //uint256 pow = uint256(keccak256(abi.encodePacked("CCTF", msg.sender,"registration", nonce))); + //require(pow < powDiff, "invalid pow"); + require(keccak256(abi.encodePacked('I_read_it')) == keccak256(abi.encodePacked(_RTFM))); // PoW can be used for harder challenges, this is Entry! + players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(address player, PlayerStatus status) external onlyAdmin { + players[player].status = status; + } + + +////////// Submit flags + mapping(bytes32 => bool) usedNs; // Against replay attack (we only check message signer) + mapping (address => mapping (uint256 => bool)) Solves; // address -> challenge ID -> solved/not + uint256 public submission_success_count = 0; // For statistics + + function SubmitFlag(bytes32 _message, bytes memory signature, uint256 _submitFor) external onlyActive { + require(players[msg.sender].status == PlayerStatus.Verified, "You are not even playing"); + require(bytes32(_message).length <= 256, "Too long message."); + require(!usedNs[_message]); + usedNs[_message] = true; + require(recoverSigner(_message, signature) == flags[_submitFor].signer, "Not signed with the correct key."); + require(Solves[msg.sender][_submitFor] == false); + + Solves[msg.sender][_submitFor] = true; + players[msg.sender].points += flags[_submitFor].points; + players[msg.sender].points = players[msg.sender].points < volMaxPoints ? players[msg.sender].points : volMaxPoints; + + if (flags[_submitFor].onlyFirstSolver) { + flags[_submitFor].points = 0; + } + + submission_success_count = submission_success_count + 1; + emit FlagSolved(_submitFor, msg.sender); + } + + function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v){ + require(sig.length == 65, "Invalid signature length"); + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + +////////// Check status, scores, etc + function getPlayerStatus(address _player) external view returns (PlayerStatus) { + return players[_player].status; + } + + function getPlayerPoints(address _player) external view returns (uint256) { + return players[_player].points < volMaxPoints ? players[_player].points : volMaxPoints; + } + + function getSuccessfulSubmissionCount() external view returns (uint256){ + return submission_success_count; + } +} diff --git a/contracts/CCTF_by_Anonz.sol b/contracts/CCTF_by_Anonz.sol new file mode 100644 index 0000000..58d93df --- /dev/null +++ b/contracts/CCTF_by_Anonz.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: Anonz team, developed at Polkadot Metaverse Championship, as part of their CCTF track solution. +// Based on Six's and Silur's CCTF 2022 code. +pragma solidity ^0.8.16; + +contract CryptoCTFX { + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint score; + } + + modifier onlyExistingContest(uint contestID) { + require(contests[contestID].admin != address(0), "Unknown contest ID"); + _; + } + + modifier onlyAdmin(uint contestID) { + require(msg.sender == contests[contestID].admin, "You are not the admin of this contest"); + _; + } + + modifier onlyOpen(uint contestID) { + require(contests[contestID].submissionsOpen, "Submissions are not open for this contest at this time"); + _; + } + + modifier onlyExistingChallenge(uint contestID, uint challengeID) { + require(contests[contestID].challenges[challengeID].obscuredFlag != address(0), "Unknown challenge ID"); + _; + } + + struct Challenge { + address obscuredFlag; // public key of the flag + uint worth; + uint256 descriptionFingerprint; + bool onlyFirstSolver; + string skill; + } + + struct Contest { + address admin; + mapping (uint => Challenge) challenges; + mapping (address => Player) players; + bool submissionsOpen; + mapping (address => mapping (uint => bool)) solves; // address -> challengeID -> solved/not + mapping (uint => bool) anySolves; // challengeID -> solved/not + } + + mapping (uint => Contest) public contests; + + event ChallengeAddedOrUpdated(uint contestID, uint indexed challengeID); + event ChallengeSolved(uint contestID, uint indexed challengeID, address indexed solver); + + function createContest(uint contestID) external { + require(contests[contestID].admin == address(0), "This contest ID has already been registered"); + contests[contestID].admin = msg.sender; + contests[contestID].submissionsOpen = false; + } + + function setAdmin(uint contestID, address newAdmin) external onlyExistingContest(contestID) onlyAdmin(contestID) { + require(newAdmin != address(0)); + contests[contestID].admin = newAdmin; + } + + function setSubmissionsStatus(uint contestID, bool open) external onlyExistingContest(contestID) onlyAdmin(contestID) { + contests[contestID].submissionsOpen = open; + } + + function addOrUpdateChallenge(uint contestID, uint challengeID, address obscuredFlag, uint worth, uint256 descriptionFingerprint, bool onlyFirstSolver, string memory skill) external onlyExistingContest(contestID) onlyAdmin(contestID) { + require(obscuredFlag != address(0), "The obscured flag value must not be 0"); + contests[contestID].challenges[challengeID] = Challenge(obscuredFlag, worth, descriptionFingerprint, onlyFirstSolver, skill); + emit ChallengeAddedOrUpdated(contestID, challengeID); + } + + function register(uint contestID, string memory password) external onlyExistingContest(contestID) { + require(contests[contestID].players[msg.sender].status == PlayerStatus.Unverified, "You are already registered or banned in this contest"); + require(keccak256(abi.encodePacked("I_read_it")) == keccak256(abi.encodePacked(password))); + contests[contestID].players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(uint contestID, address player, PlayerStatus status) external onlyExistingContest(contestID) onlyAdmin(contestID) { + contests[contestID].players[player].status = status; + } + + function submitFlag(uint contestID, uint challengeID, bytes memory signature) external onlyExistingContest(contestID) onlyExistingChallenge(contestID, challengeID) onlyOpen(contestID) { + require(contests[contestID].players[msg.sender].status == PlayerStatus.Verified, "You are unverified or banned in this contest"); + // the correct signature is an ECDSA signature where (1) the message (hash) is the sender address and (2) the private key is the flag; + // (2) is checked by testing against the public key, which can then be public information + address recoveredSigner = recoverSigner(bytes32(abi.encodePacked(msg.sender)), signature); + require(recoveredSigner != address(0), "Invalid signature"); + require(recoveredSigner == contests[contestID].challenges[challengeID].obscuredFlag, "Wrong answer"); + require(!contests[contestID].solves[msg.sender][challengeID], "You have already solved this challenge of this contest"); + + if (!contests[contestID].anySolves[challengeID] || !contests[contestID].challenges[challengeID].onlyFirstSolver) { + contests[contestID].players[msg.sender].score += contests[contestID].challenges[challengeID].worth; + } + contests[contestID].solves[msg.sender][challengeID] = true; + contests[contestID].anySolves[challengeID] = true; + + emit ChallengeSolved(contestID, challengeID, msg.sender); + } + + function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); + return ecrecover(messageHash, v, r, s); + } + + function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) { + require(sig.length == 65, "Invalid signature length"); + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + + function getPlayerStatus(uint contestID, address player) external view onlyExistingContest(contestID) returns (PlayerStatus) { + return contests[contestID].players[player].status; + } + + function getPlayerScore(uint contestID, address player) external view onlyExistingContest(contestID) returns (uint) { + return contests[contestID].players[player].score; + } +} diff --git a/frontend/abi.js b/frontend/abi.js new file mode 100644 index 0000000..c703f66 --- /dev/null +++ b/frontend/abi.js @@ -0,0 +1,798 @@ +myABI = [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_volMaxPoints", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "CCTFStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "flagSigner", + "type": "address" + } + ], + "name": "FlagAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + } + ], + "name": "FlagRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "FlagSolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum CCTF9.PlayerStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "PlayerStatusChanged", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_message", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_submitFor", + "type": "uint256" + } + ], + "name": "SubmitFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "TIME_DECAY_MAX", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "endTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flagCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flagList", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flags", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerStatus", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSuccessfulSubmissionCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "playerCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "playerList", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "players", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "powDiff", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_ethSignedMessageHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "recoverSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_RTFM", + "type": "string" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + } + ], + "name": "register", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_admin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + } + ], + "name": "setCCTFEndTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_paused", + "type": "bool" + } + ], + "name": "setCCTFPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + } + ], + "name": "setCCTFStartTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_flagId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_flagSigner", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "_flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_skill", + "type": "string" + } + ], + "name": "setFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + } + ], + "name": "setPlayerStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "name": "setPowDiff", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "sig", + "type": "bytes" + } + ], + "name": "splitSignature", + "outputs": [ + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "startTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "started", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "submission_success_count", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volMaxPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volStart", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/frontend/cctf-icon.png b/frontend/cctf-icon.png new file mode 100644 index 0000000..5ae4a6e Binary files /dev/null and b/frontend/cctf-icon.png differ diff --git a/frontend/cctf-logo.png b/frontend/cctf-logo.png new file mode 100644 index 0000000..036bc8d Binary files /dev/null and b/frontend/cctf-logo.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..99a0374 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,121 @@ + + +
+