使用原始套接字进行无状态端口扫描

通常情况下我们要判断目标机器的指定端口是否开放 可以直接通过用Socket去连接看看是否能够连接上 但是直接TCP协议的Socket去连接的话 是建立的一个全连接 比较耗费资源而且还有timeout之类的等待 如果端口数目并不是很多可以通过TCP直接连接 但是如果端口数目很多的情况下这样的方式就有点不尽人意了 而如果此时使用无状态的方式去扫描的话效果就不一样了

所谓无状态就是指无需关心TCP状态 不占用系统TCP/IP协议栈资源 忘记syn、ack、fin、timewait 不进行会话组包 通过自己来构造TCP协议的数据包来模拟TCP建立连接的过程 自己模拟和直接使用Socket建立TCP连接有什么区别就慢慢来说明 先从TCP协议开始说起 先来看看TCP是怎么建立连接的

telnet 8.8.8.8 53
上面是我通过WireShark抓取的一个我telnet谷歌DNS服务器的一组数据包 上面蓝色框里面的是建立一个TCP的过程 然后由于我连接上了DNS服务器却一段时间内没有发送数据过去谷歌那边主动把我断开了 而下面红色框里面就是断开一个连接的过程 先来看看TCP是如何建立连接的

上面是一张TCP连接连接时候的一个过程图 需要三步握手连接才算建立完成 而UDP协议则没有三步握手的过程 为什么需要三步而不是其他步 可以把上面的过程想象成一个通话的过程

A->B(SYN)       :喂 你能听到吗
B->A(SYN+ACK)   :嗯 我能听到 你能听到吗
A->B(ACK)       :嗯 可以听到你
通过这三步那么AB双方都能确认自己能够听到对方而且对方也能听到自己 那就表明这个连接比较稳定了 可能建立了 之后就是数据的正常传输了每发送一个数据包对方回应一个ACK来确认收到的数据包 通过这样的方式来传输数据就相对比较可靠了 所以说TCP是可靠的一个传输协议 而UDP则不是可靠传输 因为UDP不需要三步握手也不需要向对方确认是否收到数据

建立连接的时候需要三步握手 而关闭连接则需要四次挥手

上面是一个TCP断开连接的一个过程 同样为什么是四次 依然想象成一个通话的过程

A->B(FIN+ACK)   :还有什么要说的吗 我要挂电话了
B->A(ACK)       :没什么了
B->A(FIN+ACK)   :那挂电话咯
A->B(ACK)       :好的
如果在双方都没有什么数据传输的情况下 那么就是上面的一个过程 如果A请求断开连接 可能B还有数据没有发送完毕 则就不会回应ACK而是继续发送数据 然后再断开连接 过程大致为先由请求方发送一个FIN+ACK来请求关闭 然后被请求发发送最后的FIN+ACK来确认关闭(因为被请求方可能还有数据需要发送 所以需要被请求方来做最终关闭)

我发现有些系统并没有按照四次挥手来 根据抓包情况来看有一些是三次挥手 比如我用下面的代码来连接谷歌DNS服务器 连接后然后主动关闭连接的一段代码

Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sock.Connect(new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53));
sock.Close();
可抓包数据是这样的

意思就是变成了这样

A->B(FIN+ACK)   :还有什么要说的吗 我要挂电话了
B->A(FIN+ACK)   :那挂电话吧
A->B(ACK)       :好的
就比较霸道了 我姑且理解为节省资源吧 对方直接省一步ACK

通过上面 可以看到如果通过Socket直接使用TCP去连接一个端口(如果开放) 那么就会产生以上以上数据包 然而实际上通过上面第二个包如果对方回应的是SYN+ACK就基本可以确定对方是开放了端口的 而下面的数据包都是多余的 所以说如果这个数据包能够自己来构造那么 就可以不用发送那么多数据包了 问题的关键来了 如何自己去模拟一个数据包

通常使用Socket的时候用的最多的就是TCP或者UDP写了而他们的创建方式通常是这样的:

Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
但是现在不能这样创建Socket了 因为这样创建的套接子是上层应用 我们需要更为底层一点的套接子
//创建原始套接子
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
//设置套接子 使其可以自己构造数据头
sock.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);
注:原始套接子在windows系统上有使用限制 从XP SP2开始貌似只能使用发送UDP报文 而WIN7上完全禁止使用 当然可以使用地第三方的方式来发送原始数据包比如WinpCap 只是说原生的Socket函数已经被阉割了 但是Sever版本系统还可以使用(只在2003/2008上测试过) 所以之后出现的代码 仅在Server版本系统上使用

虽说上面的代码是C#的 但是Socket函数到每种语言上都是差不多的 只是语法上有点区别

通过上面创建出来的Socket就不像平时发送TCP或者UDP那样只管发送数据就行了 而需要自己构造整个IP层的数据包

先不说自己怎么构造 先来做一个测试 把我Telnet谷歌DNS服务器的那个SYN数据包复制出来自己再发送一个一模一样的试试

上面蓝色方框里面是IP协议报文头 而红色框里面的是TCP协议报文头 现在我把IP和TCP报文头复制出来 然后使用原始套接子再发送一次效果如下

代码如下

private void button1_Click(object sender, EventArgs e) {
    byte[] bySyn = new byte[]{
        0x45,0x00
        ,0x00,0x30,0x74,0x88,0x40,0x00,0x80,0x06,0xb5,0x10,0xc0,0xa8,0x00,0x77,0x08,0x08
        ,0x08,0x08,0x05,0xa5,0x00,0x35,0x92,0x67,0x39,0x08,0x00,0x00,0x00,0x00,0x70,0x02
        ,0xfa,0xf0,0xe5,0xb5,0x00,0x00,0x02,0x04,0x05,0xb4,0x01,0x01,0x04,0x02
    };
    Socket sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
    sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);

    sock_raw.SendTo(bySyn, new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53));
}
可以看到谷歌DNS服务器正确的返回了SYN+ACK数据包 而我的系统则发送了一次RST数据包 因为当系统接受到这个SYN+ACK数据包的时候 认为是一个错误的包 因为系统觉得自己并没有和8.8.8.8建立链接 认为对方发送错了所以发送一个RST拒绝 因为那个SYN是我自己构造的数据包发送出去的而没有经过系统的TCP协议栈 不过这个对我们来说已经没有关系了 因为第二个数据包才是我们关心的重点(SendTo函数的第二个参数EndPoint的值将会被忽略 因为sock_raw发送的不再是应用层数据了 还包含了包头数据 所以系统不会给我们构造包头数据了 在bySyn中包含了IP和端口信息)

上面的bySyn稍微格式化一下差不多就是这样的

byte[] bySyn = new byte[]{
    //IP协议头
    0x45                    //IP版本号码4 首部长度5(5个32Bit 20字节)
    ,0x00                   //TOS服务类型
    ,0x00,0x30              //数据总长度 48字节
    ,0x74,0x88              //标识
    ,0x40,0x00              //3位标志+13位片偏移
    ,0x80                   //ttl = 128
    ,0x06                   //使用TCP协议
    ,0xb5,0x10              //数据校验和 虚拟机里面出现过校验和是0的情况 但是包没被对方丢弃
    ,0xc0,0xa8,0x00,0x77    //来源端口 192.168.0.119
    ,0x08,0x08,0x08,0x08    //目标端口 8.8.8.8
    //TCP协议头
    ,0x05,0xa5              //来源端口 1445
    ,0x00,0x35              //目标端口 53
    ,0x92,0x67,0x39,0x08    //seq顺序号
    ,0x00,0x00,0x00,0x00    //ack确认号
    ,0x70                   //首部长度7(7个32Bit 28字节)
    ,0x02                   //SYN
    ,0xfa,0xf0              //窗口大小
    ,0xe5,0xb5              //检验和 虚拟机里面出现过校验和是0的情况 但是包没被对方丢弃
    ,0x00,0x00              //紧急指针
    ,0x02,0x04,0x05,0xb4,0x01,0x01,0x04,0x02    //可选字段
};

那么问题来了 要怎么样构造一个上面一样的数据包呢 可以看我之前写的一篇文章点击此处 里面有关于IP TCP UDP协议的报文头说明 可以将上面的数据与机构对照观看即可明白

然后下面来定义一个类来自己构造SYN数据包

public class TcpDefine
{
    private static Random m_rnd = new Random();
    public const int IPPROTO_TCP = 6;
    public enum Flag { URG = 32, ACK = 16, PSH = 8, RST = 4, SYN = 2, FIN = 1 }
    //定义TCP头
    public struct IP_HEADER
    {
        public byte h_verlen;                       //4位IP版本号 4位首部长度
        public byte tos;                            //8位服务类型TOS 
        public ushort total_len;                    //16位总长度(字节)
        public ushort ident;                        //16位标 
        public ushort frag_and_flags;               //3位标志 
        public byte ttl;                            //8位生存时间   TTL 
        public byte proto;                          //8位协议(TCP,UDP等) 
        public ushort checksum;                     //16位IP首部校验和 
        public uint sourceIP;                       //32位来源IP地址 
        public uint destIP;                         //32位目的IP地址 
    }
    //定义伪首部
    public struct PSD_HEADER
    {
        public uint saddr;                          //源地址 
        public uint daddr;                          //目的地址 
        public byte mbz;
        public byte ptcl;                           //协议类型 
        public ushort length;                       //上层协议数据长度
    }
    //定义TCP头 
    public struct TCP_HEADER
    {
        public ushort th_sport;                     //16位来源端口 
        public ushort th_dport;                     //16位目标端口 
        public int th_seq;                          //32位顺序号 
        public int th_ack;                          //32位确认号 
        public byte th_lenres;                      //4位首部长度/6位保留字 
        public byte th_flag;                        //6位标志位 
        public ushort th_win;                       //16位窗口大小 
        public ushort th_sum;                       //16位校验和 
        public ushort th_urp;                       //16位紧急数据偏移量 
    }
    //字节反转
    public static ushort Reverse(ushort num)
    {
        return (ushort)((num << 8) | (num >> 8));
    }
    //反转字节
    public static int Reverse(int num)
    {
        int temp = (num << 24);
        temp |= (num << 8) & 0x00FF0000;
        temp |= (num >> 8) & 0x0000FF00;
        temp |= (num >> 24) & 0x000000FF;
        return temp;
    }

    public static uint IPToINT(string strIP)
    {
        string[] strs = strIP.Split('.');
        uint num = 0;
        num = byte.Parse(strs[3]);
        num <<= 8;
        num |= byte.Parse(strs[2]);
        num <<= 8;
        num |= byte.Parse(strs[1]);
        num <<= 8;
        num |= byte.Parse(strs[0]);
        return num;
    }

    public static ushort CheckSum(byte[] byData, int size)
    {
        ulong cksum = 0;
        int index = 0;
        while (size > 1)
        {
            cksum += BitConverter.ToUInt16(byData, index);
            index += 2;
            size -= 2;
        }
        if (size == 1)
        {
            cksum += byData[index - 1];
        }
        cksum = (cksum >> 16) + (cksum &amp; 0xFFFF);
        cksum += (cksum >> 16);
        return (ushort)(~cksum);
    }

    public static TcpDefine.IP_HEADER GetIPHeader(string strSIP, string strDIP) {
        TcpDefine.IP_HEADER ip_header = new TcpDefine.IP_HEADER();
        ip_header.h_verlen = (byte)(4 << 4 | Marshal.SizeOf(ip_header) / 4);
        ip_header.tos = 0;
        ip_header.total_len = TcpDefine.Reverse((ushort)20);
        ip_header.ident = 1;
        ip_header.frag_and_flags = 0;
        ip_header.ttl = 128;
        ip_header.proto = TcpDefine.IPPROTO_TCP;
        ip_header.checksum = 0;
        ip_header.sourceIP = TcpDefine.IPToINT(strSIP);
        ip_header.destIP = TcpDefine.IPToINT(strDIP);
        return ip_header;
    }

    public static TcpDefine.TCP_HEADER GetTcpHeader(ushort usSPort, ushort usDPort, int seq, int ack, Flag flag)
    {
        TcpDefine.TCP_HEADER tcp_header = new TcpDefine.TCP_HEADER();
        tcp_header.th_sport = TcpDefine.Reverse(usSPort);
        tcp_header.th_dport = TcpDefine.Reverse(usDPort);
        tcp_header.th_seq = TcpDefine.Reverse(seq);
        tcp_header.th_ack = TcpDefine.Reverse(ack);
        tcp_header.th_lenres = (byte)(Marshal.SizeOf(tcp_header) / 4 << 4 | 0);
        tcp_header.th_flag = (byte)flag;
        tcp_header.th_win = TcpDefine.Reverse((ushort)16384);
        tcp_header.th_urp = 0;
        tcp_header.th_sum = 0;
        return tcp_header;
    }
    /// <summary>
    /// 构造目标IP和端口的SYN数据包
    /// </summary>
    /// <param name="strSIP">来源IP地址</param>
    /// <param name="strDIP">目标IP地址</param>
    /// <param name="usSPort">来源端口号</param>
    /// <param name="usDPort">目标端口号码</param>
    /// <returns>构造的SYN数据包字节数组</returns>
    public static byte[] GetSynPacket(string strSIP,string strDIP,ushort usSPort,ushort usDPort)
    {
        TcpDefine.IP_HEADER ip_header = TcpDefine.GetIPHeader(strSIP, strDIP);
        TcpDefine.TCP_HEADER tcp_header = TcpDefine.GetTcpHeader(usSPort, usDPort, m_rnd.Next(), 0, TcpDefine.Flag.SYN);
        TcpDefine.PSD_HEADER psd_header = new TcpDefine.PSD_HEADER();   //定义伪首部 检验和用
        byte[] byResult = new byte[Marshal.SizeOf(ip_header) + Marshal.SizeOf(tcp_header)];
        ip_header.total_len = (ushort)(byResult.Length);                //整个数据包长度
        //填充伪首部数据
        psd_header.saddr = ip_header.sourceIP;
        psd_header.daddr = ip_header.destIP;
        psd_header.mbz = 0;
        psd_header.ptcl = TcpDefine.IPPROTO_TCP;
        psd_header.length = TcpDefine.Reverse((ushort)(Marshal.SizeOf(tcp_header)));//头部长度 + 数据长度(这里没有数据)
        //计算TCP校验和 若包含TCP应用层数据部分 则也应该列入校验和计算(这里没有)
        IntPtr p = Marshal.AllocHGlobal(40);
        Marshal.StructureToPtr(psd_header, p, false);
        Marshal.Copy(p, byResult, 0, Marshal.SizeOf(psd_header));
        Marshal.StructureToPtr(tcp_header, p, false);
        Marshal.Copy(p, byResult, Marshal.SizeOf(psd_header), Marshal.SizeOf(tcp_header));
        tcp_header.th_sum = TcpDefine.CheckSum(byResult, Marshal.SizeOf(psd_header) + Marshal.SizeOf(tcp_header));
        //计算IP检验和 
        Marshal.StructureToPtr(ip_header, p, false);
        Marshal.Copy(p, byResult, 0, Marshal.SizeOf(ip_header));
        Marshal.StructureToPtr(tcp_header, p, false);
        Marshal.Copy(p, byResult, Marshal.SizeOf(ip_header), Marshal.SizeOf(tcp_header));
        ip_header.checksum = TcpDefine.CheckSum(byResult, Marshal.SizeOf(ip_header));//IP检验和只需要计算IP头
        //将数据拷贝到ByResult中
        Marshal.StructureToPtr(ip_header, p, false);
        Marshal.Copy(p, byResult, 0, Marshal.SizeOf(ip_header));
        Marshal.StructureToPtr(tcp_header, p, false);
        Marshal.Copy(p, byResult, 20, Marshal.SizeOf(tcp_header));
        Marshal.FreeHGlobal(p);
        return byResult;
    }
}

通过上面的GetSynPacket函数可以构造出一个SYN数据包 然后代码就可以写成这样了

Socket sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);

sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "8.8.8.8", 10086, 53), 
    new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53));
上面来源IP写成自己的IP地址 来源端口自己也写一个 因为到时候目标主机收到这个数据包后会用你填入的IP和端口来回应 所以甚至可以伪造IP地址什么的只是到时候你就收不到回应了(不要奇怪为什么这次这个代码里面的IP段是192.168.88而上面的却是192.168.0 - -!。。。我只能说 文章不是在同一台机器上写完的 我也懒得从新截图了 意思到位就行了)

下面来看看效果

可以看到数据包同样被正确的发送了出去 所使用的来源IP地址和端口都是我填写的数据 数据包也得到了回应

说道回应 那么问题来了怎么才能得到这个回应 光是发送出去了不能得到一样没有什么卵用 因为判断全靠回来的数据判断啊 所以还需要接收代码和接数据包的代码 所一在TcpDefine里面再来一段

public static List<object> DecodePacket(byte[] byData)
{
    List<object> lst = new List<object>();
    IntPtr p = Marshal.AllocHGlobal(20);
    Marshal.Copy(byData, 0, p, 20);
    TcpDefine.IP_HEADER ip_header = (TcpDefine.IP_HEADER)Marshal.PtrToStructure(p, typeof(TcpDefine.IP_HEADER));
    ip_header.total_len = TcpDefine.Reverse(ip_header.total_len);
    lst.Add(ip_header);
    Marshal.Copy(byData, (ip_header.h_verlen & 0x0F) * 4, p, 20);//这里我没判断 可能之后的数据不够一个TCP头会出问题
    TcpDefine.TCP_HEADER tcp_header = (TcpDefine.TCP_HEADER)Marshal.PtrToStructure(p, typeof(TcpDefine.TCP_HEADER));
    Marshal.FreeHGlobal(p);
    tcp_header.th_dport = TcpDefine.Reverse(tcp_header.th_dport);
    tcp_header.th_sport = TcpDefine.Reverse(tcp_header.th_sport);
    tcp_header.th_seq = TcpDefine.Reverse(tcp_header.th_seq);
    tcp_header.th_ack = TcpDefine.Reverse(tcp_header.th_ack);
    lst.Add(tcp_header);//对于我们而言byData里面的 IP和TCP才是我们要的 后面的数据部分不用管了
    return lst;
}
然后窗体上的全部代码:
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private Socket m_sock_raw;

    private void Form1_Load(object sender, EventArgs e)
    {
        m_sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
        m_sock_raw.Bind(new IPEndPoint(IPAddress.Parse("192.168.88.105"), 0));
        //设置socket自己构造头部数据包
        m_sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);
        //设置socket接收全部数据
        m_sock_raw.IOControl(IOControlCode.ReceiveAll, new byte[] { 1, 0, 0, 0 }, null);
        new Thread(ListenCallBack) { IsBackground = true }.Start();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        m_sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "8.8.8.8", 10086, 80),
            new IPEndPoint(IPAddress.Parse("8.8.8.8"), 80));
    }

    private void ListenCallBack()
    {
        byte[] byRecv = new byte[65535];
        while (true)//while(true)并不是推荐写法 这里只为演示 异步写法更佳
        {
            int len = m_sock_raw.Receive(byRecv);
            if (len == 0) break;
            if (len < 40) continue;//IP+TCP包头至少40字节 小于这个数据的不要
            List<object> lst = TcpDefine.DecodePacket(byRecv);  //解包
            TcpDefine.IP_HEADER st_ip = (TcpDefine.IP_HEADER)lst[0];
            if (st_ip.proto != TcpDefine.IPPROTO_TCP) continue; //不是TCP协议不要
            TcpDefine.TCP_HEADER st_tcp = (TcpDefine.TCP_HEADER)lst[1];
            if (st_tcp.th_dport != 10086) continue;             //不是发往指定端口的数据不要
            //没有SYN和ACK不要
            if ((st_tcp.th_flag & (byte)TcpDefine.Flag.SYN) != (byte)TcpDefine.Flag.SYN) continue;
            if ((st_tcp.th_flag & (byte)TcpDefine.Flag.ACK) != (byte)TcpDefine.Flag.ACK) continue;
            Console.WriteLine("ACK From -> " + new IPAddress(st_ip.sourceIP).ToString() + ":" + st_tcp.th_sport);
        }
    }
}
运行出来点击按钮 调试窗口上也正确的输出了数据:

来看看我内网里面开放80的情况

private void button1_Click(object sender, EventArgs e)
{
    for (int i = 1; i < 255; i++)
    {//发包不宜过快 别把自己给塞死了
        m_sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "192.168.88." + i, 10086, 80),
            new IPEndPoint(IPAddress.Parse("192.16.88." + i), 80));
    }
}

可以看到 我的主线程只管发包而另一个线程负责收包 这样只需要两个线程就够了 发完包只需要等着收就是了 而如果用Socket直接创立TCP连接 调用Socket.Connect()函数发包和收包则是一体的会被阻塞 这样就大大的节省了资源

注意 这种方式必然会出现丢包的情况 尤其是在网络情况复杂的情况下 发包的时候也不要死循环的发把自己给塞死了 至于发包的频率根据自己情况来衡量 而且是否处理丢包问题也看自己 比如发送了SYN包出去 却没有收到回应 是否重发之类的 如果直接用Socket建立TCP连接系统会自己处理 

通常正常情况下 目标主机无论开与没有开发端口都会回应一个数据包 比如


上面就是一个目标端口没有开放的情况但是对方回应了一个RST+ACK 如果端口开放就会回应SYN+ACK

如果是连目标主机都不存在的情况下 那么估计就只有等timeout了

由于没有收到回应 所以系统又重新发送了两次SYN 结果还是没有回应 只能等timeout超时了 所以说只要目标机器存在的情况下 发送一个SYN包过去都会有回应

但是不排除一些机器 比如上面的命令换成telnet 8.8.8.8 其他端口 也会出现无响应的情况 谷歌就是这么任性 没开放端口都懒得鸟你 让你自己去timeout 而且有很多大型厂商的机器都是这样的

所以是直接发完SYN包就不管了 还是没有收到数据包的再发送一次 以及发包的频率和去重处理自己决定 我在这里只是说明如何发送SYN数据包以及接收


添加时间:2016-12-12 17:43:57 编辑时间:2017-01-04 14:35:20 阅读:4613 
不作死就不会死 协议
雪碧 - 2018-05-11 10:42:42
崇拜你 博主 还有就是背景相当nice 希望你能多发一些关于c#的常识 ----来自程序员菜鸟
逝水 - 2021-11-02 22:39:00
博主真厉害,向你学习
  • 编写评论

      我觉得区分大小写是一个码农的基本素质
[访问统计] 今天:79 总数:262800 提示:未成年人 请在大人陪同下浏览本站内容 还有:世界上最帅的码农 -> 石头 RSS:http://st233.com/rss Powered by -> Crystal_lz