1 package edu.uci.iotproject.detection;
3 import edu.uci.iotproject.Conversation;
4 import edu.uci.iotproject.TcpReassembler;
5 import edu.uci.iotproject.analysis.TcpConversationUtils;
6 import org.pcap4j.core.PacketListener;
7 import org.pcap4j.core.PcapPacket;
9 import java.util.Arrays;
10 import java.util.List;
11 import java.util.Objects;
13 import static edu.uci.iotproject.util.PcapPacketUtils.*;
16 * TODO add class documentation.
18 * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
19 * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
21 public class SignatureDetector implements PacketListener {
24 * The signature that this {@link SignatureDetector} is trying to detect in the observed traffic.
26 private final List<List<PcapPacket>> mSignature;
29 * The directions of packets in the sequence that make up {@link #mSignature}.
31 private final Conversation.Direction[] mSignatureDirections;
34 * For reassembling the observed traffic into TCP connections.
36 private final TcpReassembler mTcpReassembler = new TcpReassembler();
39 * IP of the router's WAN port (if analyzed traffic is captured at the ISP's point of view).
41 private final String mRouterWanIp;
43 public SignatureDetector(List<List<PcapPacket>> signature, String routerWanIp) {
44 Objects.requireNonNull(signature, "signature cannot be null");
45 if (signature.isEmpty() || signature.stream().anyMatch(inner -> inner.isEmpty())) {
46 throw new IllegalArgumentException("signature is empty (or contains empty inner List)");
48 mSignature = signature;
49 mRouterWanIp = routerWanIp;
50 // Build the direction sequence.
51 // Note: assumes that the provided signature was captured within the local network (routerWanIp is set to null).
52 mSignatureDirections = getPacketDirections(mSignature.get(0), null);
54 * Enforce restriction on cluster/signature members: all representatives must exhibit the same direction pattern
55 * and contain the same number of packets. Note that this is a somewhat heavy operation, so it may be disabled
56 * later on in favor of performance. However, it is only run once (at instantiation), so the overhead may be
57 * warranted in order to ensure correctness, especially during the development/debugging phase.
59 if (mSignature.stream().
60 anyMatch(inner -> !Arrays.equals(mSignatureDirections, getPacketDirections(inner, null)))) {
61 throw new IllegalArgumentException(
62 "signature members must contain the same number of packets and exhibit the same packet direction " +
69 public void gotPacket(PcapPacket packet) {
70 // Present packet to TCP reassembler so that it can be mapped to a connection (if it is a TCP packet).
71 mTcpReassembler.gotPacket(packet);
75 private void performDetection() {
76 // Let's start out simple by building a version that only works for signatures that do not span across multiple
77 // TCP conversations...
78 for (Conversation c : mTcpReassembler.getTcpConversations()) {
79 for (List<PcapPacket> sequence : mSignature) {
80 boolean matchFound = isSequenceInConversation(sequence, c);
82 onSequenceDetected(sequence, c);
83 // Found signature in current conversation, so break inner loop and continue with next conversation.
89 * if no item in cluster matches, also perform a distance-based matching to cover those cases where we did
90 * not manage to capture every single mutation of the sequence during training.
92 * Need to compute average/centroid of cluster to do so...? Compute within-cluster variance, then check if
93 * distance between input conversation and cluster average/centroid is smaller than or equal to the computed
99 private void onSequenceDetected(List<PcapPacket> sequence, Conversation c) {
100 // TODO implement whatever output we want, e.g., print to std.out or notify observer objects
106 * Examine if a {@link Conversation} contains a given sequence of packets. Note: the current implementation actually
107 * searches for a substring as it does not allow for interleaved packets in {@code c} that are not in
108 * {@code sequence}; for example, if {@code sequence} consists of packet lengths [2, 3, 5] and {@code c} consists of
109 * packet lengths [2, 3, 4, 5], the result will be {@code false}. If we are to allow interleaved packets, we need
110 * a modified version of <a href="https://stackoverflow.com/a/20545604/1214974">this</a>.
111 * @param sequence The sequence to look for.
112 * @param c The {@link Conversation} to search for {@code sequence} in.
113 * @return {@code true} if {@code c} contains {@code sequence}, {@code false} otherwise.
115 private boolean isSequenceInConversation(List<PcapPacket> sequence, Conversation c) {
116 // The packets we match against differ depending on whether the signature is a TLS or non-TLS signature.
117 boolean tlsSequence = isTlsSequence(sequence);
118 if (tlsSequence && !c.isTls()) {
119 // If we're looking for a TLS signature and this conversation does not appear to be a TLS conversation, we
120 // are done. Note: this assumes that they do NOT start performing TLS on new ports that are not captured in
121 // Conversation.isTls()
124 // Based on TLS or non-TLS signature, fetch the corresponding list of packets to match against.
125 List<PcapPacket> packets = tlsSequence ? c.getTlsApplicationDataPackets() : c.getPackets();
126 // If sequence is longer than the conversation, it can obviously not be contained in the conversation.
127 if (packets.size() < sequence.size()) {
131 * Generate packet direction array for c. We have already generated the packet direction array for sequence as
132 * part of the constructor (mSignatureDirections).
134 Conversation.Direction[] cDirections = getPacketDirections(packets, mRouterWanIp);
137 while (convIdx < packets.size()) {
138 PcapPacket seqPkt = sequence.get(seqIdx);
139 PcapPacket convPkt = packets.get(convIdx);
140 // We only have a match if packet lengths and directions match.
141 if (convPkt.getOriginalLength() == seqPkt.getOriginalLength() &&
142 mSignatureDirections[seqIdx] == cDirections[convIdx]) {
143 // A match, advance both indices to consider next packet in sequence vs. next packet in conversation
146 if (seqIdx == sequence.size()) {
147 // we managed to match the full sequence in the conversation.
154 * If we managed to match parts of sequence, we restart the search for sequence in c at the index of
155 * c where the current mismatch occurred. I.e., we must reset seqIdx, but leave convIdx untouched.
160 * First packet of sequence didn't match packet at convIdx of conversation, so we move forward in
161 * conversation, i.e., we continue the search for sequence in c starting at index convIdx+1 of c.
170 private boolean isTlsSequence(List<PcapPacket> sequence) {
171 // NOTE: Assumes ALL packets in sequence pertain to the same TCP connection!
172 PcapPacket firstPkt = sequence.get(0);
173 int srcPort = getSourcePort(firstPkt);
174 int dstPort = getDestinationPort(firstPkt);
175 return TcpConversationUtils.isTlsPort(srcPort) || TcpConversationUtils.isTlsPort(dstPort);
179 * Given a {@code List<PcapPacket>}, generate a {@code Conversation.Direction[]} such that each entry in the
180 * resulting {@code Conversation.Direction[]} specifies the direction of the {@link PcapPacket} at the corresponding
181 * index in the input list.
182 * @param packets The list of packets for which to construct a corresponding array of packet directions.
183 * @param routerWanIp The IP of the router's WAN port. This is used for determining the direction of packets when
184 * the traffic is captured just outside the local network (at the ISP side of the router). Set to
185 * {@code null} if {@code packets} stem from traffic captured within the local network.
186 * @return A {@code Conversation.Direction[]} specifying the direction of the {@link PcapPacket} at the
187 * corresponding index in {@code packets}.
189 private static Conversation.Direction[] getPacketDirections(List<PcapPacket> packets, String routerWanIp) {
190 Conversation.Direction[] directions = new Conversation.Direction[packets.size()];
191 for (int i = 0; i < packets.size(); i++) {
192 PcapPacket pkt = packets.get(i);
193 if (getSourceIp(pkt).equals(getDestinationIp(pkt))) {
194 // Sanity check: we shouldn't be processing loopback traffic
195 throw new AssertionError("loopback traffic detected");
197 if (isSrcIpLocal(pkt) || getSourceIp(pkt).equals(routerWanIp)) {
198 directions[i] = Conversation.Direction.CLIENT_TO_SERVER;
199 } else if (isDstIpLocal(pkt) || getDestinationIp(pkt).equals(routerWanIp)) {
200 directions[i] = Conversation.Direction.SERVER_TO_CLIENT;
202 throw new IllegalArgumentException("no local IP or router WAN port IP found, can't detect direction");