X-Git-Url: http://plrg.eecs.uci.edu/git/?a=blobdiff_plain;ds=sidebyside;f=Code%2FProjects%2FSmartPlugDetector%2Fsrc%2Fmain%2Fjava%2Fedu%2Fuci%2Fiotproject%2FConversation.java;h=490dc4f53264ee40f9b3e7c3d995d9482902cbd0;hb=d4dfbf7b96d59119274182551139aade8e1db6ea;hp=a1008941b58308b9a7a08415e773069a2d7c3666;hpb=b375929cdbbaf664cf4e20bf27ac1957fea7336b;p=pingpong.git diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Conversation.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Conversation.java index a100894..490dc4f 100644 --- a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Conversation.java +++ b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Conversation.java @@ -1,8 +1,10 @@ package edu.uci.iotproject; +import edu.uci.iotproject.analysis.TcpConversationUtils; import edu.uci.iotproject.util.PcapPacketUtils; import org.pcap4j.core.PcapPacket; import org.pcap4j.packet.IpV4Packet; +import org.pcap4j.packet.Packet; import org.pcap4j.packet.TcpPacket; import java.util.*; @@ -47,6 +49,12 @@ public class Conversation { */ private final List mPackets; + /** + * If {@link #isTls()} is {@code true}, this list contains the subset of {@link #mPackets} which are TLS Application + * Data packets. + */ + private final List mTlsApplicationDataPackets; + /** * Contains the sequence numbers used thus far by the host that is considered the client in this * {@code Conversation}. @@ -61,13 +69,50 @@ public class Conversation { */ private final Set mSeqNumbersSrv; + /** + * List of SYN packets pertaining to this conversation. + */ + private final List mSynPackets; /** * List of pairs FINs and their corresponding ACKs associated with this conversation. */ - private List mFinPackets; + private final List mFinPackets; + + /** + * List of RST packets associated with this conversation. + */ + private final List mRstPackets; + + /** + * Boolean to mark the packet as Application Data based on the previous packet that reaches MTU + */ + private boolean mApplicationData; /* End instance properties */ + /** + * Factory method for creating a {@code Conversation} from a {@link PcapPacket}. + * @param pcapPacket The {@code PcapPacket} that wraps a TCP segment for which a {@code Conversation} is to be initiated. + * @param clientIsSrc If {@code true}, the source address and source port found in the IP datagram and TCP segment + * wrapped in the {@code PcapPacket} are regarded as pertaining to the client, and the destination + * address and destination port are regarded as pertaining to the server---and vice versa if set + * to {@code false}. + * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet. + */ + public static Conversation fromPcapPacket(PcapPacket pcapPacket, boolean clientIsSrc) { + IpV4Packet ipPacket = pcapPacket.get(IpV4Packet.class); + TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class); + String clientIp = clientIsSrc ? ipPacket.getHeader().getSrcAddr().getHostAddress() : + ipPacket.getHeader().getDstAddr().getHostAddress(); + String srvIp = clientIsSrc ? ipPacket.getHeader().getDstAddr().getHostAddress() : + ipPacket.getHeader().getSrcAddr().getHostAddress(); + int clientPort = clientIsSrc ? tcpPacket.getHeader().getSrcPort().valueAsInt() : + tcpPacket.getHeader().getDstPort().valueAsInt(); + int srvPort = clientIsSrc ? tcpPacket.getHeader().getDstPort().valueAsInt() : + tcpPacket.getHeader().getSrcPort().valueAsInt(); + return new Conversation(clientIp, clientPort, srvIp, srvPort); + } + /** * Constructs a new {@code Conversation}. * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation) @@ -82,11 +127,13 @@ public class Conversation { this.mServerIp = serverIp; this.mServerPort = serverPort; this.mPackets = new ArrayList<>(); - + this.mTlsApplicationDataPackets = new ArrayList<>(); this.mSeqNumbersClient = new HashSet<>(); this.mSeqNumbersSrv = new HashSet<>(); - + this.mSynPackets = new ArrayList<>(); this.mFinPackets = new ArrayList<>(); + this.mRstPackets = new ArrayList<>(); + this.mApplicationData = false; } /** @@ -109,6 +156,50 @@ public class Conversation { addSeqNumber(packet); // Finally add packet to list of packets pertaining to this conversation. mPackets.add(packet); + // Preserve order of packets in list: sort according to timestamp. + if (mPackets.size() > 1 && + mPackets.get(mPackets.size()-1).getTimestamp().isBefore(mPackets.get(mPackets.size()-2).getTimestamp())) { + Collections.sort(mPackets, (o1, o2) -> { + if (o1.getTimestamp().isBefore(o2.getTimestamp())) { return -1; } + else if (o2.getTimestamp().isBefore(o1.getTimestamp())) { return 1; } + else { return 0; } + }); + } + // If TLS, inspect packet to see if it's a TLS Application Data packet, and if so add it to the list of TLS + // Application Data packets. + if (isTls()) { + TcpPacket tcpPacket = packet.get(TcpPacket.class); + Packet tcpPayload = tcpPacket.getPayload(); + if (tcpPayload == null) { + return; + } + byte[] rawPayload = tcpPayload.getRawData(); + // The SSL record header is at the front of the payload and is 5 bytes long. + // The SSL record header type field (the first byte) is set to 23 if it is an Application Data packet. + if (rawPayload != null && rawPayload.length >= 5) { + if (rawPayload[0] == 23) { + mTlsApplicationDataPackets.add(packet); + // Consider the following packet a data packet if this packet's size == MTU size 1448 + if (rawPayload.length >= 1448) + mApplicationData = true; + } else if (rawPayload[0] == 20) { + // Do nothing for now - CHANGE_CIPHER_SPEC + } else if (rawPayload[0] == 21) { + // Do nothing for now - ALERT + } else if (rawPayload[0] == 22) { + // Do nothing for now - HANDSHAKE + } else { + // If it is TLS with payload, but rawPayload[0] != 23 + if (mApplicationData == true) { + // It is a continuation of the previous packet if the previous packet reaches MTU size 1448 and + // it is not either type 20, 21, or 22 + mTlsApplicationDataPackets.add(packet); + if (rawPayload.length < 1448) + mApplicationData = false; + } + } + } + } } /** @@ -121,6 +212,88 @@ public class Conversation { return Collections.unmodifiableList(mPackets); } + /** + * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list). + * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a + * duplicate by the return value being {@code false}. + * + * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet. + * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise. + */ + public boolean addSynPacket(PcapPacket synPacket) { + onAddPrecondition(synPacket); + final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class); + final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class); + if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) { + throw new IllegalArgumentException("Not a SYN packet."); + } + // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e., + // we want to discard retransmitted SYN packets. + if (mSynPackets.size() >= 2) { + return false; + } + // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same + // direction as the packet given in the argument. + boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> { + IpV4Packet pIp = p.get(IpV4Packet.class); + TcpPacket pTcp = p.get(TcpPacket.class); + boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress(). + equals(pIp.getHeader().getSrcAddr().getHostAddress()); + boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress(). + equals(pIp.getHeader().getDstAddr().getHostAddress()); + boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() == + pTcp.getHeader().getSrcPort().valueAsInt(); + boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() == + pTcp.getHeader().getDstPort().valueAsInt(); + return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch; + }); + if (matchingPrevSyn) { + return false; + } + // Update direction-dependent set of sequence numbers and record/log packet. + addSeqNumber(synPacket); + return mSynPackets.add(synPacket); + + /* + mSynPackets.stream().anyMatch(p -> { + IpV4Packet pIp = p.get(IpV4Packet.class); + TcpPacket pTcp = p.get(TcpPacket.class); + boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress(). + equals(pIp.getHeader().getSrcAddr().getHostAddress()); + boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress(). + equals(pIp.getHeader().getDstAddr().getHostAddress()); + boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() == + pTcp.getHeader().getSrcPort().valueAsInt(); + boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() == + pTcp.getHeader().getDstPort().value(); + + boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch; + + boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() == + pTcp.getHeader().getSequenceNumber(); + + if (fourTupleMatch && !seqNoMatch) { + // If the four tuple that identifies the conversation matches, but the sequence number is different, + // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence + // the given packet is NOT part of this conversation, even though the ip:port combinations are (by + // chance) selected such that they match this conversation. + throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " + + "(which is identified by the same four tuple as this conversation)"); + } + return fourTupleMatch && seqNoMatch; + }); + */ + } + + /** + * Get a list of SYN packets pertaining to this {@code Conversation}. + * The returned list is a read-only list. + * @return the list of SYN packets pertaining to this {@code Conversation}. + */ + public List getSynPackets() { + return Collections.unmodifiableList(mSynPackets); + } + /** * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation. * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation. @@ -128,6 +301,8 @@ public class Conversation { public void addFinPacket(PcapPacket finPacket) { // Precondition: verify that packet does indeed pertain to conversation. onAddPrecondition(finPacket); + // TODO: should call addSeqNumber here? + addSeqNumber(finPacket); mFinPackets.add(new FinAckPair(finPacket)); } @@ -162,6 +337,37 @@ public class Conversation { mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort)); } + /** + * Add a TCP segment for which the RST flag is set to this {@code Conversation}. + * @param packet A {@link PcapPacket} wrapping a TCP segment pertaining to this {@code Conversation} for which the + * RST flag is set. + */ + public void addRstPacket(PcapPacket packet) { + /* + * TODO: + * When now also keeping track of RST packets, should we also...? + * 1) Prevent later packets from being added once a RST segment has been added? + * 2) Extend 'isGracefullyShutdown()' to also consider RST segments, or add another method, 'isShutdown()' that + * both considers FIN/ACK (graceful) as well as RST (abrupt/"ungraceful") shutdown? + * 3) Should it be impossible to associate more than one RST segment with each Conversation? + */ + onAddPrecondition(packet); + TcpPacket tcpPacket = packet.get(TcpPacket.class); + if (tcpPacket == null || !tcpPacket.getHeader().getRst()) { + throw new IllegalArgumentException("not a RST packet"); + } + mRstPackets.add(packet); + } + + /** + * Get the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is set. + * @return the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is + * set. + */ + public List getRstPackets() { + return Collections.unmodifiableList(mRstPackets); + } + // ========================================================================================================= // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key // in a Map. @@ -242,7 +448,7 @@ public class Conversation { * @param packet The packet. * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise. */ - private boolean isRetransmission(PcapPacket packet) { + public boolean isRetransmission(PcapPacket packet) { // Extract sequence number. int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber(); switch (getDirection(packet)) { @@ -251,11 +457,53 @@ public class Conversation { case SERVER_TO_CLIENT: return mSeqNumbersSrv.contains(seqNo); default: - throw new RuntimeException(String.format("Unexpected value of enum '%s'", + throw new AssertionError(String.format("Unexpected value of enum '%s'", Direction.class.getSimpleName())); } } + /** + *

+ * Is this {@code Conversation} a TLS session? + *

+ * + * Note: the current implementation simply examines the port number(s) for 443; it does not verify if the + * application data is indeed encrypted. + * + * @return {@code true} if this {@code Conversation} is interpreted as a TLS session, {@code false} otherwise. + */ + public boolean isTls() { + /* + * TODO: + * - may want to change this to be "return mServerPort == 443 || mClientPort == 443;" in order to also detect + * TLS in those cases where it is not possible to correctly label who is the client and who is the server, + * i.e., when the trace does not contain the SYN/SYNACK exchange. + * - current implementation relies on the server using the conventional TLS port number; may instead want to + * inspect the first 4 bytes of each potential TLS packet to see if they match the SSL record header. + * + * 08/31/18: Added unconvetional TLS ports used by WeMo plugs and LiFX bulb. + * 09/20/18: Moved hardcoded ports to other class to allow other classes to query the set of TLS ports. + */ + return TcpConversationUtils.isTlsPort(mServerPort); + } + + /** + * If this {@code Conversation} is backing a TLS session (i.e., if the value of {@link #isTls()} is {@code true}), + * get the packets labeled as TLS Application Data packets. This is a subset of the full set of payload-carrying + * packets (as returned by {@link #getPackets()}). An exception is thrown if this method is invoked on a + * {@code Conversation} for which {@link #isTls()} returns {@code false}. + * + * @return A list containing exactly those packets that could be identified as TLS Application Data packets (through + * inspecting of the SSL record header). The list may be empty, if no TLS application data packets have been + * recorded for this {@code Conversation}. + */ + public List getTlsApplicationDataPackets() { + if (!isTls()) { + throw new NoSuchElementException("cannot get TLS Application Data packets for non-TLS TCP conversation"); + } + return Collections.unmodifiableList(mTlsApplicationDataPackets); + } + /** * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by * analyzing the direction of the packet. @@ -280,17 +528,19 @@ public class Conversation { mSeqNumbersSrv.add(seqNo); break; default: - throw new RuntimeException(String.format("Unexpected value of enum '%s'", + throw new AssertionError(String.format("Unexpected value of enum '%s'", Direction.class.getSimpleName())); } } /** - * Determine the direction of {@code packet}. + * Determine the direction of {@code packet}. An {@link IllegalArgumentException} is thrown if {@code packet} does + * not pertain to this conversation. + * * @param packet The packet whose direction is to be determined. * @return A {@link Direction} indicating the direction of the packet. */ - private Direction getDirection(PcapPacket packet) { + public Direction getDirection(PcapPacket packet) { IpV4Packet ipPacket = packet.get(IpV4Packet.class); String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress(); String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress(); @@ -309,8 +559,27 @@ public class Conversation { /** * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}. */ - private enum Direction { - CLIENT_TO_SERVER, SERVER_TO_CLIENT + public enum Direction { + + CLIENT_TO_SERVER { + @Override + public String toCompactString() { + return "*"; + } + }, + SERVER_TO_CLIENT { + @Override + public String toCompactString() { + return ""; + } + }; + + /** + * Get a compact string representation of this {@code Direction}. + * @return a compact string representation of this {@code Direction}. + */ + abstract public String toCompactString(); + } -} \ No newline at end of file +}