import solutions
parent
79048ae42d
commit
af8ee3f01b
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
|
@ -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 <link rel="preconnect"/> and <link rel="dns-prefetch"/>, 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 <script src="..." integrity="..."/>, and even better would be to host the code locally.
|
||||||
|
|
||||||
|
|
||||||
|
== Access through Tor
|
||||||
|
|
||||||
|
Using services through Tor is getting more annoying with every day. WordPress-hosted websites are blocking Tor, and CloudFlare-hosted websites force "anonymous"¹ Tor users to solve annoying CAPTCHA challenges; both are very large service providers. On a side-note, centralization of service providers poses a risk of world-wide service blackouts and surreptitious massive data collection practices.
|
||||||
|
|
||||||
|
¹ it seems that solving a CAPTCHA challenge is needed when the browser resists fingerprinting — "privacy lovers can suck it" — for example, FireFox have been shown to be the most fingerprintable browser (e.g. forced updates-checking, captive portal detection — why not shut the fuck up?)
|
||||||
|
|
||||||
|
On the other hand, MetaMask seems to be well-accessible through Tor, and its key services, such as downloading the latest version of a MetaMask plugin, are not significantly hindered. Likewise, the nature of submitting cryptocurrency transactions, whether through MetaMask or otherwise, is slow, so latency added due to Tor is not a problem.
|
||||||
|
|
||||||
|
This makes it likely that user experience through VPNs should be even better.
|
||||||
|
|
||||||
|
|
||||||
|
== Legal defence
|
||||||
|
|
||||||
|
In legal terms, the privacy policy is stuffed empty: essentially it's "we may collect anything in any way and use it for anything, but we're not revealing anything concrete", forcing users to keep guessing whether some data in question is collected. Regarding the GDPR and the CCPA, the privacy policy is weasel-worded: they don't nearly describe all the rights the GDPR/CCPA give users, but were probably put into the privacy to give the benefit of the doubt in the eyes of regulators.
|
||||||
|
|
||||||
|
Unfortunately, MetaMask's company, ConsenSys, is in the USA, a wild west in data processing.
|
||||||
|
|
||||||
|
|
||||||
|
== Conclusion
|
||||||
|
|
||||||
|
Although, if we discount the analytics services, the metamask.io website itself doesn't seem to be embarrassingly invasive. However, the key information that MetaMask can get at is financial information, when MetaMask is connected to a blockchain access provider, namely infura.io. In this case, if transactions are submitted through such a blockchain access provider, they can be linked to analytics information, possibly leading to de-anonymization (although infura.io also seems to be accessible through Tor). Actual de-anonymization based on blockchain analysis is a far-reaching business.
|
|
@ -0,0 +1,26 @@
|
||||||
|
This document proposes a few ideas for expanding the RMRK concepts.
|
||||||
|
|
||||||
|
|
||||||
|
== Experience NFTs
|
||||||
|
|
||||||
|
RMRK-style hierarchical building blocks can be applied to experience NFTs as well.
|
||||||
|
|
||||||
|
Experience NFTs shouldn't just be a collection of experiences — more experiences means more skills? —, but experiences should be non-commutative. As an initiation analogy, it is possible to learn Chinese history in 2 ways:
|
||||||
|
• First learn Chinese, then listen to history lectures in Chinese.
|
||||||
|
• First listen to history lectures in Chinese, then learn Chinese. In this case, the lectures would have to be memorized word-by-word and then recalled after learning Chinese. This way of learning obviously doesn't work. The experiences cannot be chained in this order.
|
||||||
|
|
||||||
|
As such, as with hierarchical equipping, experiences can be hierarchically bounded.
|
||||||
|
|
||||||
|
|
||||||
|
== Self-reinforcing loop
|
||||||
|
|
||||||
|
Not only that, but in terms of role-playing games, for example being able to use a weapon effectively is tied to gaining experience/skill in using that weapon — one doesn't gain experience/skill when a weapon is too easy to use or its successful use is only due to sheer luck.
|
||||||
|
|
||||||
|
The combination of these lend themselves to formation of a kind of game where character NFTs would have to be re-equipped with other equippable NFT from time to time, to allow the characters to gain new experiences. And an NFT is more valuable the more distinct experiences/skills it has.
|
||||||
|
|
||||||
|
Taking this further, experiences foster rank and reknown, which permits admission into more restricted areas of the world — e.g. only the most skillful ones may enter the dragon's lair —, allowing for more unique experiences yet again.
|
||||||
|
|
||||||
|
|
||||||
|
== Relationships between NFTs
|
||||||
|
|
||||||
|
Also, if marriage between character NFTs is a thing, then there ough to be a question of whether "one is good enough for the other" based on experiences.
|
Loading…
Reference in New Issue