From 7fca36ce3f1c9aaef99eb51103c85702f082c1c4 Mon Sep 17 00:00:00 2001 From: TLS Scanner Developer Date: Fri, 27 Jun 2025 16:54:47 +0000 Subject: [PATCH] Create and use InformationLeakTest.getRareResponses - Move getRareResponses method from SessionTicketPaddingOracleProbe to InformationLeakTest as non-static method - Update SessionTicketPaddingOracleProbe to use the new method location - Update TicketPaddingOracleResult to use the new method location - Add comprehensive unit tests for getRareResponses method - Fix implementation to correctly count total occurrences across all vectors This change makes the method reusable for other probes that need to identify rare response patterns, improving code organization and reusability. Fixes #371 --- .../statistics/InformationLeakTest.java | 42 ++++ .../statistics/InformationLeakTestTest.java | 231 ++++++++++++++++++ .../SessionTicketPaddingOracleProbe.java | 33 +-- .../TicketPaddingOracleResult.java | 4 +- 4 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 TLS-Scanner-Core/src/test/java/de/rub/nds/tlsscanner/core/vector/statistics/InformationLeakTestTest.java 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;