65.9K
CodeProject 正在变化。 阅读更多。
Home

TCP 会话重建工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (17投票s)

2007年9月14日

CPOL

6分钟阅读

viewsIcon

172626

downloadIcon

8316

一个用于 C# 的 TCP 会话重构工具。

引言

这是一个 C# 实用程序,用于重构嗅探器捕获的 TCP 会话(即使是不完整的会话)。它基于 libnids 和 WireShark 的部分翻译代码。由于找不到这样的解决方案,我只好自己构建了一个。

我一直在寻找一些可以从 Pcap 文件重构 TCP 会话的工具。我找到的工具大多是针对 Linux 的,或者是像 WireShark 这样功能强大的 GUI 工具。因此,我决定构建自己的工具。是时候让 C# 社区也拥有一个 TCP 会话重构工具了。

目录

背景

什么是嗅探器(Sniffer)?

“嗅探器是一种软件,它能捕获进出连接到网络的计算机的所有流量。它们适用于多个平台,有商业版和开源版。一些最简单的嗅探器实际上很容易用 C 或 Perl 实现;它们使用命令行界面并将捕获的数据转储到屏幕上。更复杂的项目则使用 GUI,绘制流量统计图,跟踪多个会话,并提供多种配置选项。嗅探器也是其他程序的引擎。入侵检测系统(IDS)使用嗅探器将数据包与规则集进行匹配,以标记任何恶意或异常的内容。网络利用率和监控程序通常使用嗅探器来收集指标和分析所需的数据。执法机构在调查期间需要监控电子邮件时,很可能会使用专门设计用于捕获特定流量的嗅探器。”

摘自 - 《嗅探器:它们是什么以及如何保护自己》

TCP 会话重构

嗅探器可以让你捕获网络上的数据包,但这些数据包形式各异、种类繁多,而且往往是无序的。TCP 重构就是将会话数据重新排序,恢复到其原始状态。

它有什么用?

简单来说,它能让你观察应用层数据。比方说,你有一个通过 tcpdump 或 WireShark 等程序获得的捕获文件。现在,你想观察你捕获的实际会话,例如,一个服务器和客户端之间的完整 HTTP 会话。你可以重构页面请求和 HTTP 服务器的响应。是的,这也意味着你实际上还可以重构通过你的网络发送的电子邮件和附件。

我在我正在开发的一个安全工具中使用了它。你可以利用这种功能行善或作恶,但它是一个强大的、目前尚缺失的功能。

首次尝试使用 libnids

libnids 是由 Rafal Wojtczuk 设计的一个库。它模拟了 Linux 2.0.x 的 IP 协议栈。libnids 提供 IP 分片重组、TCP 流组装和 TCP 端口扫描检测功能。该库有一些移植到 Win32 的版本,我找到的最新一个是针对 1.19 版本的(最新的 Linux 版本是 1.22)。要构建它,你还需要 WinPcap 库。这里的解决方案是用托管 C++ 代码包装这个库,并提供两个托管回调,一个用于服务器数据,一个用于客户端数据。

如何使用 libnids

  • 我们首先要做的是配置库的 `nids_params` 结构体。我需要处理一个捕获文件,这里就是传递文件名的地方。你可以在这里设置很多选项,包括一个过滤器,如果你有一个大的数据文件,或者你决定在网络上实时运行,这个过滤器会非常有用。
  • 调用 `nids_init()`。
  • 为库注册一个本地非托管回调。
  • 设置两个托管回调。
  • 调用 `nids_run()`。
// set the pcap file name in the nids param structure
IntPtr p = Marshal::StringToHGlobalAnsi(filename);
char *szFile = (char*)p.ToPointer();
nids_params.filename = szFile;
.
.
.
// register the managed callbacks
managedLibnids::LibnidsWrapper::m_clientCallback = clientCallback;
managedLibnids::LibnidsWrapper::m_serverCallback = serverCallback;
// init libnids
if (!nids_init ())
{
    return;
}
// register the local callback
nids_register_tcp (tcp_callback);

// start the capture ...
nids_run ();

就这么简单。当然,现在你需要将数据编组到托管回调中,但除此之外,你基本上就完成了。

这是本地回调函数中有趣的部分

void managedLibnids::tcp_callback (struct tcp_stream *a_tcp, 
                                   void ** this_time_not_needed)
{
  if (a_tcp->nids_state == NIDS_JUST_EST)
    {
    // connection described by a_tcp is established
    // here we decide, if we wish to follow this stream
    // sample condition: if (a_tcp->addr.dest!=23) return;
    // in this simple app we follow each stream, so..
      a_tcp->client.collect++; // we want data received by a client
      a_tcp->server.collect++; // and by a server, too
      a_tcp->server.collect_urg++; // we want urgent data received by a
                                   // server
      a_tcp->client.collect_urg++; // if we don't increase this value,
                                   // we won't be notified of urgent data
                                   // arrival
      return;
    }
.
.
.
  if (a_tcp->nids_state == NIDS_DATA)
    {
      // new data has arrived; gotta determine in what direction
      // and if it's urgent or not

      struct half_stream *hlf;

      // So, we have some normal data to take care of.
      if (a_tcp->client.count_new)
      {
          // new data for client
          hlf = &a_tcp->client; // from now on, we will deal with hlf var,
                                // which will point to client side of conn
      }
      else
      {
        hlf = &a_tcp->server; // analogical
      }
      //we send the newly arrived data
      int len = hlf->count_new;
      // Marshal the data
      array[byte]^ data = gcnew array[byte](len);
      Marshal::Copy((IntPtr)hlf->data,data,0, len);
      // Send the data to the callback
      if (a_tcp->client.count_new)
        LibnidsWrapper::m_clientCallback(data,a_tcp->addr.saddr,
          a_tcp->addr.source,a_tcp->addr.daddr,a_tcp->addr.dest,false);    
      else
         LibnidsWrapper::m_serverCallback(data,a_tcp->addr.saddr,
           a_tcp->addr.source,a_tcp->addr.daddr,a_tcp->addr.dest,false);    
    }
  return ;
}

仍然不够好

我发现 libnids 只能重组完整或接近完整的会话。这对于大多数人来说通常足够了,但我还需要检查不完整的会话。我通常在 WireShark 中处理特定的流,它能够跟踪 TCP 流,即使是不完整的流。我也希望拥有这种能力。

第二次尝试,研究 WireShark

幸运的是,WireShark 是一个开源项目。我把它拆开,搜索重构 TCP 会话的代码,哇!从这里开始,我所要做的就是将 ANSI C 代码翻译成纯 C# 代码。代码非常直观且易于理解,感谢 WireShark 的开发者们做得很好。

这是重构 TCP 会话的主要函数

    private void reassemble_tcp( ulong sequence, ulong length, byte[] data,
                   ulong data_length, bool synflag, long net_src,
                   long net_dst, uint srcport, uint dstport) 
    {
       long srcx, dstx;
       int src_index, j;
       bool first = false;
       ulong newseq;
       tcp_frag tmp_frag;

       src_index = -1;

       /* Now check if the packet is for this connection. */
        srcx = net_src;
        dstx = net_dst;

       /* Check to see if we have seen this source IP and port before.
       (Yes, we have to check both source IP and port; the connection
       might be between two different ports on the same machine.) */
       for( j=0; j<2; j++ ) {
           if (src_addr[j] == srcx && src_port[j] == srcport ) {
               src_index = j;
           }
       }
       /* we didn't find it if src_index == -1 */
       if( src_index < 0 ) {
           /* assign it to a src_index and get going */
           for( j=0; j<2; j++ ) {
               if( src_port[j] == 0 ) {
                   src_addr[j] = srcx;
                   src_port[j] = srcport;
                   src_index = j;
                   first = true;
                   break;
               }
           }
       }
       if( src_index < 0 ) {
           throw new Exception("ERROR in reassemble_tcp: Too many addresses!");
       }

       if( data_length < length ) {
           incomplete_tcp_stream = true;
       }

       /* now that we have filed away the srcs, lets get the sequence number stuff
       figured out */
       if( first ) {
           /* this is the first time we have seen this src's sequence number */
           seq[src_index] = sequence + length;
           if( synflag ) {
               seq[src_index]++;
           }
           /* write out the packet data */
           write_packet_data( src_index, data );
           return;
       }
       /* if we are here, we have already seen this src, let's
       try and figure out if this packet is in the right place */
       if( sequence < seq[src_index] ) {
           /* this sequence number seems dated, but
           check the end to make sure it has no more
           info than we have already seen */
           newseq = sequence + length;
           if( newseq > seq[src_index] ) {
               ulong new_len;

               /* this one has more than we have seen. let's get the
               payload that we have not seen. */

               new_len = seq[src_index] - sequence;

               if ( data_length <= new_len ) {
                   data = null;
                   data_length = 0;
                   incomplete_tcp_stream = true;
               } else {
                   data_length -= new_len;
                   byte[] tmpData = new byte[data_length];
                   for(ulong i=0; i<data_length; /> 0 && sequence > seq[src_index] ) {
               tmp_frag = new tcp_frag();
               tmp_frag.data = data;
               tmp_frag.seq = sequence;
               tmp_frag.len = length;
               tmp_frag.data_len = data_length;
               
               if( frags[src_index] != null ) {
                   tmp_frag.next = frags[src_index];
               } else {
                   tmp_frag.next = null;
               }
               frags[src_index] = tmp_frag;
           }
       }
    } /* end reassemble_tcp */

好的,我们现在可以重构一个会话了;接下来,我们只需要捕获数据包并分别存储每个会话。

捕获数据包

为此,我使用了在 CodeProject 上找到的一个很棒的库,叫做 SharpPcap,由 Tamir Gal 编写。更多信息,你可以查看这个关于如何使用 SharpPcap 的链接。

这是我用来捕获 TCP 数据包的代码

    try
    {
        //Get an offline file pcap device
        device = SharpPcap.GetPcapOfflineDevice(capFile);
        //Open the device for capturing
        device.PcapOpen();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
        return;
    }

    //Register our handler function to the 'packet arrival' event
    device.PcapOnPacketArrival +=
        new SharpPcap.PacketArrivalEvent(device_PcapOnPacketArrival);

    // add a filter so we get only tcp packets
    device.PcapSetFilter("tcp");

    //Start capture 'INFINTE' number of packets
    //This method will return when EOF reached.
    device.PcapCapture(SharpPcap.INFINITE);

    //Close the pcap device
    device.PcapClose();

最终设计

我使用的设计是一个字典对象,它持有 `(Connection, TcpRecon)` 对象对。`TcpRecon` 持有一个 `FileStream` 和 TCP 连接的状态。它负责实际重构一个 TCP 会话并将该会话存储在其 `FileStream` 中。`Connection` 持有会话信息,如源 IP、目标 IP、源端口、目标端口等。

让它们协同工作

SharpPcap 库的回调函数在字典中搜索匹配的连接。接下来,数据包被送入正确的 `TcpRecon` 对象,该对象再进行会话重构。

    // The callback function for the SharpPcap library
    private static void device_PcapOnPacketArrival(object sender, Packet packet)
    {
        TCPPacket tcpPacket = (TCPPacket)packet;
        // Creates a key for the dictionary
        Connection c = new Connection(tcpPacket);

        // create a new entry if the key does not exists
        if (!sharpPcapDict.ContainsKey(c))
        {
            string fileName = c.getFileName(path);
            TcpRecon tcpRecon = new TcpRecon(fileName);
            sharpPcapDict.Add(c, tcpRecon);
        }

        // Use the TcpRecon class to reconstruct the session
        sharpPcapDict[c].ReassemblePacket(tcpPacket);
    }

如何使用此工具:

注意,您必须安装 WinPcap 才能运行 TcpRecon!

TcpRecon <capture file name> <-nids>
The -nids flag is used to activate the libnids reconstruction 
    instead of the built in functionality.
The tool will create session files of the form:
    <server_ip>.<server_port>-<client_ip>.<client_port>.data

附加信息

谢谢

我要感谢我用来构建这个工具的所有项目背后那些出色的人们。感谢你们的分享。

历史

  • 版本 1 - 首次提交
  • 版本 1.01 - 修复了对非 TCP 数据包的处理。
© . All rights reserved.