Nice to see you again. Last time we have described our first submission to the hack the Syscoin's Ethereum bridge bounty (do follow this link also to find information about Syscoin Ethereum bridge, some understanding of it is useful to be able to grasp this report). Naturally, this time we are going to describe our second submission. Again, this vulnerability was never exploitable on the mainnet because the bridge is not active on the Syscoin mainnet yet, which was the point of this bounty. The proof of knowledge is trivial again today because the submission was made publicly as an issue on Syscoin's Github. Also this issue has already been fixed in the latest source code.
Future Time Bug
Bug type: asset inflation, theft
Bug severity: 8/10
Scenario 1
Attacker cost: low
The attacker needs certain hash rate in order to be able to create a single block. The attacker mines a block constructed in a way that prevents all agents in the network to successfully submit superblocks or challenge them. This will give the attacker an ultimate control over the superblock chain, which will allow the attacker to create invalid superblocks to which she can prove nonexisting burns on Syscoin network. This means the attacker can increase her balance of arbitrary asset on Ethereum, including SYSX asset. This allows the attacker to steal all the deposits of SYS made by users from Syscoin to Ethereum.
Description
Sysethereum Agents are observers of Syscoin and Ethereum blockchains who are responsible for creating and submitting superblocks to Ethereum smart contracts as well as challenging superblocks submitted by other agents in case they seem invalid.
A superblock is a representation of 60 blocks on Syscoin blockchain that is announced to Ethereum smart contract in order to later allow proving existence of burn transactions on Syscoin blockchain to Ethereum smart contract. The superblock design idea is derived from Dogethereum.
In the agent, SuperblockChainClient.updateChain is called every 10 seconds and it is responsible for creating and submitting new superblocks. An important part of the code follows:
Stack<Sha256Hash> allSyscoinHashesToHash = getSyscoinBlockHashesNewerThan(bestSuperblockLastBlockHash);
superblockChain.storeSuperblocks(allSyscoinHashesToHash, bestSuperblock.getSuperblockId()); // group them in superblocks
First, the code obtains hashes of all Syscoin blocks following the last block of the last superblock that the agent is aware of. Next, storeSuperblocks implements additional filtering using popBlocksBeforeTime before it creates a new superblock:
nextSuperblockSyscoinHashes = popBlocksBeforeTime(allSyscoinHashesToHash, getStoringStopTime());
The code of popBlocksBeforeTime looks as follows:
List<Sha256Hash> poppedBlocks = new ArrayList<>();
boolean haveEnoughForDuration = false;
while (!hashStack.empty() && syscoinWrapper.getBlock(hashStack.peek()).getHeader().getTime().before(endTime)) {
poppedBlocks.add(hashStack.pop());
if(poppedBlocks.size() >= SUPERBLOCK_DURATION) {
haveEnoughForDuration = true;
break;
}
}
// if we don't have SUPERBLOCK_DURATION amount then just clear, we don't have enough to create a superblock yet
if(!haveEnoughForDuration)
poppedBlocks.clear();
return poppedBlocks;
Here it's important to note that, on mainnet, SUPERBLOCK_DURATION is set to 60 (i.e. a superblock represents 60 blocks), getStoringStopTime() returns current time minus (SUPERBLOCK_DELAY - SUPERBLOCK_STORING_WINDOW), where SUPERBLOCK_DELAY is 3*3600 and SUPERBLOCK_STORING_WINDOW is 60 (i.e. only consider blocks that are more than 2 hours and 59 minutes old).
In combination with the fact that Syscoin node accepts a block with future timestamp up to 2 hours, this implementation of the agent allows the attacker to construct a block with timestamp close to 2 hours in the future and thus cause problems to popBlocksBeforeTime, which will stop progressing on this block up until its claimed timestamp is 2 hours 59 minutes old, which actually means it's real age is almost 5 hours. Therefore, the superblock which is to contain this malicious block is not going to be submitted by honest agents for very long time. This allows the attacker to make such a submission on its own.
Moreover, such a superblock will not be challenged. This is because the agent only challenges a superblock in case it locally constructs a different superblock which does not match the superblock submitted to the contract. But because the construction is delayed due to the future block time, the agent will not have a local alternative of the superblock submitted by the attacker. This allows the attacker to include invalid hashes into the superblock. When a superblock is not challenged for 10 minutes after its submission, and if its previous (parent) superblock is in the approved state, the new superblock will become approved as well immediately.
Notably, once an invalid superblock is approved, all agents' challenger wallets can be drained. Each agent will calculate its own chain which cannot be submitted because an approved block cannot be replaced. If the attacker prolongs the best superblock chain with more superblocks, honest agents will try to challenge those superblocks, but the attacker can make sure the newly submitted superblocks on the top of the malicious one are built correctly. Therefore the attacker will always be able to respond to any challenge and thus additionally steal all the money from all agents' challenger accounts.
If the attacker needs more invalid superblocks, she can either repeat the future timestamp attack, for which she needs certain hash rate, or she can wait until she sees that there are no new challenges submitted against her superblocks (i.e. all honest agents have empty challenger wallets). After that she can create invalid superblocks for free.
As a result, the attacker can steal money from agents' challenger wallets as well as increase her balance of Ethereum assets with fake proofs and thus the attacker can steal all the locked assets from the Syscoin blockchain.
To exploit the described bug, we need two changes. First one is trivial in the Syscoin node, where we just adjust time of the mined block to be equal to the time of the previous block plus 119 minutes. Second change is in the agent code, where we need to change popBlocksBeforeTime in order to create superblock and not to get stuck as the original code. We also insert invalid hash into the superblock to demonstrate that invalid superblock can get approved:
if (hashStack.empty()) {
throw new Exception("List of blocks to pop must not be empty.");
}
long lastTs = 0;
List<Sha256Hash> poppedBlocks = new ArrayList<>();
boolean haveEnoughForDuration = false;
boolean toCorruptBlock = false;
while (true) {
if (!hashStack.empty()) {
StoredBlock block = syscoinWrapper.getBlock(hashStack.peek());
// original check
boolean isOk = block.getHeader().getTime().before(endTime);
boolean isDeltaCase = false;
if (!isOk) {
long diff = (block.getHeader().getTime().getTime() - lastTs);
isDeltaCase = (diff == 119*60*1000);
}
if (isOk || isDeltaCase) {
Sha256Hash hash = hashStack.pop();
if (!toCorruptBlock) {
poppedBlocks.add(hash);
} else {
poppedBlocks.add(Sha256Hash.ZERO_HASH);
toCorruptBlock = false;
}
lastTs = block.getHeader().getTime().getTime();
if(poppedBlocks.size() >= SUPERBLOCK_DURATION) {
haveEnoughForDuration = true;
break;
}
if (isDeltaCase) {
toCorruptBlock = true;
}
} else {
break;
}
} else {
break;
}
}
// if we don't have SUPERBLOCK_DURATION amount then just clear, we don't have enough to create a superblock yet
if(!haveEnoughForDuration)
poppedBlocks.clear();