mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-15 11:45:51 +00:00
Limit initial size of some structures
Limits initial size of these structures: - Inputs and Outputs in Transaction - Transactions in Block - Hashes in PartialMerkleeTree The fix prevents this DoS attack: - Somehow the attacker needs to get a p2p connection to the bitcoinj node. - The attacker sends a tx msg that says the tx contains a trillion inputs (or a similar msg attacking any other of the structures described above). - bitcoinj tries to instantiate an ArrayList with a size of a trillion. OutOfMemoryError and the bitcoinj node is down.
This commit is contained in:
parent
a6c356c403
commit
26adf68948
@ -237,7 +237,7 @@ public class Block extends Message {
|
|||||||
|
|
||||||
int numTransactions = (int) readVarInt();
|
int numTransactions = (int) readVarInt();
|
||||||
optimalEncodingMessageSize += VarInt.sizeOf(numTransactions);
|
optimalEncodingMessageSize += VarInt.sizeOf(numTransactions);
|
||||||
transactions = new ArrayList<>(numTransactions);
|
transactions = new ArrayList<>(Math.min(numTransactions, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
for (int i = 0; i < numTransactions; i++) {
|
for (int i = 0; i < numTransactions; i++) {
|
||||||
Transaction tx = new Transaction(params, payload, cursor, this, serializer, UNKNOWN_LENGTH);
|
Transaction tx = new Transaction(params, payload, cursor, this, serializer, UNKNOWN_LENGTH);
|
||||||
// Label the transaction as coming from the P2P network, so code that cares where we first saw it knows.
|
// Label the transaction as coming from the P2P network, so code that cares where we first saw it knows.
|
||||||
|
@ -118,7 +118,7 @@ public class PartialMerkleTree extends Message {
|
|||||||
transactionCount = (int)readUint32();
|
transactionCount = (int)readUint32();
|
||||||
|
|
||||||
int nHashes = (int) readVarInt();
|
int nHashes = (int) readVarInt();
|
||||||
hashes = new ArrayList<>(nHashes);
|
hashes = new ArrayList<>(Math.min(nHashes, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
for (int i = 0; i < nHashes; i++)
|
for (int i = 0; i < nHashes; i++)
|
||||||
hashes.add(readHash());
|
hashes.add(readHash());
|
||||||
|
|
||||||
|
@ -122,11 +122,6 @@ public class Transaction extends ChildMessage {
|
|||||||
*/
|
*/
|
||||||
public static final Coin MIN_NONDUST_OUTPUT = Coin.valueOf(546); // satoshis
|
public static final Coin MIN_NONDUST_OUTPUT = Coin.valueOf(546); // satoshis
|
||||||
|
|
||||||
/**
|
|
||||||
* Max initial size of inputs and outputs ArrayList.
|
|
||||||
*/
|
|
||||||
public static final int MAX_INITIAL_INPUTS_OUTPUTS_SIZE = 20;
|
|
||||||
|
|
||||||
// These are bitcoin serialized.
|
// These are bitcoin serialized.
|
||||||
private long version;
|
private long version;
|
||||||
private ArrayList<TransactionInput> inputs;
|
private ArrayList<TransactionInput> inputs;
|
||||||
@ -606,7 +601,7 @@ public class Transaction extends ChildMessage {
|
|||||||
private void parseInputs() {
|
private void parseInputs() {
|
||||||
long numInputs = readVarInt();
|
long numInputs = readVarInt();
|
||||||
optimalEncodingMessageSize += VarInt.sizeOf(numInputs);
|
optimalEncodingMessageSize += VarInt.sizeOf(numInputs);
|
||||||
inputs = new ArrayList<>(Math.min((int) numInputs, MAX_INITIAL_INPUTS_OUTPUTS_SIZE));
|
inputs = new ArrayList<>(Math.min((int) numInputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
for (long i = 0; i < numInputs; i++) {
|
for (long i = 0; i < numInputs; i++) {
|
||||||
TransactionInput input = new TransactionInput(params, this, payload, cursor, serializer);
|
TransactionInput input = new TransactionInput(params, this, payload, cursor, serializer);
|
||||||
inputs.add(input);
|
inputs.add(input);
|
||||||
@ -619,7 +614,7 @@ public class Transaction extends ChildMessage {
|
|||||||
private void parseOutputs() {
|
private void parseOutputs() {
|
||||||
long numOutputs = readVarInt();
|
long numOutputs = readVarInt();
|
||||||
optimalEncodingMessageSize += VarInt.sizeOf(numOutputs);
|
optimalEncodingMessageSize += VarInt.sizeOf(numOutputs);
|
||||||
outputs = new ArrayList<>(Math.min((int) numOutputs, MAX_INITIAL_INPUTS_OUTPUTS_SIZE));
|
outputs = new ArrayList<>(Math.min((int) numOutputs, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
for (long i = 0; i < numOutputs; i++) {
|
for (long i = 0; i < numOutputs; i++) {
|
||||||
TransactionOutput output = new TransactionOutput(params, this, payload, cursor, serializer);
|
TransactionOutput output = new TransactionOutput(params, this, payload, cursor, serializer);
|
||||||
outputs.add(output);
|
outputs.add(output);
|
||||||
|
@ -26,7 +26,7 @@ public class TransactionWitness {
|
|||||||
private final List<byte[]> pushes;
|
private final List<byte[]> pushes;
|
||||||
|
|
||||||
public TransactionWitness(int pushCount) {
|
public TransactionWitness(int pushCount) {
|
||||||
pushes = new ArrayList<>(pushCount);
|
pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getPush(int i) {
|
public byte[] getPush(int i) {
|
||||||
|
@ -60,6 +60,13 @@ public class Utils {
|
|||||||
/** Hex encoding used throughout the framework. Use with HEX.encode(byte[]) or HEX.decode(CharSequence). */
|
/** Hex encoding used throughout the framework. Use with HEX.encode(byte[]) or HEX.decode(CharSequence). */
|
||||||
public static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
|
public static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max initial size of variable length arrays and ArrayLists that could be attacked.
|
||||||
|
* Avoids this attack: Attacker sends a msg indicating it will contain a huge number (eg 2 billion) elements (eg transaction inputs) and
|
||||||
|
* forces bitcoinj to try to allocate a huge piece of the memory resulting in OutOfMemoryError.
|
||||||
|
*/
|
||||||
|
public static final int MAX_INITIAL_ARRAY_LENGTH = 20;
|
||||||
|
|
||||||
private static BlockingQueue<Boolean> mockSleepQueue;
|
private static BlockingQueue<Boolean> mockSleepQueue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,11 @@ import org.bitcoinj.wallet.Wallet.BalanceType;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -294,4 +298,38 @@ public class BlockTest {
|
|||||||
assertTrue(block370661.isBIP66());
|
assertTrue(block370661.isBIP66());
|
||||||
assertTrue(block370661.isBIP65());
|
assertTrue(block370661.isBIP65());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseBlockWithHugeDeclaredTransactionsSize() throws Exception{
|
||||||
|
Block block = new Block(UNITTEST, 1, Sha256Hash.ZERO_HASH, Sha256Hash.ZERO_HASH, 1, 1, 1, new ArrayList<Transaction>()) {
|
||||||
|
@Override
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
|
Utils.uint32ToByteStreamLE(getVersion(), stream);
|
||||||
|
stream.write(getPrevBlockHash().getReversedBytes());
|
||||||
|
stream.write(getMerkleRoot().getReversedBytes());
|
||||||
|
Utils.uint32ToByteStreamLE(getTimeSeconds(), stream);
|
||||||
|
Utils.uint32ToByteStreamLE(getDifficultyTarget(), stream);
|
||||||
|
Utils.uint32ToByteStreamLE(getNonce(), stream);
|
||||||
|
|
||||||
|
stream.write(new VarInt(Integer.MAX_VALUE).encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] bitcoinSerialize() {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
bitcoinSerializeToStream(baos);
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
byte[] serializedBlock = block.bitcoinSerialize();
|
||||||
|
try {
|
||||||
|
UNITTEST.getDefaultSerializer().makeBlock(serializedBlock, serializedBlock.length);
|
||||||
|
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
//Expected, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ import org.junit.*;
|
|||||||
import org.junit.runner.*;
|
import org.junit.runner.*;
|
||||||
import org.junit.runners.*;
|
import org.junit.runners.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.*;
|
import java.math.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -202,4 +204,34 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
|||||||
// Peer 1 goes away.
|
// Peer 1 goes away.
|
||||||
closePeer(peerOf(p1));
|
closePeer(peerOf(p1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseHugeDeclaredSizePartialMerkleTree() throws Exception{
|
||||||
|
final byte[] bits = new byte[1];
|
||||||
|
bits[0] = 0x3f;
|
||||||
|
final List<Sha256Hash> hashes = new ArrayList<>();
|
||||||
|
hashes.add(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000001"));
|
||||||
|
hashes.add(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000002"));
|
||||||
|
hashes.add(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000003"));
|
||||||
|
PartialMerkleTree pmt = new PartialMerkleTree(UNITTEST, bits, hashes, 3) {
|
||||||
|
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
|
uint32ToByteStreamLE(getTransactionCount(), stream);
|
||||||
|
// Add Integer.MAX_VALUE instead of hashes.size()
|
||||||
|
stream.write(new VarInt(Integer.MAX_VALUE).encode());
|
||||||
|
//stream.write(new VarInt(hashes.size()).encode());
|
||||||
|
for (Sha256Hash hash : hashes)
|
||||||
|
stream.write(hash.getReversedBytes());
|
||||||
|
|
||||||
|
stream.write(new VarInt(bits.length).encode());
|
||||||
|
stream.write(bits);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
byte[] serializedPmt = pmt.bitcoinSerialize();
|
||||||
|
try {
|
||||||
|
new PartialMerkleTree(UNITTEST, serializedPmt, 0);
|
||||||
|
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
//Expected, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,13 @@ import org.bitcoinj.testing.*;
|
|||||||
import org.easymock.*;
|
import org.easymock.*;
|
||||||
import org.junit.*;
|
import org.junit.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import static org.bitcoinj.core.Utils.HEX;
|
import static org.bitcoinj.core.Utils.HEX;
|
||||||
|
|
||||||
|
import static org.bitcoinj.core.Utils.uint32ToByteStreamLE;
|
||||||
import static org.easymock.EasyMock.*;
|
import static org.easymock.EasyMock.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
@ -469,4 +472,103 @@ public class TransactionTest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseTransactionWithHugeDeclaredInputsSize() throws Exception {
|
||||||
|
Transaction tx = new HugeDeclaredSizeTransaction(UNITTEST, true, false, false);
|
||||||
|
byte[] serializedTx = tx.bitcoinSerialize();
|
||||||
|
try {
|
||||||
|
new Transaction(UNITTEST, serializedTx);
|
||||||
|
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
//Expected, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseTransactionWithHugeDeclaredOutputsSize() throws Exception {
|
||||||
|
Transaction tx = new HugeDeclaredSizeTransaction(UNITTEST, false, true, false);
|
||||||
|
byte[] serializedTx = tx.bitcoinSerialize();
|
||||||
|
try {
|
||||||
|
new Transaction(UNITTEST, serializedTx);
|
||||||
|
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
//Expected, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseTransactionWithHugeDeclaredWitnessPushCountSize() throws Exception {
|
||||||
|
Transaction tx = new HugeDeclaredSizeTransaction(UNITTEST, false, false, true);
|
||||||
|
byte[] serializedTx = tx.bitcoinSerialize();
|
||||||
|
try {
|
||||||
|
new Transaction(UNITTEST, serializedTx);
|
||||||
|
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
//Expected, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HugeDeclaredSizeTransaction extends Transaction {
|
||||||
|
|
||||||
|
private boolean hackInputsSize;
|
||||||
|
private boolean hackOutputsSize;
|
||||||
|
private boolean hackWitnessPushCountSize;
|
||||||
|
|
||||||
|
public HugeDeclaredSizeTransaction(NetworkParameters params, boolean hackInputsSize, boolean hackOutputsSize, boolean hackWitnessPushCountSize) {
|
||||||
|
super(params);
|
||||||
|
this.protocolVersion = NetworkParameters.ProtocolVersion.WITNESS_VERSION.getBitcoinProtocolVersion();
|
||||||
|
Transaction inputTx = new Transaction(params);
|
||||||
|
inputTx.addOutput(Coin.FIFTY_COINS, LegacyAddress.fromKey(params, ECKey.fromPrivate(BigInteger.valueOf(123456))));
|
||||||
|
this.addInput(inputTx.getOutput(0));
|
||||||
|
this.getInput(0).disconnect();
|
||||||
|
TransactionWitness witness = new TransactionWitness(1);
|
||||||
|
witness.setPush(0, new byte[] {0});
|
||||||
|
this.getInput(0).setWitness(witness);
|
||||||
|
Address to = LegacyAddress.fromKey(params, ECKey.fromPrivate(BigInteger.valueOf(1000)));
|
||||||
|
this.addOutput(Coin.COIN, to);
|
||||||
|
|
||||||
|
this.hackInputsSize = hackInputsSize;
|
||||||
|
this.hackOutputsSize = hackOutputsSize;
|
||||||
|
this.hackWitnessPushCountSize = hackWitnessPushCountSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
|
||||||
|
// version
|
||||||
|
uint32ToByteStreamLE(getVersion(), stream);
|
||||||
|
// marker, flag
|
||||||
|
if (useSegwit) {
|
||||||
|
stream.write(0);
|
||||||
|
stream.write(1);
|
||||||
|
}
|
||||||
|
// txin_count, txins
|
||||||
|
long inputsSize = hackInputsSize ? Integer.MAX_VALUE : getInputs().size();
|
||||||
|
stream.write(new VarInt(inputsSize).encode());
|
||||||
|
for (TransactionInput in : getInputs())
|
||||||
|
in.bitcoinSerialize(stream);
|
||||||
|
// txout_count, txouts
|
||||||
|
long outputsSize = hackOutputsSize ? Integer.MAX_VALUE : getOutputs().size();
|
||||||
|
stream.write(new VarInt(outputsSize).encode());
|
||||||
|
for (TransactionOutput out : getOutputs())
|
||||||
|
out.bitcoinSerialize(stream);
|
||||||
|
// script_witnisses
|
||||||
|
if (useSegwit) {
|
||||||
|
for (TransactionInput in : getInputs()) {
|
||||||
|
TransactionWitness witness = in.getWitness();
|
||||||
|
long pushCount = hackWitnessPushCountSize ? Integer.MAX_VALUE : witness.getPushCount();
|
||||||
|
stream.write(new VarInt(pushCount).encode());
|
||||||
|
for (int i = 0; i < witness.getPushCount(); i++) {
|
||||||
|
byte[] push = witness.getPush(i);
|
||||||
|
stream.write(new VarInt(push.length).encode());
|
||||||
|
stream.write(push);
|
||||||
|
}
|
||||||
|
|
||||||
|
in.getWitness().bitcoinSerializeToStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lock_time
|
||||||
|
uint32ToByteStreamLE(getLockTime(), stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user