From af8ee3f01b6106b2289ed6e0f2c919234eaca278 Mon Sep 17 00:00:00 2001 From: money36 Date: Wed, 7 Dec 2022 10:46:50 +0100 Subject: [PATCH] import solutions --- cryptoctf.sol | 127 +++++++++++++++++++++++++++++++++++++++++++++++ main_cctf.txt | 13 +++++ side_privacy.txt | 47 ++++++++++++++++++ side_rmrk.txt | 26 ++++++++++ 4 files changed, 213 insertions(+) create mode 100644 cryptoctf.sol create mode 100644 main_cctf.txt create mode 100644 side_privacy.txt create mode 100644 side_rmrk.txt diff --git a/cryptoctf.sol b/cryptoctf.sol new file mode 100644 index 0000000..2aaa33b --- /dev/null +++ b/cryptoctf.sol @@ -0,0 +1,127 @@ +pragma solidity ^0.8.7; + +contract CryptoCTF10 { + 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/main_cctf.txt b/main_cctf.txt new file mode 100644 index 0000000..cc7983a --- /dev/null +++ b/main_cctf.txt @@ -0,0 +1,13 @@ +== CryptoCTF smart-contract rewrite + +Key feature: generalization of contest creation, it is possible to run an arbitrary number of independent contests through a single deployment of the smart-contract. + +Known security bugs have been fixed. Key solution: one has to sign his/her own address. + +The correctness has been thoroughly tested. Result: we've also found yet another (new) security bug in the original smart contract: +If the signature is garbled, in particular when its last byte is neither one of 0x1b and 0x1c (on the Ethereum mainnet, at least), then the `ecrecover()` function returns a 0 address, which happens to equal the value that is "stored" for challenges that have not yet been added. This would cause the system to believe that a correct answer was submitted. This can be used as an attack in 2 ways: +• A contestant can submit seemingly correct answers for an unbounded number of non-existent challenges, thus spamming the logs with "challenge solved" messages. +• If challenges are added while submissions are open, then a contestant can solve such challenges before they are added; if this challenge belongs to the class where "only the first solver is credited", then this will prevent others from really solving the challenges. The contestant doesn't get any points for doing this though, because not-yet-existent challenges are worth 0 points per default "storage". + + +Pending/incomplete feature (would have been): a user-friendly UI that works in conjunction with MetaMask, and where it is sufficient to input a literal flag into a field, as opposed to the need to use unreadable Python scripts and smart-contract code. diff --git a/side_privacy.txt b/side_privacy.txt new file mode 100644 index 0000000..7b07fc8 --- /dev/null +++ b/side_privacy.txt @@ -0,0 +1,47 @@ +This report analyzes a chosen website, metamask.io and some of its externally located pages, from a data processing and regulation compliance perspective. + + +== Web access + +The website, through HTML and JavaScript, causes web browsers to send web requests to the following 3rd-party URLs +• px.ads.linkedin.com, www.linkedin.com, snap.licdn.com (LinkedIn, direct ad and analytics purposes) +• i.ytimg.com (YouTube, via thumbnails) +• images.ctfassets.net (a content delivery network/) + +These references can be analyzed in more detail e.g. www.google-analytics.com is referred to as and , not to mention the fact that Google Analytics and LinkedIn Analytics code is inlined. + +Some, but not all, of these, are blocked by uBlock Origin (a popular browser plugin with large databases targeting ad-blocking and tracker-blocking). + +The website grabs some fonts, though they are locally hosted. + + +== Blind trust of remote code + +One important problem that is universally prevalent is the enforced trust on remote code (JavaScript in this case) by the lack of integrity checking. Any remote code provider could run mild cryptojacking, fingerprinting, Spectre-based memory scanning tools or other malware on the computers of users that visit websites that rely on remote code, and such users would never know, because they don't practice checking things manually. + +This includes metamask.io's use of Google Analytics. Also, the privacy polity et al is hosted on consensys.net, which takes JS code from acsbapp.com. + +It would be possible for the website operators to review a piece of code and then to record a hash of the code at one time, as in