diff --git a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTest.java b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTest.java index 25437a28b..31eacf532 100644 --- a/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTest.java +++ b/TLS-Scanner-Core/src/main/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTest.java @@ -8,10 +8,14 @@ */ package de.rub.nds.tlsscanner.core.vector.statistics; +import de.rub.nds.tlsscanner.core.vector.Vector; import de.rub.nds.tlsscanner.core.vector.VectorResponse; import de.rub.nds.tlsscanner.core.vector.response.ResponseFingerprint; +import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.math3.distribution.ChiSquaredDistribution; import org.apache.commons.math3.stat.inference.ChiSquareTest; @@ -119,4 +123,42 @@ protected boolean isFisherExactUsable() { } return responseFingerprintSet.size() <= 2; } + + /** + * Get responses that occurred at most {@code mostOccurrences} times across all vectors. + * + * @param mostOccurrences Maximum number of occurrences for a response to be considered rare + * @return List of VectorResponse objects representing rare responses + */ + public List getRareResponses(int mostOccurrences) { + // Map from ResponseFingerprint to all vectors that produced this fingerprint + Map> fingerprintToVectors = new HashMap<>(); + // Map from ResponseFingerprint to total count across all vectors + Map fingerprintTotalCount = new HashMap<>(); + + for (VectorContainer container : getVectorContainerList()) { + for (ResponseCounter counter : container.getDistinctResponsesCounterList()) { + ResponseFingerprint fingerprint = counter.getFingerprint(); + fingerprintToVectors.computeIfAbsent(fingerprint, k -> new ArrayList<>()); + if (!fingerprintToVectors.get(fingerprint).contains(container.getVector())) { + fingerprintToVectors.get(fingerprint).add(container.getVector()); + } + fingerprintTotalCount.merge(fingerprint, counter.getCounter(), Integer::sum); + } + } + + List ret = new ArrayList<>(); + for (Map.Entry> entry : fingerprintToVectors.entrySet()) { + ResponseFingerprint fingerprint = entry.getKey(); + List vectors = entry.getValue(); + Integer totalCount = fingerprintTotalCount.get(fingerprint); + + if (totalCount != null && totalCount <= mostOccurrences) { + for (Vector vector : vectors) { + ret.add(new VectorResponse(vector, fingerprint)); + } + } + } + return ret; + } } diff --git a/TLS-Scanner-Core/src/test/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTestTest.java b/TLS-Scanner-Core/src/test/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTestTest.java new file mode 100644 index 000000000..d691e0360 --- /dev/null +++ b/TLS-Scanner-Core/src/test/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTestTest.java @@ -0,0 +1,231 @@ +/* + * TLS-Scanner - A TLS configuration and analysis tool based on TLS-Attacker + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsscanner.core.vector.statistics; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.tlsattacker.transport.socket.SocketState; +import de.rub.nds.tlsscanner.core.vector.Vector; +import de.rub.nds.tlsscanner.core.vector.VectorResponse; +import de.rub.nds.tlsscanner.core.vector.response.ResponseFingerprint; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class InformationLeakTestTest { + + private static class TestVector implements Vector { + private final String name; + + public TestVector(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TestVector) { + return name.equals(((TestVector) obj).name); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } + + private static class SimpleTestInfo extends TestInfo { + @Override + public String getTechnicalName() { + return "SimpleTest"; + } + + @Override + public List getFieldNames() { + return List.of(); + } + + @Override + public List getFieldValues() { + return List.of(); + } + + @Override + public String getPrintableName() { + return "Simple Test"; + } + + @Override + public boolean equals(Object o) { + return o instanceof SimpleTestInfo; + } + + @Override + public int hashCode() { + return getTechnicalName().hashCode(); + } + } + + @Test + public void testGetRareResponsesWithNoResponses() { + List responses = new ArrayList<>(); + InformationLeakTest test = + new InformationLeakTest<>(new SimpleTestInfo(), responses); + + List rareResponses = test.getRareResponses(1); + assertTrue(rareResponses.isEmpty()); + } + + @Test + public void testGetRareResponsesWithUniqueResponse() { + List responses = new ArrayList<>(); + TestVector vector1 = new TestVector("vector1"); + TestVector vector2 = new TestVector("vector2"); + TestVector vector3 = new TestVector("vector3"); + + ResponseFingerprint fingerprint1 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.CLOSED); + ResponseFingerprint fingerprint2 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.TIMEOUT); + + // vector1 and vector2 have fingerprint1, vector3 has unique fingerprint2 + responses.add(new VectorResponse(vector1, fingerprint1)); + responses.add(new VectorResponse(vector2, fingerprint1)); + responses.add(new VectorResponse(vector3, fingerprint2)); + + InformationLeakTest test = + new InformationLeakTest<>(new SimpleTestInfo(), responses); + + // Get responses that occurred at most once + List rareResponses = test.getRareResponses(1); + assertEquals(1, rareResponses.size()); + assertEquals(vector3, rareResponses.get(0).getVector()); + assertEquals(fingerprint2, rareResponses.get(0).getFingerprint()); + } + + @Test + public void testGetRareResponsesWithMultipleRareResponses() { + List responses = new ArrayList<>(); + TestVector vector1 = new TestVector("vector1"); + TestVector vector2 = new TestVector("vector2"); + TestVector vector3 = new TestVector("vector3"); + TestVector vector4 = new TestVector("vector4"); + + ResponseFingerprint fingerprint1 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.CLOSED); + ResponseFingerprint fingerprint2 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.TIMEOUT); + ResponseFingerprint fingerprint3 = + new ResponseFingerprint( + new ArrayList<>(), new ArrayList<>(), SocketState.DATA_AVAILABLE); + + // vector1 and vector2 have fingerprint1, vector3 has fingerprint2, vector4 has fingerprint3 + responses.add(new VectorResponse(vector1, fingerprint1)); + responses.add(new VectorResponse(vector2, fingerprint1)); + responses.add(new VectorResponse(vector3, fingerprint2)); + responses.add(new VectorResponse(vector4, fingerprint3)); + + InformationLeakTest test = + new InformationLeakTest<>(new SimpleTestInfo(), responses); + + // Get responses that occurred at most twice + List rareResponses = test.getRareResponses(2); + assertEquals(4, rareResponses.size()); // All responses occur at most twice + + // Get responses that occurred at most once + rareResponses = test.getRareResponses(1); + assertEquals(2, rareResponses.size()); // vector3 and vector4 + + // Verify the rare responses + boolean foundVector3 = false; + boolean foundVector4 = false; + for (VectorResponse response : rareResponses) { + if (response.getVector().equals(vector3)) { + foundVector3 = true; + assertEquals(fingerprint2, response.getFingerprint()); + } else if (response.getVector().equals(vector4)) { + foundVector4 = true; + assertEquals(fingerprint3, response.getFingerprint()); + } + } + assertTrue(foundVector3); + assertTrue(foundVector4); + } + + @Test + public void testGetRareResponsesWithNoRareResponses() { + List responses = new ArrayList<>(); + TestVector vector1 = new TestVector("vector1"); + TestVector vector2 = new TestVector("vector2"); + TestVector vector3 = new TestVector("vector3"); + + ResponseFingerprint fingerprint1 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.CLOSED); + + // All vectors have the same fingerprint + responses.add(new VectorResponse(vector1, fingerprint1)); + responses.add(new VectorResponse(vector2, fingerprint1)); + responses.add(new VectorResponse(vector3, fingerprint1)); + + InformationLeakTest test = + new InformationLeakTest<>(new SimpleTestInfo(), responses); + + // Get responses that occurred at most twice + List rareResponses = test.getRareResponses(2); + assertTrue(rareResponses.isEmpty()); // All responses occur 3 times + + // Get responses that occurred at most 3 times + rareResponses = test.getRareResponses(3); + assertEquals(3, rareResponses.size()); // All responses occur exactly 3 times + } + + @Test + public void testGetRareResponsesIntegrationWithExtendedTest() { + List initialResponses = new ArrayList<>(); + TestVector vector1 = new TestVector("vector1"); + TestVector vector2 = new TestVector("vector2"); + + ResponseFingerprint fingerprint1 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.CLOSED); + ResponseFingerprint fingerprint2 = + new ResponseFingerprint(new ArrayList<>(), new ArrayList<>(), SocketState.TIMEOUT); + + initialResponses.add(new VectorResponse(vector1, fingerprint1)); + initialResponses.add(new VectorResponse(vector2, fingerprint2)); + + InformationLeakTest test = + new InformationLeakTest<>(new SimpleTestInfo(), initialResponses); + + // Initially both responses are unique + List rareResponses = test.getRareResponses(1); + assertEquals(2, rareResponses.size()); + + // Extend the test with more responses + List additionalResponses = new ArrayList<>(); + additionalResponses.add(new VectorResponse(vector1, fingerprint1)); + additionalResponses.add(new VectorResponse(vector1, fingerprint1)); + + test.extendTestWithVectorResponses(additionalResponses); + + // Now only vector2's response is rare (occurring once) + rareResponses = test.getRareResponses(1); + // Since vector1 now occurs 3 times (1 initial + 2 additional) with fingerprint1, + // but getRareResponses returns one VectorResponse per vector that had that fingerprint, + // we expect 1 response for vector2 (which still occurs once) + assertEquals(1, rareResponses.size()); + assertEquals(vector2, rareResponses.get(0).getVector()); + assertEquals(fingerprint2, rareResponses.get(0).getFingerprint()); + } +} diff --git a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/SessionTicketPaddingOracleProbe.java b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/SessionTicketPaddingOracleProbe.java index f1a033a89..6b6cbfb2d 100644 --- a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/SessionTicketPaddingOracleProbe.java +++ b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/SessionTicketPaddingOracleProbe.java @@ -19,13 +19,9 @@ import de.rub.nds.tlsscanner.core.constants.TlsProbeType; import de.rub.nds.tlsscanner.core.task.FingerPrintTask; import de.rub.nds.tlsscanner.core.task.FingerprintTaskVectorPair; -import de.rub.nds.tlsscanner.core.vector.Vector; import de.rub.nds.tlsscanner.core.vector.VectorResponse; -import de.rub.nds.tlsscanner.core.vector.response.ResponseFingerprint; import de.rub.nds.tlsscanner.core.vector.statistics.InformationLeakTest; -import de.rub.nds.tlsscanner.core.vector.statistics.ResponseCounter; import de.rub.nds.tlsscanner.core.vector.statistics.TestInfo; -import de.rub.nds.tlsscanner.core.vector.statistics.VectorContainer; import de.rub.nds.tlsscanner.serverscanner.leak.TicketPaddingOracleLastByteTestInfo; import de.rub.nds.tlsscanner.serverscanner.leak.TicketPaddingOracleSecondByteTestInfo; import de.rub.nds.tlsscanner.serverscanner.probe.result.VersionDependentSummarizableResult; @@ -41,10 +37,8 @@ import de.rub.nds.tlsscanner.serverscanner.report.ServerReport; import de.rub.nds.tlsscanner.serverscanner.selector.ConfigSelector; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -183,7 +177,7 @@ private boolean shouldCheckOffset(int offset, int ticketLength) { private boolean foundDefinitiveResult( InformationLeakTest secondByteLeakTest) { return secondByteLeakTest.isSignificantDistinctAnswers() - && !getRareResponses(secondByteLeakTest, 1).isEmpty(); + && !secondByteLeakTest.getRareResponses(1).isEmpty(); } private TicketPaddingOracleResult checkPaddingOracle(ProtocolVersion version) { @@ -222,7 +216,7 @@ private TicketPaddingOracleResult checkPaddingOracle(ProtocolVersion version) { new ArrayList<>(); if (lastByteLeakTest.isSignificantDistinctAnswers()) { - List rareResponses = getRareResponses(lastByteLeakTest, 2); + List rareResponses = lastByteLeakTest.getRareResponses(2); LOGGER.debug( "At Offset {} found significant difference with {} rare response(s)", offset, @@ -273,29 +267,6 @@ && foundDefinitiveResult(secondByteLeakTest)) { return new TicketPaddingOracleResult(offsetResults); } - public static List getRareResponses( - InformationLeakTest informationLeakTest, int mostOccurrences) { - Map> map = new HashMap<>(); - for (VectorContainer container : informationLeakTest.getVectorContainerList()) { - for (ResponseCounter counter : container.getDistinctResponsesCounterList()) { - map.computeIfAbsent(counter.getFingerprint(), k -> new ArrayList<>()); - map.get(counter.getFingerprint()).add(container.getVector()); - } - } - - List ret = new ArrayList<>(); - for (var entry : map.entrySet()) { - ResponseFingerprint fingerprint = entry.getKey(); - List vectors = entry.getValue(); - if (vectors.size() <= mostOccurrences) { - for (Vector vector : vectors) { - ret.add(new VectorResponse(vector, fingerprint)); - } - } - } - return ret; - } - private List createPaddingVectorsLastByte(Integer paddingIvOffset) { List vectorList = new ArrayList<>(XOR_VALUES_LAST_BYTE.length); diff --git a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/result/sessionticket/TicketPaddingOracleResult.java b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/result/sessionticket/TicketPaddingOracleResult.java index bece4be54..afe122916 100644 --- a/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/result/sessionticket/TicketPaddingOracleResult.java +++ b/TLS-Server-Scanner/src/main/java/de/rub/nds/tlsscanner/serverscanner/probe/result/sessionticket/TicketPaddingOracleResult.java @@ -14,7 +14,6 @@ import de.rub.nds.tlsscanner.core.vector.statistics.InformationLeakTest; import de.rub.nds.tlsscanner.core.vector.statistics.TestInfo; import de.rub.nds.tlsscanner.serverscanner.leak.TicketPaddingOracleSecondByteTestInfo; -import de.rub.nds.tlsscanner.serverscanner.probe.SessionTicketPaddingOracleProbe; import de.rub.nds.tlsscanner.serverscanner.probe.sessionticket.vector.TicketPaddingOracleVectorLast; import de.rub.nds.tlsscanner.serverscanner.probe.sessionticket.vector.TicketPaddingOracleVectorSecond; import de.rub.nds.tlsscanner.serverscanner.probe.sessionticket.vector.TicketVector; @@ -76,8 +75,7 @@ public TicketPaddingOracleResult(TestResults overallResult) { private List getVectorsWithRareResponses( InformationLeakTest leakTest, Class vectorClass, int maxOccurences) { List ret = new ArrayList<>(); - for (VectorResponse response : - SessionTicketPaddingOracleProbe.getRareResponses(leakTest, maxOccurences)) { + for (VectorResponse response : leakTest.getRareResponses(maxOccurences)) { ret.add(vectorClass.cast(response.getVector())); } return ret;