1 package edu.uci.iotproject;
3 import static edu.uci.iotproject.analysis.UserAction.Type;
5 import edu.uci.iotproject.analysis.*;
6 import edu.uci.iotproject.io.PrintWriterUtils;
7 import edu.uci.iotproject.io.TriggerTimesFileReader;
8 import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
9 import edu.uci.iotproject.trafficreassembly.layer3.TcpReassembler;
10 import edu.uci.iotproject.util.PcapPacketUtils;
11 import edu.uci.iotproject.util.PrintUtils;
12 import org.apache.commons.math3.stat.clustering.Cluster;
13 import org.apache.commons.math3.stat.clustering.DBSCANClusterer;
14 import org.pcap4j.core.*;
15 import org.pcap4j.packet.namednumber.DataLinkType;
18 import java.net.UnknownHostException;
19 import java.time.Duration;
20 import java.time.Instant;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
27 * This is a system that reads PCAP files to compare
28 * patterns of DNS hostnames, packet sequences, and packet
29 * lengths with training data to determine certain events
30 * or actions for smart home devices.
32 * @author Janus Varmarken
33 * @author Rahmadi Trimananda (rtrimana@uci.edu)
36 public class SignatureGenerator {
39 * If set to {@code true}, output written to the results file is also dumped to standard out.
41 private static boolean DUPLICATE_OUTPUT_TO_STD_OUT = true;
43 * File name for logging.
45 private static String LOG_EXTENSION = "_signature-generation.log";
47 * Directory for logging.
49 private static String LOG_DIRECTORY = "./";
51 public static void main(String[] args) throws PcapNativeException, NotOpenException, EOFException,
52 TimeoutException, UnknownHostException, IOException {
53 // -------------------------------------------------------------------------------------------------------------
54 // ------------ # Code for extracting traffic generated by a device within x seconds of a trigger # ------------
55 if (args.length < 11) {
56 String errMsg = String.format("Usage: %s inputPcapFile outputPcapFile triggerTimesFile deviceIp" +
57 " onSignatureFile offSignatureFile onClusterAnalysisFile offClusterAnalysisFile epsilon" +
58 " deletedSequencesOn deletedSequencesOff" +
59 "\n inputPcapFile: the target of the detection" +
60 "\n outputPcapFile: the processed PCAP file through 15-second window filtering" +
61 "\n triggerTimesFile: the trigger timestamps" +
62 "\n deviceIp: the IP address of the device we want to generate a signature for" +
63 "\n onSignatureFile: name of the ON signature file" +
64 "\n offSignatureFile: name of the OFF signature file" +
65 "\n onClusterAnalysisFile: name of the ON signature cluster analysis file" +
66 "\n offClusterAnalysisFile: name of the OFF signature cluster analysis file" +
67 "\n epsilon: epsilon value of the DBSCAN algorithm" +
68 "\n deletedSequencesOn: sequences to be deleted from the final ON signature" +
69 " (please separate with commas, e.g., 0,1,2, or put '-1' if not needed)" +
70 "\n deletedSequencesOff: sequences to be deleted from the final OFF signature" +
71 " (please separate with commas, e.g., 0,1,2, or put '-1' if not needed)",
72 SignatureGenerator.class.getSimpleName());
73 System.out.println(errMsg);
76 boolean verbose = true;
77 final String inputPcapFile = args[0];
78 final String outputPcapFile = args[1];
79 final String triggerTimesFile = args[2];
80 final String deviceIp = args[3];
81 final String onSignatureFile = args[4];
82 final String offSignatureFile = args[5];
83 final String onClusterAnalysisFile = args[6];
84 final String offClusterAnalysisFile = args[7];
85 final double eps = Double.parseDouble(args[8]);
86 final String deletedSequencesOn = args[9];
87 final String deletedSequencesOff = args[10];
88 final String logFile = inputPcapFile + LOG_EXTENSION;
90 // Prepare file outputter.
91 File outputFile = new File(logFile);
92 outputFile.getParentFile().mkdirs();
93 final PrintWriter resultsWriter = new PrintWriter(new FileWriter(outputFile));
95 // =========================================== TRAFFIC FILTERING ============================================
97 TriggerTimesFileReader ttfr = new TriggerTimesFileReader();
98 List<Instant> triggerTimes = ttfr.readTriggerTimes(triggerTimesFile, false);
99 // Tag each trigger with "ON" or "OFF", assuming that the first trigger is an "ON" and that they alternate.
100 List<UserAction> userActions = new ArrayList<>();
101 for (int i = 0; i < triggerTimes.size(); i++) {
102 userActions.add(new UserAction(i % 2 == 0 ? Type.TOGGLE_ON : Type.TOGGLE_OFF, triggerTimes.get(i)));
104 TriggerTrafficExtractor tte = new TriggerTrafficExtractor(inputPcapFile, triggerTimes, deviceIp);
105 final PcapDumper outputter = Pcaps.openDead(DataLinkType.EN10MB, 65536).dumpOpen(outputPcapFile);
106 DnsMap dnsMap = new DnsMap();
107 TcpReassembler tcpReassembler = new TcpReassembler();
108 TrafficLabeler trafficLabeler = new TrafficLabeler(userActions);
109 tte.performExtraction(pkt -> {
112 } catch (NotOpenException e) {
115 }, dnsMap, tcpReassembler, trafficLabeler);
119 if (tte.getPacketsIncludedCount() != trafficLabeler.getTotalPacketCount()) {
120 // Sanity/debug check
121 throw new AssertionError(String.format("mismatch between packet count in %s and %s",
122 TriggerTrafficExtractor.class.getSimpleName(), TrafficLabeler.class.getSimpleName()));
125 // Extract all conversations present in the filtered trace.
126 List<Conversation> allConversations = tcpReassembler.getTcpConversations();
127 // Group conversations by hostname.
128 Map<String, List<Conversation>> convsByHostname =
129 TcpConversationUtils.groupConversationsByHostname(allConversations, dnsMap);
130 PrintWriterUtils.println("Grouped conversations by hostname.", resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
131 // For each hostname, count the frequencies of packet lengths exchanged with that hostname.
132 final Map<String, Map<Integer, Integer>> pktLenFreqsByHostname = new HashMap<>();
133 convsByHostname.forEach((host, convs) -> pktLenFreqsByHostname.put(host,
134 TcpConversationUtils.countPacketLengthFrequencies(convs)));
135 PrintWriterUtils.println("Counted frequencies of packet lengths exchanged with each hostname.",
136 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
137 // For each hostname, count the frequencies of packet sequences (i.e., count how many
138 // conversations exchange a sequence of packets of some specific lengths).
139 final Map<String, Map<String, Integer>> pktSeqFreqsByHostname = new HashMap<>();
140 convsByHostname.forEach((host, convs) -> pktSeqFreqsByHostname.put(host,
141 TcpConversationUtils.countPacketSequenceFrequencies(convs)));
142 PrintWriterUtils.println("Counted frequencies of packet sequences exchanged with each hostname.",
143 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
144 // For each hostname, count frequencies of packet pairs exchanged
145 // with that hostname across all conversations
146 final Map<String, Map<String, Integer>> pktPairFreqsByHostname =
147 TcpConversationUtils.countPacketPairFrequenciesByHostname(allConversations, dnsMap);
148 PrintWriterUtils.println("Counted frequencies of packet pairs per hostname.",
149 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
150 // For each user action, reassemble the set of TCP connections occurring shortly after
151 final Map<UserAction, List<Conversation>> userActionToConversations =
152 trafficLabeler.getLabeledReassembledTcpTraffic();
153 final Map<UserAction, Map<String, List<Conversation>>> userActionsToConvsByHostname =
154 trafficLabeler.getLabeledReassembledTcpTraffic(dnsMap);
155 PrintWriterUtils.println("Reassembled TCP conversations occurring shortly after each user event.",
156 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
159 * NOTE: no need to generate these more complex on/off maps that also contain mappings from hostname and
160 * sequence identifiers as we do not care about hostnames and sequences during clustering.
161 * We can simply use the UserAction->List<Conversation> map to generate ON/OFF groupings of conversations.
163 // Contains all ON events: hostname -> sequence identifier -> list of conversations with that sequence
164 Map<String, Map<String, List<Conversation>>> ons = new HashMap<>();
165 // Contains all OFF events: hostname -> sequence identifier -> list of conversations with that sequence
166 Map<String, Map<String, List<Conversation>>> offs = new HashMap<>();
167 userActionsToConvsByHostname.forEach((ua, hostnameToConvs) -> {
168 Map<String, Map<String, List<Conversation>>> outer = ua.getType() == Type.TOGGLE_ON ? ons : offs;
169 hostnameToConvs.forEach((host, convs) -> {
170 Map<String, List<Conversation>> seqsToConvs = TcpConversationUtils.
171 groupConversationsByPacketSequence(convs, verbose);
172 outer.merge(host, seqsToConvs, (oldMap, newMap) -> {
173 newMap.forEach((sequence, cs) -> oldMap.merge(sequence, cs, (list1, list2) -> {
182 // ============================================== PAIR CLUSTERING ============================================
183 // TODO: No need to use the more convoluted on/off maps; Can simply use the UserAction->List<Conversation> map
184 // TODO: when don't care about hostnames and sequences (see comment earlier).
185 // ===========================================================================================================
186 List<Conversation> onConversations = userActionToConversations.entrySet().stream().
187 filter(e -> e.getKey().getType() == Type.TOGGLE_ON). // drop all OFF events from stream
188 map(e -> e.getValue()). // no longer interested in the UserActions
189 flatMap(List::stream). // flatten List<List<T>> to a List<T>
190 collect(Collectors.toList());
191 List<Conversation> offConversations = userActionToConversations.entrySet().stream().
192 filter(e -> e.getKey().getType() == Type.TOGGLE_OFF).
193 map(e -> e.getValue()).
194 flatMap(List::stream).
195 collect(Collectors.toList());
196 //Collections.sort(onConversations, (c1, c2) -> c1.getPackets().)
198 List<PcapPacketPair> onPairs = onConversations.stream().
199 map(c -> c.isTls() ? TcpConversationUtils.extractTlsAppDataPacketPairs(c) :
200 TcpConversationUtils.extractPacketPairs(c)).
201 flatMap(List::stream). // flatten List<List<>> to List<>
202 collect(Collectors.toList());
203 List<PcapPacketPair> offPairs = offConversations.stream().
204 map(c -> c.isTls() ? TcpConversationUtils.extractTlsAppDataPacketPairs(c) :
205 TcpConversationUtils.extractPacketPairs(c)).
206 flatMap(List::stream). // flatten List<List<>> to List<>
207 collect(Collectors.toList());
208 // Note: need to update the DnsMap of all PcapPacketPairs if we want to use the IP/hostname-sensitive distance.
209 Stream.concat(Stream.of(onPairs), Stream.of(offPairs)).flatMap(List::stream).forEach(p -> p.setDnsMap(dnsMap));
210 // Perform clustering on conversation logged as part of all ON events.
211 // Calculate number of events per type (only ON/only OFF), which means half of the number of all timestamps.
212 int numberOfEventsPerType = triggerTimes.size() / 2;
213 int lowerBound = numberOfEventsPerType - (int)(numberOfEventsPerType * 0.1);
214 int upperBound = numberOfEventsPerType + (int)(numberOfEventsPerType * 0.1);
215 int minPts = lowerBound;
216 DBSCANClusterer<PcapPacketPair> onClusterer = new DBSCANClusterer<>(eps, minPts);
217 List<Cluster<PcapPacketPair>> onClusters = onClusterer.cluster(onPairs);
218 // Perform clustering on conversation logged as part of all OFF events.
219 DBSCANClusterer<PcapPacketPair> offClusterer = new DBSCANClusterer<>(eps, minPts);
220 List<Cluster<PcapPacketPair>> offClusters = offClusterer.cluster(offPairs);
221 // Sort the conversations as reference
222 List<Conversation> sortedAllConversation = TcpConversationUtils.sortConversationList(allConversations);
224 PrintWriterUtils.println("========================================", resultsWriter,
225 DUPLICATE_OUTPUT_TO_STD_OUT);
226 PrintWriterUtils.println(" Clustering results for ON ", resultsWriter,
227 DUPLICATE_OUTPUT_TO_STD_OUT);
228 PrintWriterUtils.println(" Number of clusters: " + onClusters.size(), resultsWriter,
229 DUPLICATE_OUTPUT_TO_STD_OUT);
231 List<List<List<PcapPacket>>> ppListOfListReadOn = new ArrayList<>();
232 List<List<List<PcapPacket>>> ppListOfListListOn = new ArrayList<>();
233 List<List<List<PcapPacket>>> corePointRangeSignatureOn = new ArrayList<>();
234 for (Cluster<PcapPacketPair> c : onClusters) {
235 PrintWriterUtils.println(String.format("<<< Cluster #%02d (%03d points) >>>", ++count, c.getPoints().size()),
236 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
237 PrintWriterUtils.println(PrintUtils.toSummaryString(c), resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
238 if(c.getPoints().size() > lowerBound && c.getPoints().size() < upperBound) {
240 List<List<PcapPacket>> ppListOfList = PcapPacketUtils.clusterToListOfPcapPackets(c);
241 // Check for overlaps and decide whether to do range-based or conservative checking
242 corePointRangeSignatureOn.add(PcapPacketUtils.extractRangeCorePoints(ppListOfList, eps, minPts));
243 ppListOfListListOn.add(ppListOfList);
246 PrintWriterUtils.println("========================================", resultsWriter,
247 DUPLICATE_OUTPUT_TO_STD_OUT);
248 PrintWriterUtils.println(" Clustering results for OFF ", resultsWriter,
249 DUPLICATE_OUTPUT_TO_STD_OUT);
250 PrintWriterUtils.println(" Number of clusters: " + offClusters.size(), resultsWriter,
251 DUPLICATE_OUTPUT_TO_STD_OUT);
253 List<List<List<PcapPacket>>> ppListOfListReadOff = new ArrayList<>();
254 List<List<List<PcapPacket>>> ppListOfListListOff = new ArrayList<>();
255 List<List<List<PcapPacket>>> corePointRangeSignatureOff = new ArrayList<>();
256 for (Cluster<PcapPacketPair> c : offClusters) {
257 PrintWriterUtils.println(String.format("<<< Cluster #%03d (%06d points) >>>", ++count, c.getPoints().size()),
258 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
259 PrintWriterUtils.println(PrintUtils.toSummaryString(c), resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
260 if(c.getPoints().size() > lowerBound && c.getPoints().size() < upperBound) {
262 List<List<PcapPacket>> ppListOfList = PcapPacketUtils.clusterToListOfPcapPackets(c);
263 // Check for overlaps and decide whether to do range-based or conservative checking
264 corePointRangeSignatureOff.add(PcapPacketUtils.extractRangeCorePoints(ppListOfList, eps, minPts));
265 ppListOfListListOff.add(ppListOfList);
269 // =========================================== SIGNATURE CREATION ===========================================
271 ppListOfListListOn = PcapPacketUtils.concatSequences(ppListOfListListOn, sortedAllConversation);
272 // Remove sequences in the list that have overlap
273 StringTokenizer stringTokenizerOn = new StringTokenizer(deletedSequencesOn, ",");
274 while(stringTokenizerOn.hasMoreTokens()) {
275 int sequenceToDelete = Integer.parseInt(stringTokenizerOn.nextToken());
276 if (sequenceToDelete == -1) { // '-1' means there is no removal
279 PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, sequenceToDelete);
281 ppListOfListListOn = PcapPacketUtils.sortSequences(ppListOfListListOn);
284 ppListOfListListOff = PcapPacketUtils.concatSequences(ppListOfListListOff, sortedAllConversation);
285 // Remove sequences in the list that have overlap
286 StringTokenizer stringTokenizerOff = new StringTokenizer(deletedSequencesOff, ",");
287 while(stringTokenizerOff.hasMoreTokens()) {
288 int sequenceToDelete = Integer.parseInt(stringTokenizerOff.nextToken());
289 if (sequenceToDelete == -1) { // '-1' means there is no removal
292 PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, sequenceToDelete);
294 ppListOfListListOff = PcapPacketUtils.sortSequences(ppListOfListListOff);
295 // Write the signatures into the screen
296 PrintWriterUtils.println("========================================", resultsWriter,
297 DUPLICATE_OUTPUT_TO_STD_OUT);
298 PrintWriterUtils.println(" ON Signature ", resultsWriter,
299 DUPLICATE_OUTPUT_TO_STD_OUT);
300 PrintWriterUtils.println("========================================", resultsWriter,
301 DUPLICATE_OUTPUT_TO_STD_OUT);
302 PcapPacketUtils.printSignatures(ppListOfListListOn, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
303 PrintWriterUtils.println("========================================", resultsWriter,
304 DUPLICATE_OUTPUT_TO_STD_OUT);
305 PrintWriterUtils.println(" OFF Signature ", resultsWriter,
306 DUPLICATE_OUTPUT_TO_STD_OUT);
307 PrintWriterUtils.println("========================================", resultsWriter,
308 DUPLICATE_OUTPUT_TO_STD_OUT);
309 PcapPacketUtils.printSignatures(ppListOfListListOff, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
310 // Printing signatures into files
311 PrintUtils.serializeIntoFile(onSignatureFile, ppListOfListListOn);
312 PrintUtils.serializeIntoFile(offSignatureFile, ppListOfListListOff);
313 // Printing cluster analyses into files
314 PrintUtils.serializeIntoFile(onClusterAnalysisFile, corePointRangeSignatureOn);
315 PrintUtils.serializeIntoFile(offClusterAnalysisFile, corePointRangeSignatureOff);
317 // =========================================== SIGNATURE DURATION ===========================================
318 List<Instant> firstSignatureTimestamps = new ArrayList<>();
319 List<Instant> lastSignatureTimestamps = new ArrayList<>();
320 if (!ppListOfListListOn.isEmpty()) {
321 List<List<PcapPacket>> firstListOnSign = ppListOfListListOn.get(0);
322 List<List<PcapPacket>> lastListOnSign = ppListOfListListOn.get(ppListOfListListOn.size() - 1);
323 // Load ON signature first and last packet's timestamps
324 for (List<PcapPacket> list : firstListOnSign) {
325 // Get timestamp Instant from the last packet
326 firstSignatureTimestamps.add(list.get(0).getTimestamp());
328 for (List<PcapPacket> list : lastListOnSign) {
329 // Get timestamp Instant from the last packet
330 int lastPacketIndex = list.size() - 1;
331 lastSignatureTimestamps.add(list.get(lastPacketIndex).getTimestamp());
335 if (!ppListOfListListOn.isEmpty()) {
336 List<List<PcapPacket>> firstListOffSign = ppListOfListListOff.get(0);
337 List<List<PcapPacket>> lastListOffSign = ppListOfListListOff.get(ppListOfListListOff.size() - 1);
338 // Load OFF signature first and last packet's timestamps
339 for (List<PcapPacket> list : firstListOffSign) {
340 // Get timestamp Instant from the last packet
341 firstSignatureTimestamps.add(list.get(0).getTimestamp());
343 for (List<PcapPacket> list : lastListOffSign) {
344 // Get timestamp Instant from the last packet
345 int lastPacketIndex = list.size() - 1;
346 lastSignatureTimestamps.add(list.get(lastPacketIndex).getTimestamp());
349 // Sort the timestamps
350 firstSignatureTimestamps.sort(Comparator.comparing(Instant::toEpochMilli));
351 lastSignatureTimestamps.sort(Comparator.comparing(Instant::toEpochMilli));
353 Iterator<Instant> iterFirst = firstSignatureTimestamps.iterator();
354 Iterator<Instant> iterLast = lastSignatureTimestamps.iterator();
356 long maxDuration = Long.MIN_VALUE;
357 PrintWriterUtils.println("========================================", resultsWriter,
358 DUPLICATE_OUTPUT_TO_STD_OUT);
359 PrintWriterUtils.println(" Signature Durations ", resultsWriter,
360 DUPLICATE_OUTPUT_TO_STD_OUT);
361 PrintWriterUtils.println("========================================", resultsWriter,
362 DUPLICATE_OUTPUT_TO_STD_OUT);
363 while (iterFirst.hasNext() && iterLast.hasNext()) {
364 Instant firstInst = iterFirst.next();
365 Instant lastInst = iterLast.next();
366 Duration dur = Duration.between(firstInst, lastInst);
367 duration = dur.toMillis();
368 // Check duration --- should be below 15 seconds
369 if (duration > TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS) {
370 while (duration > TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS && iterFirst.hasNext()) {
371 // that means we have to move to the next trigger
372 firstInst = iterFirst.next();
373 dur = Duration.between(firstInst, lastInst);
374 duration = dur.toMillis();
376 } else { // Below 0/Negative --- that means we have to move to the next signature
377 while (duration < 0 && iterLast.hasNext()) {
378 // that means we have to move to the next trigger
379 lastInst = iterLast.next();
380 dur = Duration.between(firstInst, lastInst);
381 duration = dur.toMillis();
384 PrintWriterUtils.println(duration, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
385 // Update duration if this bigger than the max value and still less than the window inclusion time
386 maxDuration = maxDuration < duration && duration <= TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS ?
387 duration : maxDuration;
389 // Just assign the value 0 if there is no signature
390 if (maxDuration == Long.MIN_VALUE) {
393 PrintWriterUtils.println("========================================", resultsWriter,
394 DUPLICATE_OUTPUT_TO_STD_OUT);
395 PrintWriterUtils.println("Max signature duration: " + maxDuration, resultsWriter,
396 DUPLICATE_OUTPUT_TO_STD_OUT);
397 PrintWriterUtils.println("========================================", resultsWriter,
398 DUPLICATE_OUTPUT_TO_STD_OUT);
399 resultsWriter.flush();
400 resultsWriter.close();
401 // ==========================================================================================================