2014年10月17日 星期五

透過 Java 讀取、分析封包攔截記錄 (1):使用 jNetPcap 函式庫讀取封包擷取記錄

如果想要在電腦上攔截網卡發出的封包,一般直覺會想到的就是 Wireshark 這個軟體
它在攔截了一些封包之後,可以透過另存新檔的方式,將攔截下來的內容儲存成一個檔案(*.pcap)
接著還想用程式化的方式自動分析封包的話,在 Java 上可以使用 jNetPcap 函式庫 [1] 來分析攔截下來的封包內容了。

從 jNetPcap 官方網站 [1] 上下載了二進位檔之後,解開會看到裡面除了 jar 以外還有其他檔案
其中在 Windows 平台上必須將 jnetpcap.dll 複製到系統路徑裡,例如複製到 C:\Windows\System32 資料夾內
此外,電腦上還必須有安裝 Wireshark(不過...或許實際需要安裝的是 WinPcap 而不是整個 Wireshark 吧?)
否則執行時可能會出現類似以下的錯誤訊息:

java.lang.UnsatisfiedLinkError: C:\Windows\System32\jnetpcap.dll: Can't find dependent libraries
 at java.lang.ClassLoader$NativeLibrary.load(Native Method)
 at java.lang.ClassLoader.loadLibrary1(ClassLoader.java:1965)
 at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1890)
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1880)
 at java.lang.Runtime.loadLibrary0(Runtime.java:849)
 at java.lang.System.loadLibrary(System.java:1088)
 at org.jnetpcap.nio.JMemory.<clinit>(Unknown Source)
 at PcapTest.sniffer(PcapTest.java:97)
 at Sniffer.main(Sniffer.java:10)

接著,先手動產生一個 *.pcap 的檔案,內容需要攔截到想透過程式處理的封包
在本篇的範例中,目標是攔截 SMTP 協定的封包,並且把從本機透過任意郵件軟體寄出去的信的附件全部另存新檔。

以下是程式碼。程式碼主要的概念是在掃 pcap 檔的過程中,如果遇到 TCP 協定的封包、且封包的目的地是 25 port,就會對該封包進行內容複製
把內容暫存到一個 Map 上,Map 的 key 是封包的 sequence number。
同時因為封包的 sequence number 會變,因此每次處理完一個封包,都會將該內容對應的 sequence number 更新為下一個封包的 sequence number
以確保內容在眾多封包夾雜的狀態下,仍然可以正確地把一連串的封包內容都串連起來,組回完整且正確的內容。(封包的串接原理可以參考 [2])

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jnetpcap.Pcap;
import org.jnetpcap.packet.PcapPacket;
import org.jnetpcap.packet.PcapPacketHandler;
import org.jnetpcap.protocol.tcpip.Tcp;

public class PcapTest {
  private static Logger log = LogManager.getLogger("PcapTest");
  
  public static void main(String[] args) {
    try {
      Path pcapFilePath = Paths.get("D:", "test", "manyFile.pcap");
      log.debug("Source file: " + pcapFilePath.toString());
      
      Path outputPath = Paths.get("D:", "test", "output");
      log.debug("Output path: " + outputPath.toString());
      
      PcapTest test = new PcapTest();
      test.capture(pcapFilePath, outputPath);
    } catch (Exception | Error e) {
      e.printStackTrace();
    }
  }

  /**
   * Capture the bytes from a specified pcap file, and stores the attachments to the specified output folder.
   * @param pcapPath Path of the pcap file.
   * @param outputFolder Folder for storing the attachments.
   */
  public void capture (Path pcapPath, Path outputFolder) {
    /***************************************************************************
     * First we setup error buffer and name for our file
     **************************************************************************/
    // For any error
    final StringBuilder errbuf = new StringBuilder();
    // Path of the off line sniffered file (*.pcap).
    final String filePath = pcapPath.toString();
    // Parser for parsing packet using TCP protocol.
    final Tcp tcp = new Tcp();
    // Map for temporary storing the un-complete bytes.
    final Map<Long, ByteArrayOutputStream> packetMap = new HashMap<Long, ByteArrayOutputStream>();

    /***************************************************************************
     * Second we open up the selected file using openOffline call
     **************************************************************************/
    Pcap pcap = Pcap.openOffline(filePath, errbuf);
    
    // Error occurred while accessing the specified *.pcap file.
    if (pcap == null) {
      log.error("Error while opening device for capture: " + errbuf.toString());
      return;
    }

    /***************************************************************************
     * Third we create a packet handler which will receive packets from
     * the libpcap loop.
     **************************************************************************/
    PcapPacketHandler<Path> jpacketHandler = new PcapPacketHandler<Path>() {
      
      /**
       * Get the next packet from the captured file.
       * @param packet Captured packet.
       * @param outputPath Path for outputting the captured result.
       */
      public void nextPacket(PcapPacket packet, Path outputPath) {
        // Filter packets which is using TCP protocol and the destination port is 25 (which is SMTP protocol).
        if (packet.hasHeader(tcp) && tcp.destination() == 25) {
          // Prepare the byte array for caching the captured bytes. 
          ByteArrayOutputStream outputStream = (packetMap.containsKey(tcp.seq())) ? 
              packetMap.remove(tcp.seq()) : new ByteArrayOutputStream();
              
          try {
            // Write the captured payload and append it to the end of the cached byte array.
            outputStream.write(tcp.getPayload());
            // Predict the sequence number of the next packet.
            long ack = tcp.seq() + tcp.getPayloadLength();
            // If this packet is the end of a TCP connection, try to output the content through outputPacket().
            // Otherwise, put the stream back to the map with updating its sequence number.
            if(tcp.getPayloadLength() > 0)
              packetMap.put(ack, outputStream);
            else
              outputPacket(outputStream, outputPath);
          } catch (IOException | MessagingException e) {
            e.printStackTrace();
          }
        }
      }
    };

    try {
      pcap.loop(-1, jpacketHandler, outputFolder);
    } catch (Exception | Error e) {
      e.printStackTrace();
    } finally {
      pcap.close();
    }
  }
  
  /**
   * Output the captured packets of SMTP protocol to the storage.
   * @param outputStream Cached stream which stores the content of captured bytes.
   * @param outputFolderPath Path of the folder for outputting the content.
   * @throws MessagingException
   * @throws IOException
   */
  public void outputPacket (ByteArrayOutputStream outputStream, Path outputFolderPath) throws MessagingException, IOException {
    MimeMessage msg = new MimeMessage(null, new ByteArrayInputStream(outputStream.toByteArray()));

    if (msg.getContentType().contains("multipart")) {
      // Get the body part from the multipart content.
      Multipart multipart = (Multipart) msg.getContent();
      
      for (int i = 0; i < multipart.getCount(); i++) {
        try {
          MimeBodyPart bodyPart = (MimeBodyPart) multipart.getBodyPart(i);
          
          // Stores only the attachments from the multipart content to files.
          if (StringUtils.isNotEmpty(bodyPart.getDisposition()) &&
              bodyPart.getDisposition().equalsIgnoreCase(MimeBodyPart.ATTACHMENT)) {
            String fileName = MimeUtility.decodeText(bodyPart.getFileName());
            log.debug("Store attachment '" + fileName + "'.");
            
            // Save the attachment to a file.
            bodyPart.saveFile(Paths.get(outputFolderPath.toString(), fileName).toString());
          }
        } catch (Exception | Error e) {
          e.printStackTrace();
        }
      }
    }
  }
}

上述程式的執行過程中,最後會將找到的附件檔名印出,並且將那些附件輸出到指定的輸出資料夾。
以下是在我的本機讀取某個 *.pcap 檔案後,印出的內容:

2014-10-17 13:53:29.425 | DEBUG | PcapTest > Source file: D:\test\manyFile.pcap
2014-10-17 13:53:29.426 | DEBUG | PcapTest > Output path: D:\test\output
2014-10-17 13:53:30.146 | DEBUG | PcapTest > Store attachment 'abc.jpg'.
2014-10-17 13:53:30.167 | DEBUG | PcapTest > Store attachment 'Chrysanthemum.jpg'.
2014-10-17 13:53:30.176 | DEBUG | PcapTest > Store attachment 'Desert.jpg'.
2014-10-17 13:53:30.184 | DEBUG | PcapTest > Store attachment 'Hydrangeas.jpg'.
2014-10-17 13:53:30.190 | DEBUG | PcapTest > Store attachment 'Jellyfish.jpg'.
2014-10-17 13:53:30.198 | DEBUG | PcapTest > Store attachment 'Koala.jpg'.
2014-10-17 13:53:30.205 | DEBUG | PcapTest > Store attachment 'Lighthouse.jpg'.
2014-10-17 13:53:30.211 | DEBUG | PcapTest > Store attachment 'Penguins.jpg'.
2014-10-17 13:53:30.219 | DEBUG | PcapTest > Store attachment 'Tulips.jpg'.
2014-10-17 13:53:30.229 | DEBUG | PcapTest > Store attachment '未命名.png'.

參考資料:
1、jNetPcap
2、TCP 與 UDP

沒有留言: